Version 1.0 | Canonical Source: scenic_driver_local
This document specifies the binary protocol used for communication between a Scenic driver (BEAM/Elixir) and a native renderer (Android, iOS, desktop, browser).
+------------------+ binary protocol +------------------+
| BEAM / Scenic | --------------------------> | Remote Renderer |
| ViewPort | <--- events (touch, keys) --| (Android / iOS) |
| + driver | TCP / WS / Unix | Canvas / Metal |
+------------------+ +------------------+
The driver serializes Scenic script operations into framed binary messages and pushes them to the renderer. The renderer sends back input events which the driver translates into Scenic input events.
| Component | Language | Repository |
|---|---|---|
| scenic_driver_remote | Elixir | scenic_driver_remote |
| scenic_renderer_native | C | scenic_renderer_native |
All messages (commands and events) share one frame format:
+--------+----------------+------------------+
| Type | Length | Payload |
| 1 byte | 4 bytes BE | Length bytes |
+--------+----------------+------------------+
- Type: Command or event identifier (uint8)
- Length: Payload length in bytes (uint32, big-endian)
- Payload: Type-specific binary data
Header size: 5 bytes
| Type | Size | Encoding |
|---|---|---|
| u8 | 1 byte | unsigned |
| i32 | 4 bytes | big-endian, signed |
| u32 | 4 bytes | big-endian, unsigned |
| u64 | 8 bytes | big-endian, unsigned |
| f32 | 4 bytes | IEEE 754 single precision, big-endian |
Commands flow from the Scenic driver to the native renderer.
| Code | Name | Payload |
|---|---|---|
| 0x01 | PUT_SCRIPT | id_len:u32 id:bytes script:bytes |
| 0x02 | DEL_SCRIPT | id_len:u32 id:bytes |
| 0x03 | RESET | (empty) |
| 0x04 | GLOBAL_TX | a:f32 b:f32 c:f32 d:f32 e:f32 f:f32 |
| 0x05 | CURSOR_TX | a:f32 b:f32 c:f32 d:f32 e:f32 f:f32 |
| 0x06 | RENDER | (empty) |
| 0x08 | CLEAR_COLOR | r:f32 g:f32 b:f32 a:f32 |
| 0x0A | REQUEST_INPUT | flags:u32 |
| 0x20 | QUIT | (empty) |
| 0x40 | PUT_FONT | name_len:u32 data_len:u32 name:bytes data:bytes |
| 0x41 | PUT_IMAGE | id_len:u32 data_len:u32 w:u32 h:u32 fmt:u32 id:bytes data:bytes |
Store or update a script for rendering.
Payload:
id_len: u32 - Length of script ID
id: bytes - Script ID (binary string)
script: bytes - Serialized Scenic script (remaining bytes)
Delete a script.
Payload:
id_len: u32 - Length of script ID
id: bytes - Script ID to delete
Clear all scripts, fonts, and images.
Payload: (empty)
Set global transformation matrix (2D affine transform).
Payload:
a: f32 - Scale X
b: f32 - Skew Y
c: f32 - Skew X
d: f32 - Scale Y
e: f32 - Translate X
f: f32 - Translate Y
Matrix form:
| a c e |
| b d f |
| 0 0 1 |
Set cursor transformation matrix.
Payload: (same as GLOBAL_TX)
Trigger a frame render.
Payload: (empty)
Set the background clear color.
Payload:
r: f32 - Red (0.0 - 1.0)
g: f32 - Green (0.0 - 1.0)
b: f32 - Blue (0.0 - 1.0)
a: f32 - Alpha (0.0 - 1.0)
Request specific input types.
Payload:
flags: u32 - Input type flags (bitmask)
Shutdown the renderer.
Payload: (empty)
Load a font.
Payload:
name_len: u32 - Length of font name/hash
data_len: u32 - Length of font data
name: bytes - Font name/hash (UTF-8)
data: bytes - Font file data (TTF/OTF)
Load an image/texture.
Payload:
id_len: u32 - Length of image ID
data_len: u32 - Length of image data
width: u32 - Image width (0 if encoded format)
height: u32 - Image height (0 if encoded format)
format: u32 - Image format (see below)
id: bytes - Image ID/hash
data: bytes - Image data
Events flow from the native renderer back to the Scenic driver.
| Code | Name | Payload |
|---|---|---|
| 0x01 | STATS | bytes_received:u64 |
| 0x05 | RESHAPE | width:u32 height:u32 |
| 0x06 | READY | (empty) |
| 0x08 | TOUCH | action:u8 x:f32 y:f32 |
| 0x0A | KEY | key:u32 scancode:u32 action:i32 mods:u32 |
| 0x0B | CODEPOINT | codepoint:u32 mods:u32 |
| 0x0C | CURSOR_POS | x:f32 y:f32 |
| 0x0D | MOUSE_BUTTON | button:u32 action:u32 mods:u32 x:f32 y:f32 |
| 0x0E | SCROLL | x_off:f32 y_off:f32 x:f32 y:f32 |
| 0x0F | CURSOR_ENTER | entered:u8 |
| 0xA0 | LOG_INFO | message:bytes |
| 0xA1 | LOG_WARN | message:bytes |
| 0xA2 | LOG_ERROR | message:bytes |
Statistics report from renderer.
Payload:
bytes_received: u64 - Total bytes received
Window/viewport resize notification.
Payload:
width: u32 - New width in pixels
height: u32 - New height in pixels
Renderer is initialized and ready to receive commands.
Payload: (empty)
Touch/pointer event.
Payload:
action: u8 - Touch action (see below)
x: f32 - X coordinate
y: f32 - Y coordinate
Touch actions:
- 0: DOWN (finger pressed)
- 1: UP (finger released)
- 2: MOVE (finger moved)
Keyboard key event.
Payload:
key: u32 - Key code
scancode: u32 - Hardware scancode
action: i32 - 0=release, 1=press, 2=repeat
mods: u32 - Modifier flags
Unicode character input.
Payload:
codepoint: u32 - Unicode codepoint
mods: u32 - Modifier flags
Mouse cursor movement.
Payload:
x: f32 - X coordinate
y: f32 - Y coordinate
Mouse button event.
Payload:
button: u32 - Button ID (0=left, 1=right, 2=middle)
action: u32 - 0=release, 1=press
mods: u32 - Modifier flags
x: f32 - X coordinate
y: f32 - Y coordinate
Scroll wheel event.
Payload:
x_offset: f32 - Horizontal scroll amount
y_offset: f32 - Vertical scroll amount
x: f32 - Cursor X coordinate
y: f32 - Cursor Y coordinate
Cursor entered/left window.
Payload:
entered: u8 - 1=entered, 0=left
Log messages from renderer.
Payload:
message: bytes - UTF-8 log message
| Code | Name | Description |
|---|---|---|
| 0 | ENCODED | Compressed file (PNG, JPEG) - decoder determines dimensions |
| 1 | GRAY | Grayscale, 1 byte/pixel |
| 2 | GRAY_A | Grayscale + Alpha, 2 bytes/pixel |
| 3 | RGB | RGB, 3 bytes/pixel |
| 4 | RGBA | RGBA, 4 bytes/pixel |
Renderer Driver
| |
|<-------- TCP connect ----------|
| |
|-------- READY event --------->|
| |
|<-- PUT_FONT (for each font) --|
|<-- PUT_IMAGE (for each img) --|
|<-- PUT_SCRIPT (for each) -----|
|<-------- RENDER --------------|
| |
|-------- RESHAPE event ------->|
| |
|<-------- GLOBAL_TX -----------|
|<-------- RENDER --------------|
| |
| (normal operation loop) |
| |
|<-- PUT_SCRIPT (updates) ------|
|<-------- RENDER --------------|
| |
|-------- TOUCH event --------->|
|-------- KEY event ----------->|
| ... |
- Client connects (TCP/Unix socket handshake)
- Renderer sends READY event
- Driver resyncs all fonts, images, and scripts
- Driver sends RENDER
- Renderer sends RESHAPE with its screen dimensions
- Driver computes GLOBAL_TX (scale + offset) and sends it
- Normal operation: scene updates flow as PUT_SCRIPT + RENDER; input flows back
The driver maintains a "design size" (the viewport size in Scenic). When the renderer sends RESHAPE with actual screen dimensions, the driver computes a transformation matrix:
scale_x = screen_width / design_width
scale_y = screen_height / design_height
GLOBAL_TX = (scale_x, 0, 0, scale_y, 0, 0)
This stretches the design canvas to fill the device screen. For aspect-ratio-preserving scaling with letterboxing:
scale = min(scale_x, scale_y)
offset_x = (screen_width - design_width * scale) / 2
offset_y = (screen_height - design_height * scale) / 2
GLOBAL_TX = (scale, 0, 0, scale, offset_x, offset_y)
The protocol is transport-agnostic. Supported transports:
| Transport | Use Case |
|---|---|
| Unix Socket | Local IPC (Android/iOS with co-located BEAM) |
| TCP | Remote rendering, debugging, multi-device |
| WebSocket | Browser-based renderers |
| Version | Date | Changes |
|---|---|---|
| 1.0 | 2025-02 | Initial specification, aligned with scenic_driver_local |
Apache-2.0