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(input_size).

The design goal is practical interoperability: browser and native hosts can adapt input to this ABI, and modules stay tiny.

Core Contract #

Required exports:

Output Format

Little-endian note:

Rendering Target Interop

We chose rgba8_srgb with stride = width * 4 for direct compatibility with browser <canvas> 2D context’s putImageData(), which expects RGBA bytes in that logical order.

Event Semantics

key_event(...):

pointer_event(...):

Track pointer state at the same granularity as the interaction. Normalize pointer coordinates once into a semantic hit target, such as { cell_index, subcell_index }, and use that result for both hover and click handling. Compare the proposed hover target with the current one and ignore moves that would render identical pixels. Entering a different target or leaving it is accepted. This avoids calling tick for every raw pointer coordinate while preserving responsive hover feedback.

Tick + Render Flow

We intentionally split simulation from rendering.

tick(now_ms) return value:

Time source requirements:

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:

Rules:

Host Loop Pattern #

Typical host flow:

  1. Deliver input as it arrives via key_event(...) / pointer_event(...).
  2. Call tick(now_ms) when at least one event is accepted, or when host time reaches next_wake_at_ms. Do not tick a batch containing only ignored events.
  3. Call render(0) after each tick that was run, then read output_ptr() bytes.
  4. Schedule the next host wake from the returned next_wake_at_ms (if non-zero) and pending input times.

This gives low-latency input handling while keeping frame scheduling host-driven.

Browser Presentation #

The interactive ABI describes framebuffer pixels, not CSS layout. Browser hosts can scale a fixed framebuffer at presentation time.

For <qip-play>, the page can set presentation attributes:

The same sizing values can also come from CSS custom properties when a page wants layout to live in stylesheets:

Attributes win over CSS custom properties.

<qip-play> leaves image-rendering at the browser default. That is the better default for high-DPI downscaling, and 1:1 rendering is unaffected.

Pointer events are still converted from the displayed canvas box into render-space pixels. This lets a component render a 2x backing buffer, while the page displays it at a 1x CSS size for high-DPI screens.

The stats line starts with stable module facts before live frame timings: Wasm byte size and current linear memory size. memory is the module's current linear memory buffer size, not allocator live-use inside the component. Byte sizes are formatted with decimal kB and MB units to match Chrome DevTools: divide bytes by 1000, then by 1000 again for megabytes. WebAssembly memory still grows in 64 KiB pages, so one page is shown as 65.5 kB. tick and render show a space-padded count followed by the latest timing, so the line moves less while the component runs.

For debug instrumentation, add debug to <qip-play>. In debug mode the host compares each rendered framebuffer with the previous frame, reports unchanged renders, and shows the latest exact-compare time. This is opt-in because scanning the framebuffer can be as expensive as, or more expensive than, drawing it to the canvas.

Browser Module Policy #

<qip-play> can reject modules before WebAssembly.instantiate:

<qip-play max-memory="1048576" fixed-memory>
  <source src="/components/interactive/sudoku.wasm" type="application/wasm" />
</qip-play>

Why This Shape #

This ABI keeps packet parsing out of the Wasm module. Hosts adapt browser or native input at the edge, then modules handle a familiar game/UI loop: receive input events, advance state with tick, and render the current frame.

See Testing Interactive Components for direct Wasm, host-loop, framebuffer, and browser testing approaches.