Передача данных между потоками через SharedArrayBuffer

В библиотеке Oimo.js для физического моделирования часто требуется высокая производительность при симуляции большого количества тел. Использование SharedArrayBuffer позволяет передавать данные между основным потоком JavaScript и рабочими потоками (Web Workers) без копирования, что критично для сложных и интенсивных симуляций.

SharedArrayBuffer создаёт общий блок памяти, доступный одновременно из нескольких потоков. Это позволяет одновременно обновлять позиции, скорости и другие физические параметры тел в многопоточном окружении.

const buffer = new SharedArrayBuffer(1024); // 1024 байта общей памяти
const floatView = new Float32Array(buffer);

Здесь floatView обеспечивает доступ к общей памяти в виде массива чисел с плавающей запятой. Любые изменения из одного потока становятся видимыми в другом без дополнительной сериализации или копирования данных.


Создание и настройка World с многопоточностью

Oimo.js поддерживает настройку World с использованием Web Workers. Для этого создаётся отдельный рабочий поток, который обрабатывает шаги симуляции. Основной поток отвечает за рендеринг и взаимодействие с пользователем.

import { World, Body, Box, Vec3 } from 'oimo';

const world = new World({
    timestep: 1/60,
    broadphase: 2, // SAP
    worldScale: 1,
    random: true
});

// Создание тел
const box = world.add({
    type: "box",
    size: [1, 1, 1],
    pos: [0, 5, 0],
    move: true
});

Для передачи данных в SharedArrayBuffer требуется подготовить массивы для хранения ключевых параметров: позиции (position), ориентации (quaternion), линейной и угловой скорости (velocity, angularVelocity).

const numBodies = world.bodies.length;
const sab = new SharedArrayBuffer(Float32Array.BYTES_PER_ELEMENT * numBodies * 10);
const stateArray = new Float32Array(sab);

Здесь каждый объект использует 10 элементов: 3 для позиции, 4 для кватерниона, 3 для линейной скорости. Этот формат позволяет рабочему потоку и основному потоку синхронизировать состояние без конфликтов.


Чтение и запись данных в SharedArrayBuffer

Рабочий поток выполняет физический шаг и обновляет массив stateArray. Основной поток читает массив и обновляет визуальные объекты.

function updateState() {
    for (let i = 0; i < world.bodies.length; i++) {
        const body = world.bodies[i];
        const offset = i * 10;
        stateArray[offset] = body.getPosition().x;
        stateArray[offset + 1] = body.getPosition().y;
        stateArray[offset + 2] = body.getPosition().z;

        const q = body.getQuaternion();
        stateArray[offset + 3] = q.x;
        stateArray[offset + 4] = q.y;
        stateArray[offset + 5] = q.z;
        stateArray[offset + 6] = q.w;

        const v = body.getVelocity();
        stateArray[offset + 7] = v.x;
        stateArray[offset + 8] = v.y;
        stateArray[offset + 9] = v.z;
    }
}

Главное — соблюдать последовательность записи и чтения, чтобы основной поток корректно интерпретировал данные. Любые нарушения структуры массива могут привести к некорректной визуализации или ошибкам симуляции.


Синхронизация с Atomics

При одновременном доступе к SharedArrayBuffer может возникнуть гонка данных. Для предотвращения этого используются методы объекта Atomics:

  • Atomics.store() — безопасная запись;
  • Atomics.load() — безопасное чтение;
  • Atomics.exchange() — атомарная замена значения.

Пример безопасного обновления позиции:

Atomics.store(stateArray, offset, body.getPosition().x);
const x = Atomics.load(stateArray, offset);

Использование Atomics особенно важно при симуляции с большим количеством тел и интенсивным рендерингом, когда рабочий поток может обновлять состояние параллельно с основным потоком.


Передача данных между потоками через Worker

Для полной многопоточности создаётся Web Worker, который получает SharedArrayBuffer при инициализации:

const worker = new Worker('physicsWorker.js');
worker.postMessage({ buffer: sab, numBodies });

Внутри physicsWorker.js:

self.onmess age = function(e) {
    const { buffer, numBodies } = e.data;
    const stateArray = new Float32Array(buffer);

    function step() {
        world.step();
        for (let i = 0; i < numBodies; i++) {
            const body = world.bodies[i];
            const offset = i * 10;
            stateArray[offset] = body.getPosition().x;
            // остальное аналогично
        }
        requestAnimationFrame(step);
    }

    step();
};

Основной поток просто читает stateArray и обновляет объекты визуализации, без блокировки физического потока.


Оптимизация использования SharedArrayBuffer

  1. Выделение фиксированного размера блока памяти для всех тел, чтобы избежать динамического перераспределения.
  2. Групповая запись данных с минимизацией вызовов Atomics, так как каждый вызов добавляет накладные расходы.
  3. Выбор компактного формата для хранения состояния: часто используется 10–12 чисел на тело, чтобы включить позицию, ориентацию и скорости.
  4. Использование TypedArray для быстрого доступа к числам с плавающей точкой.
  5. Отдельный буфер для визуализации, если требуется сглаживание или интерполяция между шагами симуляции.

Применение в Oimo.js

Использование SharedArrayBuffer и Web Workers в Oimo.js позволяет:

  • Снизить нагрузку на основной поток при обновлении физики;
  • Поддерживать сотни и тысячи динамических тел с высокой частотой обновления;
  • Реализовать интерактивные симуляции с параллельной обработкой коллизий и силовых взаимодействий.

Совмещение многопоточности, Atomics и SharedArrayBuffer создаёт мощный инструмент для профессиональных физических симуляций на JavaScript, обеспечивая производительность, близкую к нативным приложениям.