Skip to content

RUNE-300: ABI Interop Proposed

Updated2026-08-23

Summary

Rune exposes a cdecl ABI built around explicit RuneEngine, RuneSurface, RuneContext, RuneNode, and RuneSnapshot handles instead of process-global initialization. Nodes are engine-owned authoring objects committed to immutable snapshots; contexts own submitted roots for surfaces; the managed surface stays thin with typed readonly partial struct wrappers and a higher-level Drawing element abstraction.

Motivation

The interop boundary must support realtime windows, deterministic offscreen rendering, capture/export, and native/managed testing without hidden global state. Explicit runtime, surface, context, node, and snapshot handles make ownership, threading, and lifetime visible in the ABI while keeping the managed wrapper AOT-friendly and close to native representation.

Design

Explicit Runtime, Surface, and Context Handles

Rune removes the old process-global rune_init() / rune_shutdown() model. Callers create a runtime, one or more surfaces, then one context per surface:

c
RuneEngine* rune_engine_create(uint8_t device);
uint8_t rune_engine_device(RuneEngine* rt);
void rune_engine_set_frame_time(RuneEngine* rt, uint64_t time_ns);
RuneStatus rune_engine_start_loop(RuneEngine* rt);
RuneStatus rune_engine_render_frame(RuneEngine* rt, uint64_t time_ns);
void rune_release(void* handle);

RuneSurface* rune_surface_create_offscreen(RuneEngine* rt, uint32_t w, uint32_t h);
RuneStatus rune_surface_resize(RuneSurface* target, uint32_t w, uint32_t h);
RuneStatus rune_surface_read_pixels(RuneSurface* target, uint8_t* rgba, uint32_t w, uint32_t h);
RuneImage* rune_surface_export_image(RuneSurface* target);
bool rune_surface_get_d3d11_device(RuneSurface* surface, void** out_device);
RuneContext* rune_context_create(RuneEngine* rt, RuneSurface* target);
RuneSnapshot* rune_context_capture(RuneContext* ctx);
void rune_context_submit(RuneContext* ctx, RuneSnapshot* snapshot);
void rune_context_request_render(RuneContext* ctx);

device is a RuneEngineDevice: RUNE_ENGINE_DEVICE_WARP for deterministic rendering or RUNE_ENGINE_DEVICE_HARDWARE for hardware rendering. New runtimes start manual/idle; rune_engine_start_loop() switches to automatic rendering, after which manual rune_engine_render_frame() returns invalid state.

Node Commit and Snapshot Submission

Nodes are created from an engine, not from a context:

c
RuneNode* rune_node_create(RuneEngine* engine,
                           void(*on_render)(RuneNode* node, void* user_data),
                           void* user_data);
RuneSnapshot* rune_node_commit(RuneNode* node);
void rune_node_mark_dirty(RuneNode* node);
void rune_node_begin_record(RuneNode* node);
RuneSnapshot* rune_node_finish_snapshot(RuneNode* node);

rune_node_commit() internally performs the recording cycle for dirty nodes (begin_update -> on_render -> end_update), publishes fresh or shell snapshots bottom-up, and returns an owned immutable root snapshot. The caller then decides which RuneContext should render that root by calling rune_context_submit().

Child Replay as an Explicit ABI Concept

Child content is replayed through explicit draw commands:

c
void rune_node_cmd_draw_node(RuneNode* node, RuneNode* child_node);      // live edge
void rune_node_cmd_draw_snapshot(RuneNode* node, RuneSnapshot* snapshot); // frozen edge

During recording, commit resolves those inputs to snapshot-local child_slots[] entries and writes CMD_DRAW_CHILD with the chosen slot index. This lets clean parents publish shell snapshots with updated child references while sharing command buffers.

Managed Wrapper Pattern

The managed layer mirrors native handles directly:

csharp
public readonly partial struct RuneEngine {
    public static RuneEngine Create(RuneEngineDevice device = RuneEngineDevice.Hardware);
    public void SetFrameTime(ulong timeNs);
    public Status StartLoop();
    public Status RenderFrame(ulong timeNs);
}

public readonly partial struct Surface {
    public static Surface CreateOffscreen(RuneEngine engine, uint width, uint height);
    public Status Resize(uint width, uint height);
    public Status ReadPixels(Span<byte> rgba, uint width, uint height);
    public Image ExportImage();
}

public readonly partial struct RuneContext {
    public static RuneContext Create(RuneEngine engine, Surface target);
    public Snapshot Capture();
    public void Submit(Snapshot snapshot);
}

public readonly partial struct RuneNode {
    public static RuneNode Create(RuneEngine engine, RuneRenderCallback onRender, nint userData);
    public Snapshot Commit();
    public void MarkDirty();
    public void CmdDrawNode(RuneNode child);
    public void CmdDrawSnapshot(Snapshot child);
}

Handles stay as nint-backed readonly partial struct wrappers. RuneValue, RuneBrush, RunePath, AnimationMetadata, and resource structs are blittable ABI mirrors.

D3D11 Export Extension

Offscreen targets support two extraction paths. rune_surface_read_pixels() copies RGBA bytes to CPU memory. rune_surface_export_image() exports the latest offscreen surface as a RuneImage* snapshot, and rune_d2d_image_get_d3d11_texture() exposes borrowed same-process D3D11 pointers for supported image kinds.

Ownership, Threading, and Errors

  • creator owns every handle unless an API explicitly transfers or retains it
  • rune_context_submit() and snapshot child slots addref retained snapshots
  • shared resources such as slots, images, fonts, shaped text, nine-slice resources, and shader programs must outlive every snapshot that references them; snapshots keep them alive through resource_refs[]
  • RuneNode destruction must be serialized with commit because node teardown mutates UI-thread authoring state
  • stateful operations return RuneStatus for invalid arguments, invalid state, unsupported targets, device loss, and generic failure

Dependencies

  • RUNE-303 defines immutable snapshots and shell publication.
  • RUNE-304 defines command encoding, RuneBrush, RunePath, and CMD_DRAW_CHILD.
  • RUNE-306 defines the public ShapedText query surface.

Test Strategy

  1. verify runtime/surface/context creation and manual vs automatic render-loop state transitions
  2. verify rune_node_commit() returns owned snapshots and rune_context_submit() swaps submitted roots by addref/release
  3. verify live vs frozen child draws populate child_slots[] correctly
  4. verify managed wrappers stay blittable and match the audited native signatures
  5. verify offscreen readback and D3D11 export honor the documented ownership rules

Rune Project brings Rune Story authoring together with the Rune Engine rendering foundation.