Skip to content

RFC: Pluggable backends — make the assembler backend-agnostic - #378

Merged
apotema merged 13 commits into
mainfrom
rfc/pluggable-backends
Jun 29, 2026
Merged

RFC: Pluggable backends — make the assembler backend-agnostic#378
apotema merged 13 commits into
mainfrom
rfc/pluggable-backends

Conversation

@apotema

@apotema apotema commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Draft RFC for #377 (revision 13). Synthesizes the design discussion + the runnable POC (window/render/context contracts) into a concrete proposal.

Thesis: backends are a closed enum (config.zig:50) while plugins are open (resolved-by-name, manifest-driven). Close that gap — a backend becomes a resolved-by-name package implementing versioned comptime contracts (render = gfx's existing Backend, plus input/audio/window). The ABI home is labelle-core itself (7 of 8 contracts already live there; the 8th, gfx's Backend(Impl), relocates from gfx to core) — not a new labelle-platform-abi package, which earlier revisions proposed before the inventory showed core already holds the surface. The GPU context stays package-private at comptime (never in the contract — avoids *anyopaque).

Covers: the runtime contract (POC-validated), the backend-as-package model, per-layer changes (core / gfx / assembler / engine / extracted backends), the codegen-splice as the core remaining work, the pre_wire/post_wire build-hook escape hatch (constrained to a versioned, documented HookContext; manifest ~95%, hook ~5%) for dynamic build-graph cases (e.g. sokol's with_imgui, NDK/emcc setup), and an incremental migration (audio pilot → sokol-desktop conversion → bgfx-Android GPU-context pilot → open the resolver → extract the rest), keeping the enum as a resolver shorthand.

Rev 12–13 add: the runtime-vs-loader split for both audio (playback AudioInterface vs the decodeAudio/uploadSound loader contract in labelle-engine/audio_backend) and render (draw API vs asset-streaming sub-surface); an explicit surfaceLost/surfaceRestored lifecycle pair (engine responds via gpuResourcesInvalidatedreuploadAssets, no deinit/init overload); and an Opening the ecosystem section — provider identity (canonical <namespace>.<name> IDs, labelle.* reserved), capability negotiation (declared .capabilities checked at resolve time → early errors), and per-contract conformance suites (behavior, not just @hasDecl shape).

All six open questions are answered (Q#1 lifecycle ABI, Q#2 contract home + versioning, Q#3 monorepo, Q#4 gamepad-as-input-extension, Q#5 build-graph manifest, Q#6 GUI-bridge compatibility), plus three ecosystem concerns added in rev 13 (provider identity, capability negotiation, conformance suites). Residuals are migration-gated, not design-blockers. The Accept-readiness gate is the bgfx-Android pilot (migration step 4) validating the surfaceLost/surfaceRestored re-upload story — the only pilot with a GPU context that can exercise the TERM_WINDOW+INIT_WINDOW cycle.

Rendered at RFC-PLUGGABLE-BACKENDS.md. Posting as a PR for review — not implementing. Stays Draft until the bgfx-Android pilot lands.

Summary by CodeRabbit

  • Documentation
    • Added an RFC proposing a backend-agnostic assembler design with plugin-style backend resolution.
    • Introduces versioned runtime contracts for render/window/input/audio plus lifecycle hooks (including mobile backgrounding and GPU context loss).
    • Defines build-manifest schema v2 for backend/platform wiring, including optional pre/post build hooks and lazy native dependency loading.
    • Clarifies input extensibility (gamepad as an input extension) and updates GUI-bridge compatibility to follow the resolved render provider name, with notes on the bgfx-Android pilot.

@cursor

cursor Bot commented Jun 21, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Documentation-only RFC with no runtime, build, or config changes.

Overview
Adds RFC-PLUGGABLE-BACKENDS.md (draft rev 5) as the design doc for #377 — documentation only, no implementation.

The RFC argues backends should work like plugins (name/repo + manifest) instead of the closed Backend enum in config.zig. It defines four comptime contracts (render, input, audio, window) in a proposed labelle-platform-abi, with per-contract composition, platform-qualified (platform, render) → window/input/audio defaults, and GPU context kept package-private (not in the ABI).

Codegen is reframed from text-splicing main() to a backend-blind Game (init / frame / deinit) driven by backend-owned, platform-specific entry points. A provider manifest covers the backend × platform matrix (entries, targets, APK/wasm packaging) and reuses existing plugin resolution rails; lazy native deps are called out as a packaging requirement for slim fetch.

Also covers incremental migration (ABI → audio pilot → sokol → open resolver), per-layer impact (gfx, assembler, engine, extracted labelle-backends monorepo), and six open questions (lifecycle hooks, contract versioning, build-graph wiring, GUI-bridge lookup, etc.).

Reviewed by Cursor Bugbot for commit 7f60ca9. Bugbot is set up for automated code reviews on this repo. Configure here.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an RFC proposing to make the assembler backend-agnostic by decoupling backends into separate packages and establishing a versioned runtime contract (ABI). The review feedback recommends aligning terminology with the Zig ecosystem by replacing 'crate' with 'package' or 'module', clarifying allocator ownership within the deinit() lifecycle contract, and ensuring Android target detection in build scripts is compatible with Zig 0.16.0.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread RFC-PLUGGABLE-BACKENDS.md Outdated
Comment on lines +118 to +121
### NEW — `labelle-platform-abi` (a thin leaf crate)
Houses the four comptime contracts + the shared value types; almost no deps.
gfx, engine, and every backend depend on it. A backend author opens exactly one
crate to see "here's everything I must implement, here's the version I pin."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In Zig, the standard terminology is "package" or "module" rather than "crate" (which is Rust-specific). To maintain consistency with the Zig ecosystem and the rest of the codebase, it is recommended to replace "crate" with "package" or "module" throughout the RFC.

Suggested change
### NEW — `labelle-platform-abi` (a thin leaf crate)
Houses the four comptime contracts + the shared value types; almost no deps.
gfx, engine, and every backend depend on it. A backend author opens exactly one
crate to see "here's everything I must implement, here's the version I pin."
### NEW — `labelle-platform-abi` (a thin leaf package)
Houses the four comptime contracts + the shared value types; almost no deps.
gfx, engine, and every backend depend on it. A backend author opens exactly one
package to see "here's everything I must implement, here's the version I pin."

Comment thread RFC-PLUGGABLE-BACKENDS.md Outdated
Comment on lines +183 to +184
2. **Where the contracts live + versioning** — gfx owns `render`; does it
relocate into the ABI crate, or stay in gfx and be re-exported? How does a

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Replace the Rust-specific term "crate" with "package" or "module" to align with Zig's terminology.

Suggested change
2. **Where the contracts live + versioning** — gfx owns `render`; does it
relocate into the ABI crate, or stay in gfx and be re-exported? How does a
2. **Where the contracts live + versioning** — gfx owns `render`; does it
relocate into the ABI package, or stay in gfx and be re-exported? How does a

Comment thread RFC-PLUGGABLE-BACKENDS.md
4. **window** — the inversion-of-control crux. The window **owns the run loop**
and the per-frame render target:
```zig
// required: init(cfg) → deinit() → shouldQuit() → beginFrame() *Target → endFrame()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When defining the deinit() contract, consider clarifying the allocator ownership model. In Zig (especially with 0.16), unmanaged data structures or certain allocation patterns require passing the allocator to deinit(allocator). Specifying whether the allocator is stored during init(cfg) or passed explicitly to lifecycle methods like deinit will prevent API mismatches for backend authors.

Comment thread RFC-PLUGGABLE-BACKENDS.md Outdated
Comment on lines +189 to +190
4. **Mobile/android + gamepad** (`android_gamepad`, `sdl_gamepad`, the
`templates/mobile.txt` path) — extra contract surface beyond the desktop four.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When extending the contract surface for Android/mobile support, keep in mind that in Zig 0.16.0, target.result.isAndroid() does not exist. Any platform-specific checks in the build scripts or backend code should use an explicit ABI check comparing target.result.abi against .android and .androideabi to properly support both 64-bit and 32-bit Android targets.

@cursor

cursor Bot commented Jun 21, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_e55754fd-b771-4964-a10b-5e8add365501)

@cursor

cursor Bot commented Jun 21, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_3311b0c7-23c7-4431-b0f8-bf199a92795c)

@cursor

cursor Bot commented Jun 21, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_e06c7106-5cdc-495f-84dd-e4e118252a49)

@apotema

apotema commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

Review findings:

  1. High: Runtime contract omits asset-loader methods the generated game already requires

RFC-PLUGGABLE-BACKENDS.md:62-70, RFC-PLUGGABLE-BACKENDS.md:145-158

The RFC defines render as draw/loadTexture and audio as play/loadSound slots, but current generated code also depends on backend module contracts for asset streaming: BackendGfx.decodeImage/uploadTexture/unloadTexture, optional compressed texture decls, font decls, and BackendAudio.decodeAudio/uploadSound/unloadSound. See src/codegen/blocks/asset_wiring.zig:49-164, src/codegen/blocks/asset_wiring.zig:203-265, and src/codegen/blocks/asset_wiring.zig:322-437. If labelle-platform-abi only captures the four method bags described here, extracted backends can “conform” while still failing generated builds. The RFC should explicitly split or include the loader contracts.

  1. High: The default cascade is not platform-qualified and would misroute existing mobile/Android backends

RFC-PLUGGABLE-BACKENDS.md:120-140

The examples say .render = .bgfx defaults to window=.glfw, input=.glfw, but bgfx currently has an Android path with distinct app/window/input wiring, and sokol has desktop/wasm/mobile template differences. See src/build_files.zig:237-265, src/build_files.zig:442-465, and src/root.zig:981-1014. The RFC later lists mobile/android as an open question, but the canonical cascade is presented as already decided. The resolver needs to be defined as (platform, render) -> window/input/audio defaults, or the RFC should explicitly scope these examples to desktop.

  1. Medium: GUI bridge compatibility is not covered by the per-contract provider model

RFC-PLUGGABLE-BACKENDS.md:113-140, RFC-PLUGGABLE-BACKENDS.md:191-198

Raw backend GUIs are currently resolved by the closed backend enum, not by render/window/input slots. See src/gui_resolve.zig:202-234. Build wiring also has backend-specific GUI flags such as sokol with_imgui and bgfx gui_enabled in src/build_files.zig:214-265. With independent providers, it is unclear whether a GUI bridge targets render, window, input, or a full-stack composition. A project using .render = .bgfx, .window = .glfw or a third-party renderer would have no specified bridge lookup or compatibility rule.

  1. Medium: The “one monorepo + lazy deps preserves slim-fetch” claim is too absolute

RFC-PLUGGABLE-BACKENDS.md:160-167

Zig lazy dependencies can avoid fetching unused transitive native deps, but only if those deps are not vendored into the fetched package and are modeled lazily at the right package boundary. The RFC states the result as guaranteed even though the current repo layout contains multiple backend subpackages plus shared in-tree packages. This should be reframed as a packaging requirement: official providers must keep heavyweight native deps as lazy external deps, and the root package must not reference unused provider deps during build graph construction.

Verification: git diff --check origin/main...HEAD passed. PR is docs-only, so no runtime/build tests were applicable.

@apotema apotema left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RFC review (revision 4)

Solid RFC — the problem statement is accurate against the current tree, the incremental migration is credible, and revision 4’s per-contract composition + render-anchored cascade is the right framing. This is ready to guide implementation planning; a few gaps are worth closing before calling it “accepted.”

What checks out

  • Problem diagnosissrc/config.zig:50 closed enum, ~4 GB bundled native deps (sokol 2.0G / bgfx 812M / wgpu 555M / raylib 335M on disk), and the plugin asymmetry (cache.resolvePlugin vs enum switches) all match reality.
  • Audio as proof of independent contracts — bgfx audio.zig (~790 LOC) + wgpu audio.zig (~296 LOC) vs backends that already delegate to a device lib is exactly the duplication the RFC targets. Audio-first extraction (step 2) is the lowest-risk, highest-payoff starting point.
  • Codegen splice reframing (rev 2) — comparing backends/sokol/templates/desktop.txt (callback init/frame exports) vs backends/raylib/templates/desktop.txt (pub fn main + while loop) makes the “text merge is impossible” argument concrete. The Game-lifecycle hook model is the right successor.
  • Context stays package-private — keeping GPU context out of the cross-backend contract avoids the *anyopaque trap; POC v3 on #377 backs this up.
  • Monorepo + lazy deps (rev 3) — resolving open Q#3 this way is pragmatic; contract granularity ≠ repo count is an important distinction.

Suggestions before acceptance

  1. Terminology — still uses “crate” in four places (lines 174, 177, 202, 265). Zig ecosystem convention is package; align with the rest of the toolkit docs.

  2. AudioInterface home — RFC says labelle-core (correct: labelle-core/src/audio.zig), but every backend comment says “engine AudioInterface(Impl)”. Call out explicitly that engine re-exports core’s contract and that the ABI crate will be the single canonical import — this is the core-diamond story in miniature.

  3. Cascade rules need a spec sketch — the render→window→input defaulting is principled, but authors will need:

    • where defaults are declared (per render provider manifest? a resolver table in assembler?)
    • what happens on incompatible overrides (e.g. .render = .bgfx + .window = .sokol_app)
    • platform-conditioned defaults (bgfx desktop ⇒ GLFW, bgfx Android ⇒ custom android_app path — not GLFW)

    Even a short “resolver pseudocode” block would de-risk step 4.

  4. project.labelle type shape — the per-contract struct examples are clear ergonomically, but worth one paragraph on how shorthand enum tags (.sokol) and full repo refs (.{ .repo = "github:…" }) unify in the config parser — this is the plugin-model mirror.

  5. Game-lifecycle ABI (open Q#1) — the sokol template already carries more than frame: sokolEvent forwarding, screenshot state, GUI event hooks. When pinning the hook surface, inventory what templates/{desktop,mobile}.txt inject today so nothing regresses (especially suspend/resume on mobile and context-loss on Android).

  6. Gamepad backendsandroid_gamepad / sdl_gamepad look like input extensions composed alongside the window provider rather than a fifth top-level contract. Worth stating explicitly so open Q#4 doesn’t sprawl the ABI.

  7. Stale wording in “The backends” — §“The backends” still says “one labelle-backends monorepo, or per-backend repos” but open Q#3 is struck through as resolved. Tighten to match.

  8. Build splice (open Q#5) — the hardest remaining work. A minimal manifest sketch (what a provider ships: build.zig hook? zon fragment? lazy native dep list?) would help parallelize design from the runtime ABI work.

Verdict

Approve direction; keep as Draft until Q#1 (Game-lifecycle ABI) and Q#5 (build-graph manifest) get at least outline answers. The runtime contract + per-contract composition story is convincing; the POC on #377 credibly de-risks the window/render/context side. Audio extraction as step 2 is the right “prove the rails” move before touching sokol’s entry-point shape.

Nice iterative revisions — rev 2’s codegen insight and rev 4’s cascade are the two biggest leaps.

@apotema apotema left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline follow-ups on the review above.

Comment thread RFC-PLUGGABLE-BACKENDS.md Outdated

## Per-layer changes

### NEW — `labelle-platform-abi` (a thin leaf crate)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: replace "crate" with "package" throughout (Zig terminology). Still present at 174/177/202/265.

Comment thread RFC-PLUGGABLE-BACKENDS.md Outdated

(The bare `.backend = .sokol` is accepted only as sugar for `.{ .render = .sokol }`.)

**`render` is the anchor and the other slots cascade**, because they aren't

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a short resolver spec: where defaults live, error on incompatible overrides, and platform-conditioned window defaults (bgfx Android ≠ GLFW).

Comment thread RFC-PLUGGABLE-BACKENDS.md
- The window / input / audio contracts (today implicit in the engine + the
generated game) get formalized and given a versioned home in the ABI crate.

### The backends

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open Q#3 resolves to one monorepo, but this section still offers "or per-backend repos" — suggest aligning.

@apotema

apotema commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

Addressed in revision 5 (7f60ca9). This was sharp review — thanks. Mapping:

Findings

  1. Render/audio omit asset-loaders (HIGH) — fixed. The render + audio contracts now spell out the full surface: decodeImage/uploadTexture/unloadTexture + compressed-texture + font decls on render; decodeAudio/uploadSound/unloadSound on audio, with the explicit note that conforming to the thin "draw/play bag" isn't enough or the generated build fails.
  2. Cascade not platform-qualified (HIGH) — fixed. The resolver is now stated as (platform, render) → window/input/audio (bgfx-Android ⇒ NativeActivity, not GLFW), and incompatible overrides are resolve-time errors. The per-platform default table lives in the new manifest section.
  3. GUI-bridge compat (MED) — added as open question fix: Android build template — libc.txt, addLibrary, Apple Silicon emulator #6 (imgui bridges are enum-resolved today; per-contract providers need a defined bridge lookup).
  4. "Lazy-deps preserves slim-fetch" too absolute (MED) — reframed as a packaging requirement (lazy external deps, no vendoring, no eager root reference), not a guarantee.

Suggestionscratepackage (all 4); AudioInterface home clarified (core, engine re-exports, ABI package canonical); cascade spec + resolver semantics now in the new Platform packaging & the manifest section (the (backend × platform) matrix, a manifest schema sketch, window-entry vs shared platform-packager); gamepad reframed as an input-extension (Q#4); lifecycle-ABI inventory note added to Q#1 (sokolEvent/screenshot/GUI hooks); stale "monorepo or per-backend" wording tightened; build-splice (Q#5) tied to the manifest's build side.

Kept as Draft per your verdict — Q#1 (lifecycle ABI) and Q#5 (build-graph/manifest) still need outline answers before "accepted."

@cursor

cursor Bot commented Jun 22, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_d639007b-1211-4e7b-ba17-ef2d713cf154)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Draft RFC proposing a design to make labelle-assembler backend-agnostic by treating backends as resolved-by-name packages implementing versioned comptime contracts (render/window/input/audio), plus a migration plan and open questions.

Changes:

  • Adds RFC-PLUGGABLE-BACKENDS.md describing the “backends as plugins” model and its ABI/contracts.
  • Defines the proposed contract decomposition and platform-qualified defaulting/cascades.
  • Documents migration steps and remaining design unknowns (build-graph splice, lifecycle ABI, GUI bridge compatibility).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread RFC-PLUGGABLE-BACKENDS.md
Comment on lines +263 to +268
**(backend × platform) matrix** — the most platform-specific, most-hardcoded
thing in the assembler today: every backend ships
`templates/{desktop,mobile,android,wasm,headless}.txt`, plus the APK packaging
(`package_apk.sh`, generated `AndroidManifest.xml`, the NDK build) and the
wasm/emscripten shell. A `Platform` enum (`desktop`/`android`/`ios`/`wasm`) and
per-platform asset compression already exist.
Comment thread RFC-PLUGGABLE-BACKENDS.md
Comment on lines +216 to +218
- **Emit a context-free skeleton** (the `main()` the POC's `run` models) and
**splice** the backend's manifest-provided run-loop / build fragments *blind*.
(The codegen-splice — the hard part, see below.)
@apotema

apotema commented Jun 27, 2026

Copy link
Copy Markdown
Contributor Author

Review — RFC mismatches its description

The PR is described (and bot-reviewed) as docs-only, but the diff touches 5 non-doc files with substantive code deletions that appear to be leftover WIP from the audio-extraction pilot (RFC migration step 2), not RFC scaffolding.

Mismatched code changes

The diff deletes ~145 LOC from backends/bgfx/src/video/backend.zig, 55 from desktop.zig, deletes fit.zig entirely (116 LOC + tests), strips 7 lines from src/templates/build_zig.txt, and downgrades the version 0.55.1 → 0.54.0. These deletions:

  1. Remove the audio-injection seam (AudioBackend, setAudioBackend, attachAudio, music_id, the close() extern) — the entire A/V-sync mechanism the assembler wires (referenced in the RFC's own comments at src/codegen/blocks/asset_wiring.zig).
  2. Inline fit.zig into drawVideoFullscreen (backends/bgfx/src/video/backend.zig:125-160) and delete the file + its tests. The rewrite is correct but loses the host-tested unit coverage the RFC itself leans on ("fit.zig (host-tested)" in the comment it deletes).
  3. Strip Android audio decoding (android_audio.decodeTrack call site) and desktop ffmpeg PCM decode (decodeAudioPcm + its test).
  4. Unlink mediandk/aaudio from the Android build template — but the RFC §"audio" explicitly calls out keeping these paths during extraction.

RFC content (the docs)

Revision 5 is solid and the rev-4 review findings are well-addressed: the full asset-loader surface on render/audio, the platform-qualified cascade (platform, render) → window/input/audio, the lazy-deps-as-requirement framing, and the new Platform-packaging/manifest section. Open questions #1 (lifecycle ABI) and #5 (build-graph manifest) are correctly still flagged as blockers for "accepted."

Verdict

The RFC doc is ready to merge as Draft. The bundled code deletions are not — they look like in-progress audio-extraction work that shouldn't ship inside an RFC PR. Recommend:

  • Split RFC-PLUGGABLE-BACKENDS.md into its own PR, or strip the code diff down to truly docs-only.
  • Keep the version at 0.55.1 (or bump to 0.56.0) — the 0.54.0 downgrade collides with the published 0.56.0 in build.zig.zon.
  • If the bgfx video/audio deletions are intentional groundwork, they belong on a branch named after the audio-extraction pilot, with the mediandk/aaudio link removal verified against a real Android build.

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

💤 Files selected but had no reviewable changes (1)
  • RFC-PLUGGABLE-BACKENDS.md
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 284495bd-e4d1-4a67-8d07-0a65705169d2

📥 Commits

Reviewing files that changed from the base of the PR and between c8b0c01 and 55de3b2.

📒 Files selected for processing (1)
  • RFC-PLUGGABLE-BACKENDS.md

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This RFC documents a backend-agnostic assembler design with pluggable providers, manifest-driven wiring, versioned core contracts, and named GUI/input integration points. It also records remaining migration questions.

Changes

Pluggable backend RFC

Layer / File(s) Summary
RFC framing and runtime contracts
RFC-PLUGGABLE-BACKENDS.md
The RFC metadata, problem statement, goals, and runtime contract model introduce backend-independent render, input, audio, and window contracts with capability gating.
Codegen splice and hooks
RFC-PLUGGABLE-BACKENDS.md
The assembler emits a backend-blind Game lifecycle surface, and the RFC defines the GameHooks ABI with required and optional lifecycle callbacks.
Input and GUI manifest extensions
RFC-PLUGGABLE-BACKENDS.md
Gamepad is defined as an input extension, and GUI bridge lookup is keyed by render provider name with schema and compatibility rules.
Core contract location and versioning
RFC-PLUGGABLE-BACKENDS.md
The ABI is placed in labelle-core, the render backend contract is relocated there, and compatibility is governed by contract and target version fields.
Build-graph manifest and migration residuals
RFC-PLUGGABLE-BACKENDS.md
The RFC defines manifest-driven provider resolution, module and artifact wiring, platform packaging delegation, build hooks, slim-fetch requirements, and remaining migration-gated residuals.

Estimated Code Review Effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

Poem

A rabbit twitched my whiskers bright,
New backend paths now hop in sight.
Contracts split and hooks take flight,
Manifests moonbeam through the night.
I nibble RFC clover — what a delight! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the RFC’s main change: making the assembler backend-agnostic via pluggable backends.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rfc/pluggable-backends

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@RFC-PLUGGABLE-BACKENDS.md`:
- Around line 198-202: The migration plan uses two different ABI package names,
which makes the ownership model contradictory. In the RFC section that
introduces the thin leaf package and in the later status/goals wording, collapse
everything to a single ABI home by renaming the `labelle-platform-abi`
references to match the chosen package name used elsewhere (the `labelle-core`
path, if that is the intended home) and keep the surrounding text consistent
across the plan, goals, and dependency descriptions.
- Around line 1315-1343: The build hook contract is too late in the flow to
handle dynamic dependency setup like with_imgui, NDK sysroot selection, and
emccStep wiring. Move the hook integration earlier in the assembler flow so
provider logic can influence dependency construction before generic
module/artifact/system-lib wiring, and update the build.zig HookContext/wire()
contract to reflect that it can participate in graph setup rather than only
supplementing it. Use the existing wire(), HookContext, and build_hook sections
to relocate the hook to the point where backend-specific build graph decisions
are made.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b42d0173-11cd-4c14-9e17-3d498bbd169b

📥 Commits

Reviewing files that changed from the base of the PR and between 01cb4e3 and f46a0c2.

📒 Files selected for processing (1)
  • RFC-PLUGGABLE-BACKENDS.md

Comment thread RFC-PLUGGABLE-BACKENDS.md Outdated
Comment thread RFC-PLUGGABLE-BACKENDS.md Outdated
@apotema

apotema commented Jun 27, 2026

Copy link
Copy Markdown
Contributor Author

Two pre-Accept items — both localized, neither threatens the architecture

Rev 10 is in great shape — grounding every answer in file:line inventories of shipped code is exactly right. Two findings I'd want resolved before flipping Draft → Accepted.

1. The validation plan doesn't cover the residual it's staked on (Q#1)

The RFC gates Accept-readiness on the audio-extraction pilot "validating the context-handoff story," and contextLost semantics are flagged as the single most-open question. But audio has zero GPU context, so the pilot is structurally incapable of exercising contextLost / the TERM_WINDOW+INIT_WINDOW surface-recreation cycle. The pilot de-risks contract-home, versioning, and build-graph mechanics — everything except the one residual it's named as validating.

Suggestion: either reframe the pilot's claim ("validates extraction mechanics, not GPU context loss"), or add a second pilot on a GPU backend that actually cycles the surface. bgfx-Android is the natural choice — it already has the init_done one-shot guard for exactly this cycle, so it's where contextLost vs full deinit+init semantics get decided in practice.

2. The contract_version check direction is backwards for the common case (Q#2)

The generated assert is:

if (backend_gfx.targets_backend_contract > labelle_core.BACKEND_CONTRACT_VERSION)
    @compileError("backend targets contract vN but core provides vM …");

So targets <= provided passes. That diagnoses old core + new backend (rare). But the dominant ecosystem failure is the reverse — new core + old third-party backend: core bumps to v3 (adds a required decl), a third-party backend still declares targets = 2. Then 2 > 3 is false → no version error → it falls straight through to the raw @compileError("Backend must define 'foo'"), i.e. the exact mysterious error the versioning was introduced to replace.

This also contradicts the "Still open → third-party contract pinning" bullet, which claims "the comptime version check catches the required-decl mismatch." Per the >-only assert, in that direction it doesn't — raw @hasDecl does.

Since the change table specifies that optional/@hasDecl-gated additions don't bump the version, targets == provided is the natural invariant: the only time targets < provided occurs is a genuine breaking bump — precisely when you want the diagnostic, not when you want to wave it through.

Suggested fix — gate on equality, branch the message by direction so both stale-backend and stale-core cases get a versioned diagnostic instead of a raw decl error:

comptime {
    const t = backend_gfx.targets_backend_contract;
    const p = labelle_core.BACKEND_CONTRACT_VERSION;
    if (t > p)
        @compileError(std.fmt.comptimePrint(
            "backend targets backend-contract v{d} but this labelle-core provides v{d} — upgrade labelle-core or use an older backend", .{ t, p }));
    if (t < p)
        @compileError(std.fmt.comptimePrint(
            "backend targets backend-contract v{d} but this labelle-core is v{d} — a breaking contract change landed; upgrade the backend", .{ t, p }));
}

(If you want to keep allowing a lagging backend to compile when no required decls actually changed, the alternative is to leave the > gate but add a t < p branch that runs before @hasDecl validation and prepends the version context to the message — but == is simpler and the change table already guarantees optional additions don't trip it.)

Either way, update the "Still open" bullet so it doesn't claim the check catches a mismatch it currently lets through.


Everything else — "the ABI package is labelle-core", the engine-facing vs backend-facing split, killing the extern struct reinterpret hack at backend.zig:48-54, and the 8-sites → 1-walk core-diamond generalization in Q#5 — is sound and well-argued. (Minor, non-blocking: Q#5's build_hook ordering residual has a shipped consumer today — sokol's with_imgui must be set at b.dependency time — so the single-wire-after-generic-wiring contract is already known-insufficient; worth deciding the pre_wire/post_wire split now rather than deferring.)

@apotema

apotema commented Jun 27, 2026

Copy link
Copy Markdown
Contributor Author

Revision 11 — addresses all four findings

Pushed in 9fe3509. Four fixes:

1. Collapse labelle-platform-abilabelle-core (CodeRabbit, 2 inline comments). The migration plan + per-layer-changes section still used the old name after Q#2 established the ABI package IS labelle-core. All references renamed; the only historical mention is in Q#2 where the rename is explained.

2. Split the build hook into pre_wire/post_wire (CodeRabbit + apotema minor). The single wire()-after-generic-wiring contract was known-insufficient — sokol with_imgui is a shipped consumer that needs b.dependency-time options. pre_wire returns DependencyOptions the assembler passes to b.dependency; post_wire supplements the graph after generic wiring (NDK sysroot, emcc shell-out, extra links). Removed the hook-ordering residual from Q#5 — it is now answered, not deferred.

3. Reframe the Accept gate to the bgfx-Android pilot (apotema finding #1). The audio-extraction pilot validates extraction mechanics (contract home, versioning, build-graph wiring, lazy deps) but has zero GPU context — it cannot exercise contextLost or the TERM_WINDOW+INIT_WINDOW surface-recreation cycle. The Accept gate is now explicitly on the bgfx-Android pilot (migration step 3), which already has the init_done one-shot guard for exactly this cycle. Both the Status header and the Q#1 residual updated.

4. Fix the contract_version check direction (apotema finding #2). The rev-8 check (targets > provided) only caught old-core+new-backend (rare). The dominant ecosystem failure — new-core+old-backend, targets < provided — fell through to the raw @compileError("Backend must define 'foo'"), defeating the purpose of versioning. Now gates on strict equality with direction-branched diagnostics: t > p = "upgrade core", t < p = "upgrade backend". The third-party-pinning residual updated to match — the t < p branch is what catches the stale-third-party-backend case.

The build_hook ordering non-blocker (apotema's "minor, non-blocking" note) is also resolved by fix #2pre_wire runs before b.dependency, post_wire runs after.

@apotema

apotema commented Jun 27, 2026

Copy link
Copy Markdown
Contributor Author

Review — RFC: Pluggable backends

Type: Documentation-only. 1 file, +1493/-0 (RFC-PLUGGABLE-BACKENDS.md), Draft revision 11. Zero code/build risk.

Verdict

Mergeable as a Draft RFC, but I'd request changes on the doc-correctness issues below before flipping to Accepted. The design is high-quality, exceptionally well-grounded, and its hard problems (context handoff, contextLost) are correctly deferred to a pilot rather than overclaimed. The blocking concern is accuracy of a few load-bearing technical claims, not the design itself.

Verified findings (checked against the code)

High — AudioInterface is conflated with the audio asset-loader contract. labelle-core/src/audio.zig:4 only requires runtime playback (playSound/stopSound, optional loadSound/unloadSound(id: u32)). The decodeAudio/uploadSound/unloadSound(Sound) surface the RFC attributes to core.AudioInterface actually lives in a separate loader contract at labelle-engine/audio_backend/src/backend.zig:35 (whose own doc comment explicitly says "Runtime audio playback (AudioInterface-style) lives in labelle-core and stays there — this repo is decoder/loader-side only"). RFC lines 74-78 and 892-894 should split these two surfaces. This matters because the RFC's "audio is already contracted in core" argument (the justification for audio being the first/lowest-risk extraction) rests on the loader being where it isn't.

Medium — versioning prose contradicts its own code sample. The generated check uses strict equality (t > p and t < p both @compileError, RFC:1014/1023), but the summary says the assembler asserts N <= M (RFC:1077). N <= M would permit old-backend-against-new-core, which is exactly the dominant failure the t < p branch is designed to reject. Fix the summary to N == M.

Medium — pilot/migration step is internally inconsistent. Migration step 3 says convert sokol (RFC:321); Q#1 calls the GPU-context Accept gate bgfx-Android while labeling it "step 3 — sokol conversion" (RFC:470). Sokol (the headless-screenshot-verifiable desktop backend) cannot exercise the TERM_WINDOW/INIT_WINDOW surface-recreation cycle that the gate requires; only bgfx-Android can. These need to be two distinct steps or clearly relabeled.

Low — stale line count. src/templates/build_zig.txt cited as 1135 lines (RFC:1135); actual is 1142.

Low — stale PR description. The PR body still references labelle-platform-abi, 5 open questions, and the older acceptance framing; the RFC body now says labelle-core, 6 answered questions, and the bgfx-Android gate.

What's accurate (and it's a lot)

Spot-checks held up exactly: config.zig:50 enum verbatim; 7 of 8 contracts in labelle-core; Backend(Impl) in labelle-gfx/src/backend.zig:135; version pins core 1.19.0 / gfx 1.16.1 / engine 1.63.0; game.zig gamepad routing; getBridgeForBackend/GuiMissingBridge; with_imgui/gui_enabled wiring in build_files.zig:222-264; file line counts for gui_resolve/deps_linker/build_files/plugin_manifest/cache; sdl_gamepad 809 LOC / android_gamepad 741 LOC. The extern struct reinterpret comment exists verbatim.

Recommendation

  1. Fix the High audio-contract conflation (74-78, 892-894) — distinguish runtime AudioInterface (core) from the loader Backend (engine/audio_backend).
  2. Fix the versioning summary N <= MN == M (1077).
  3. Reconcile the sokol-vs-bgfx-Android pilot step (321 vs 470).
  4. Correct the line count (1142) and refresh the PR description.
  5. Keep Status: Draft until the bgfx-Android pilot lands, as the RFC's own gate states; link/vendor the Design: make the assembler backend-agnostic — backends as plugins (3rd-party authorable) #377 POC so reviewers can validate the context-handoff claim the whole design rests on.

No blockers for merging the document as Draft; the four content fixes should land before it's promoted to Accepted.

apotema added a commit that referenced this pull request Jun 27, 2026
Five findings from the rev-11 review:

1. High — audio-contract conflation. The RFC attributed the loader
   surface (decodeAudio/uploadSound/unloadSound(Sound)) to core's
   AudioInterface. It isn't there: core only contracts runtime playback
   (playSound/stopSound, optional loadSound(id)/music). The loader is a
   separate Backend(Impl) in labelle-engine/audio_backend. Split both the
   contract inventory (audio bullet) and the gfx Backend(Impl) section so
   the "already contracted" claim is scoped to playback only.

2. Medium — versioning summary contradicted its code sample. Changed the
   prose "asserts N <= M" to "N == M"; the generated check @compileerrors
   on both N > M and N < M (N <= M would permit the dominant
   old-backend-vs-new-core failure the t < p branch rejects).

3. Medium — pilot/migration inconsistency. Q#1 called the GPU-context
   Accept gate "step 3 — sokol conversion", but sokol-desktop can't
   exercise TERM_WINDOW/INIT_WINDOW. Split into step 3 (sokol-desktop,
   extraction mechanics) and a distinct step 4 (bgfx-Android GPU-context
   gate); renumbered resolver/extract to 5/6 and fixed all cross-refs.

4. Low — refreshed the build_zig.txt line count to 1142.

5. Low — last "crate" → "package"; PR description refreshed separately
   (labelle-core, six answered questions, bgfx-Android gate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
apotema and others added 8 commits June 27, 2026 12:49
…377)

Draft RFC synthesizing the design discussion + the runnable POC: promote
backends from a closed enum to the open plugin model, behind four versioned
comptime contracts (render/input/audio/window) in a new labelle-platform-abi
crate, with the GPU context kept package-private at comptime. Captures the
per-layer changes, the gfx impact, the codegen-splice as the core remaining
work, an incremental migration, and the open questions.
…cycle hook contract

POC'd open question #1 (#377): the run-loop splice isn't a text merge (which
can't even span desktop-loop vs mobile-callback entry shapes) — the assembler
emits a backend-blind Game (init/frame/deinit hooks); the backend owns its entry
point and drives them, composing at the module level. The code splice is largely
answered; the residuals are the build-splice (#5) and the full lifecycle ABI
(input/resize/suspend-resume + context-loss, the new #1).
…worked example

Correct the 'one package = four contracts' framing: the four contracts are
independently pluggable (audio is the proof — AudioInterface is already in
labelle-core, yet bgfx+wgpu each reimplement a WAV mixer because their render
lib has no audio). A 'backend' is a composition of per-contract providers,
declared as full-stack packages with per-contract overrides. Audio decomposes
into a shared labelle-audio mixer + pluggable device sinks (sokol_audio/
miniaudio/...) and is the ideal first extraction (already contracted, zero
context-sharing). Packaging: one labelle-backends monorepo + Zig lazy deps
(contract granularity ≠ repo count); resolves the monorepo open question. Adds
the audio pilot to the migration plan.
…anchored cascade

Make the per-contract struct the canonical declaration (self-documenting) and
demote bare `.backend = .sokol` to sugar for `.{ .render = .sokol }`. Document
the cascade: render is the anchor (render needs a compatible window → window
default), window→input (input ships with the window lib), audio independent
(sokol→sokol_audio else miniaudio). You pin render + any slot to override; the
defaulting is principled (render⇄window coupling), not a magic table.
…ackaging/manifest

Folds in the review findings:
- HIGH: render/audio contracts are thicker than draw/play — spell out the
  asset-loader surface (decodeImage/uploadTexture/unloadTexture/compressed/font,
  decodeAudio/uploadSound/unloadSound) so a backend can't conform-but-fail.
- HIGH: the cascade is platform-qualified — resolver is (platform, render) ->
  window/input/audio (bgfx Android != GLFW); incompatible overrides are errors.
- MED: lazy-deps reframed from a guarantee to a packaging requirement.
- MED: new GUI-bridge open question (imgui bridges vs per-contract providers).
- New 'Platform packaging & the manifest' section: the (backend x platform)
  matrix, the manifest schema sketch, window-entry vs shared platform-packager.
- Suggestions: crate->package, AudioInterface home, gamepad-as-input-extension,
  lifecycle-ABI inventory note, stale 'monorepo or per-backend' wording fixed.
Answers all six open questions, grounded in inventories of the shipped
codebase. Each answer cites the specific files + line counts it is
verified against.

Q#1 — the Game-lifecycle ABI (rev 6): the full hook surface is
init/deinit/frame(dt)/running/event/suspend_/resume_/contextLost (last
four @hasDecl/null-gated). Grounded in an inventory of all seven shipped
templates. Per-frame work (screenshot, preview, GUI, setScreenSize)
stays codegen, not lifecycle. Residual: contextLost semantics + the
Event type shape — both gated on the audio-extraction pilot.

Q#2 — where the contracts live + versioning (rev 8): the ABI package IS
labelle-core (7 of 8 contracts already live there). Backend(Impl) +
its value types relocate from gfx to core. Versioning: a
contract_version integer on each contract + a
targets_<contract>_version on each backend, asserted at comptime.

Q#3 — monorepo (rev 3, resolved): one labelle-backends monorepo + Zig
lazy deps per provider.

Q#4 — gamepad as an input-extension (rev 9): gamepad is NOT a fifth
contract. The three existing sources are packages composed alongside
the input provider. The manifest input_extensions field replaces
deps_linker.zig staging switches.

Q#5 — the build-graph manifest (rev 7): the manifest build-side schema
(.modules/.artifacts/.system_libs/.frameworks/.platforms/.build_hook),
the core-diamond generalization (8 hand-coded sites to 1 generic walk),
the build-hook escape hatch, and lazy native deps as a zon-level
requirement.

Q#6 — GUI-bridge compatibility (rev 10): bridges keyed by render
provider name (not a closed enum). Two integration patterns: external
C++ bridge (default) and in-backend adapter (via provider manifest
build_options). The with_imgui/gui_enabled flags are replaced by
manifest-declared options. render_interface GUIs are unaffected.

All residuals are migration-gated, not design-blockers. The RFC is
ready to move from Draft to Accepted pending the audio-extraction
pilot validating the context-handoff story.
Four fixes from the rev-10 review:

1. Collapse labelle-platform-abi to labelle-core everywhere (CodeRabbit).
   The migration plan and per-layer-changes section used the old name
   even after Q#2 established that the ABI package IS labelle-core.
   Renamed all remaining references; the only historical mention is in
   Q#2 where the rename is explained.

2. Split the build hook into pre_wire/post_wire (CodeRabbit + apotema).
   The single wire()-after-generic-wiring contract was
   known-insufficient: sokol with_imgui is a shipped consumer that must
   set b.dependency options BEFORE the artifact is built. pre_wire
   returns DependencyOptions the assembler passes to b.dependency;
   post_wire supplements the graph after generic wiring (NDK sysroot,
   emcc shell-out, extra links). Removed the hook-ordering residual
   from Q#5 open-questions — it is now answered.

3. Reframe the Accept gate to the bgfx-Android pilot (apotema).
   The audio-extraction pilot (step 2) validates extraction mechanics
   (contract home, versioning, build-graph wiring, lazy deps) but has
   ZERO GPU context — it cannot exercise contextLost or the
   TERM_WINDOW+INIT_WINDOW surface-recreation cycle. The Accept gate
   is now on the bgfx-Android pilot (step 3), which already has the
   init_done one-shot guard for exactly this cycle.

4. Fix the contract_version check direction (apotema).
   The rev-8 check (targets > provided) only caught old-core+new-backend
   (rare). The dominant ecosystem failure is new-core+old-backend
   (targets < provided) — which fell through to the raw
   @CompileError("Backend must define 'foo'") the validator emits,
   defeating the purpose of versioning. Now gates on strict equality
   with direction-branched diagnostics: t > p = "upgrade core", t < p =
   "upgrade backend". Updated the third-party-pinning residual to
   match.
Five findings from the rev-11 review:

1. High — audio-contract conflation. The RFC attributed the loader
   surface (decodeAudio/uploadSound/unloadSound(Sound)) to core's
   AudioInterface. It isn't there: core only contracts runtime playback
   (playSound/stopSound, optional loadSound(id)/music). The loader is a
   separate Backend(Impl) in labelle-engine/audio_backend. Split both the
   contract inventory (audio bullet) and the gfx Backend(Impl) section so
   the "already contracted" claim is scoped to playback only.

2. Medium — versioning summary contradicted its code sample. Changed the
   prose "asserts N <= M" to "N == M"; the generated check @compileerrors
   on both N > M and N < M (N <= M would permit the dominant
   old-backend-vs-new-core failure the t < p branch rejects).

3. Medium — pilot/migration inconsistency. Q#1 called the GPU-context
   Accept gate "step 3 — sokol conversion", but sokol-desktop can't
   exercise TERM_WINDOW/INIT_WINDOW. Split into step 3 (sokol-desktop,
   extraction mechanics) and a distinct step 4 (bgfx-Android GPU-context
   gate); renumbered resolver/extract to 5/6 and fixed all cross-refs.

4. Low — refreshed the build_zig.txt line count to 1142.

5. Low — last "crate" → "package"; PR description refreshed separately
   (labelle-core, six answered questions, bgfx-Android gate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@apotema
apotema force-pushed the rfc/pluggable-backends branch from a9b6c02 to c8b0c01 Compare June 27, 2026 15:49
apotema and others added 2 commits June 27, 2026 12:59
Seven design points from the rev-12 review:

1. Separate runtime vs asset-loader contracts. Audio was split in rev 12;
   this names render's two sub-surfaces — draw API (drawTriangle/...) vs
   asset-streaming/loader (decodeImage/uploadTexture/font decls) — making
   the split symmetric with audio.

2. Conformance suites per contract. New "Opening the ecosystem" section:
   each contract ships a shared conformance suite in labelle-core (next to
   mock_backend), parameterized over the provider Impl, checking behavior
   (round-trips, event mapping, surface-loss state preservation) — not just
   @hasDecl shape. A provider is conformant iff it passes the suite for
   every capability it advertises.

3. Explicit lifecycle. Replaced the single under-specified contextLost
   with surfaceLost/surfaceRestored, with the engine responding via
   gpuResourcesInvalidated -> reuploadAssets instead of overloading
   deinit/init for mobile surface recreation. Only the re-upload
   granularity stays pilot-gated (bgfx-Android, step 4).

4. Constrain build hooks. Manifest now ~95%, hook ~5%; HookContext/
   DependencyOptions are versioned types, hook may read only documented
   ctx fields and construct build-graph nodes only — no arbitrary FS/
   network/shell-out. Extend the manifest, not the hook.

5. Provider identity & collisions. Canonical <namespace>.<name> IDs;
   labelle.* reserved for the official monorepo and is what the enum
   shorthands resolve to; collision (or a third party claiming labelle.*)
   is a hard resolve-time error. The ID is the stable key for GUI bridges
   and capabilities.

6. Capability negotiation. Providers declare a .capabilities set; the
   assembler checks project-required capabilities (explicit .requires +
   derived from platform/target/GUI) before emitting the build graph,
   erroring with a project-level message instead of a deep @CompileError.

7. Migration pilots — already addressed in rev 12 (audio = extraction
   mechanics, sokol-desktop = full-stack, bgfx-Android = lifecycle/context
   gate); status/residual wording updated to match the new hook names.

Added open-questions 7-9 (identity, capabilities, conformance) and bumped
the status header to revision 13.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Audio — the worked example" section (and migration step 2) still
described labelle-audio as "the AudioInterface impl (decode + mix)" and
called audio "already contracted (core.AudioInterface)", conflating the two
surfaces rev 12 split. Corrected: playback/mix IS AudioInterface (core);
decode/upload is the separate audio-loader contract (Backend(Impl) in
labelle-engine/audio_backend). "Already contracted" is now scoped to the
playback half, with the loader half noted as having a ready home.

PR description updated separately (revision 11 → 13; contextLost →
surfaceLost/surfaceRestored; rev 12-13 additions summarized).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@apotema

apotema commented Jun 27, 2026

Copy link
Copy Markdown
Contributor Author

Sakana review

Verdict: Excellent RFC. Keep it Draft until the bgfx-Android surface-loss pilot validates the lifecycle story.

Revision 13 addresses the major prior design concerns:

  • Audio runtime-vs-loader split is now correct: AudioInterface is playback; decodeAudio/uploadSound belongs to the separate audio loader contract.
  • Render is split into draw API vs asset-streaming/loader sub-surfaces.
  • Versioning now correctly describes strict equality (N == M) for contract versions.
  • contextLost has been replaced by explicit surfaceLost / surfaceRestored, with the engine response framed as GPU-resource invalidation and asset re-upload rather than overloading deinit/init.
  • Migration pilots are now correctly separated: audio for extraction mechanics, sokol-desktop for full-stack extraction, bgfx-Android for the real GPU-context/surface-loss gate.
  • The “Opening the ecosystem” section adds the right missing pieces: provider identity, capability negotiation, conformance suites, and constrained build hooks.

Findings

Low — build-hook constraints are policy, not enforcement, unless implemented.

The RFC says hooks have no arbitrary filesystem/network/shell-out access outside documented surfaces. That is the right policy, but a Zig build.zig fragment can still do arbitrary work unless the assembler enforces or audits that boundary. I’d add one sentence making clear whether this is trust-based for now, enforced later, or enforced by limiting what hook code is allowed to expose.

Low — historical contextLost references remain in revision-history prose.

These are understandable as historical notes, but they make the current model slightly harder to scan. Not blocking.

Recommendation

Merge/keep as Draft RFC. Promote to Accepted only after the bgfx-Android pilot proves surfaceLost/surfaceRestored and the re-upload granularity in practice.

— Sakana

Phases 1 & 2 shipped (labelle-core #45 render contract; labelle-audio v0.3.0
shared Mixer(Sink)+DeviceSink i16+f32; bgfx/wgpu/sokol collapsed). Mark them DONE
in the migration plan; correct the audio worked-example (raudio/sdl_audio are
monolithic engines, NOT shared-mixer device sinks — raylib/sdl delegate decode+
mix and didn't collapse); add the composable-vs-monolithic provider distinction
to the resolver (Phase 5); flag the WAV-shared/OGG-backend decode split + the
writeAudioBackendWiring codegen as Phase-6 targets; record that audio proved the
provider-composition mechanic + no-codegen-change for a module dep but NOT the
run-loop splice (Phase 3 remains the gating crux).
A throwaway manifest-driven generation path for sokol-desktop produced
byte-identical main.zig+build.zig with no =>.sokol branch in the splice logic,
builds + runs (headless screenshot, negative-control verified). Verdict: the
build splice is VIABLE; externalizing the embedded build_zig.txt sections was
the easy part. Refines the model to 'manifest declarations + fixed
assembler-computed params + capability-flag-keyed lifecycle block library' (NOT
pure-data) and names the three things that stay code + the name->package
registry that is the Phase-5 pluggability seam.
…ated)

The surfaceLost/surfaceRestored GPU-context-loss gate — the one validation CI
structurally can't run — passed on real hardware (Tab A7 / Adreno 610 / Android
12): 9 surface destroy/recreate cycles, hooks fire in order, 0 crashes, assets
re-upload, GPU memory plateaus. That was the last gating residual, so the RFC
moves Draft→Accepted. Remaining work is Phase-6 implementation, not design.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants