Binary data и типизированные массивы

В основе высокопроизводительной визуализации в WebGL лежит минимизация затрат на передачу данных между JavaScript и GPU. Традиционные структуры на основе объектов JavaScript оказываются слишком тяжёлыми: они требуют сериализации, занимают больше памяти и приводят к лишним преобразованиям при подготовке атрибутов вершин. В этом контексте бинарные данные и типизированные массивы становятся ключевым механизмом эффективной работы.

Deck.gl изначально проектировался с расчётом на работу с большими наборами геопространственных данных, поэтому его внутренняя модель данных тесно связана с ArrayBuffer и TypedArray. Основная цель — хранить и передавать данные в виде непрерывных блоков памяти, которые напрямую могут быть использованы WebGL-буферами.


ArrayBuffer как фундамент хранения данных

ArrayBuffer представляет собой фиксированный блок памяти, который не содержит логики интерпретации. Он лишь описывает «сырой» участок памяти, а интерпретация задаётся через представления:

  • Float32Array
  • Uint8Array
  • Uint32Array
  • Int16Array

и другие типизированные массивы.

В Deck.gl именно ArrayBuffer используется как контейнер для хранения вершинных атрибутов, таких как координаты, цвета, индексы и дополнительные пользовательские свойства.

Ключевая особенность:

  • один и тот же ArrayBuffer может иметь несколько представлений
  • данные не копируются при создании view
  • обеспечивается минимальный overhead при доступе

Типизированные массивы и их роль в WebGL-пайплайне

WebGL работает с буферами GPU, которые ожидают данные в строго определённом формате. Например:

  • позиции вершин: Float32Array
  • цвета: Uint8Array или нормализованный Uint8Array
  • индексы: Uint16Array или Uint32Array

Deck.gl использует типизированные массивы как промежуточный слой между JavaScript и GPU.

Типичный поток данных:

  1. пользователь предоставляет высокоуровневый data array (объекты или GeoJSON)
  2. Deck.gl трансформирует данные в бинарный формат
  3. создаются TypedArray буферы
  4. буферы передаются в WebGL через bufferData

Преобразование объектов в бинарные атрибуты

Наиболее затратная операция — конвертация структурированных объектов в плоские массивы.

Пример исходных данных:

[
  { position: [10, 20], value: 5, color: [255, 0, 0] },
  { position: [15, 25], value: 10, color: [0, 255, 0] }
]

В бинарном представлении Deck.gl формирует несколько параллельных буферов:

  • positions: [10, 20, 15, 25]Float32Array
  • values: [5, 10]Float32Array или Int32Array
  • colors: [255, 0, 0, 0, 255, 0]Uint8Array

Такой подход называется structure-of-arrays (SoA) и является предпочтительным для GPU, в отличие от array-of-structures (AoS).


Structure of Arrays против Array of Structures

Array of Structures (AoS)

[
  {x: 1, y: 2},
  {x: 3, y: 4}
]

Проблема:

  • данные разрознены в памяти
  • невозможна эффективная векторизация
  • лишние обращения к памяти CPU

Structure of Arrays (SoA)

x = [1, 3]
y = [2, 4]

Преимущества:

  • последовательное хранение
  • высокая кеш-локальность
  • прямое отображение на GPU-атрибуты

Deck.gl внутренне стремится к SoA при любой трансформации данных.


AttributeManager и генерация бинарных буферов

Внутренний механизм Deck.gl для управления атрибутами основан на AttributeManager. Он отвечает за:

  • создание TypedArray буферов
  • обновление данных при изменении props
  • минимизацию перерасчётов
  • ленивую генерацию атрибутов

Каждый слой (Layer) описывает, какие атрибуты ему нужны:

  • getPosition
  • getColor
  • getElevation
  • пользовательские атрибуты

AttributeManager вызывает соответствующие функции и заполняет бинарные буферы.


Прямое использование TypedArray в пользовательских слоях

Deck.gl позволяет напрямую возвращать бинарные данные из accessor-функций, минуя промежуточную обработку.

Пример:

getPosition: d => new Float32Array([d.x, d.y, d.z])

Однако более эффективный вариант:

getPosition: (d, {index, target}) => {
  target[0] = d.x;
  target[1] = d.y;
  target[2] = d.z;
}

Здесь target — заранее выделенный участок TypedArray. Это позволяет:

  • избежать создания новых массивов
  • снизить нагрузку на GC
  • повысить throughput при больших наборах данных

Packed attributes и межлэйаерная оптимизация

Deck.gl использует концепцию packed attributes, когда несколько логически разных значений упаковываются в один буфер.

Пример:

  • position (x, y, z)
  • intensity
  • category

Могут храниться как:

[x, y, z, intensity, category, x, y, z, intensity, category, ...]

или раздельно, в зависимости от оптимизации слоя.

Packed формат снижает количество WebGL buffer bindings, но требует аккуратного описания stride и offset.


Interleaved buffers и stride

Interleaving — это хранение нескольких атрибутов в одном TypedArray с фиксированным шагом.

[x, y, r, g, b, x, y, r, g, b]

Параметры:

  • stride — количество элементов на один vertex
  • offset — смещение конкретного атрибута внутри stride

WebGL использует:

gl.vertexAttribPointer(
  location,
  size,
  type,
  normalized,
  stride,
  offset
)

Deck.gl автоматически вычисляет эти параметры при создании атрибутов.


Работа с индексными буферами

Индексные массивы позволяют переиспользовать вершины.

[0, 1, 2, 2, 3, 0]

Типичный тип:

  • Uint16Array для небольших геометрий
  • Uint32Array для больших наборов данных

Deck.gl использует индексы в слоях:

  • PolygonLayer
  • GeoJsonLayer
  • PathLayer

Индексные буферы уменьшают объём памяти и ускоряют рендеринг за счёт уменьшения количества вершин.


Бинарные форматы в загрузке данных (loaders.gl)

При работе с внешними источниками Deck.gl часто использует бинарные форматы напрямую:

  • FlatBuffers
  • Arrow
  • custom binary formats

Преимущество:

  • данные уже приходят в структуре, близкой к GPU-формату
  • минимизируется CPU parsing
  • возможен zero-copy доступ через ArrayBuffer

Пример: GeoJSON → бинарная трансформация

  1. парсинг GeoJSON
  2. извлечение координат
  3. упаковка в Float32Array
  4. создание attribute buffers

Zero-copy подход и memory mapping

В высокопроизводительных сценариях Deck.gl стремится к zero-copy обработке:

  • ArrayBuffer создаётся один раз
  • разные TypedArray view используют одну память
  • отсутствует дублирование данных при трансформациях

Пример:

const buffer = new ArrayBuffer(1024);
const positions = new Float32Array(buffer);
const colors = new Uint8Array(buffer);

Разные представления позволяют интерпретировать одну память по-разному, но требуют строгого контроля смещений.


TypedArray и производительность рендеринга

Основные факторы влияния:

  • количество аллокаций TypedArray
  • частота пересоздания буферов
  • размер batch обновлений
  • использование shared buffers между слоями

Оптимизации Deck.gl:

  • переиспользование buffer objects
  • частичное обновление атрибутов
  • инкрементальные обновления при изменении данных

Частичные обновления и dirty flags

При изменении данных Deck.gl не пересоздаёт все бинарные буферы.

Используются флаги:

  • dataChanged
  • attributeChanged
  • viewportChanged

Если изменяется только часть данных:

  • обновляется только соответствующий TypedArray segment
  • GPU buffer перезаливается частично через bufferSubData

GPU upload pipeline и TypedArray

Финальный этап — передача TypedArray в GPU:

gl.bufferData(gl.ARRAY_BUFFER, float32Array, gl.DYNAMIC_DRAW)

или

gl.bufferSubData(gl.ARRAY_BUFFER, offset, subArray)

Deck.gl автоматически выбирает стратегию:

  • полная перезаливка при структурных изменениях
  • частичное обновление при инкрементальных изменениях

Масштабирование бинарных структур в больших данных

При миллионах точек критичны следующие аспекты:

  • плотность хранения (bytes per feature)
  • количество атрибутов на vertex
  • выравнивание памяти
  • избегание JavaScript объектов в горячем пути

Типичная оптимизация:

  • хранить координаты в Float32Array
  • цвета в Uint8Array
  • дополнительные свойства кодировать в Uint16Array

Связь бинарных данных с слоями Deck.gl

Каждый слой предъявляет разные требования:

  • ScatterplotLayer: позиции + радиус + цвет
  • PathLayer: массивы линий в бинарной форме
  • PolygonLayer: индексы + треугольная триангуляция
  • HexagonLayer: агрегация в GPU-friendly buffers

Во всех случаях конечная цель — свести данные к компактным TypedArray структурам, пригодным для прямой загрузки в WebGL буферы.