Interactive ABI
This ABI is for stateful, event-driven modules that render an output frame. We prefer a small host/module contract over protocol complexity: send key and pointer events directly, advance state with tick, then pull pixels with render_output.
The design goal is practical interoperability: browser and native hosts can adapt input to this ABI, and modules stay tiny.
Core Contract
Required exports:
memory
output_ptr() -> i32
output_bytes_cap() -> i32
key_event(x11_key: i32, flags: i32, now_ms: i32) -> i32
pointer_event(button_mask: i32, x_px: i32, y_px: i32, now_ms: i32) -> i32
tick(now_ms: i32) -> i32
render_output() -> i32
Output Format
- Pixel format is fixed: RGBA8 bytes in memory order
[R, G, B, A].
- Output is row-major, top-left origin, tightly packed.
- Total bytes for a full frame:
render_width_px * render_height_px * 4.
Little-endian note:
- Hosts that view pixels as
u32 will see each pixel as 0xAABBGGRR on little-endian systems.
- For portability, treat output as byte-addressed RGBA8, not host-endian
u32 values.
Event Semantics
key_event(...):
x11_key uses X11 keysym values (aligned with RFB KeyEvent semantics).
flags is a bitfield:
bit 0: key down (1) / key up (0)
bit 1: repeat
bit 2: shift
bit 3: ctrl
bit 4: alt
bit 5: meta
now_ms is monotonic elapsed milliseconds from the same timeline used by tick(now_ms).
- Return
1 when accepted, 0 when ignored.
pointer_event(...):
button_mask uses only the low 3 bits: primary=1, middle=2, secondary=4.
x_px and y_px are integer pixel coordinates in current render space.
now_ms is monotonic elapsed milliseconds from the same timeline used by tick(now_ms).
- Return
1 when accepted, 0 when ignored.
Tick + Render Flow
We intentionally split simulation from rendering.
tick(now_ms) advances state.
render_output() writes/returns output bytes from current state.
tick(now_ms) return value:
0: no visual change (host may skip render_output())
1: output changed (host should call render_output())
This dirty bit pattern is common in interactive systems and helps avoid unnecessary redraw work.
Time source requirements:
now_ms should be monotonic time in milliseconds.
- Hosts should call
tick(0) first, then pass elapsed milliseconds since that start tick.
- Do not use wall-clock time for simulation.
Sizing Variants
Use the same core ABI for both variants.
Static Size Variant
Module decides size at instantiation and keeps it fixed.
Additional required exports:
render_width_px() -> i32
render_height_px() -> i32
Rules:
- Size does not change during instance lifetime.
- Host validates
output_bytes_cap() >= render_width_px()*render_height_px()*4.
Dynamic Size Variant
Host/user can request size changes within declared limits.
Additional required exports:
render_width_px() -> i32
render_height_px() -> i32
render_max_width_px() -> i32
render_max_height_px() -> i32
render_resize(width_px: i32, height_px: i32) -> i32
Rules:
render_resize(...) returns 1 on success, 0 on rejection.
- Rejection should be used for out-of-range or unsupported sizes.
- Width/height getters must reflect current active size.
- Host validates capacity after resize using current size.
Host Loop Pattern
Typical host flow:
- Deliver input as it arrives via
key_event(...) / pointer_event(...).
- On frame callback (for example
requestAnimationFrame), call tick(now_ms).
- If
tick returns 1, call render_output() and read output_ptr() bytes.
This gives low-latency input handling while keeping frame scheduling host-driven.
Why This Shape
Claim: this ABI is a better default than embedding a full protocol stream.
Reason:
- It keeps wasm modules free of packet parsing logic.
- It aligns with familiar game/UI separation (
tick then render).
- It composes with multiple hosts (browser/native) by adapting input at the edge.
Tradeoff:
- You lose protocol-level extensibility in v1.
- If batching or replay is needed later, add an optional batched event API in a future version without breaking this core.