Tableaux typés - Données binaires dans le navigateur

Ilmari Heikkinen

Introduction

Les tableaux typés sont un ajout relativement récent des navigateurs, qui découle de la nécessité de disposer d'un moyen efficace de gérer les données binaires dans WebGL. Un tableau typé est une quantité de mémoire contenant une vue typée, de la même manière que les tableaux fonctionnent en C. Étant donné qu'un tableau typé s'appuie sur de la mémoire brute, le moteur JavaScript peut transmettre la mémoire directement aux bibliothèques natives sans avoir à convertir minutieusement les données en une représentation native. Par conséquent, les tableaux typés sont beaucoup plus performants que les tableaux JavaScript pour transmettre des données à WebGL et à d'autres API traitant des données binaires.

Les vues de tableaux typés agissent comme des tableaux de type unique pour un segment d'un ArrayBuffer. Il existe des vues pour tous les types numériques habituels, avec des noms autodescriptifs tels que Float32Array, Float64Array, Int32Array et Uint8Array. Il existe également une vue spéciale qui a remplacé le type de tableau de pixels dans ImageData de Canvas: Uint8ClampedArray.

DataView constitue le deuxième type de vue. Il est conçu pour traiter des données hétérogènes. Au lieu d'avoir une API de type tableau, l'objet DataView vous fournit une API get/set pour lire et écrire des types de données arbitraires à des décalages d'octets arbitraires. DataView est idéal pour lire et écrire des en-têtes de fichier et d'autres données de type struct.

Principes de base de l'utilisation des tableaux typés

Vues d'un tableau typé

Pour utiliser des tableaux typés, vous devez créer un objet ArrayBuffer et une vue de celui-ci. Le moyen le plus simple consiste à créer une vue de tableau typée de la taille et du type souhaités.

// Typed array views work pretty much like normal arrays.
var f64a = new Float64Array(8);
f64a[0] = 10;
f64a[1] = 20;
f64a[2] = f64a[0] + f64a[1];

Il existe plusieurs types de vues de tableaux typés. Ils partagent tous la même API, donc une fois que vous savez en utiliser une, vous savez à peu près comment les utiliser toutes. Je vais créer une de chaque vue de tableau typée existante dans l'exemple suivant.

// Floating point arrays.
var f64 = new Float64Array(8);
var f32 = new Float32Array(16);

// Signed integer arrays.
var i32 = new Int32Array(16);
var i16 = new Int16Array(32);
var i8 = new Int8Array(64);

// Unsigned integer arrays.
var u32 = new Uint32Array(16);
var u16 = new Uint16Array(32);
var u8 = new Uint8Array(64);
var pixels = new Uint8ClampedArray(64);

Le dernier est un peu spécial, il limite les valeurs d'entrée comprises entre 0 et 255. Cette approche est particulièrement utile pour les algorithmes de traitement des images Canvas, car vous n'avez plus besoin de limiter manuellement vos calculs de traitement des images pour éviter de dépasser la plage de 8 bits.

Par exemple, voici comment appliquer un facteur gamma à une image stockée dans un Uint8Array. Pas très joli:

u8[i] = Math.min(255, Math.max(0, u8[i] * gamma));

Avec Uint8ClampedArray, vous pouvez ignorer la limitation manuelle:

pixels[i] *= gamma;

L'autre façon de créer des vues d'un tableau typé consiste à créer d'abord un ArrayBuffer, puis à créer des vues qui pointent vers celui-ci. Les API qui envoient des données externes traitent généralement les ArrayBuffers. C'est donc la façon dont vous obtenez une vue de tableau typée pour ces API.

var ab = new ArrayBuffer(256); // 256-byte ArrayBuffer.
var faFull = new Uint8Array(ab);
var faFirstHalf = new Uint8Array(ab, 0, 128);
var faThirdQuarter = new Uint8Array(ab, 128, 64);
var faRest = new Uint8Array(ab, 192);

Vous pouvez également avoir plusieurs vues dans le même ArrayBuffer.

var fa = new Float32Array(64);
var ba = new Uint8Array(fa.buffer, 0, Float32Array.BYTES_PER_ELEMENT); // First float of fa.

Pour copier un tableau typé dans un autre tableau typé, le moyen le plus rapide consiste à utiliser la méthode d'ensemble de tableaux typés. Pour une utilisation de type "Memcpy", créez des Uint8Array dans les tampons des vues et utilisez "set" pour copier les données.

function memcpy(dst, dstOffset, src, srcOffset, length) {
  var dstU8 = new Uint8Array(dst, dstOffset, length);
  var srcU8 = new Uint8Array(src, srcOffset, length);
  dstU8.set(srcU8);
};

DataView

Pour utiliser des ArrayBuffers contenant des données avec des types hétérogènes, le moyen le plus simple consiste à utiliser un DataView dans le tampon. Supposons que nous ayons un format de fichier comportant un en-tête avec un entier non signé de 8 bits suivi de deux entiers de 16 bits, puis un tableau de charge utile de floats 32 bits. La relecture avec des vues de tableau typées est faisable, mais elle peut s'avérer pénible. Avec un DataView, nous pouvons lire l'en-tête et utiliser une vue typée pour le tableau float.

var dv = new DataView(buffer);
var vector_length = dv.getUint8(0);
var width = dv.getUint16(1); // 0+uint8 = 1 bytes offset
var height = dv.getUint16(3); // 0+uint8+uint16 = 3 bytes offset
var vectors = new Float32Array(width*height*vector_length);
for (var i=0, off=5; i<vectors.length; i++, off+=4) {
  vectors[i] = dv.getFloat32(off);
}

Dans l'exemple ci-dessus, toutes les valeurs que j'ai lues sont de type big-endian. Si les valeurs du tampon sont de type "small-endian", vous pouvez transmettre le paramètre facultatif petitEndian au getter:

...
var width = dv.getUint16(1, true);
var height = dv.getUint16(3, true);
...
vectors[i] = dv.getFloat32(off, true);
...

Notez que les vues de tableau typées sont toujours dans l'ordre natif des octets. afin de les rendre plus rapides. Vous devez utiliser un objet DataView pour lire et écrire les données lorsque l'endianité risque de poser problème.

DataView dispose également de méthodes pour écrire des valeurs dans des tampons. Ces setters sont nommés de la même manière que les getters, "set" suivi du type de données.

dv.setInt32(0, 25, false); // set big-endian int32 at byte offset 0 to 25
dv.setInt32(4, 25); // set big-endian int32 at byte offset 4 to 25
dv.setFloat32(8, 2.5, true); // set little-endian float32 at byte offset 8 to 2.5

Discussion sur l'endianisme

L’endianité, ou ordre des octets, est l’ordre dans lequel les nombres à plusieurs octets sont stockés dans la mémoire de l’ordinateur. Le terme big-endian décrit une architecture de processeur qui stocke en premier l'octet le plus significatif, little-endian, l'octet le moins significatif en premier. Le boutisme utilisé dans une architecture de processeur donnée est complètement arbitraire. Il y a de bonnes raisons de choisir l'une ou l'autre. En fait, certains processeurs peuvent être configurés pour prendre en charge les données big-endian et Little-endian.

Pourquoi devez-vous vous soucier de l'endianisme ? La raison est simple. Lors de la lecture ou de l'écriture de données à partir du disque ou du réseau, l'extrémité des données doit être spécifiée. Cela garantit que les données sont interprétées correctement, quelle que soit l'envie du processeur qui est utilisé. Dans un monde de plus en plus connecté, il est essentiel de prendre en charge tous les types d'appareils, qu'ils soient big-end (ou big-endian), qui pourraient avoir besoin de travailler avec des données binaires provenant de serveurs ou d'autres pairs sur le réseau.

L'interface DataView est spécialement conçue pour lire et écrire des données depuis et vers des fichiers et le réseau. DataView opère sur les données avec une tendance spécifiée. L'expérimentateur, petit ou grand, doit être spécifié pour chaque accès à chaque valeur, afin d'obtenir des résultats cohérents et corrects lors de la lecture ou de l'écriture de données binaires, quelle que soit l'envergure du processeur sur lequel le navigateur s'exécute.

En règle générale, lorsque votre application lit des données binaires sur un serveur, vous devez les analyser une seule fois afin de les convertir en structures de données que votre application utilise en interne. Vous devez utiliser DataView au cours de cette phase. Il est déconseillé d'utiliser les vues de tableaux typés à plusieurs octets (Int16Array, Uint16Array, etc.) directement avec les données récupérées via XMLHttpRequest, FileReader ou toute autre API d'entrée/sortie, car ces vues utilisent l'endianité native du processeur. Nous reviendrons sur ce point.

Voyons quelques exemples simples. Aux débuts de Windows, le format de fichier BMP de Windows était le format standard de stockage d'images. La documentation dont le lien figure ci-dessus indique clairement que toutes les valeurs entières du fichier sont stockées au format Little Endian. Voici un extrait de code qui analyse le début de l'en-tête BMP à l'aide de la bibliothèque DataStream.js associée à cet article:

function parseBMP(arrayBuffer) {
  var stream = new DataStream(arrayBuffer, 0,
    DataStream.LITTLE_ENDIAN);
  var header = stream.readUint8Array(2);
  var fileSize = stream.readUint32();
  // Skip the next two 16-bit integers
  stream.readUint16();
  stream.readUint16();
  var pixelOffset = stream.readUint32();
  // Now parse the DIB header
  var dibHeaderSize = stream.readUint32();
  var imageWidth = stream.readInt32();
  var imageHeight = stream.readInt32();
  // ...
}

Voici un autre exemple tiré de la démonstration du rendu High Dynamic Range dans le projet d'exemples WebGL. Cette démonstration télécharge des données brutes à virgule flottante de type petite endian, représentant des textures à plage dynamique élevée, et doit être importée sur WebGL. Voici l'extrait de code qui interprète correctement les valeurs à virgule flottante sur toutes les architectures de processeur. Supposons que la variable "arrayBuffer" soit un ArrayBuffer qui vient d'être téléchargé à partir du serveur via XMLHttpRequest:

var arrayBuffer = ...;
var data = new DataView(arrayBuffer);
var tempArray = new Float32Array(
  data.byteLength / Float32Array.BYTES_PER_ELEMENT);
var len = tempArray.length;
// Incoming data is raw floating point values
// with little-endian byte ordering.
for (var jj = 0; jj < len; ++jj) {
  tempArray[jj] =
    data.getFloat32(jj * Float32Array.BYTES_PER_ELEMENT, true);
}
gl.texImage2D(...other arguments...,
  gl.RGB, gl.FLOAT, tempArray);

La règle d'or est la suivante: lors de la réception de données binaires du serveur Web, effectuez une transmission avec un DataView. Lisez les valeurs numériques individuelles et stockez-les dans une autre structure de données : un objet JavaScript (pour de petites quantités de données structurées) ou une vue de tableau typé (pour de grands blocs de données). Cela garantira que votre code fonctionne correctement sur tous les types de processeurs. Utilisez également DataView pour écrire des données dans un fichier ou sur le réseau. Veillez à spécifier de manière appropriée l'argument littleEndian dans les différentes méthodes set afin de produire le format de fichier que vous créez ou utilisez.

N'oubliez pas que toutes les données qui transitent par le réseau ont implicitement un format et un sens de l'entier (du moins pour les valeurs multi-octets). Veillez à définir et documenter clairement le format de toutes les données que votre application envoie sur le réseau.

API de navigateur utilisant des tableaux typés

Je vais vous présenter brièvement les différentes API de navigateur qui utilisent actuellement les tableaux typés. Le recadrage actuel inclut WebGL, Canvas, API Web Audio, XMLHttpRequests, WebSockets, Web Workers, API Media Source et API File. D'après la liste des API, les tableaux typés sont bien adaptés aux opérations multimédias sensibles aux performances, ainsi qu'à la transmission efficace de données.

WebGL

La première utilisation des tableaux typés a eu lieu dans WebGL, où ils étaient utilisés pour transmettre des données de tampon et des données d'image. Pour définir le contenu d'un objet de tampon WebGL, utilisez l'appel gl.bufferData() avec un tableau typé.

var floatArray = new Float32Array([1,2,3,4,5,6,7,8]);
gl.bufferData(gl.ARRAY_BUFFER, floatArray);

Les tableaux typés sont également utilisés pour transmettre des données de texture. Voici un exemple de base de transmission de contenu de texture à l'aide d'un tableau typé.

var pixels = new Uint8Array(16*16*4); // 16x16 RGBA image
gl.texImage2D(
  gl.TEXTURE_2D, // target
  0, // mip level
  gl.RGBA, // internal format
  16, 16, // width and height
  0, // border
  gl.RGBA, //format
  gl.UNSIGNED_BYTE, // type
  pixels // texture data
);

Vous avez également besoin de Typed Array pour lire les pixels à partir du contexte WebGL.

var pixels = new Uint8Array(320*240*4); // 320x240 RGBA image
gl.readPixels(0, 0, 320, 240, gl.RGBA, gl.UNSIGNED_BYTE, pixels);

Canvas 2D

L'objet Canvas ImageData a récemment été conçu pour fonctionner avec la spécification Typed Arrays. Vous pouvez désormais obtenir une représentation sous forme de tableau typé des pixels d'un élément canevas. Cette fonctionnalité est utile, car vous pouvez désormais créer et modifier des tableaux de pixels de canevas sans avoir à manipuler l'élément canevas.

var imageData = ctx.getImageData(0,0, 200, 100);
var typedArray = imageData.data // data is a Uint8ClampedArray

XMLHttpRequest2

XMLHttpRequest a obtenu un boost de typed Array et vous pouvez désormais recevoir une réponse de typed Array au lieu d'avoir à analyser une chaîne JavaScript dans un Typed Array. C'est très pratique pour transmettre les données extraites directement aux API multimédias et pour analyser les fichiers binaires extraits du réseau.

Il vous suffit de définir le champ "responseType" de l'objet XMLHttpRequest sur "arraybuffer".

xhr.responseType = 'arraybuffer';

N'oubliez pas que vous devez être conscient des problèmes d'endianisme lorsque vous téléchargez des données à partir du réseau ! Reportez-vous à la section sur l'endianisme ci-dessus.

API File

FileReader peut lire le contenu des fichiers en tant que ArrayBuffer. Vous pouvez ensuite associer des vues de tableau typées et des DataViews au tampon pour manipuler son contenu.

reader.readAsArrayBuffer(file);

Vous devez également garder à l'esprit l'endianisme. Pour en savoir plus, consultez la section consacrée à l'endianisme.

Objets transférables

Les objets transférables dans postMessage permettent de transmettre des données binaires à d'autres fenêtres et workers beaucoup plus rapidement. Lorsque vous envoyez un objet à un worker en tant que "Transferable", l'objet devient inaccessible dans le thread d'envoi et le worker destinataire obtient la propriété de l'objet. Cela permet une implémentation très optimisée où les données envoyées ne sont pas copiées, seule la propriété du Typed Array est transférée au destinataire.

Pour utiliser des objets transférables avec des Web Workers, vous devez utiliser la méthode webkitPostMessage sur le worker. La méthode webkitPostMessage fonctionne comme postMessage, mais elle utilise deux arguments au lieu d'un seul. Le deuxième argument ajouté est un tableau d'objets que vous souhaitez transférer au worker.

worker.webkitPostMessage(oneGBTypedArray, [oneGBTypedArray]);

Pour récupérer les objets auprès du nœud de calcul, celui-ci peut les transmettre au thread principal de la même manière.

webkitPostMessage({results: grand, youCanHaveThisBack: oneGBTypedArray}, [oneGBTypedArray]);

Aucune copie, waouh !

API Media Source

Récemment, les éléments multimédias ont également obtenu des avantages sous la forme de l'API Media Source. Vous pouvez transmettre directement un tableau Typed Array contenant des données vidéo à un élément vidéo à l'aide de webkitSourceAjouter. L'élément vidéo ajoute ainsi les données vidéo après la vidéo existante. SourceAjouter est idéal pour créer des interstitiels, des playlists, des diffusions en direct et d'autres utilisations qui nécessitent de lire plusieurs vidéos à l'aide d'un seul élément vidéo.

video.webkitSourceAppend(uint8Array);

WebSockets binaires

Vous pouvez également utiliser des tableaux typés avec WebSockets pour éviter d'avoir à chaîner toutes vos données. Idéal pour écrire des protocoles efficaces et minimiser le trafic réseau.

socket.binaryType = 'arraybuffer';

Ouf ! Voilà qui conclut l'examen de l'API. Passons à présent aux bibliothèques tierces pour gérer les tableaux typés.

Bibliothèques tierces

jDataView

jDataView implémente un shim DataView pour tous les navigateurs. DataView était auparavant une fonctionnalité WebKit uniquement, mais elle est désormais prise en charge par la plupart des autres navigateurs. L'équipe de développeurs Mozilla est en train de publier un correctif permettant d'activer DataView dans Firefox.

Eric Bidelman, membre de l'équipe Chrome Developer Relations, a rédigé un petit exemple de lecteur de tags MP3 ID3 utilisant jDataView. Voici un exemple d'utilisation tiré de l'article de blog:

var dv = new jDataView(arraybuffer);

// "TAG" starts at byte -128 from EOF.
// See http://en.wikipedia.org/wiki/ID3
if (dv.getString(3, dv.byteLength - 128) == 'TAG') {
  var title = dv.getString(30, dv.tell());
  var artist = dv.getString(30, dv.tell());
  var album = dv.getString(30, dv.tell());
  var year = dv.getString(4, dv.tell());
} else {
  // no ID3v1 data found.
}

stringencoding

L'utilisation de chaînes dans des tableaux typés est un peu pénible pour le moment, mais la bibliothèque stringencoding peut vous y aider. Le codage Stringencoding met en œuvre la spécification d'encodage de chaîne Typed Array proposée. Il s'agit donc également d'un bon moyen de se faire une idée de ce qui va suivre.

Voici un exemple d'utilisation de base de l'encodage de chaîne:

var uint8array = new TextEncoder(encoding).encode(string);
var string = new TextDecoder(encoding).decode(uint8array);

BitView.js

J'ai écrit une petite bibliothèque de manipulation de bits pour les tableaux typés appelée BitView.js. Comme son nom l'indique, elle fonctionne un peu comme la DataView, à la différence qu'elle fonctionne avec des bits. Avec BitView, vous pouvez obtenir et définir la valeur d'un bit à un décalage de bits donné dans un ArrayBuffer. BitView propose également des méthodes pour stocker et charger des entiers 6 bits et 12 bits à des décalages de bits arbitraires.

Les entiers 12 bits sont utiles pour travailler avec des coordonnées d'écran, car les écrans ont tendance à avoir moins de 4 096 pixels dans les dimensions les plus longues. En utilisant des entiers 12 bits au lieu de 32 bits, vous obtenez une réduction de 62% de la taille. Pour un exemple plus extrême, je travaillais avec des fichiers de forme qui utilisent des floats 64 bits pour les coordonnées, mais je n'avais pas besoin de précision car le modèle n'était affiché qu'à la taille de l'écran. Le passage à des coordonnées de base de 12 bits avec des deltas de 6 bits pour encoder les modifications de la coordonnée précédente a réduit la taille du fichier à un dixième. Pour visionner la démonstration, cliquez ici.

Voici un exemple d'utilisation de BitView.js:

var bv = new BitView(arrayBuffer);
bv.setBit(4, 1); // Set fourth bit of arrayBuffer to 1.
bv.getBit(17); // Get 17th bit of arrayBuffer.

bv.getBit(50*8 + 3); // Get third bit of 50th byte in arrayBuffer.

bv.setInt6(3, 18); // Write 18 as a 6-bit int to bit position 3 in arrayBuffer.
bv.getInt12(9); // Read a 12-bit int from bit position 9 in arrayBuffer.

DataStream.js

L'un des aspects les plus intéressants des tableaux typés est qu'ils facilitent le traitement des fichiers binaires en JavaScript. Au lieu d'analyser une chaîne caractère par caractère et de convertir manuellement les caractères en nombres binaires, vous pouvez désormais obtenir un ArrayBuffer avec XMLHttpRequest et le traiter directement à l'aide d'un DataView. Cela facilite, par exemple, le chargement d'un fichier MP3 et la lecture des balises de métadonnées à utiliser dans votre lecteur audio. ou charger un fichier de forme et le transformer en modèle WebGL. Vous pouvez également lire les tags EXIF d'un fichier JPEG et les afficher dans votre application de diaporama.

Le problème avec les requêtes XHR ArrayBuffer est que la lecture de données de type struct à partir du tampon est un peu pénible. DataView permet de lire quelques nombres à la fois en toute sécurité en mode endian. Les vues de tableau typés sont idéales pour lire des tableaux de nombres endian natifs en fonction de la taille de l'élément. Il nous manquait un moyen de lire les tableaux et les structs de données d'une manière pratique, adaptée à endian. Saisissez DataStream.js.

DataStream.js est une bibliothèque de tableaux typés qui lit et écrit des scalaires, des chaînes, des tableaux et des structures de données de ArrayBuffers dans un fichier.

Exemple de lecture dans un tableau de valeurs flottantes à partir d'un ArrayBuffer:

// without DataStream.js
var dv = new DataView(buffer);
var f32 = new Float32Array(buffer.byteLength / 4);
var littleEndian = true;
for (var i = 0; i<f32.length; i++) {
  f32[i] = dv.getFloat32(i*4, littleEndian);
}

// with DataStream.js
var ds = new DataStream(buffer);
ds.endianness = DataStream.LITTLE_ENDIAN;
var f32 = ds.readFloat32Array(ds.byteLength / 4);

DataStream.js s'avère vraiment utile pour la lecture de données plus complexes. Supposons que vous disposiez d'une méthode qui lit les repères JPEG:

// without DataStream.js
var dv = new DataView(buffer);
var objs = [];
for (var i=0; i<buffer.byteLength;) {
  var obj = {};
  obj.tag = dv.getUint16(i);
  i += 2;
  obj.length = dv.getUint16(i);
  i += 2;
  obj.data = new Uint8Array(obj.length - 2);
  for (var j=0; j<obj.data.length; j++,i++) {
    obj.data[j] = dv.getUint8(i);
  }
  objs.push(obj);
}

// with DataStream.js
var ds = new DataStream(buffer);
ds.endianness = ds.BIG_ENDIAN;
var objs = [];
while (!ds.isEof()) {
  var obj = {};
  obj.tag = ds.readUint16();
  obj.length = ds.readUint16();
  obj.data = ds.readUint8Array(obj.length - 2);
  objs.push(obj);
}

Vous pouvez également utiliser la méthode DataStream.readStruct pour lire les structs de données. La méthode readStruct reçoit un tableau de définition de structure contenant les types des membres de structure. Il dispose de fonctions de rappel pour gérer les types complexes, ainsi que les tableaux de données et les structs imbriqués:

// with DataStream.readStruct
ds.readStruct([
  'objs', ['[]', [ // objs: array of tag,length,data structs
    'tag', 'uint16',
    'length', 'uint16',
    'data', ['[]', 'uint8', function(s,ds){ return s.length - 2; }], // get length with a function
  '*'] // read in as many struct as there are
]);

Comme vous pouvez le voir, la définition d'une structure est un tableau plat de paires [nom, type]. Les structs imbriqués se font en disposant d'un tableau pour le type. Les tableaux sont définis à l'aide d'un tableau à trois éléments, où le deuxième élément est de type tableau et le troisième élément est la longueur du tableau (soit en tant que nombre, en tant que référence à un champ précédemment lu ou en tant que fonction de rappel). Le premier élément de la définition du tableau n'est pas utilisé.

Les valeurs possibles pour le type sont les suivantes:

Number types

Unsuffixed number types use DataStream endianness.
To explicitly specify endianness, suffix the type with
'le' for little-endian or 'be' for big-endian,
e.g. 'int32be' for big-endian int32.

  'uint8' -- 8-bit unsigned int
  'uint16' -- 16-bit unsigned int
  'uint32' -- 32-bit unsigned int
  'int8' -- 8-bit int
  'int16' -- 16-bit int
  'int32' -- 32-bit int
  'float32' -- 32-bit float
  'float64' -- 64-bit float

String types

  'cstring' -- ASCII string terminated by a zero byte.
  'string:N' -- ASCII string of length N.
  'string,CHARSET:N' -- String of byteLength N encoded with given CHARSET.
  'u16string:N' -- UCS-2 string of length N in DataStream endianness.
  'u16stringle:N' -- UCS-2 string of length N in little-endian.
  'u16stringbe:N' -- UCS-2 string of length N in big-endian.

Complex types

  [name, type, name_2, type_2, ..., name_N, type_N] -- Struct

  function(dataStream, struct) {} -- Callback function to read and return data.

  {get: function(dataStream, struct) {}, set: function(dataStream, struct) {}}
  -- Getter/setter functions to reading and writing data. Handy for using the
     same struct definition for both reading and writing.

  ['', type, length] -- Array of given type and length. The length can be either
                        a number, a string that references a previously-read
                        field, or a callback function(struct, dataStream, type){}.
                        If length is set to '*', elements are read from the
                        DataStream until a read fails.

Pour consulter un exemple en direct de lecture de métadonnées JPEG, cliquez ici. La démonstration utilise DataStream.js pour lire la structure au niveau des balises du fichier JPEG (ainsi qu'une analyse EXIF), et jpg.js pour décoder et afficher l'image JPEG en JavaScript.

Historique des tableaux typés

Les tableaux typés ont fait leur apparition au début de l'implémentation de WebGL, lorsque nous avons constaté que la transmission de tableaux JavaScript au pilote graphique entraînait des problèmes de performances. Avec les tableaux JavaScript, la liaison WebGL devait allouer un tableau natif et le remplir en parcourant le tableau JavaScript et en caster chaque objet JavaScript du tableau sur le type natif requis.

Pour corriger le goulot d'étranglement lors de la conversion des données, Vladimir Vukicevic de Mozilla a écrit CanvasFloatArray: un tableau de valeurs flottantes de style C doté d'une interface JavaScript. Vous pouvez maintenant modifier le CanvasFloatArray en JavaScript et le transmettre directement à WebGL sans avoir à effectuer de travail supplémentaire dans la liaison. Dans d'autres itérations, CanvasFloatArray a été renommé WebGLFloatArray, puis s'appelle Float32Array et divisé en un ArrayBuffer de sauvegarde et un Float32Array-view typé pour accéder au tampon. Des types ont également été ajoutés pour d'autres tailles entières et à virgule flottante et variantes signées/non signées.

Considérations de conception

Dès le départ, la conception des tableaux typés a été motivée par la nécessité de transmettre efficacement des données binaires aux bibliothèques natives. Pour cette raison, les vues de tableau typés opèrent sur des données alignées dans l'endianisme natif du processeur hôte. Ces décisions permettent à JavaScript d'atteindre des performances maximales lors d'opérations telles que l'envoi de données de sommet à la carte graphique.

DataView est spécialement conçu pour les E/S de fichiers et de réseau, où les données ont toujours une finalité spécifiée et peuvent ne pas être alignées pour des performances optimales.

La division de la conception entre l'assemblage de données en mémoire (à l'aide des vues de tableau typées) et les E/S (à l'aide de DataView) était consciente. Les moteurs JavaScript modernes optimisent considérablement les vues de tableau typées et atteignent des performances élevées pour les opérations numériques. Les niveaux de performances actuels des vues de tableaux typés ont été rendus possibles grâce à cette décision de conception.

Références