Skip to content

Shared Frame Buffer

Ember uses a modular zero-copy frame delivery pipeline that lets native Rust code write decoded video frames directly into renderer-visible memory.

All offsets are little-endian, 4-byte aligned.

OffsetTypeFieldValue
0x00u32magic0x53464D42 (‘SFMB’)
0x04u32version1
0x08u32maxWidth2048 (default)
0x0Cu32maxHeight2048 (default)
0x10u32slotSizemaxWidth * maxHeight * 4
0x14u32slotCount2 (default)
0x18u32currentWidth
0x1Cu32currentHeight
0x20u32pitch
0x24u32pixelFormat3 = RGBA8888
0x28u32readySlotatomic, 0=none, 1=slot0, 2=slot1
0x2Cu32sequenceatomic, increments each frame
0x30reservedup to 0x100
0x100bytesslot 0RGBA8888 pixel data
0x100+slotSizebytesslot 1RGBA8888 pixel data

Traditional video pipelines copy frames multiple times between decoder, CPU, and GPU. The Shared Frame Buffer ABI eliminates these copies by:

  1. Rust writes decoded RGBA pixels directly into a SharedArrayBuffer
  2. The renderer reads the same memory without copying
  3. WebGL uploads the buffer as a texture

Because SharedArrayBuffer cannot cross child_process boundaries, the libretro addon (which runs in an isolated process for V8 signal-handler safety) uses Node.js structured clone:

  1. Rust getFrame() converts to RGBA and returns a Vec<u8>
  2. Worker receives it as a Buffer
  3. process.send() copies via structured clone to the main process
  4. ipcMain.handle returns to the renderer
  5. Renderer creates Uint8Array and uploads to WebGL

This is 3 copies total, but avoids the catastrophic JSON-serialization path that previously converted every byte to a JSON number.

When a future native module (e.g. libmpv) runs in the main process, it can use the same SharedFrameBuffer ABI and the renderer can consume it with zero copies.