Skip to content

Explore linear memory interop frameworks #112

Description

@cowboyd

Context

Every WASM boundary crossing today goes through hand-written pack/unpack code
against a manual wire format:

  • Render out (TS → C): pack() / packSize() in ops.ts write a
    Uint32Array into WASM linear memory. reduce() in src/clayterm.c reverses
    the exact same layout with rd() and offset arithmetic.
  • Input events (C → TS): the parser writes event structs into linear
    memory; input.ts walks them back out with a DataView (see readEvent).

The wire format is duplicated across the packer, the size calculator, and the
unpacker. Every field addition or reorder is a lockstep change across TS and C,
policed only by comments (see ops.ts:217-227, which explicitly warns that C
treats field order as an invariant).

This issue is to explore whether an off-the-shelf zero-copy interop
framework would serve us better. It is not a proposal to adopt one.

Candidate frameworks

Non-exhaustive; add more in comments.

  • FlatBuffers — schema-driven, zero-copy reads via
    vtable-guided field access, wide language support.
  • Cap'n Proto — similar model, more compact vtables,
    wide language support.
  • Shared C headers only — skip the framework, share a .h describing the
    layout, generate TS accessors from it. Lightest possible dependency.

Potential benefits

  • One schema as the single source of truth in place of three hand-synced code
    paths.
  • Eliminates the explicit unpack step on the C side (zero-copy access, though
    the exact cost model differs by framework — see open questions).
  • Type-safe generated accessors on both sides in place of offset math and bit
    shifts.
  • Forward compatibility — schema-based frameworks allow adding fields with
    defaults instead of hard-breaking every wire change.
  • Cross-language reach — third parties consuming clayterm.wasm from Go, Rust,
    Python, etc. get generated bindings instead of having to reimplement the
    packer.
  • Debuggability — schema-based frameworks can dump a buffer to JSON for
    inspection; the current format is opaque bytes.

Open questions to answer

  1. WASM binary size delta. The framework runtime lands in clayterm.wasm.
    How much does each candidate add? Compare against current baseline reported
    by size-report.yml.
  2. JS bundle size delta. The builder/reader library ships to consumers.
    How much does each candidate add over the ~600 lines of custom pack code?
  3. Per-frame allocation profile. The current packer writes straight into
    WASM linear memory with no intermediate objects. Do the candidate TS
    builders introduce allocations that create GC pressure at frame rate?
  4. Access cost on the C side. "Zero-copy" is not free — flatbuffers and
    Cap'n Proto both use vtable-guided lookups. What is the actual per-op cost
    vs. the current rd() loop?
  5. Buffer size delta. Current bit-packing (padding in a word, attrs in the
    color alpha byte) is dense. How much larger does a typical frame get with
    each candidate?
  6. Toolchain cost. flatc / capnp need to plug into the Makefile and
    Deno tasks. What does the dev setup look like?
  7. Scope. Does the framework replace both directions (render + input),
    or just one?

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionFurther information is requested

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions