Tablice z określonymi danymi – dane binarne w przeglądarce.

Ilmari Heikkinen

Wprowadzenie

Typowane tablice to stosunkowo niedawne uzupełnienie przeglądarek, które powstało z potrzeby efektywnego przetwarzania danych binarnych w WebGL. Tablicowa tablica to blok pamięci z typowym widokiem, podobnie jak tablice w języku C. Typowe tablice są obsługiwane przez nieprzetworzoną pamięć, więc silnik JavaScript może przekazywać pamięć bezpośrednio do natywnych bibliotek bez konieczności żmudnego konwertowania danych na natywną reprezentację. W rezultacie tablice typowane działają znacznie lepiej niż tablice JavaScriptu w przypadku przekazywania danych do WebGL i innych interfejsów API obsługujących dane binarne.

Widoki tablic typów działają jak tablice jednotypowe w przypadku segmentu ArrayBuffer. Istnieją widoki dla wszystkich typów liczbowych, które mają nazwy opisowe, np. Float32Array, Float64Array, Int32Array i Uint8Array. Dostępny jest też specjalny widok, który zastąpił typ tablicy pikseli w danych obrazu Canvas: Uint8ClampedArray.

Widok danych to drugi typ widoku, który służy do obsługi danych niejednorodnych. Zamiast interfejsu przypominającego tablicę obiekt DataView udostępnia interfejs get/set do odczytu i zapisu dowolnych typów danych w dowolnych przesunięciach bajtów. Narzędzie DataView doskonale sprawdza się do odczytywania i zapisywania nagłówków plików oraz innych podobnych danych strukturalnych.

Podstawy korzystania z tablic z typem

Widoki tablic z typem

Aby używać tablic typowanych, musisz utworzyć tablicę ArrayBuffer i widok dla niej. Najprostszym sposobem jest utworzenie widoku tablicy typowanej o pożądanym rozmiarze i typie.

// 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];

Istnieje kilka różnych typów widoków tablic typowanych. Wszystkie korzystają z tego samego interfejsu API, więc gdy już nauczysz się go używać, będziesz wiedzieć, jak korzystać z nich wszystkich. W następnym przykładzie utworzymy po jednym z każdego z obecnie istniejących widoków tablicy typu.

// 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);

Ostatni z nich jest nieco szczególny, ponieważ ogranicza wartości wejściowe do zakresu 0–255. Jest to szczególnie przydatne w przypadku algorytmów przetwarzania obrazów w usłudze Canvas, ponieważ teraz nie musisz ręcznie ograniczać wartości liczbowych w celu uniknięcia przepełnienia zakresu 8-bitowego.

Oto przykład zastosowania współczynnika gamma do obrazu zapisanego w tablicy Uint8Array. Niezbyt ładnie:

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

Dzięki Uint8ClampedArray możesz pominąć ręczne ograniczanie:

pixels[i] *= gamma;

Innym sposobem tworzenia widoków tablic typowanych jest najpierw utworzenie tablicy ArrayBuffer, a potem widoków, które do niej odwołują. Interfejsy API, które pobierają dane zewnętrzne, zwykle działają z użyciem tablic ArrayBuffers, więc w taki sposób możesz uzyskać widok tablicy typów.

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);

Możesz też mieć kilka widoków tego samego ArrayBuffer.

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

Aby skopiować tablicę typów do innej tablicy typów, najszybszym sposobem jest użycie metody tablicy typów. Aby uzyskać działanie podobne do memcpy, utwórz tablice Uint8 do buforów widoków i użyj zestawu do kopiowania danych.

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

Aby używać tablic ArrayBuffer zawierających dane o różnych typach, najłatwiej jest użyć do bufora widoku danych. Załóżmy, że mamy format pliku z nagłówkiem z 8-bitową liczbą bez znaku, po której następują 2 16-bitowe liczby całkowite i tablica danych o 32-bitowych liczbach zmiennoprzecinkowych. Odczytywanie tego z typowanymi widokami tablic jest możliwe, ale trochę kłopotliwe. Dzięki widokowi danych możemy odczytać nagłówek i użyć widoku tablicy typowanej dla tablicy typu 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);
}

W powyższym przykładzie wszystkie odczytane wartości są w porządku wielkim. Jeśli wartości w buforze są w postaci little-endian, możesz przekazać parametr littleEndian do metody getter:

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

Pamiętaj, że widoki tablic typowanych są zawsze w rodzimej kolejności bajtów. Ma to na celu przyspieszenie ich działania. Do odczytu i zapisu danych, w przypadku których problemem może być endianność, należy użyć widoku danych.

Widok danych ma też metody zapisywania wartości do buforów. Te metody ustawiania mają nazwy takie same jak metody pobierania, czyli „set” z dodatkiem typu danych.

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

Dyskusja na temat endianności

Endianność, czyli kolejność bajtów, to kolejność, w jakiej liczby wielobajtowe są przechowywane w pamięci komputera. Termin big-endian określa architekturę procesora, która przechowuje najpierw najbardziej znaczący bajt, a little-endian – najpierw najmniej znaczący bajt. Który endianness jest używany w danej architekturze procesora, jest całkowicie dowolny; istnieją dobre powody, aby wybrać jedną z tych opcji. Niektóre procesory można skonfigurować tak, aby obsługiwały dane w układzie dużych i małych bajtów.

Dlaczego trzeba zwracać uwagę na endianność? Powód jest prosty. Podczas odczytu lub zapisu danych z dysku lub sieci należy określić endian danych. Dzięki temu dane są interpretowane prawidłowo, niezależnie od endianności procesora, z którym współpracują. W naszym coraz bardziej sieciowym świecie ważne jest, aby prawidłowo obsługiwać wszystkie rodzaje urządzeń, big-endian lub little-endian, które mogą potrzebować obsługi danych binarnych pochodzących z serwerów lub innych urządzeń w sieci.

Interfejs DataView został zaprojektowany specjalnie do odczytu i zapisu danych do i z plików oraz sieci. DataView działa na danych o określonym układzie bajtów. Wielkość i mała endianność muszą być określone przy każdym dostępie do każdej wartości, aby zapewnić spójne i prawidłowe wyniki podczas odczytu lub zapisu danych binarnych, niezależnie od wielkości i małej endianności procesora, na którym działa przeglądarka.

Gdy aplikacja odczytuje dane binarne z serwera, musi je zwykle przeskanować, aby przekonwertować je na struktury danych, których używa wewnętrznie. Na tym etapie należy używać widoku danych. Nie zalecamy używania widoków tablic typowych o wielu bajtach (np. Int16Array, Uint16Array) bezpośrednio z danymi pobieranymi za pomocą XMLHttpRequest, FileReader lub dowolnego innego interfejsu API wejścia/wyjścia, ponieważ widoki tablic typowych używają natywnej kolejności bajtów procesora. Więcej informacji na ten temat znajdziesz później.

Przyjrzyjmy się kilku prostym przykładom. W początkach systemu Windows format pliku Windows BMP był standardowym formatem przechowywania obrazów. W powiązanej wyżej dokumentacji wyraźnie zaznaczono, że wszystkie wartości całkowite w pliku są przechowywane w formacie little-endian. Oto fragment kodu, który analizuje początek nagłówka BMP za pomocą biblioteki DataStream.js dołączonej do tego artykułu:

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();
  // ...
}

Oto kolejny przykład, tym razem z demo renderowania w zakresie HDRprojekcie przykładów WebGL. Ta prezentacja pobiera dane w postaci nieprzetworzonych danych zmiennoprzecinkowych w postaci little-endian, które reprezentują tekstury o wysokim zakresie dynamicznym, i musi przesłać je do WebGL. Oto fragment kodu, który prawidłowo interpretuje wartości zmiennoprzecinkowe na wszystkich architekturach procesorów. Załóżmy, że zmienna „arrayBuffer” to tablica ArrayBuffer, która została właśnie pobrana z serwera za pomocą 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);

Zasada jest taka, że po otrzymaniu danych binarnych z serwera internetowego należy je przejrzeć za pomocą DataView. Przeczytaj poszczególne wartości liczbowe i przechowuj je w innej strukturze danych, np. w obiekcie JavaScriptu (w przypadku niewielkich ilości uporządkowanych danych) lub w widoku tablicy typowanej (w przypadku dużych bloków danych). Dzięki temu Twój kod będzie działać prawidłowo na wszystkich procesorach. Używaj też DataView do zapisywania danych w pliku lub sieci. Pamiętaj, aby odpowiednio określić argument littleEndian w różnych metodach set, aby uzyskać format pliku, który tworzysz lub używasz.

Pamiętaj, że wszystkie dane przesyłane przez sieć mają domyślnie określony format i sposób kodowania bajtów (przynajmniej w przypadku wartości wielobajtowych). Dokładnie zdefiniuj i zapisz format wszystkich danych przesyłanych przez aplikację przez sieć.

Interfejsy API przeglądarki korzystające z tablic z typem

Przedstawię Ci krótki przegląd różnych interfejsów API przeglądarki, które obecnie korzystają z tablic typowanych. Obecne ograniczenie dotyczy interfejsów WebGL, Canvas, Web Audio API, XMLHttpRequests, WebSockets, Web Workers, Media Source API i File API. Z listy interfejsów API wynika, że tablice typowane są odpowiednie do pracy z multimediami, w których liczy się wydajność, a także do efektywnego przekazywania danych.

WebGL

Typowe tablice zostały po raz pierwszy użyte w WebGL, gdzie służą do przekazywania danych bufora i danych obrazu. Aby ustawić zawartość obiektu bufora WebGL, użyj wywołania gl.bufferData() z tablicą typowaną.

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

Typowe tablice są też używane do przekazywania danych tekstury. Oto podstawowy przykład przekazywania zawartości tekstury za pomocą tablicy typowanej.

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
);

Aby odczytać piksele z kontekstu WebGL, musisz też użyć tablic typowanych.

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

Canvas 2D

Niedawno obiekt Canvas ImageData został dostosowany do specyfikacji tablic typowych. Teraz możesz uzyskać tablice typowych pikseli w elemencie canvas. Jest to przydatne, ponieważ teraz możesz też tworzyć i edytować tablice pikseli odbitki na płótnie bez konieczności manipulowania elementem odbitki.

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

XMLHttpRequest2

Interfejs XMLHttpRequest został ulepszony o typ tablicy Typed Array, dzięki czemu możesz teraz otrzymywać odpowiedzi w tym formacie zamiast analizować ciąg znaków JavaScriptu w typie Typed Array. Jest to bardzo przydatne rozwiązanie do przekazywania pozyskanych danych bezpośrednio do interfejsów multimedialnych API oraz do analizowania plików binarnych pozyskanych z sieci.

Wystarczy, że ustawisz responseType obiektu XMLHttpRequest na „arraybuffer”.

xhr.responseType = 'arraybuffer';

Pamiętaj, że podczas pobierania danych z sieci musisz mieć świadomość problemów związanych z endianem. Zobacz sekcję o orientacji danych powyżej.

Interfejsy API plików

Interfejs FileReader może odczytać zawartość pliku jako tablicę ArrayBuffer. Następnie możesz dołączyć do bufora widoki tablic i widoków danych, aby manipulować ich zawartością.

reader.readAsArrayBuffer(file);

Tutaj też należy pamiętać o orientacji bajtów. Szczegółowe informacje znajdziesz w sekcji dotyczącej endianności.

Przenośne obiekty

Przenośne obiekty w postMessage znacznie przyspieszają przekazywanie danych binarnych do innych okien i elementów Web Worker. Gdy wysyłasz obiekt do pracownika jako obiekt możliwy do przeniesienia, staje się on niedostępny w wątku wysyłania, a pracownik, który go odbiera, staje się jego właścicielem. Umożliwia to bardzo zoptymalizowane wdrożenie, w którym wysłane dane nie są kopiowane, a jedynie własność tablicy typów jest przenoszona do odbiorcy.

Aby używać obiektów przenośnych z procesami webowymi, musisz użyć metody webkitPostMessage w procesie. Metoda webkitPostMessage działa tak samo jak postMessage, ale zamiast jednego argumentu przyjmuje 2 argumenty. Dodany drugi argument to tablica obiektów, które chcesz przekazać do workera.

worker.webkitPostMessage(oneGBTypedArray, [oneGBTypedArray]);

Aby odzyskać obiekty z instancji roboczej, ta może przekazać je z powrotem do głównego wątku w taki sam sposób.

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

Zero kopii, super!

Interfejs Media Source API

Niedawno elementy multimedialne zostały wzbogacone o tablice typów w postaci interfejsu Media Source API. Za pomocą metody webkitSourceAppend możesz bezpośrednio przekazywać tablicę Typed Array zawierającą dane wideo do elementu wideo. Dzięki temu element wideo dołączy dane wideo po istniejącym filmie. Funkcja SourceAppend jest świetna do tworzenia reklam pośrednich, playlist, transmisji strumieniowych i innych zastosowań, w których chcesz odtwarzać kilka filmów za pomocą jednego elementu wideo.

video.webkitSourceAppend(uint8Array);

Binarne WebSockets

Aby uniknąć konieczności konwertowania wszystkich danych na ciągi znaków, możesz też używać tablic typowanych z WebSockets. Jest ona przydatna do tworzenia wydajnych protokołów i minimalizowania ruchu w sieci.

socket.binaryType = 'arraybuffer';

Uff! To już koniec sprawdzania interfejsu API. Przyjrzyjmy się teraz bibliotekom innych firm, które obsługują tablice typów.

Biblioteki innych firm

jDataView

jDataView implementuje element DataView shim dla wszystkich przeglądarek. Funkcja DataView była kiedyś dostępna tylko w WebKit, ale teraz obsługuje ją większość innych przeglądarek. Zespół deweloperów Mozilli pracuje nad łatką, która umożliwi też korzystanie z DataView w Firefox.

Eric Bidelman z zespołu ds. relacji z deweloperami Chrome opracował małe przykładowe rozwiązanie do odczytu tagów MP3 ID3, które korzysta z jDataView. Oto przykład użycia z postu na blogu:

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

Praca z ciągłymi w typowych tablicach jest obecnie nieco uciążliwa, ale można skorzystać z biblioteki do kodowania i dekodowania napisów. Stringencoding wdraża proponowaną specyfikację kodowania ciągów znaków w tablicy typowanej, więc jest to też dobry sposób na zapoznanie się z przyszłością.

Oto przykład podstawowego użycia kodowania ciągu znaków:

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

BitView.js

Napisałem małą bibliotekę do manipulowania bitami w typowanych tablicach o nazwie BitView.js. Jak sama nazwa wskazuje, działa ona podobnie jak widok danych, ale z bitami. Za pomocą interfejsu BitView możesz pobierać i ustawiać wartość bitu w danym przesunięciu bitowym w tablicy ArrayBuffer. BitView zawiera też metody przechowywania i wczytywania 6- i 12-bitowych liczb całkowitych w dowolnych przesunięciach bitowych.

12-bitowe liczby całkowite są przydatne do pracy z koordynatami ekranu, ponieważ wyświetlacze mają zwykle mniej niż 4096 pikseli na dłuższym boku. Korzystanie z 12-bitowych liczb całkowitych zamiast 32-bitowych powoduje zmniejszenie rozmiaru o 62%. W bardziej skrajnym przykładzie pracowałem z plikami Shape, które używają 64-bitowych liczb zmiennoprzecinkowych do współrzędnych, ale nie potrzebowałem takiej dokładności, ponieważ model miał być wyświetlany tylko w rozmiarach ekranu. Przejście na 12-bitowe współrzędne podstawowe z 6-bitowymi różnicami w celu zakodowania zmian w porównaniu z poprzednimi współrzędnymi spowodowało zmniejszenie rozmiaru pliku o 10%. Demo tej funkcji znajdziesz tutaj.

Oto przykład użycia biblioteki 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

Jedną z najbardziej ekscytujących rzeczy związanych z tablicami typowanymi jest to, że ułatwiają one obsługę plików binarnych w JavaScript. Zamiast analizować ciąg znak po znaku i ręcznie konwertować znaki na liczby binarne, możesz teraz uzyskać ArrayBuffer za pomocą XMLHttpRequest i bezpośrednio przetworzyć go za pomocą DataView. Dzięki temu możesz np. wczytać pliki MP3 i czytać tagi metadanych, aby używać ich w odtwarzaczu audio. Możesz też załadować plik shape i przekształcić go w model WebGL. Możesz też odczytać tagi EXIF z pliku JPEG i wyświetlić je w aplikacji do pokazu slajdów.

Problem z ArrayBuffer XHR polega na tym, że odczytywanie danych typu struct z bufora jest nieco uciążliwe. Widok danych jest przydatny do odczytu kilku liczb naraz w bezpiecznym formacie endian, a typowane widoki tablic są przydatne do odczytu tablic liczb w natywnym formacie endian z wyrównaniem do rozmiaru elementu. Brakowało nam sposobu na odczytywanie tablic i struktur danych w wygodny sposób, który nie zależy od endianności. Wpisz DataStream.js.

DataStream.js to biblioteka tablic typowych, która odczytuje i zapisuje skalary, ciągi znaków, tablice i struktury danych z ArrayBuffers w sposób podobny do pliku.

Przykład odczytu tablicy liczb zmiennoprzecinkowych z 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);

Biblioteka DataStream.js jest szczególnie przydatna przy odczytywaniu bardziej złożonych danych. Załóżmy, że masz metodę, która odczytuje znaczniki 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);
}

Możesz też użyć metody DataStream.readStruct, aby odczytać struktury danych. Metoda readStruct przyjmuje tablicę definicji struktury zawierającą typy elementów struktury. Zawiera funkcje wywołania zwrotnego do obsługi złożonych typów oraz obsługuje tablice danych i zagnieżdżone struktury:

// 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
]);

Jak widać, definicja struktury to płaska tablica par [nazwa, typ]. Struktury zagnieżdżone są tworzone przez tablicę typu. Tablice są definiowane za pomocą tablicy o 3 elementach, w której 2. element jest typem elementu tablicy, a 3. element jest długością tablicy (jako liczba, odwołanie do wcześniej odczytanego pola lub funkcja wywołania zwrotnego). Pierwszy element definicji tablicy jest nieużywany.

Możliwe wartości typu:

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.

Przykład odczytu metadanych JPEG znajdziesz tutaj. W tym przykładzie do odczytu struktury na poziomie tagów pliku JPEG (oraz do częściowego parsowania EXIF) używana jest biblioteka DataStream.js, a do dekodowania i wyświetlania obrazu JPEG w JavaScriptie – biblioteka jpg.js.

Historia tablic z typem

Tablice typów pojawiły się na wczesnym etapie wdrażania WebGL, gdy odkryliśmy, że przekazywanie tablic JavaScript do sterownika grafiki powoduje problemy z wydajnością. W przypadku tablic JavaScript musiano przydzielić tablicę natywną i wypełnić ją, przechodząc po tablicy JavaScript i przekształcając każdy obiekt JavaScript w tablicy w wymagany typ natywny.

Aby rozwiązać problem z wąskim gardłem w konwersji danych, Vladimir Vukicevic z Mozilla napisał bibliotekę CanvasFloatArray: tablicę typu float w stylu C z interfejsem JavaScript. Teraz możesz edytować tablicę CanvasFloat w JavaScript i przekazywać ją bezpośrednio do WebGL bez konieczności wykonywania dodatkowych czynności w ramach powiązania. W kolejnych iteracjach nazwa CanvasFloatArray została zmieniona na WebGLFloatArray, a następnie na Float32Array i podzielona na ArrayBuffer i typowany widok Float32Array, aby uzyskać dostęp do bufora. Dodaliśmy też typy dla innych rozmiarów liczb całkowitych i zmiennoprzecinkowych oraz dla wersji z wartością dodatnią i ujemną.

Uwagi dotyczące projektowania

Od samego początku projektowanie tablic typowanych był podyktowany potrzebą efektywnego przekazywania danych binarnych do bibliotek natywnych. Z tego powodu widoki tablic typu działają na danych wyrównanych w rodzimej kolejności bajtów procesora hosta. Dzięki tym decyzjom JavaScript może osiągnąć maksymalną wydajność podczas operacji takich jak wysyłanie danych wierzchołków do karty graficznej.

Biblioteka DataView została zaprojektowana specjalnie do obsługi wejść/wyjść plików i sieci, gdzie dane mają zawsze określoną kolejność bajtów i mogą nie być wyrównane pod kątem maksymalnej wydajności.

Podział projektu na składanie danych w pamięci (z wykorzystaniem widoków tablic typowanych) i wejście/wyjście (z wykorzystaniem widoku danych) był świadomy. Nowoczesne silniki JavaScriptu znacznie optymalizują widoki tablic typowych i osiągają wysoką wydajność w przypadku operacji numerycznych z ich użyciem. Ta decyzja projektowa umożliwiła osiągnięcie obecnego poziomu wydajności widoków tablic typu.

Odniesienia