Status: IMPLEMENTATION AUTHORIZED — B0–B5 complete; B6 complete through all of CBIND-035 and CBIND-036, plus CBIND-037A and CBIND-037B1–B3/B4a–B4c, verified under HEADLESS, SDL_RENDERER, SOFTWARE and a combined ASan+UBSan tree (2026-08-15). Coverage: 4,182 implemented / 2,082 planned — see Current status for the remaining order of work. This document is the plan for a native C API, implemented inside the main CNA repository. It is intentionally not a plan for C#, .NET, JavaScript/TypeScript, Rust, Python, Java, Zig, Go, Swift, or any other language-specific binding. Such work must not begin, nor be planned here, without a new explicit owner instruction.
Authoritative design inputs (read-only):
analysis_binding.mdandanalysis_binding_sharp_runtime.md. The behavioral reference for the underlying XNA-facing C++ implementation remains the local FNA tree required byAGENTS.md.
Expose a documented, testable and eventually complete C ABI over canonical CNA C++.
C application
↓
CNA public C headers and C ABI
↓
canonical CNA C++ implementation
↓
Sharp Runtime and native renderer/platform dependencies
The C API is a first-class CNA public interface, not a mechanical export of the C++ ABI and not a
separate cna-c repository. It must be able to evolve atomically with the CNA modules it adapts.
In scope:
- a C-compatible, versioned ABI inside this repository;
- a documented C-native equivalent for every public CNA C++ type, member, overload, constant and event, using handles, POD values and callbacks where C cannot express the original C++ form;
- C applications that link the native CNA library and use only public C headers;
- the C API's own lifecycle, graphics, input, content, audio, data-transfer and callback contracts;
- C-only compile/link/runtime tests, native adapter tests, documentation, export inspection and supported-platform packaging.
Out of scope:
- a language-specific binding, wrapper, package, generator or sample for any language other than C;
- a separate C engine or a second implementation of CNA;
- exporting arbitrary C++ classes, Sharp Runtime, STL, renderer-private or platform-private objects;
- declaring ABI 1.0 before experimental releases are exercised by real C applications.
"Complete" means behavioral and conceptual coverage of the public CNA API, not a false claim that C can use C++ inheritance, templates, exceptions, overload resolution or Sharp Runtime object layouts directly. Every such member must instead have a documented C mapping or a documented, testable native limitation in the C API coverage matrix; omissions are tracked as incomplete.
- CNA C++ remains the sole canonical implementation; the C API is an adapter layer.
- The public header surface compiles as real C without C++ mode, C++ headers, templates, namespaces,
references, exceptions, RTTI,
std::*,System::*, or Sharp Runtime names. - Every fallible entry point returns
CNA_Result; no C++ or Sharp Runtime exception may cross the ABI boundary. - Public primitives have fixed-width representations. The design must not expose
long,unsigned long,wchar_t, compiler-dependent enums, raw C++bool, or implementation-defined ownership. - Text is explicit UTF-8 bytes plus a fixed-width length. Returned text has a documented lifetime and uses a caller-buffer/copy contract unless a separately reviewed ownership design says otherwise.
- C++ objects and raw pointers never cross the ABI. Resource identity uses validated opaque handles with stale-handle detection.
- Every handle parameter documents whether it is owned, borrowed, transferred, nullable, or valid only during a callback. Double release, invalid kind and stale generation fail deterministically.
- Plain ABI structs are layout-versioned when extensible, explicitly initialized, and covered by C and C++ layout tests. New fields are appended only under the documented versioning rules.
- Callback signatures use C function pointers plus an opaque context pointer. Their thread, re-entrancy, lifetime, cancellation and shutdown rules are contractual, not inferred.
- Sharp Runtime is strictly native-only. The C API adapts its strings, collections, streams, exceptions, delegates and time types once into CNA-neutral forms.
- Renderer selection remains CNA-native. The C ABI reports the selected renderer and capabilities; it does not invent a parallel renderer system.
- High-frequency operations transfer data in bulk. The initial API must not force per-pixel, per-vertex, per-sprite, or per-key FFI calls.
The exact filenames remain subject to the first design gate, but implementation belongs in the existing physical module layout:
modules/c-api/
├── CMakeLists.txt
├── include/CNA/C/
│ ├── cna.h # umbrella only
│ ├── abi.h # version, export, result, fixed ABI primitives
│ ├── core.h # handles, errors, strings, buffers, capabilities
│ ├── runtime.h # instance/game lifecycle and callbacks
│ ├── graphics.h # graphics/device/2D resources and batches
│ ├── input.h # snapshot input APIs
│ ├── content.h # content/root-directory APIs
│ ├── content_readers.h # compiled-asset readers and the type-reader registry
│ ├── net.h # network identities, values and packet buffers
│ ├── net_gamers.h # network gamers, machines and event descriptions
│ ├── net_sessions.h # discovered sessions, collections and network sessions
│ ├── gamer_services.h # signed-in gamers (minimum the session slice needs)
│ ├── storage.h # storage devices, containers and file streams
│ └── audio.h # only after its explicit phase is approved
├── src/
├── tests/
│ ├── pure_c/
│ └── cpp/
└── examples/ # C-only examples, only after the test foundation is green
docs/c-api/
├── README.md
├── ABI_VERSIONING.md
├── HANDLES.md
├── OWNERSHIP.md
├── ERRORS.md
├── STRINGS_AND_BUFFERS.md
├── CALLBACKS_AND_THREADING.md
├── RENDERERS_AND_CAPABILITIES.md
└── SHARP_RUNTIME_BOUNDARY.md
└── COVERAGE.md
The module's exported target and CMake options must be selected during CBIND-007; the plan does
not presume that the current header-only CNA aggregate can itself serve as a binary C ABI library.
Do one phase at a time. Completing a phase means meeting its stated acceptance criteria, updating this plan and the relevant documentation, running the required tests, and committing that focused task. Do not start a later broad API phase merely because an earlier skeleton compiles.
| Phase | Purpose | Entry condition |
|---|---|---|
| B0 | Design and compatibility contract | Owner authorizes C ABI implementation planning to proceed |
| B1 | Build/module/export foundation | B0 design gate accepted |
| B2 | Common ABI substrate | B1 pure-C header gate green |
| B3 | Runtime/game callback vertical slice | B2 handle/error contracts green |
| B4 | Minimal usable 2D graphics and input | B3 real C loop green |
| B5 | Content and expanded input/audio | B4 ownership and renderer matrix green |
| B6 | Full public CNA API coverage | B5 foundation and the coverage inventory are green |
| B7 | Hardening, packaging and experimental release | B3–B6 selected scope is complete |
| # | Task | Status | Acceptance criteria |
|---|---|---|---|
| CBIND-000 | Record the C ABI implementation plan | ✅ | plan_binding.md, NEXT.md, AUDIT.md and AGENTS.md identify the C-only scope, all planned phases and the two read-only analysis sources. No implementation or ABI commitment is made. |
| # | Task | Status | Acceptance criteria |
|---|---|---|---|
| CBIND-001 | Freeze the C ABI charter | ✅ | docs/c-api/README.md states scope, experimental status, C++-canonical ownership, complete-public-API coverage requirement, no language-specific binding scope, supported platform policy, and the non-negotiable invariants above. |
| CBIND-002 | Define ABI naming, export and version policy | ✅ | docs/c-api/ABI_VERSIONING.md specifies CNA_* types, cna_* functions, a platform export macro, ABI semantic-version encoding/query, experimental/stable tiers, deprecation rules, and a no-breaking-change-within-major policy. |
| CBIND-003 | Define primitive and POD layout policy | ✅ | docs/c-api/ABI_VERSIONING.md and docs/c-api/STRINGS_AND_BUFFERS.md select fixed-width integer, float, boolean, enum and length/count representations; define struct alignment/initialization rules, struct_size/struct_version use, nullability, overflow conversion and C17 baseline. They prohibit size_t in ABI fields/parameters. |
| CBIND-004 | Define handles and ownership model | ✅ | docs/c-api/HANDLES.md and docs/c-api/OWNERSHIP.md specify opaque-handle encoding, slot/generation validation, runtime type checks, retain/release policy, borrowed-callback validity, parent/child lifetime, thread-affine release policy and teardown behavior. |
| CBIND-005 | Define error, UTF-8, buffer and collection contracts | ✅ | docs/c-api/ERRORS.md and docs/c-api/STRINGS_AND_BUFFERS.md specify CNA_Result, error categories, per-thread error retrieval, UTF-8 validation, caller-buffer query/copy semantics, pointer/count bulk transfers, capacity/written semantics and overflow behavior. |
| CBIND-006 | Define callback, threading and re-entrancy contract | ✅ | docs/c-api/CALLBACKS_AND_THREADING.md specifies callback result propagation, context lifetime, registration/unregistration, permitted re-entry, thread requirements, cross-thread calls and shutdown order. |
B0 gate: the six documents form one reviewed contract. No public C header or exported function is added before their decisions are consistent with each other and with current CNA renderer behavior.
| # | Task | Status | Acceptance criteria |
|---|---|---|---|
| CBIND-007 | Add opt-in physical C API module | ✅ | modules/c-api/ is an opt-in physical module included in the source-partition validator. Its initial shared library links only canonical cna_core; each later C API family must add the exact CNA module it adapts rather than prematurely linking the renderer aggregate. |
| CBIND-008 | Enable a real C consumer build path | ✅ | CNA_BUILD_C_API=ON enables C17 before dependencies/modules are created. C17 smoke executables compile and link through the normal CMake build without changing C++-only configurations when the option is off. |
| CBIND-009 | Produce a consumable native library | ✅ | cna_c_api / CNA::CApi builds as libcna_c_api with CMake install/export rules, public include installation and PIC enabled before static dependencies are created. A C compiler links and runs smoke executables against the shared library. |
| CBIND-010 | Establish visibility and symbol discipline | ✅ | CNA_C_API supplies platform export/import declarations; ELF C++ visibility is hidden by default. The HEADLESS build's dynamic export inspection contains only the documented cna_get_abi_version and error-query symbols. |
| CBIND-011 | Establish public-header quality gates | ✅ | C17 and C++23 object targets compile both leaf headers and the umbrella header under strict direct compiler checks; CTest smoke consumers include only CNA/C/cna.h. |
B1 gate: a minimal cna_get_abi_version()/capability query can be included, compiled from C,
linked to the intended library form and run on a supported native configuration. It must not expose
any CNA C++ object.
| # | Task | Status | Acceptance criteria |
|---|---|---|---|
| CBIND-012 | Implement result and structured-error boundary | ✅ | The thread-local versioned error/query/copy substrate is backed by a reusable exception firewall. It maps allocation, argument/range, I/O, standard and unknown C++ failures to stable results/categories without exposing exception objects; focused tests verify mapping and diagnostic copying. |
| CBIND-013 | Implement validated handle registry | ✅ | Slot/generation/kind/thread-affinity validation now backs the public owned CNA_Game handle as well as focused stale/double-release/reuse tests. The one-active-game state is released only after callback/native teardown; wrong-thread and stale public calls fail safely. |
| CBIND-014 | Implement neutral value and string conversion | ✅ | UTF-8 string-view validation/copy covers nullability, overlong encodings and optional embedded-NUL rejection. The first vertical slice adds independently laid out/tested CNA_GameTime and CNA_Color POD values; no C POD is reinterpreted as a C++ object. |
| CBIND-015 | Implement buffer/count-copy helpers | ✅ | Reusable pointer/count and element-size helpers validate null/zero cases, checked uint64_t multiplication and native-size conversion. Focused tests cover zero/null, nonzero-null and overflow; error-copy tests cover undersized capacity with no partial write. |
| CBIND-016 | Audit the Sharp Runtime boundary | ✅ | docs/c-api/SHARP_RUNTIME_BOUNDARY.md records the mapping table. A CMake lexical scanner and strict C17/C++23 compiler gates audit each public header; the pure-C umbrella consumer remains the authoritative boundary test. |
B2 gate: all common contracts have focused C and C++ tests, and sanitizers find no invalid handle, conversion, ownership or exception-escape defect in the exercised paths.
| # | Task | Status | Acceptance criteria |
|---|---|---|---|
| CBIND-017 | Design and implement runtime/game creation | ✅ | Versioned CNA_GameCreateInfo/CNA_GameCallbacks create one owned, generation-checked C game over the canonical CNA Game. The compile-time renderer remains CNA-owned; no runtime renderer switch is invented. |
| CBIND-018 | Implement lifecycle callback bridge | ✅ | The copied C callback table covers load/update/draw/unload/exit with a caller context, callback-scoped game handle, CNA_GameTime where applicable and copied versioned callback diagnostics. Failure stops the loop and reports CNA_RESULT_CALLBACK; run/destroy re-entry is refused. |
| CBIND-019 | Expose frame timing, clear and window-title minimum | ✅ | CNA_GameTime, one-frame/blocking run, exit request, CNA_Color clear and UTF-8 title functions adapt canonical Game, GraphicsDevice and GameWindow operations without exposing their C++ types. |
| CBIND-020 | Add C-only headless lifecycle test | ✅ | LifecycleSmoke.c creates, drives, clears, exits and destroys C games under HEADLESS; it tests callback order/values, callback diagnostics, stale handles, wrong-thread rejection and a blocking run path. |
| CBIND-021 | Add native-renderer lifecycle smoke test | ✅ | The same strict-C lifecycle source builds and passes against SDL_RENDERER with SDL's dummy video driver and software renderer. The earlier ranlib failure was traced to overlapping verification builds rather than a CNA archive defect; a single clean serial build completed through cna_c_api_lifecycle_smoke. |
B3 gate: a C application can own its lifecycle, receive callbacks, exercise UTF-8 and error conversion, and shut down cleanly without any C++ source or header dependency.
| # | Task | Status | Acceptance criteria |
|---|---|---|---|
| CBIND-022 | Expose borrowed graphics-device access and capability discovery | ✅ | graphics.h defines callback-scoped device borrowing, stable identities for every canonical GraphicsRendererType, versioned renderer info, UTF-8 renderer-name count/copy and the complete canonical GraphicsCapability query/bit set. Callback return generation-invalidates the borrowed handle; identity, maximum texture size and support answers delegate to CNA rather than a duplicate renderer feature table. |
| CBIND-023 | Expose Texture2D ownership and bulk upload |
✅ | All canonical SurfaceFormat identities are frozen; the initial supported Color subset provides versioned create/info, full-level bulk RGBA8 SetData/readback and explicit dispose/release. Pointer/count, dimensions, capacity, stale/double-destroy and parent-before-child errors are C-tested under HEADLESS and SDL_RENDERER; game destruction refuses live C graphics children. |
| CBIND-024 | Expose a batched SpriteBatch command path |
✅ | All five native sort identities and both effect bits are frozen; an owned same-game SpriteBatch accepts a fully prevalidated, versioned POD command array through one C ABI call. The initial state set is explicitly fixed to XNA defaults, textures are retained through successful End, active destruction cancels safely, and native NotSupportedException maps to CNA_RESULT_NOT_SUPPORTED; HEADLESS and SDL_RENDERER C tests cover state, validation, lifetime and stale handles. |
| CBIND-025 | Expose input as snapshots | ✅ | input.h freezes all 160 canonical Keys identities and captures a fresh canonical 256-key KeyboardState POD per call on the active game's creation thread. Key tests and ascending count/copy are runtime-free POD helpers valid on any thread; full-array, invalid-key, no-partial-copy and wrong-thread behavior is C-tested under HEADLESS and SDL_RENDERER. No live input object, per-key native call or callback crosses the ABI; mouse/game-pad/touch remain in the already planned expanded-input task. |
| CBIND-026 | Validate 2D results through C | ✅ | The strict-C lifecycle program creates and uploads a 2×2 RGBA texture, submits deterministic SpriteBatch commands and uses a versioned logical-backbuffer descriptor plus full RGBA8 count/copy readback. HEADLESS proves CNA_RESULT_NOT_SUPPORTED with untouched output; SDL_RENDERER proves exact red/green/blue texture pixels and an untouched clear pixel before presentation. |
| CBIND-027 | Document the initial C API feature matrix | ✅ | docs/c-api/FEATURE_MATRIX.md publishes the exact experimental 0.1 function families, HEADLESS/SDL_RENDERER evidence, ownership/thread/capacity/error behavior and explicit unavailable families. It distinguishes enumerated renderer identities from tested support and repeatedly states that the slice is not complete CNA/XNA coverage. |
B4 gate: a pure C 2D application can create a game, upload a texture, submit a batched draw, read an input snapshot and release all owned resources under at least one real renderer plus the HEADLESS deterministic control.
Each row begins only after a concrete C application needs it. APIs remain compact and semantic; they do not export C++ collections or attempt to mirror C++ overload sets mechanically.
| # | Task | Status | Acceptance criteria |
|---|---|---|---|
| CBIND-028 | Expose ContentManager minimum |
✅ | content.h owns a game-child native manager created from the callback-scoped device, copies/counts its UTF-8 root, exposes explicit cache unload/destroy and provides the first approved typed load for Color Texture2D. Every successful load returns a new independently owned existing C texture handle that survives manager unload/destruction; missing assets and invalid names map predictably, and no native path, stream, service provider or template type crosses the ABI. HEADLESS and SDL_RENDERER strict-C tests cover pixels, cache/unload lifetime, parent order, stale handles and thread/UTF-8/capacity failures. |
| CBIND-029 | Expose expanded input snapshots | ✅ | input.h now freezes fixed-layout mouse, four-player gamepad and eight-location touch snapshots. Capture is fresh and creation-thread-bound; disconnected devices return successful rest/empty values. Both native GamePad state overloads, all three dead-zone modes and all 31 current button bits are mapped, with exact pure-POD normalization/button helpers. Touch capability/state includes previous locations and CNA pressure plus local FindById/TryGetPrevious behavior. Strict-C HEADLESS and SDL_RENDERER tests cover all player/mode paths, synthetic numeric edge cases, absence, invalid inputs and wrong-thread refusal; ABI layout tests freeze every new POD. |
| CBIND-030 | Expose minimal audio resource/control surface | ✅ | audio.h maps canonical channel/state identities and a concrete owned PCM16LE SoundEffect → controllable SoundEffectInstance route: duration, play/pause/resume, immediate/release-tail stop, volume/pitch/pan/loop/info and explicit destruction. Bytes are copied; instance-before-effect-before-game ordering is enforced; all public calls are creation-thread-bound while the internal mixer keeps no C callback/context. No-device creation maps to NOT_SUPPORTED, native track disposal defines return-time handle invalidation, and strict-C dummy-audio tests freeze layout, validation, transitions, stale handles, parent order and wrong-thread refusal. |
| CBIND-031 | Add pure-C content/audio regression programs | ✅ | ContentSmoke.c now loads its exact pixel fixture through a real valid non-ASCII UTF-8 filename while retaining malformed/embedded-NUL, missing-file IO, cache and ownership coverage. AudioSmoke.c covers the successful dummy-device lifecycle, and isolated AudioUnavailableSmoke.c forces a nonexistent SDL driver twice to prove stable NOT_SUPPORTED, invalid output handles, structured diagnostics, no leaked child count and clean game shutdown. The same strict-C programs pass with HEADLESS and SDL_RENDERER and depend on no future language binding. |
| CBIND-032 | Extend capability reporting | ✅ | Graphics renderer identity plus all 13 native graphics capabilities, touch connection/count and now native audio playback availability have stable versioned C query routes. cna_audio_get_capabilities probes CNA's real process-wide mixer, returns SUCCESS plus false when no device can open, creates no owned C resource, and preserves argument/handle/thread failures. Strict-C dummy and deliberately invalid audio drivers prove both outcomes under HEADLESS and SDL_RENDERER; fixed layout and zeroed reserves are ABI-tested. |
This phase is the commitment to complete coverage of the public CNA surface. Each API family still needs a C-native design review: complete coverage never permits a raw C++ ABI leak or an untested mechanical wrapper.
| # | Task | Status | Acceptance criteria |
|---|---|---|---|
| CBIND-033 | Inventory the complete public CNA surface | ✅ | Doxygen-backed tools/c-api/generate_coverage_inventory.py deterministically tracks all 414 public headers and 6,415 public/protected declarations while explicitly excluding 95 Internal/Detail headers and the C API itself. coverage_mappings.json assigns reviewed current mappings; every remaining symbol has a C-native mapping proposal, test obligation, status and owner task. The snapshot records 443 implemented, 21 partial, 5,881 planned and 70 explicitly deleted/not-applicable declarations; --check proves drift without prematurely wiring the CBIND-043 CI gate. |
| CBIND-034 | Add render targets, sprite fonts and graphics state coverage | ✅ | graphics_state.h, display.h, render_target.h and sprite_font.h map every inventory row in this family: fixed identities and complete state PODs/presets/device round-trips, sampler slots and explicit-state SpriteBatch Begin; display/adapter/presentation values and safe native-handle/refresh limitations; owned 2D/cube targets with applied-property snapshots and atomic singular/MRT binding; copied glyph SpriteFonts retaining their source Texture2D. Strict-C HEADLESS and SDL_RENDERER tests cover ABI layouts, properties, UTF-8, ownership, stale/wrong-thread handles, real 2D binding and honest unavailable-backend paths. The inventory now records 814 implemented, 21 partial, 5,510 planned and 70 not applicable rows, with no planned CBIND-034 row. |
| CBIND-035 | Add 3D resources, effects, models and draw-submission coverage | ✅ | Design C-native vertex/index data layouts, effects/model/state handles and bulk submissions for all public APIs in these families. Require real-renderer correctness tests; do not claim all renderer parity from structural tests. Work is decomposed into CBIND-035A–G below; the parent becomes complete only when all seven rows and every CBIND-035 inventory row are closed. |
| CBIND-036 | Add stream, storage, networking and asynchronous-operation coverage | ✅ | Define stream callbacks, storage/network objects and neutral operation handles where the canonical API needs them, with documented ownership, thread, cancellation and error conversion. Never expose System::IO::Stream, Task, std::future or a C++ pointer. Work is decomposed into CBIND-036A–E below; the parent becomes complete only when all five rows and every CBIND-036 inventory row are closed. Closed by CBIND-036E5: no planned storage, content or net inventory row remains, and the snapshot is 3,841 implemented, 30 partial, 2,428 planned and 116 not applicable. Sanitizer evidence matches the CBIND-035B–E bar: all 50 C API tests pass under a combined ASan+UBSan SOFTWARE/CNAEXT build (cmake-build-binding-asan) with leak detection enabled, so the storage, content and network slices report no leak, no invalid access and no undefined behavior. |
| CBIND-037 | Add collections, events, services, media and devices coverage | 🟨 | Map every public collection/event/service/media/device API to count/copy, stable-handle or callback forms. Prohibit public container layouts and test mutation, capacity, ownership and thread rules. Work is decomposed into CBIND-037A–G below; the parent becomes complete only when all seven rows and every CBIND-037 inventory row are closed. |
| CBIND-043 | Maintain a machine-checked coverage gate | ✅ | A CI checker compares the public-header inventory to COVERAGE.md and fails if a public type/member/constant/event has no mapping/status. New C++ public API cannot land without its C API row and tests in the same change. Done: the matrix is a gate now, not a report. The generator and its --check mode already existed; docs/c-api/COVERAGE.md itself said making it mandatory was reserved for this task. Two places enforce it: the CTest test CApiCoverageMatrix (8 s, registered under CNA_BUILD_TESTS — deliberately NOT under CNA_BUILD_C_API, since the check compiles nothing and the rule is about the C++ surface, so gating it on the C API being built would mean the ordinary build never notices an unmapped symbol), and .github/workflows/c-api-coverage-gate.yml, which is build-free and therefore cheap enough to run on every push. Proven to catch the thing it exists for: adding one public declaration to CNAHelper.hpp turns the check from pass to "Coverage inventory is stale", naming the command that fixes it. The CI job also asserts the generator is DETERMINISTIC — --write must be a no-op after --check passes — because a generator that churns makes the matrix unreviewable; verified locally. |
| CBIND-044A | Complete the SpriteBatch Begin overloads | ✅ | The first of CBIND-044's partial rows to become a route rather than a recorded limitation, and the one a 2D consumer actually misses: cna_sprite_batch_begin_with_effect covers both canonical overloads that take a custom Effect, with the transform overload expressed by the same route because the difference between them is exactly what a null argument means. A null transform_matrix is the identity the effect-only overload uses; CNA_INVALID_HANDLE selects the default sprite effect, which is what a null Effect* means to the canonical call. The transform is validated before any handle is touched, so a non-finite component is refused as an argument failure rather than after the batch has begun, and an effect belonging to a different game is refused rather than silently drawn with. SpriteBatch::Begin's five symbols move from partial to implemented and the snapshot becomes 6,072 implemented, 23 partial, 0 planned, 320 not-applicable. The ABI baseline gate did its job on the way: the new export turned up as an addition, which the evolution policy permits, rather than as a break — re-recorded, 2,721 exports. Green in all four trees, 88/88 with every gate. |
| CBIND-044B | Complete the SpriteBatch Draw overloads | ✅ | The largest remaining partial group, ten symbols, closed by adding the half of the canonical Draw family the ABI could not express: placing a sprite by position and scale rather than by destination rectangle. CNA_SpriteScaledCommand and cna_sprite_batch_submit_scaled_many are a new structure and a new route rather than more fields on the existing command, for two reasons that both matter. With a position the origin is measured in source-texture pixels and the scale applies after that offset, so a caller cannot reach it by computing a rectangle without repeating the canonical arithmetic; and appending fields would have changed sizeof(CNA_SpriteCommand), which the ABI baseline would have reported as a break and which would have forced a minor-version bump — evolution path 1 (a new name) costs nothing, path 2 (append) would have cost the promise. A source rectangle of zero width and height is the empty optional that draws the whole texture; equal scale components are the uniform-scale overload, so both scale forms are one route. Every field is validated before anything is submitted. The snapshot becomes 6,082 implemented, 13 partial, 0 planned, 320 not-applicable; the baseline records the new struct and route as additions (167 structs, 2,722 exports) and the hand-written ABI walls pin the new layout in both languages. Green in all four trees, 81/81 and 88/88. |
| CBIND-044C | Complete the signed-in gamer collection | ✅ | The last partial row that could become a route rather than a recorded limitation. Gamer::getSignedInGamersProperty returns a collection object, and giving it a handle would mean generalizing the whole cna_gamer_collection_* family to a second element type for the sake of one property — so the ABI names what a caller does with it instead, which is the same judgment GameServiceContainer got: cna_gamer_get_signed_in_gamer_at for the positional indexer, cna_gamer_signed_in_index_of and cna_gamer_signed_in_contains join the count and the existing PlayerIndex lookup. Position and player index are different questions with different answers, which is why they stay different routes. Not being in the collection is an answer, not a failure: index_of succeeds and reports -1, exactly as the canonical IndexOf does. The test re-learned a trap this campaign already recorded — a refused lookup clears its output handle first, so the refusal probes must take a handle of their own or they destroy the one under test. The snapshot becomes 6,083 implemented, 12 partial, 0 planned, 320 not-applicable, and every one of the 12 is now a structural impossibility rather than unfinished work. Green in all four trees, 81/81 and 88/88. |
| CBIND-044D | Record a disposition for every remaining limitation | ✅ | The mechanical half of CBIND-044. Each of the 8 remaining partial rules now carries a disposition — what kind of limitation it is (C cannot name a C++ type, the value is a Sharp Runtime object, the value is type-erased, the canonical value is a proxy) and the callable route a caller uses instead — and generate_limitations.py fails if a partial row lacks one. That is what "no unspecified omission" can be made to mean mechanically: not that nothing is missing, but that nothing is missing silently. LIMITATIONS.md's partial table is now three columns a consumer can act on rather than a wall of prose. Verified to fail by deleting one disposition. What remains is not implementation but a decision: the 12 symbols are recorded as awaiting owner approval. |
| CBIND-044 | Close the public API coverage matrix | ✅ | Closed 2026-08-16. Every row is implemented and tested, or carries an owner-approved native limitation with a callable C API that reports it. CBIND-044A–C turned the last three implementable rows into routes; CBIND-044D gave each remaining group a recorded disposition and the route that reports it; and the project owner approved the twelve on 2026-08-16 — four GameServiceContainer lookups and ContentManager::Load (C cannot name a C++ type), four content-manager service-provider members (the value is a Sharp Runtime object), two untyped content reads (the value is type-erased) and the network-session properties indexer (the canonical value is a proxy). The approval is measured, not minuted: generate_limitations.py fails if a partial row lacks a disposition or an approval, and the release gate's coverage-closed criterion fails if any partial mapping is unapproved — verified by clearing one, which turns both red. Final snapshot: 6,415 public C++ declarations, 6,083 implemented, 12 approved partial, 0 planned, 320 not applicable. This closes Phase B7 and the CBIND campaign. |
| # | Task | Status | Acceptance criteria |
|---|---|---|---|
| CBIND-035A | Establish 3D value and identity ABI | ✅ | math_values.h now defines fixed-layout Point, Vector4, Quaternion, Matrix, Plane, Ray and bounding-volume PODs, all 17 public PackedVector raw-storage aliases and stable containment/plane/curve identities. graphics3d.h freezes buffer/index/primitive/SetData/vertex identities and the four-field CNA_VertexElement. Strict C17 and C++23 assertions cover every represented storage width, representative/full field offsets and identity ordinals under HEADLESS and SDL_RENDERER. Coverage maps only the 169 directly represented type/field/property/identity rows; all constructors, constants and operations remain owned by CBIND-035B. |
| CBIND-035B | Complete math, geometry and packed-value operations | ✅ | Every public math and PackedVector row is mapped through fixed values, validated handles or C-native scalar/bulk operations. Numeric, IEEE, lifetime, capacity, aliasing and failure behavior is covered in strict C under HEADLESS and SDL_RENDERER plus focused ASan+UBSan runs. Completed as CBIND-035B1–B7. |
| CBIND-035C | Add texture, buffer and vertex-resource coverage | ✅ | texture.h, texture_volume.h, vertex_values.h, vertex_resources.h, index_resources.h and the common graphics_resource.h map all 402 owned rows through fixed values, generation/type/thread-validated handles, caller-window transfers and explicit backend limits. Decomposed into and completed as CBIND-035C1–C7. |
| CBIND-035D | Add effects, shaders and parameter coverage | ✅ | All 653 Effect/technique/pass/parameter/annotation, stock/custom effect and shader/material rows are mapped without exposing bytecode objects, C++ containers or backend pointers. Completed by CBIND-035D1–D9 with strict-C HEADLESS/SDL_RENDERER and focused sanitizer evidence. |
| CBIND-035E | Add model, mesh and animation coverage | ✅ | Model/bone/mesh/part collections, morph and both skeletal-animation paths are mapped through stable handles, deep-copied descriptors, deterministic count/copy operations and tested resource lifetimes. |
| CBIND-035F | Complete graphics-device and draw submission | ✅ | Map remaining device properties/events/clear/present/draw overloads, viewport/scissor, texture collections and SpriteBatch transform/effect/text routes using validated descriptors and bulk submissions. Work is decomposed into CBIND-035F1–F7 below; the parent becomes complete only when all seven rows are closed. |
| CBIND-035G | Close and verify CBIND-035 | ✅ | No planned CBIND-035 inventory row remains: the snapshot is 3,476 implemented, 23 partial, 2,843 planned and 73 not applicable, and every remaining planned row belongs to CBIND-036, CBIND-037 or CBIND-044. Draw3DSmoke.c adds the missing real-output evidence: on a backend without the 3D capability it asserts deterministic refusal of buffer creation and all five draw routes, and on the CPU-raster SOFTWARE backend it clears to a known color and proves the center pixel changed through four independent routes — converted user primitives, indexed user primitives, buffered indexed geometry, and a full owned Model whose mesh part references real vertex/index buffers and a BasicEffect. Pixel readback is treated as a capability separate from 3D, so HEADLESS draws without claiming pixel evidence. Adding the third tree exposed three suites that branched on renderer identity rather than capability, contradicting this project's own rule that an enumerated identity is not a support claim; CApi_TextureSmoke, CApi_TextureVolumeSmoke and CApi_LifecycleSmoke now probe the actual behavior instead, which also turned SOFTWARE's real cube storage, mip upload and exact drawn texels into new positive evidence. All three trees are green at 47/47. This closes parent CBIND-035. |
| # | Task | Status | Acceptance criteria |
|---|---|---|---|
| CBIND-035B1 | Complete Point and Rectangle operations | ✅ | math.h maps both complete source headers through 37 exported operations covering constructors, named zero/empty values, every property/overload/operator, hashes and exact UTF-8 count/copy strings. Unsigned-bit arithmetic preserves C# unchecked 32-bit wraparound without C++ signed-overflow UB; division rejects zero and the unrepresentable minimum/-1 quotient without partial output. MathValuesSmoke.c calls every entry point and covers boundaries, mutation, half-open containment, intersection/union, capacity and null failures under HEADLESS, SDL_RENDERER and ASan+UBSan. |
| CBIND-035B2 | Complete MathHelper and Vector2/3/4 operations | ✅ | math.h and vectors.h map every MathHelper and Vector2/3/4 public inventory row through exact constants and fallible scalar/value/bulk operations. All overload-equivalent, finite/non-finite, exact-string, null/range-atomicity and sequential-aliasing contracts are covered in strict C under HEADLESS, SDL_RENDERER and ASan+UBSan. Completed as CBIND-035B2a–B2d. |
| CBIND-035B3 | Complete Quaternion and Matrix operations | ✅ | quaternion.h and matrix.h map every remaining public row through 85 fallible operations. Both constructors/constants/properties and all member/static/operator math, decomposition/interpolation/transformation/factory routes are covered with row-major, singular, projection-failure, non-finite and aliasing evidence under both backends and ASan+UBSan. Completed as CBIND-035B3a–B3b. |
| CBIND-035B4 | Complete planes, rays and bounding-volume operations | ✅ | geometry.h maps every remaining Plane, Ray, BoundingBox, BoundingSphere and BoundingFrustum row through C-native values, explicit optional hits and caller-capacity corners. Strict-C HEADLESS/SDL_RENDERER and ASan+UBSan tests cover every exported operation, including atomic capacity/failure paths and the canonical unsupported boundary-ray case. Completed as CBIND-035B4a–B4d. |
| CBIND-035B5 | Complete Curve value, collection and evaluation operations | ✅ | curve.h maps all 60 Curve, CurveKey and CurveKeyCollection rows through fixed values and validated handles without leaking C++ containers. Ordered collection mutation, retained mutable key views, all loop/evaluation/tangent behavior and lifetime/error boundaries are covered in strict C under both backends and ASan+UBSan. Completed as CBIND-035B5a–B5c. |
| CBIND-035B6 | Complete Color operations and named constants | ✅ | color.h and named_colors.h map the complete 175-row Color header through the four-byte POD, direct channels, 24 operations and 141 directly usable named value expressions. Every packed value is checked independently and all value/error behavior passes strict C under both backends and ASan+UBSan. Completed as CBIND-035B6a–B6b. |
| CBIND-035B7 | Complete PackedVector operations and close math coverage | ✅ | packed_vectors.h maps all 132 remaining concrete PackedVector, HalfTypeHelper and IPackedVector rows through 17 stable format identities, four generic format-tagged pack/unpack/equality operations and three half conversions. Raw/default constructors remain direct fixed-width values; specialized scalar/Vector2 routes collapse to the matching generic output. Integer formats reject non-finite consumed components before native conversion, half formats preserve IEEE special values, and narrower raw values reject upper bits without output mutation. PackedVectorSmoke.c covers every operation and format under both backends and ASan+UBSan; C/C++ assertions freeze every identity. This closes parent CBIND-035B. |
The 402 rows owned by CBIND-035C are partitioned once by dependency boundary so each slice is reviewable and independently committable. The counts below are inventory rows, not exported function counts.
| # | Rows | Task | Status | Acceptance criteria |
|---|---|---|---|---|
| CBIND-035C1 | 104 | Complete built-in vertex values | ✅ | vertex_values.h maps the seven built-in VertexPosition* structures, all remaining VertexElement operations and IVertexType declaration routes through seven fixed POD layouts, a stable type tag and generic default/equality/hash/string/stride/element-copy operations. Parameterized constructors remain aggregate initialization. Strict C17 tests cover every operation/type, exact native strings and canonical GPU declarations; C/C++ assertions freeze identities, sizes and offsets under HEADLESS and SDL_RENDERER plus ASan+UBSan. |
| CBIND-035C2 | 14 | Complete vertex declarations and bindings | ✅ | vertex_resources.h owns standalone declarations through generation/type/thread-validated handles and maps empty, computed-stride and explicit-stride construction, exact type names, stride and atomic element copies without exposing native vectors. CNA_VertexBufferBinding is a fixed 16-byte descriptor: a zero aggregate is the default and its initializer validates a nonzero future buffer token plus nonnegative offset/frequency; actual token kind/generation remains a required consumption-time check in C6/F. Strict-C tests cover every route, invalid elements/ranges, capacity, wrong-kind/stale/wrong-thread handles and lifetime under both backends and ASan+UBSan; C/C++ assertions freeze both handle and descriptor layouts. |
| CBIND-035C3 | 21 | Complete the GraphicsResource common contract | ✅ | graphics_resource.h maps the complete base contract for Texture2D, RenderTarget2D, RenderTargetCube and VertexDeclaration handles: callback-scoped owning-device identity, disposal state and idempotent disposal, exact validated UTF-8 Name/ToString count-copy, a fixed C-owned 64-bit opaque tag and synchronous Disposing subscriptions with owned registration handles. Native System::Object* Tag and protected base construction/copy/move remain encapsulated. Strict-C tests cover every route across standalone and device-owned resources, generic/typed disposal, post-destroy unsubscription, capacity/encoding failures and wrong-kind/stale/wrong-thread handles under both backends plus ASan+UBSan; registry tests prove tag reset and C/C++ assertions freeze both public scalar handles. |
| CBIND-035C4 | 134 | Complete Texture and Texture2D | ✅ | texture.h completes all 134 previously unfinished rows plus the two inherited partial Texture properties through standalone/game-owned default, device, file, RGBA8, CPU-only and encoded-memory factories; common/2D/storage snapshots; all 18 native typed full/mip/rectangle transfer representations; the direct raw-RGBA8 SetDataRGBA route; format/block/alignment validation; exact type text; and PNG/JPEG count-copy/file routes. Streams and native renderer/weak pointers stay behind the ABI. Strict-C tests cover every route, all 27 formats, dispatch/rejection for every transfer identity, image/file round-trips, lifecycle and atomic failure cases under HEADLESS and SDL_RENDERER; HEADLESS proves mip upload, SDL maps its native mip-upload limit to NOT_SUPPORTED, and ASan+UBSan is clean. |
| CBIND-035C5 | 40 | Complete Texture3D and TextureCube | ✅ | texture_volume.h maps all 40 rows through owned game-child handles, versioned 3D/cube snapshots and full mip/box or six-face/rectangle Color transfer descriptors, including raw Texture3D bytes, exact type text, copied-memory DDS decoding, common Texture/GraphicsResource operations and RenderTargetCube inheritance. Native streams and renderer pointers stay private. The strict-C suite covers all faces, regions, capacity atomicity, lifecycle and invalid/stale/wrong-kind/wrong-thread paths under HEADLESS and SDL_RENDERER; both backends truthfully reject Texture3D creation and cube storage, while ASan+UBSan is clean. |
| CBIND-035C6 | 57 | Complete vertex buffers | ✅ | vertex_resources.h maps all 57 static/dynamic VertexBuffer rows through owned game-child handles, copied declaration metadata, versioned info and caller-array transfers for all seven built-in vertex types, raw bytes, all four dynamic option overloads, exact type text, generic GraphicsResource state and a distinct owned ContentLost registration. Count and window overloads preserve caller-array semantics and atomic readback without exposing CPU shadows or renderer pointers. Strict-C HEADLESS tests cover every value/option route, WriteOnly, disposal/events, capacity and invalid/stale/wrong-kind/wrong-thread paths; SDL_RENDERER verifies its no-3D capability as atomic NOT_SUPPORTED, and ASan+UBSan is clean. |
| CBIND-035C7 | 32 | Complete index buffers | ✅ | index_resources.h maps all 32 static/dynamic IndexBuffer rows through owned game-child handles, versioned metadata and caller-array transfer descriptors for both 16- and 32-bit indices, all dynamic streaming options, exact type text, generic GraphicsResource state and a distinct owned ContentLost registration. Count and window overloads preserve caller-array semantics with copied/aligned input and atomic scratch readback. Strict-C HEADLESS tests cover both widths/kinds/options, WriteOnly, disposal/events, capacity and invalid/stale/wrong-kind/wrong-thread paths; SDL_RENDERER verifies its no-3D capability as atomic NOT_SUPPORTED, C/C++ ABI assertions freeze all descriptors and ASan+UBSan is clean. This closes parent CBIND-035C. |
The 653 rows owned by CBIND-035D are partitioned by dependency boundary. Collection and stock effect slices build only on the earlier identity/value/handle contracts; no slice exposes native containers, shader objects or renderer pointers.
| # | Rows | Task | Status | Acceptance criteria |
|---|---|---|---|---|
| CBIND-035D1 | 17 | Establish effect-parameter identities | ✅ | effects.h defines fixed-width CNA_EffectParameterClass and CNA_EffectParameterType identities with all five class and ten type constants at their native ordinals. Strict C17 and C++23 assertions cover every value and storage width under HEADLESS and SDL_RENDERER. |
| CBIND-035D2 | 30 | Complete effect annotations | ✅ | effects.h maps all EffectAnnotation/Collection rows through owned immutable annotation handles created from copied UTF-8/raw-value metadata and owned mutable collection snapshots. Versioned info, exact strings, every scalar/vector/matrix getter and copied count/index/name operations preserve empty/default/native bit-storage behavior without exposing references, vectors or iterators. Strict-C tests cover all operations, collection-copy independence, capacity atomicity and invalid/stale/wrong-kind/wrong-thread paths under both backends plus ASan+UBSan; C/C++ assertions freeze handle and descriptor layouts. |
| CBIND-035D3 | 84 | Complete effect parameters | ✅ | effects.h maps all EffectParameter/Collection rows through owned mutable handles and stable collection-element aliases. Versioned copied metadata, exact strings, tagged scalar/array values, distinct matrix-transpose and texture overload dispatch, nested element/member/annotation views and count/index/name/semantic operations expose no C++ references, vectors or iterators. Assigned texture handles are retained per overload slot. Strict-C tests cover every value family, defaults, stable growth/destruction aliases, nesting, Texture2D retention, capacity atomicity and invalid/stale/wrong-kind/wrong-thread paths under both backends plus ASan+UBSan; C/C++ assertions freeze descriptor and tag layouts. |
| CBIND-035D4 | 67 | Complete techniques, passes and collections | ✅ | effects.h maps EffectTechnique/EffectPass and both collection families through owned handles and stable collection-element aliases. Both technique constructors, canonical P0, exact names, non-pointer identities, nested pass/annotation views, canonical Apply dispatch and construction-plus-add/count/index/name operations replace owner pointers, references, vectors and iterators. Strict-C tests cover construction, identity, ownerless native Apply, nesting, stable aliases across growth/destruction, capacity atomicity and invalid/stale/wrong-kind/wrong-thread paths under both backends plus ASan+UBSan; effect-owned current-technique validation uses the same route once D5 supplies effect lifecycle handles. |
| CBIND-035D5 | 70 | Complete Effect, ShaderEffect, EffectMaterial and SpriteEffect | ✅ | effects.h maps all 68 callable rows plus the two explicitly deleted/non-callable copy operations through owned game-child CNA_EffectHandle values: a minimal concrete base adapter, native EffectMaterial/ShaderEffect/SpriteEffect construction, same-type clone, dispose/apply, borrowed device identity, stable parameter/technique/current-technique views, exact type/source strings, shader validity/renderer queries, all uniform/texture/matrix routes and exact stock-sprite recognition. Compiled XNA .fx bytecode returns the native callable NOT_SUPPORTED limitation; renderer pointers/GpuDrawParams remain private behind Apply/draw paths. Strict-C tests cover lifecycle, current-pass validation, transitive descendant lifetime after parent destruction, texture retention, all shader calls and invalid/stale/wrong-kind/wrong-thread paths under both backends plus ASan+UBSan; C/C++ assertions freeze the handle. |
| CBIND-035D6 | 90 | Complete BasicEffect, DirectionalLight and effect interfaces | ✅ | effects.h maps all 90 BasicEffect, DirectionalLight and IEffectMatrices/Fog/Lights rows through owned BasicEffect handles, standalone or stable nested directional-light handles and generic interface operations reusable by later stock effects. All transform, fog, lighting, vertex-color, material, alpha, texture and per-pixel properties plus exact three-light/default-lighting behavior are exposed. Same-device Texture2D assignments are retained and cloned safely; live nested light aliases transitively retain their effect and game after the parent handle is destroyed. Renderer-only GpuDrawParams stays behind Apply/draw. Strict-C tests cover exact defaults/preset constants, every operation, clone/retention/lifetime and invalid/stale/wrong-kind/wrong-thread paths under both backends plus ASan+UBSan; C/C++ assertions freeze the light handle. |
| CBIND-035D7 | 114 | Complete AlphaTest, DualTexture and EnvironmentMap effects | ✅ | effects.h maps all 114 AlphaTestEffect, DualTextureEffect and EnvironmentMapEffect rows through owned game-child effect handles, shared lifecycle/type/matrix/fog/light routes and complete concrete material, alpha-test, two-layer and environment-map state. Texture2D and TextureCube assignments require the same graphics device, retain their C resources per slot and are copied independently into native clones; invalid enums/bools/indices are rejected while native unclamped signed ReferenceAlpha and stock scalar behavior are preserved. EnvironmentMapEffect's always-on lighting maps a false setter to INVALID_STATE, and renderer-only GpuDrawParams stays behind Apply/draw. Strict-C tests cover exact defaults, every operation, clone/retention/lifetime, cross-owner refusal and invalid/stale/wrong-kind/wrong-thread paths under both backends plus ASan+UBSan. |
| CBIND-035D8 | 52 | Complete SkinnedEffect | ✅ | effects.h maps all 52 SkinnedEffect rows through an owned game-child effect, the shared lifecycle/type/matrix/fog/light routes and complete material, per-pixel, texture, weights and CNA vertex-color state. CNA_SKINNED_EFFECT_MAX_BONES freezes the native 72-matrix maximum; copied set and atomic count/capacity copy operations preserve the native one-through-72 bounds and identity defaults without exposing vectors. Texture2D assignments require the same graphics device and retain independently across clones; always-on lighting maps false to INVALID_STATE, while GpuDrawParams remains behind Apply/draw. Strict-C tests cover all defaults, every operation, exact bone transfer, bounds/capacity errors, texture clone retention, nested-light lifetime and invalid/stale/wrong-kind/wrong-thread paths under both backends plus ASan+UBSan; C/C++ assertions freeze MaxBones. |
| CBIND-035D9 | 129 | Complete ColorMatrix, PbrEffect and SkinnedPbrEffect extensions | ✅ | effects.h maps all 129 extension rows through owned game-child effects, a fixed 64-byte row-major color matrix, shared PBR material/fog/light/matrix routes, five retained Texture2D slots and a fixed 72-bone SkinnedPbr palette. Matrix/offset inputs reject non-finite values; always-on PBR lighting maps false to INVALID_STATE; copied palette operations preserve one-through-72 bounds and atomic capacity behavior. Strict-C tests cover exact defaults, all operations, clone/resource retention, nested-light lifetime and invalid/stale/wrong-kind/wrong-thread paths under HEADLESS and SDL_RENDERER plus ASan+UBSan; C/C++ assertions freeze the POD, slot identities and MaxBones. This closes parent CBIND-035D. |
The 178 rows owned by CBIND-035E are partitioned by stable-handle dependency. Bone, part and mesh views land before aggregate Model ownership; standalone morph/skinning data then supports the animation-player layer. Native containers and iterators collapse to live count/index/name views or copied bulk transfers.
| # | Rows | Task | Status | Acceptance criteria |
|---|---|---|---|---|
| CBIND-035E1 | 23 | Complete ModelBone and ModelBoneCollection | ✅ | models.h maps standalone and hierarchy-owned bones through stable handles, exact UTF-8 names, signed indices, copied transforms, optional parent views, retained child relationships and live count/index/name/contains collections. Self-parenting and ancestor cycles are rejected; weak parent metadata prevents a retained child from exposing a dangling native pointer. Strict-C tests cover every route, hierarchy lifetime and invalid/stale/wrong-kind/wrong-thread behavior under HEADLESS and SDL_RENDERER plus ASan+UBSan; C/C++ assertions freeze both handles. |
| CBIND-035E2 | 28 | Complete ModelMeshPart and its collection | ✅ | models.h maps both constructors, exact signed scalar state, optional same-device Effect/VertexBuffer/IndexBuffer associations and a C-owned opaque 64-bit tag through stable shared handles. Snapshot collections retain parts and replace native pointers/iterators with count/index aliases; the representation is ready for ModelMesh-owned live views in E3. Retained graphics resources reject typed destroy and generic dispose until cleared or the final part alias is released. Strict-C tests cover state, alias/snapshot lifetime, handle/thread/array errors and supported-buffer or honest renderer-refusal paths under HEADLESS and SDL_RENDERER plus ASan+UBSan; C/C++ assertions freeze both handles and the tag. |
| CBIND-035E3 | 38 | Complete ModelMesh, ModelMeshCollection and ModelEffectCollection | ✅ | models.h maps both game-child ModelMesh constructors, exact UTF-8 names, bounding sphere/tag/retained-parent state, live part/effect views and capability-gated Draw. Retained mesh snapshots expose count/index/find/contains aliases; live effect views preserve duplicate Add, first-match Remove and identity while blocking early effect disposal. Parts belong to one live mesh and switch to synchronized detached native state when its final owner expires, preventing dangling parent access. Strict-C tests cover every route, transitive aliases, resource lifetime, invalid inputs/threading and HEADLESS success versus SDL_RENDERER Draw refusal plus ASan+UBSan; C/C++ assertions freeze all handles/tags. |
| CBIND-035E4 | 14 | Complete Model | ✅ | models.h maps the default, aggregate and explicit parent/root constructors through CNA_ModelHandle; bone/mesh/root aliases retain stable objects, tags are opaque 64-bit values, and native shared_ptr<void> ownership becomes a C context/release callback with deterministic replacement/clear/destruction. Count/copy local and absolute transforms are capacity-atomic, transform input is copied before mutation, and non-empty Draw is capability-gated. Strict-C tests cover every route, parent composition, transitive lifetime, invalid arrays/root/counts, callback releases and thread/renderer errors under HEADLESS and SDL_RENDERER plus ASan+UBSan; C/C++ assertions freeze handle/tag widths. |
| CBIND-035E5 | 20 | Complete morph-target extension values and operations | ✅ | models.h maps keyframes, tracks and target deltas through fixed deep-copied descriptors and owns validated MorphTargetDataEXT handles. Atomic count/copy routes expose every nested field without C++ vectors; mutable weights/tracks, LINEAR/STEP/Hermite evaluation, base-byte blending, retained ModelMeshPart attachment and supported VertexBuffer upload map all native operations. Strict-C tests cover ABI, exact blend/evaluation math, malformed shapes/flags/times/counts, capacity atomicity, lifetime and handle/thread errors under HEADLESS and SDL_RENDERER plus ASan+UBSan. |
| CBIND-035E6 | 36 | Complete SkinnedModelEXT | ✅ | CNA_SkinnedModelEXTHandle deeply copies fixed skeleton/keyframe/track/clip descriptors, exposes deterministic count/copy and native transform-sampling routes, and supports normalized native move semantics. A stable lifetime sidecar retains same-device VertexBuffer/IndexBuffer/ModelMeshPart/optional Texture2D resources, blocks premature disposal and maps ordered part access, replace-by-name attach/remove and owned counts. Strict-C tests cover all layouts and operations, exact interpolation/clamp/loop math, validation/capacity atomicity, moves, lifetime and handle/thread failures under HEADLESS and SDL_RENDERER on dummy virtual video plus ASan+UBSan. |
| CBIND-035E7 | 19 | Complete SkinningData and AnimationPlayer | ✅ | CNA_SkinningDataHandle deeply copies validated hierarchy/bind/inverse-bind/root-prefix/named-clip state and exposes type, deterministic clip and atomic field copies. CNA_AnimationPlayerHandle retains data, starts exact named clips, maps relative/absolute loop/clamp Update and exposes position/current clip plus atomic local/world/skin matrices. Strict-C tests cover every route, deep-copy/lifetime, exact prefix/interpolation composition, capacity and input/handle/thread failures under HEADLESS and SDL_RENDERER plus ASan+UBSan; this closes parent CBIND-035E. |
The 406 rows owned by CBIND-036 are partitioned once by dependency boundary. One boundary was
corrected while implementing: LocalNetworkGamer moved from CBIND-036D to CBIND-036E, because its
receive and send paths dereference the owning session, so the 65/104 split became 47/122. Storage lands first
because it is self-contained and introduces the C stream contract every later file-facing row
reuses; content follows; the networking families are ordered so identities, values and packet
buffers exist before the session and gamer objects that consume them.
| # | Rows | Task | Status | Acceptance criteria |
|---|---|---|---|---|
| CBIND-036A | 42 | Complete storage devices, containers and file streams | ✅ | storage.h and CnaCApiStorage.cpp map every storage row: owned CNA_StorageDeviceHandle, CNA_StorageContainerHandle and CNA_StorageStreamHandle families that nest strictly and refuse destruction while a child is live; free/total space, connection state, both events (the static DeviceChanged without a device handle, per-instance Disposing through one), container display/type-name count-copy, directory and file create/exists/delete, both listing overloads as count plus indexed copy with an empty pattern selecting the no-argument overload, CreateFile and all three OpenFile overloads, and container deletion keeping the canonical containment guard. All four BeginShowSelector/EndShowSelector pairs and BeginOpenContainer/EndOpenContainer collapse into single synchronous calls that still invoke the canonical completion callback, so no System::IAsyncResult or invented operation handle exists in C. System::IO::Stream stays behind the adapter; wider-than-Int32 counts are refused rather than truncated and stream capabilities are queried, not inferred. filesystem_error, System::IO::IOException and StorageDeviceNotConnectedException gained boundary conversions to CNA_RESULT_IO, CNA_RESULT_IO and CNA_RESULT_INVALID_STATE, each proven in cna_c_api_boundary_detail_test. Strict-C StorageSmoke.c plus C/C++ ABI assertions run green in all three trees (48/48). The snapshot is now 3,518 implemented, 23 partial, 2,801 planned and 73 not applicable, with no planned storage row left. |
| CBIND-036B | 97 | Complete content readers, managers and manifests | ✅ | Map ContentReader, the remaining ContentManager rows, ContentTypeReader/ContentTypeReaderManager, ContentManifestEntry, ResourceContentManager, LooseFileContentTypeReader, KnownUnsupportedContentTypeReader and ContentLoadException without exposing C++ type-reader templates, streams or containers. Decomposed into CBIND-036B1–B2 below. |
| CBIND-036C | 98 | Complete network identities, values and packet transfer | ✅ | net.h and CnaCApiNet.cpp map all five identity enumerations at their canonical ordinals, the CNA_QualityOfService value with both canonical factories, an owned CNA_NetworkSessionPropertiesHandle over the optional-integer list with an owned enumerator handle, and owned packet read/write buffers with one route per canonical read and write. Two canonical behaviors are preserved rather than tidied up -- the list reports itself read-only while still mutating, and an out-of-range write appends -- and two are decided in C because the canonical implementation does not decide them at all: Insert/RemoveAt are range-checked before the unchecked native call, and the enumerator's before-first read becomes CNA_RESULT_INVALID_STATE instead of an out-of-bounds dereference. The canonical color write/read asymmetry is preserved and proved in both directions. NetworkSessionJoinException converts to CNA_RESULT_INVALID_STATE and its join error is recorded per thread for cna_net_get_last_join_error, cleared by any later failure; the conversion is proved in cna_c_api_boundary_detail_test. Two _ext routes move packet bytes because the canonical API never exposes them. Strict-C NetSmoke.c plus C/C++ ABI assertions run green in all three trees (50/50). |
| CBIND-036D | 47 | Complete network gamers, machines and events | ✅ | net_gamers.h and CnaCApiNetGamers.cpp map NetworkGamer, NetworkMachine and all seven event-argument types: an owned gamer handle carrying every canonical flag, the session-local identifier, the round-trip time as ticks and the owning session handle, with the CNA extension setters kept under an _ext suffix so a consumer can see which state the canonical API otherwise leaves fixed; an owned machine handle with a counted roster whose borrowed gamer views block the machine's release, and a removal route that reports the canonical always-throwing placeholder as NOT_SUPPORTED; and seven fixed CNA_*EventInfo descriptions with _init routines that validate their payload gamer handle. getMachineProperty hands back an independent copy because the canonical setter already takes its machine by value and the canonical machine exposes no mutator. Re-partitioned: LocalNetworkGamer moved to CBIND-036E — its receive and send paths dereference the owning session, so it cannot exist before sessions do. NetSmoke.c grew the gamer, machine and event coverage and runs green in all three trees (50/50). |
| CBIND-036E | 122 | Complete network sessions, local gamers and discovery | ✅ | Map NetworkSession, LocalNetworkGamer, AvailableNetworkSession and AvailableNetworkSessionCollection, including creation/find/join, session state, packet send and receive and every session event, through owned handles, count/copy collections and documented asynchronous-operation conversion. LocalNetworkGamer moved here from CBIND-036D because its receive and send paths dereference the owning session. |
The 122 rows CBIND-036E owns are partitioned by what each part needs to exist. Available sessions are a self-contained discovery value and land first; the session object then arrives in three passes — its own state and identity, its ten events, and the creation/discovery/join surfaces whose fake-async pairs need the session object to already exist — and the local gamer lands last, because its receive and send paths dereference the session it belongs to.
| # | Rows | Task | Status | Acceptance criteria |
|---|---|---|---|---|
| CBIND-036E1 | 17 | Complete available sessions and their collection | ✅ | net_sessions.h and CnaCApiNetSessions.cpp map both types: an owned discovered-session handle built from a versioned creation structure, every scalar property, count/copy host gamertag and connect address, connect port and session type, a copied CNA_QualityOfService and an independently copied property list, and both equality operators as explicit routes. Only the round-trip sample can be carried into a quality of service, because that is the only measurement the canonical type accepts. The collection is an owned handle with disposal state, explicit dispose and release, a count and an indexed copy-out whose element survives the collection it came from. NetSmoke.c grew the discovered-session coverage and runs green in all three trees (50/50). |
| CBIND-036E2 | 60 | Complete session identity, state and gamer management | ✅ | net_sessions.h gains an owned CNA_NetworkSessionHandle that owns the caller-owned pointer canonical creation returns, both limits as constants, the queued-event identities with a fixed CNA_NetworkEventInfo, every state property and setter, all four rosters as a roster identity plus count and indexed borrowed views, an independently copied property list, the exact type name, disposal, the pump, local-gamer addition, identifier lookup, ready reset, start and end, and the three CNA extension routes. A borrowed gamer view blocks its session's release, and a remote gamer is retained by the C layer because the canonical add deliberately does not take ownership. Re-partitioned: the three Create overloads moved here from CBIND-036E4 (57→60 and 20→17), because none of this slice's state is reachable without a session object. Borrowed from CBIND-037: the canonical session constructor selects its host from its local gamers and therefore cannot run with no signed-in gamer, so gamer_services.h maps the minimum needed — SignedInGamer::CreateInternal, SignedInGamerCollection::CreateInternal, both Gamer signed-in collection accessors and the gamertag — five rows recorded against this task. NetSmoke.c grew the session coverage and runs green in all three trees (50/50). |
| CBIND-036E3 | 10 | Complete session event registrations | ✅ | One cna_network_session_subscribe_* route per event, each with a typed callback that receives the matching CNA_*EventInfo description, plus one shared cna_network_session_unsubscribe. A payload gamer is handed over as a handle that lives only for the duration of the callback, so a consumer can never retain a pointer into session-owned state. An instance registration holds a weak reference to its session, so releasing it after the session is gone is a no-op; InviteAccepted is static and its subscription belongs to the process. Borrowed from CBIND-037: the four InviteAcceptedEventArgs rows, mapped to CNA_InviteAcceptedEventInfo, because this slice maps the event that carries them. NetSmoke.c proves the canonical gamer-joined replay, real join/leave/start/end/host-change/session-end deliveries through the pump, and stale registration refusal; all three trees stay green (50/50). |
| CBIND-036E4 | 17 | Complete session discovery, join and the fake-async pairs | ✅ | Every Begin/End pair collapses into one synchronous C route that still invokes the canonical completion delegate, because CNA completes the pair before Begin returns; the delegate receives only the caller's own context, and no System::IAsyncResult or std::any is exposed. The three asynchronous creations are deliberately not aliases of the synchronous ones — the canonical end step substitutes its own gamer limit instead of forwarding the caller's, and NetSmoke.c asserts that difference. Both Find overloads, both asynchronous searches, Join, JoinInvited and their asynchronous forms are mapped, and the canonical refusal of a local-only search type plus the invited path's fixed session type are preserved. All three trees stay green (50/50). |
| CBIND-036E5 | 18 | Complete local network gamers | ✅ | net_sessions.h and CnaCApiNetSessions.cpp map LocalNetworkGamer over the same owned CNA_NetworkGamerHandle, because a local gamer is a network gamer; every route refuses a handle whose gamer is not local with CNA_RESULT_INVALID_HANDLE rather than reinterpreting it. The data-available and backing signed-in-gamer queries, all three ReceiveData overloads, all six SendData overloads including both PacketWriter forms, the canonical internal factory as cna_local_network_gamer_create_ext and the two CNAEXT queue routes are mapped; a payload crosses as a pointer plus a byte count and the sender comes back as a borrowed gamer view that keeps its session alive. Three canonical behaviors are preserved and asserted rather than tidied up: the offset receive consumes its packet before rejecting an out-of-range offset, the packet-reader receive always reports zero bytes even when it consumed a packet, and EnableSendVoice/SendPartyInvites are declared no-ops whose routes validate and succeed without pretending to do more. NetSmoke.c grew the local-gamer coverage and runs green in all three trees (50/50). This closes parent CBIND-036: the net module has no planned row left. |
The 2,428 rows CBIND-037 owns are partitioned once, by module — which here is also the dependency
boundary, because each module is its own library, its own include tree and its own C header family.
The order is by what each part needs to exist: core has no dependency at all and goes first; the
leaf device and content families follow; runtime comes after them because Game composes the
graphics, input and audio surfaces; and gamer-services, the largest, comes last because its guide
and dispatcher surfaces sit on top of the runtime. A slice larger than roughly a hundred rows is
sub-partitioned when it is reached, as CBIND-035 and CBIND-036 were.
| # | Rows | Task | Status | Acceptance criteria |
|---|---|---|---|---|
| CBIND-037A | 72 | Complete the CNA core module | ✅ | core_ext.h and CnaCApiCoreExt.cpp map every core row: one cna_logger_* route per canonical static so C never depends on a defaulted argument, the process-wide minimum level, the compile-time platform and desktop operating system, both backend classifications for any of the 46 public renderer identities plus their compiled-in forms, and the compiled-in renderer identity and name. Names use the project's count/copy pair rather than the canonical static-storage std::string_view, so no pointer into CNA storage crosses the ABI. CNA::CNAException gained a central boundary conversion to CNA_RESULT_INVALID_STATE, which is what makes the canonical non-desktop refusal of getCurrentDesktopOS observable in C instead of collapsing into a generic internal failure. The canonical EXPERIMENT log level keeps its ordinal 100 rather than being renumbered into a dense range, and 6 is refused. CNAEXT is not-applicable: a documentation-only marker macro with no callable behavior. Strict-C CoreExtSmoke.c plus C/C++ ABI assertions and two new cna_c_api_boundary_detail_test return codes run green in all three trees (51/51) and under ASan+UBSan with leak detection on. The core module has no planned row left. |
| CBIND-037B | 599 | Complete the input module | ✅ | GamePadCapabilities, the remaining GamePad/Mouse/Keyboard/TouchPanel surfaces, MouseCursor, TextInputEXT, the touch collection and gesture types, and the whole CNA::Input extension family (haptics, joysticks, sensors, clipboard, power, device enumeration) are mapped. Decomposed into CBIND-037B1–B7 below; closed by CBIND-037B7b, after which the input module records 834 implemented, 0 partial, 0 planned and 27 not applicable. |
| CBIND-037C | 325 | Complete the media module | ✅ | Map MediaPlayer, Song, VideoPlayer, Video, the media library and every media collection through count/copy collections and owned handles, without exposing a native stream or decoder. Decomposed into CBIND-037C1–C7 below; closed by CBIND-037C7, after which the media module records 276 implemented, 0 partial, 0 planned and 52 not applicable. |
| CBIND-037D | 289 | Complete the devices and devices-ext modules | 🟨 | Map the Microsoft::Devices::Sensors family, VibrateController, and the CNA::Devices extensions (camera, clipboard, file dialog, message box, system tray, power, locale, display and system info). Decomposed into CBIND-037D1–D4 below; the parent becomes complete only when all four rows and every devices/devices-ext inventory row are closed. |
| CBIND-037E1 | 93 | Establish the component model and the service container | ✅ | runtime_components.h and CnaCApiGameComponents.cpp map IGameComponent, IUpdateable, IDrawable, GameComponent, DrawableGameComponent, the component collection with its event argument, and GameServiceContainer. This is the slice where the ABI's direction reverses: a component is behavior the caller supplies, and since C cannot implement a C++ interface, a component is a callback set this ABI wraps in a derived object. That derivation is also why the canonical protected content hooks are mapped here while a sensor's protected members were not — the deciding question is whether this ABI has a derived class to hang them on. The service container is the one canonical type C cannot fully have: it is keyed by C++ type identity, so lookup and removal are exposed over a named-identity subset (recorded partial) and registration has no C form at all (recorded not-applicable). Canonical behaviors reported rather than corrected: the comparison is inverted, a second disposal is a no-op, adding the same component twice is allowed, and a missing component answers -1. One deliberate addition with no canonical counterpart: releasing a component removes it from the collection first, because a handle-based ABI must not leave the runtime holding a released pointer. Strict-C RuntimeComponentsSmoke.c; green in all four trees (63/63) and under ASan+UBSan with leak detection on. |
| CBIND-037E2 | 57 | Complete the game object and the frame | ✅ | The rest of Game's own state and frame control, its four events, GameTime's constructors, LaunchParameters, FrameworkDispatcher, TitleContainer and TitleLocation. The slice's real decision is the five canonical frame hooks: they arrive as a second table, CNA_GameFrameHooks, rather than as new members on the published CNA_GameCallbacks. Appending would have been ABI-safe — the structure is size-prefixed exactly so it can grow — but it leaves every positional initializer a consumer has already written incomplete, which a strict build rejects; that was tried first and reverted, and the finding belongs in ABI_VERSIONING.md's eventual growth section. One rule extended: cna_game_tick is refused from inside a lifecycle callback, joining running and destroying the game, because a frame step called from within a frame re-enters the loop it is part of. Canonical behaviors preserved: launch parameters split on a colon, drop what they cannot parse and keep the first occurrence of a name; the title path is process-wide; and a missing title file's plain runtime error is reported as I/O rather than as an internal failure. One deliberate narrowing: title content is read whole, because this ABI has no stream handle for it. Two rows are deliberately left: the content-manager property to CBIND-037E2b and the window accessors to CBIND-037E3. Strict-C RuntimeGameSmoke.c; green in all four trees (64/64) and under ASan+UBSan with leak detection on. |
| CBIND-037E2b | 3 | Map the game's content manager | ✅ | The contract the slice was waiting on is settled and written into GAME_COMPONENTS.md: a game owns its content manager as a value member, so cna_game_get_content_manager_ext answers a borrowed handle — the same one every time, refused by cna_content_manager_destroy, released with the game, and accepted by every other content-manager route. cna_game_set_content_manager_ext copies, because the canonical setter copy-assigns: the caller keeps its own manager and later changes to it never reach the game. Covered in RuntimeGameSmoke.c; green in all four trees (65/65) and under ASan+UBSan with leak detection on. The runtime module is now 11 rows from closed: CNA::Runtime and CNA::RuntimeOptions remain, which CBIND-037E5 takes. |
| CBIND-037E5 | 11 | Complete the CNA runtime facade | ✅ | The slice turned out to have no routes to write, and finding that out was the work. CNA::Runtime is declared and defined nowhere: all five methods would fail to link if anything called them, nothing in the tree calls them, no translation unit includes CNA/Misc.hpp, and the built runtime archive contains no CNA::Runtime:: symbol — a conclusion the repository's own audit/include/CNA/Misc.hpp.audit.md had already reached independently. This ABI cannot bind a symbol that does not exist, so all 11 rows are recorded not-applicable with that reason rather than left planned as if they were work waiting. RuntimeOptions is a sound value on its own but only parameterizes Initialize, so mapping it alone would hand a consumer a structure that configures nothing. The record is guarded rather than asserted: the new CApi_UnimplementedRuntimeFacade check inspects the built archive and fails the moment any CNA::Runtime:: symbol appears, verified against a stub that produces one. Green in all four trees (66/66) and under ASan+UBSan with leak detection on. This closes the runtime module — 223 implemented, 4 partial, 0 planned, 69 not applicable. |
| CBIND-037E3 | 35 | Complete the game window | ✅ | runtime_window.h and the window half of CnaCApiGameProperties.cpp map GameWindow, its two aliases and Game::getWindowProperty. The one-per-game question is answered the fourth time the same way — every route addresses the game handle — and the reason is written down beside the other three. Two canonical shapes collapse into one route each: the platform-handle property and the native-window accessor answer the same pointer, and the name-only screen-device-change overload is the sized one with the current client size, so a non-positive size means keep it. The slice found a result code this ABI had not been using: a window state change is a request to the platform, and one the platform refuses reports CNA_RESULT_PLATFORM rather than an internal failure — which is exactly what a dummy video driver answers for minimize on a window it never really showed, and what made this the one slice whose first green run was tree-dependent. The window's protected hooks are not mapped, by the same test E1 applied with the opposite result: this ABI derives a component and does not derive a window. Strict-C coverage in RuntimeGameSmoke.c; green in all four trees (64/64), with and without a real SDL window, and under ASan+UBSan with leak detection on. |
| CBIND-037E4 | 80 | Complete the graphics device manager | ✅ | runtime_graphics_manager.h and CnaCApiGraphicsDeviceManager.cpp map the manager, the candidate configuration, the interface, the settings event argument and PresentationMode. The manager is the one runtime object a C caller creates, and creating it registers both services CBIND-037E1 named — closing that loop. The adapter inside a configuration is named by index, because a pointer into the runtime's adapter list is nothing C could hold. Two findings, both reported rather than smoothed over. First, releasing a manager keeps the C++ object alive until its game is destroyed: the canonical game caches a raw IGraphicsDeviceService* and never clears it, so the obvious implementation reproduced a heap-use-after-free on the next frame under ASan — a canonical defect this ABI works around, documented in GAME_COMPONENTS.md. Second, PreparingDeviceSettings cannot change the settings in this runtime at all: the canonical handler collection delivers a const reference, so the argument's mutable accessor is unreachable from any subscriber, C++ or C; the C callback is read-only and says why. IGraphicsDeviceManager gets no caller-provided implementation, the opposite of what E1 decided for components, because the runtime constructs the manager itself. Strict-C GraphicsDeviceManagerSmoke.c; green in all four trees (65/65) and under ASan+UBSan with leak detection on. |
| CBIND-037F1 | 48 | Complete sound effects and their instances | ✅ | SoundEffect gains three creation routes beside the one CBIND-035E already had — the seven-argument range-and-loop constructor, the stream factory (taking the bytes it would have read, since C has no stream) and the file constructor — plus the disposal and name queries, the four process-wide 3D-audio settings, both fire-and-forget play routes, both static sample computations and type names for the effect and its instances. Two canonical behaviors are reported rather than evened out: pan is range-checked while pitch is clamped, and an empty asset path yields a silent effect rather than an error. The C range route adds the boundary validation the canonical constructor lacks — a negative offset, an empty count or a range leaving the buffer is refused before the decoder sees a length nobody checked. Move operations are not-applicable: a handle already names an object C never copies or moves. Both audio exceptions now convert in the exception firewall rather than in the one creation route that caught the first locally. Strict-C AudioSoundEffectSmoke.c, including a WAV built in memory and decoded; green in all four trees (67/67) and under ASan+UBSan with leak detection on. |
| CBIND-037F2 | 49 | Complete streaming and capture audio | ✅ | A streaming instance is a sound-effect instance: it lives under the same handle kind, so every cna_sound_effect_instance_* route accepts it and the canonical overrides dispatch virtually behind them, while the streaming-only routes refuse an ordinary instance with INVALID_STATE. It has no parent effect — the caller is the source — which needed the existing destroy route taught not to dereference a parent that is not there. Submitted buffers are copied, which is what makes submission safe from a producer thread while playback runs, and the test proves it by overwriting its own buffer afterwards. Microphones are index-addressed, because the canonical list hands out pointers the runtime owns; the default follows the availability-separate-from-the-answer rule; and cna_microphone_get_data_at is the one count/copy route in this ABI where a short read is not a failure, because capture is a stream rather than a value. No verification tree has a capture device, so the count is zero and every index route refuses — the device's real availability, recorded like the compass's and the camera's. NoMicrophoneConnectedException joins the firewall. Strict-C AudioStreamingSmoke.c; green in all four trees (68/68) and under ASan+UBSan with leak detection on. |
| CBIND-037F3 | 24 | Complete 3D audio | ✅ | The emitter and the listener are fixed values, not handles: the canonical types carry settings and no behavior, so CNA_AudioEmitter and CNA_AudioListener are structures the caller fills in, with cna_audio_emitter_init / cna_audio_listener_init writing the canonical defaults (origin, facing -Z with +Y up, at rest, and for the emitter alone a Doppler scale of 1). They stay two structures rather than one, because the listener has no Doppler scale. cna_sound_effect_instance_apply_3d positions against one listener and reads the four process-wide settings CBIND-037F1 mapped; cna_sound_effect_instance_apply_3d_multi_ext covers the array overload and is _ext because it reports a limit C could not otherwise see — this runtime supports exactly one listener, so a count of zero or two is NOT_SUPPORTED rather than a silent fallback, while a null array stays an argument failure. Positioning latches: the spatial gain, pan and pitch survive into every later call and ..._set_pan stops reaching the output, while the properties keep reading back what the caller set — asserted directly rather than smoothed over. RendererDetail's 7 rows plus its class row moved to CBIND-037F4: its constructor is private and AudioEngine::getRendererDetailsProperty is its only source, so it cannot be reached without the engine. The planned count was 32 against an actual inventory of 24 here and 8 there. Strict-C Audio3DSmoke.c; green in all four trees (69/69) and under ASan+UBSan with leak detection on. |
| CBIND-037F4 | 78 | Complete the XACT audio surface | ✅ | The family is reachable, and the decision the plan asked for went the other way from CBIND-037E5: a binary XACT file is authorable, so XactSmoke.c writes the settings file, the wave bank and the sound bank itself and drives the real parsers. No fixture had to ship and nothing had to be recorded unreachable. Ownership runs one way and is enforced: engine under game, banks and categories under engine, prepared cue under sound bank. RendererDetail is addressed by index, never held, because its constructor is private and the engine's list is its only source; the look-ahead constructor's two extra arguments are accepted and ignored, since there is one backend. AudioCategory is the opposite: the canonical lookup answers a value, so the handle is just somewhere for C to keep it, and categories compare by name alone — two from different engines are equal. InstanceLimitDecision is the one type with no C form: it is public but every producer of it is private, so its 4 rows are not-applicable. Engine-global and cue-scoped variables are separate domains that refuse each other's names, both clamp to the authored range, and a read-only write succeeds silently because the canonical route never reports that refusal. A prepared cue is owned and each lookup answers a new one; a fire-and-forget cue gets no handle at all. A cue's eight predicates are one snapshot, and two canonical facts are asserted rather than smoothed: a cue from a bank arrives prepared, not created, and pausing leaves it playing. Strict-C XactSmoke.c; green in all four trees (70/70) and under ASan+UBSan with leak detection on. This closes the audio module — 217 implemented, 41 not applicable, no partial and no planned row. |
| CBIND-037G | 665 | Complete the gamer-services module | ✅ | Map the remaining gamer, profile, presence, privilege, achievement, leaderboard, avatar and guide surfaces on top of the minimum signed-in-gamer surface CBIND-036E2 and E3 already borrowed. Split into seven slices below, all complete. |
The 599 rows CBIND-037B owns split by device family, and within the gamepad family by what each
part needs to exist: the capabilities value is independent, the state values compose into a
snapshot, and the GamePad statics need both.
| # | Rows | Task | Status | Acceptance criteria |
|---|---|---|---|---|
| CBIND-037B1 | 86 | Complete gamepad capabilities and controller type | ✅ | input_gamepad.h maps GamePadType at its canonical ordinals and the whole GamePadCapabilities surface as one fixed 48-byte value: struct_size/struct_version, the controller type and 35 directly readable and writable flags, because every canonical property has both a getter and a setter. A value rather than a handle, since the canonical type is a copyable snapshot with no identity, and direct fields rather than 74 routes, since that is what the getter/setter pair means in C. The ten CNA extension properties keep an _ext suffix on their fields. cna_gamepad_capabilities_init reproduces the canonical default constructor exactly. Borrowed from CBIND-037B3: GamePad::GetCapabilities is mapped here as cna_gamepad_get_capabilities (86 rows here, 24 left there), because a capabilities value with no producer cannot be tested against anything real. It takes an active game handle for the same reason cna_gamepad_get_state does, and an empty slot is an ordinary answer rather than a failure. InputSnapshotsSmoke.c cross-checks connection against the state snapshot so the two can never disagree, and runs green in all three trees (51/51) plus ASan+UBSan with leak detection on. |
| CBIND-037B2 | 65 | Complete gamepad state values | ✅ | input_gamepad.h maps all five canonical value types onto the representations the C API already had, so the ABI never grows a second spelling of the same numbers. GamePadButtons and GamePadDPad are the existing CNA_GamePadButtonFlags mask — the pad restricted to its four bits — because that is exactly what each canonical type holds and how CNA itself derives one from the other. CNA_GamePadThumbSticks (16 B) and CNA_GamePadTriggers (8 B) are new plain values that are byte-identical to the two halves of the analog block a snapshot already carried, asserted rather than assumed. The eleven named button getters and the four pad getters each collapse into one _is_pressed route that answers through the canonical getter owning the button. Three canonical behaviors are preserved and asserted rather than tidied up: the thumbstick constructor square-clamps to ±1 and the trigger constructor clamps to 0..1; trigger equality is an epsilon comparison, proved with the next representable float above a value; and the pad hash uses its own weighting (Down 1, Left 2, Right 4, Up 8), not the button bits. GamePadState gains both public constructions with their derived trigger and virtual-stick bits, the four component projections, the _ext packet-number setter, equality, the hash that mixes only buttons and packet number, and the fixed type-name string. One representational limit is documented, not hidden: the C snapshot carries a single button mask — as does every state CNA itself builds, since the capture path derives both the button set and the pad from one raw mask — so a supplied pad is merged into the button set. The Buttons flag operators need no route: C composes the uint32_t identity with its own operators, and every route validates against CNA_GAMEPAD_BUTTON_ALL. InputSnapshotsSmoke.c grew four validators with per-family return codes and runs green in all three trees (51/51) plus ASan+UBSan with leak detection on. |
| CBIND-037B3 | 45 | Complete the GamePad statics | ✅ | input_gamepad.h maps every remaining GamePad static as one cna_gamepad_* route, each taking an active game handle for the same reason the state and capability captures do: CNA is event-driven, so a device query is only meaningful on the game thread of a running game. ExcludeAxisDeadZone is the exception and takes none, being a pure value operation; the three dead-zone constants were already exposed as macros carrying the canonical expressions verbatim. Two canonical shapes are preserved rather than collapsed: a query reporting availability through its return value and its answer through an output reference keeps both answers separate in C, so "no sensor" is an ordinary answer rather than a failure, and the four identity strings use the project count/copy protocol. The touchpad finger query's four output references become one fixed 16-byte CNA_GamePadTouchpadFinger. No std::string, Vector3 or CNA::Input enumeration crosses the boundary. Borrowed from CBIND-037B7: GamePadButtonLabelEXT, GamePadConnectionStateEXT and PowerStateEXT (21 rows), because three of these statics return them and cannot be mapped without them — 45 rows here, 117 left there. InputSnapshotsSmoke.c asserts the shape of an empty-slot answer for every route, the count/copy round trip for all four identity strings, and the per-route out-of-range, null-output, undefined-bit and wrong-thread refusals; all three trees green (51/51) plus ASan+UBSan with leak detection on. |
| CBIND-037B4 | 80 | Complete keyboard, mouse and text input | ✅ | Map the remaining Keyboard/KeyboardState, Mouse/MouseState, KeyState, MouseCursor and TextInputEXT rows. Split further by device, because the cursor is an owned disposable type and the text-input surface is event-driven while the keyboard and mouse are plain snapshots. Closed by CBIND-037B4d; all four sub-slices are complete and no planned row remains in any of their headers. |
| CBIND-037B4a | 35 | Complete the keyboard | ✅ | input_keyboard.h maps KeyState, the whole KeyboardState value surface and every Keyboard static over the versioned 256-slot bit field the C API already had. cna_keyboard_state_init_from_keys maps both canonical set-taking constructors, since an initializer list and an unordered set are the same deduplicated array in C. Documented deviation: the canonical constructors silently drop a key outside the 256-slot field; C refuses instead, so a caller can never lose a key without being told, and the refusal matches every other keyboard route. The player-slot GetState overload reports the same snapshot for every slot, because CNA has one keyboard. Both name families use the project count/copy protocol with borrowed CNA_StringView reverse lookups, so no std::string crosses the boundary, and an unknown name answers with the canonical none identity rather than failing. Borrowed from CBIND-037B7: KeyModifiersEXT and its five operators (15 rows), because GetModStateEXT returns it — 35 rows here, 102 left there. The flag operators need no route: unlike the gamepad button identities these really are flags, and C masks them with its own operators. InputSnapshotsSmoke.c covers every value operation and query plus their refusals, green in all three trees (51/51) and under ASan+UBSan with leak detection on. |
| CBIND-037B4b | 21 | Complete the mouse | ✅ | input_mouse.h maps the whole MouseState value surface and every remaining Mouse static. Each construction takes the same CNA_MouseButtonFlags bit set the snapshot already carries rather than five separately ordered button-state arguments, so a consumer cannot silently transpose two of them; the eight-argument form leaves the horizontal wheel at zero exactly as the canonical one does. Unlike the gamepad and keyboard snapshots this type does override its string conversion, and C reproduces the canonical format exactly, None included. The window handle crosses as an opaque uint64_t the C API never dereferences. A request no backend can satisfy answers CNA_FALSE through an applied output rather than failing, and the global-position query — canonically void with two output references — cannot fail at all. The static ClickedEXT event becomes an owned CNA_MouseEventRegistrationHandle that takes no game handle, because the canonical event belongs to the process; INTERNAL_onClicked becomes the raise route that makes it observable without a device, and ResetForTests is documented as dropping every subscription, including ones this API handed out, so a release afterwards is a no-op. Re-partitioned: Mouse::SetCursor moved to CBIND-037B4c (21 rows here, 22 there), because it cannot be mapped before a cursor handle exists. InputSnapshotsSmoke.c proves the click round trip and the reset-drops-subscriptions behavior; green in all three trees (51/51) and under ASan+UBSan with leak detection on. |
| CBIND-037B4c | 22 | Complete the mouse cursor | ✅ | input_cursor.h maps MouseCursor as an owned CNA_MouseCursorHandle plus Mouse::SetCursor, moved here from CBIND-037B4b because it cannot be mapped before a cursor handle exists. All twelve stock accessors collapse into one route taking a CNA_MOUSE_CURSOR_STOCK_* identity, and the handle they return is a borrowed view: the canonical stock cursors are process-lifetime singletons whose disposal is a deliberate no-op, so destroying the handle never frees the shared native cursor and disposing it succeeds without doing anything. The default constructor, the texture factory and both lifetime operations are mapped; the texture factory does not keep its texture alive, because the canonical one copies the pixels. Four rows are not-applicable and each says why: the SDL_Cursor* constructor and GetSDLCursor would put a native backend pointer in the ABI, and the move constructor and move assignment have no counterpart because a handle is the only name C has for a cursor. InputSnapshotsSmoke.c probes the texture-derived cursor by behavior — whichever documented answer the backend gives, the success path is exercised fully and the refusal path must leave the output handle invalid — and proves the stock no-op disposal by reusing an identity after disposing and releasing it. Green in all three trees (51/51) and under ASan+UBSan with leak detection on. |
| CBIND-037B4d | 27 | Complete text input | ✅ | input_text.h maps every TextInputEXT row through cna_text_input_* free functions, because C has no static class. All three events become owned CNA_TextInputRegistrationHandle values with one shared release route, since a registration already knows which event it came from; the subscriptions take no game handle, as the canonical events are process-wide statics. A committed code unit crosses as a uint16_t and an above-BMP code point arrives as two surrogate calls; the two multi-field events hand over fixed versioned infos whose UTF-8 text and candidate strings are CNA_StringViews borrowed only for the callback, so neither std::string nor std::vector crosses the ABI. The three INTERNAL_On* dispatchers become the raise routes that make the events observable without a keyboard. Two canonical quirks are preserved and asserted: composition start/length are byte offsets forwarded verbatim and selected is not range-checked, because the canonical dispatch checks neither. One deliberate deviation: an undefined type hint is refused, where the canonical conversion silently falls back to plain text. Borrowed from CBIND-037B7: TextInputTypeEXT and its nine values (10 rows) — 27 rows here, 92 left there. This closes parent CBIND-037B4. The suite never branches on renderer identity: it forces the unbound case to prove the null-guarded contract on every backend, then restores whatever the backend really bound — which on SDL_RENDERER is a live window where start genuinely activates text input and stop genuinely deactivates it, asserted as a relationship rather than a fixed answer. Green in all four trees (51/51) and under ASan+UBSan with leak detection on. |
| CBIND-037B5 | 80 | Complete touch and gestures | ✅ | input_touch.h maps the whole touch family onto the representations the C API already had. GestureType is a uint32_t bit set whose four canonical operators need no route — unlike the gamepad button identities these really are flags, so C composes and masks them with its own operators and every route validates against CNA_GESTURE_TYPE_ALL. GestureSample is a fixed 64-byte value rather than a handle, since it is a copyable snapshot with no identity; its eight canonical getters are plain fields and System::TimeSpan crosses as int64_t 100-nanosecond ticks, the spelling runtime.h and audio.h already use. TouchLocation, TouchPanelCapabilities and the entire TouchCollection mutation surface extend the existing fixed eight-slot CNA_TouchState/CNA_TouchLocation/CNA_TouchCapabilities values, so the ABI never grows a second spelling of a touch snapshot. Four canonical behaviors are preserved and asserted rather than smoothed over: equality, the hash and the text all ignore the pressure extension and the text carries only the position; reading an empty gesture queue throws canonically and so is refused in C rather than answered with a default sample; a raised touch event feeds gesture detection and not the snapshot, and is dropped outright until a display size is published, because the dispatch scales by it; and ResetForTests clears the display metrics and window handle even though the canonical class comment claims they survive — the C contract follows the implementation and says so. CopyTo inserts and shifts rather than overwriting, which is why the destination's element count is an argument. Three deliberate C deviations are documented: a negative maximum touch count, a pressure outside zero through one, and an append past the fixed capacity are refused rather than stored or silently dropped. Five rows are not-applicable with reasons: the four iterator overloads, because an iterator has no C counterpart and C indexes the fixed array directly, and the class-local intcs alias, which declares no operation. InputSnapshotsSmoke.c grew three pure validators and one in-game validator with their own return codes, proving the pressure-blind match, the insert-semantics copy, the exact text, the enqueue/read round trip, the empty-queue refusal, the released frame after a slot is cleared, and the reset really clearing the display metrics. Green in all four trees (51/51) and under ASan+UBSan with leak detection on. |
| CBIND-037B6 | 126 | Complete the haptics extension family | ✅ | input_haptics.h maps the whole CNA::Input haptics surface. This is the first input slice with no XNA counterpart at all, so the whole header maps a CNA-namespace surface and its routes take no _ext suffixes, following the core_ext.h precedent. It is also the first input slice to produce an owned handle rather than a value: HapticDevice becomes CNA_HapticDeviceHandle (ObjectKind 68), with the destructor/Dispose split MouseCursor established, so a caller can close a device without giving up its handle. The decision that makes the family testable at all is that a closed device is not an error state: the three open routes never fail for want of hardware, they hand back a real handle whose open flag reports whether anything is behind it, and every route on a closed device answers CNA_FALSE, zero or -1 through its output — exactly as the canonical class behaves. No verification tree has force-feedback hardware, so that path is the one actually exercised, and it is asserted rather than skipped. HapticFeatureEXT is a uint32_t bit set whose five operators need no route, C composing them itself; its canonical bit gaps (LeftRight at 11, Custom at 15, the four global capabilities at 16–19) are reproduced exactly and pinned by ABI assertions. HapticEffectTypeEXT and HapticDirectionTypeEXT are ordinal identities and an out-of-range value is refused. Two representational decisions are documented, not hidden: a custom waveform travels beside the effect value rather than inside it, so the 108-byte CNA_HapticEffect stays a plain copyable POD owning no heap, and the device name is left out of the capability value and read through the count/copy pair — which is why cna_haptic_capabilities_equals takes both names as arguments, reproducing the canonical comparison exactly instead of quietly comparing fewer fields than it does. Canonical pass-throughs are preserved: rumble strength, gain and autocenter reach the platform unvalidated, and freeing an unknown effect identifier is a successful no-op because the canonical operation reports nothing. RunEffectEXT's defaulted iteration count is passed explicitly. Three rows are not-applicable with reasons: the SDL_Haptic* constructor, which would put a native backend pointer in the ABI, and the move constructor and move assignment, which have no counterpart because a handle is the only name C has for a device. The slice gets its own strict-C HapticsSmoke.c and CApi_HapticsSmoke target rather than growing InputSnapshotsSmoke.c further, matching its own adapter file. Green in all four trees (52/52) and under ASan+UBSan with leak detection on. |
| CBIND-037B7 | 92 | Complete the remaining input extensions | ✅ | Map the remaining CNA::Input joystick, sensor, device-enumeration, clipboard and power surfaces. The key-modifier, button-label, text-input-type and connection-state identities this row originally owned were borrowed into CBIND-037B3, B4a and B4d, which is why 92 rows remain rather than the 102 first partitioned. Split by concern into CBIND-037B7a–B7b below, because the joystick family is a device surface with its own values, snapshot and hot-plug events while the rest are small host-system queries. |
| CBIND-037B7a | 54 | Complete the raw joystick family | ✅ | input_joystick.h and CnaCApiInputJoystick.cpp map JoystickTypeEXT, JoystickHatPositionEXT, JoystickInfoEXT, JoystickCapabilitiesEXT, JoystickStateEXT and the Joysticks facade. Like haptics this is a CNA-namespace surface, so the routes take no _ext suffix except the two hot-plug events and the test reset, which follow their canonical member names. The one deliberate departure from the input families' fixed-POD rule is the snapshot, and it is the decision the slice turns on: JoystickStateEXT carries four heterogeneous variable-length arrays with no canonical maximum — unlike the touch panel's fixed eight slots — so a fixed value would have to invent a capacity that silently truncates a real HOTAS setup, while four independent per-array queries would answer from four different instants. cna_joysticks_capture_state therefore captures once into an owned CNA_JoystickStateHandle (ObjectKind 69) and each array is read with its own count/copy pair against that one instant; trackball motion is relative, so capturing consumes it, which is another reason one capture must serve all four arrays. The hat is an identity, not a bit set — the plan's own guess said "probably a bit set", and the canonical header says the opposite: the platform's combinable up/down and left/right bits are enumerated as the nine reachable combinations, so RIGHT_UP is the ordinal 5 and composing these values is wrong. That is pinned by an ABI assertion and stated in the header. The haptics closed-device-is-not-an-error contract carries over unchanged: an unconnected identifier answers cna_joysticks_get_capabilities with the canonical disconnected defaults, a power percent of -1 meaning "unknown" rather than "empty", and two empty strings, and answers cna_joysticks_capture_state with four empty arrays — which is the path every verification tree actually exercises, and it is asserted rather than skipped. The device name and GUID stay outside the capability value for the same reason the haptic device name does, so cna_joystick_capabilities_equals takes both strings alongside both values and reproduces the canonical ten-field comparison exactly; cna_joystick_info_equals does the same with the descriptor name. Both static multicast fields become owned registrations (ObjectKind 70) with one shared release route, mirroring the text-input surface, plus raise routes that invoke the same public field the platform layer invokes — no Internal bridge crosses the ABI. The slice gets its own strict-C JoystickSmoke.c and CApi_JoystickSmoke target, matching its own adapter file. Green in all four trees (53/53) and under ASan+UBSan with leak detection on. |
| CBIND-037B7b | 38 | Complete host sensors, device enumeration, clipboard and power | ✅ | input_devices.h and CnaCApiInputDevices.cpp map the last four CNA::Input extensions by reusing the shapes CBIND-037B7a settled rather than inventing new ones: the descriptor value with its name outside the POD and an _equals taking both names, the index-addressed enumeration, and the process-wide event registration (ObjectKind 71, one shared release route for all four events). Three decisions are worth recording. The two sensor reads follow the availability-separate-from-the-answer rule the gamepad sensors established, and go one step further: when the flag reports no sensor the reading output is left exactly as the caller left it, because that is what the canonical query does with its reference — the test proves it by pre-filling sentinel components and asserting they survive. CNA_InputDeviceInfo carries a uint64_t identifier where the sensor and joystick descriptors carry uint32_t, because a touch-device identifier is 64-bit natively; the test round-trips a value above the 32-bit range so a narrowing conversion could not pass unnoticed. And cna_clipboard_set_text reports that the request was made, not that it succeeded: the canonical setter returns nothing, so there is no platform outcome to forward and this ABI does not invent one — a headless session or a gesture-gated browser may ignore the write. The clipboard is process-external state the suite does not own, so its test captures the pre-existing content, asserts a relationship (if the write took effect, the read must return exactly those bytes; the presence flag must agree with a non-empty read in both directions), proves the empty and buffer-too-small cases only on a platform that actually stored the text, and restores the original content. One strict-C InputDevicesSmoke.c covers all four families, driving the three device enumerations through a single shared validator so the protocol is proven identically for mice, keyboards and touch devices. Green in all four trees (54/54) and under ASan+UBSan with leak detection on. This closes parent CBIND-037B7, parent CBIND-037B and the whole input module: 834 implemented, 27 not applicable, no partial and no planned row left. |
The 289 devices and devices-ext rows split by what can be tested together: the reading values
are pure PODs and need nothing, the sensor devices produce them and own the events and exception
types, and the two CNA::Devices groups are independent system services. The owner decision of
2026-08-15 applies to this whole slice: cmake-build-binding-sdlrenderer and
cmake-build-binding-asan were reconfigured with -DCNA_DEVICES=ON so the #ifdef CNA_DEVICES
half of devices-ext is genuinely exercised, while headless and software stay OFF and prove
the compiled-out contract. Both states must stay green.
| # | Rows | Task | Status | Acceptance criteria |
|---|---|---|---|---|
| CBIND-037D1 | 70 | Establish sensor readings, timestamps and state | ✅ | sensors.h and CnaCApiSensors.cpp map SensorState, ISensorReading and all five reading types as fixed values. The slice settles the ABI's second point-in-time form: CNA_DateTimeOffset is two 100-nanosecond tick counts — local time from 0001-01-01 plus the UTC offset — because that is the canonical runtime type's own base, exactly as the picture date uses the Unix epoch because its canonical type does. The reading interface becomes the timestamp field every reading carries rather than an abstract type C cannot use, and its virtual destructor is the one not-applicable row. Three canonical behaviors are preserved rather than tidied: each constructor keeps its own argument order even though the five disagree (the accelerometer takes the timestamp first, the gyroscope the rate first, the compass the true heading last) — normalizing them would make the C API easier to remember and harder to check; equality pairs the values with the timestamp; and the text conversions carry only part of each reading, with the motion reading's omitting the attitude and rotation rate a reader would expect. All six state identities are exposed, including the two the canonical header records as currently unreachable, because an identity is not a claim that something produces it. cna_c_api gained its cna_devices link edge here. Strict-C SensorValuesSmoke.c needs no game at all; green in all four trees (59/59) and under ASan+UBSan with leak detection on. |
| CBIND-037D2a | 80 | Map the motion sensors, their failures and the test-support surface | ✅ | Accelerometer and Gyroscope are owned handles in sensors.h; CnaCApiSensors.cpp carries them, and the two exception types become one route rather than a type. The common base is a class template, so C repeats its contract per sensor instead of modeling a base — the reading-changed callback delivers the reading itself, because the event-argument wrapper adds nothing. Three canonical behaviors are reported, not smoothed: reading an unsupported sensor's value fails INVALID_STATE (the canonical property throws rather than defaulting), a second disposal is refused where every other disposable in this ABI is idempotent, and there is no disposal query at all because the canonical flag is protected — the disposed state is observed through the refusals. SensorFailedException's error id reaches C exactly as the network join error does: recorded per thread by the barrier, read back with cna_sensors_get_last_error_id_ext. The test-support surface is mapped deliberately — no verification machine has motion sensors, so set_supported_for_tests_ext plus inject_synthetic_update_ext are what let a C consumer reach the supported path and the real dispatch chain; the injector takes platform units, so 9.80665 m/s² reads back as 1 g. Strict-C SensorDeviceSmoke.c covers both dispatch paths, the detaching registration, the double-start failure and its error id, the disposal hook firing once, and every post-disposal and stale-handle refusal; green in all four trees (60/60) and under ASan+UBSan with leak detection on. |
| CBIND-037D2b | 46 | Complete the remaining sensors and the reading events | ✅ | Compass and Motion are owned handles on the shape D2a settled, and the three canonical event-argument types resolve three different ways, by payload: the reading wrapper is a class template holding one reading, so it is flattened into the callback; the calibration argument carries nothing, so it becomes a payload-free callback and a value-free type-name pair; and the legacy accelerometer argument carries three separate components rather than a vector, so it earns a real value, CNA_AccelerometerReadingEventInfo, and cna_accelerometer_subscribe_reading_changed — whose canonical firing order after the current-value handlers this ABI reports rather than reserves. Both sensors are unsupported on every platform this ABI is verified on, which is the device's real answer rather than a gap, so this ABI supplies its own installable backend (cna_<sensor>_set_test_backend_ext plus reading and calibration injection) where the canonical hook takes a C++ object C cannot write. Three canonical limits are reported rather than smoothed: the eleventh simultaneous instance is refused, a backend cannot be swapped while acquisition runs, and the motion sensor's north-referenced answer is vacuously true before a backend starts. Strict-C SensorEventsSmoke.c drives all of it; green in all four trees (61/61) and under ASan+UBSan with leak detection on. |
| CBIND-037D3 | 69 | Complete VibrateController and the CNA system services | ✅ | devices.h and CnaCApiDevices.cpp carry two different things, and the header says which is which: vibration belongs to the always-present canonical layer, everything else is the #ifdef CNA_DEVICES extension, exported in both states and reporting NOT_SUPPORTED when compiled out, with cna_devices_ext_is_available as the probe. The clipboard question is answered: both canonical types wrap one platform clipboard, so the reads stay the input module's and only the acceptance flag the extension adds becomes a new route. Four services end in something no test can complete — a modal dialog, an asynchronous picker, a real tray, a motor nothing here has — so this ABI supplies the backend C cannot write for three of them and mirrors the tray's canonical second-constructor seam with a second creation route; cna_url_launcher_open_ext has no seam and is deliberately never called with a real URL, a gap recorded rather than papered over. Canonical behaviors preserved: vibration bounds its duration but clamps its intensity, a not-a-number strength becomes no vibration, a tray index past the last entry is ignored rather than refused, and a windowless session answers a zero scale and an empty safe area. Strict-C DevicesSmoke.c covers both build states; green in all four trees (62/62) and under ASan+UBSan with leak detection on. |
| CBIND-037D4 | 24 | Complete the camera extension | ✅ | The frame question is answered by the canonical signature: TryAcquireFrame fills a Texture2D the caller owns and keeps, so the C route takes an existing texture handle rather than lending one — deliberately not the borrowed per-frame texture CBIND-037C7 settled for video, because nothing here is lent and nothing is invalidated by the next call. Two canonical behaviors are preserved rather than corrected: no frame ready is an ordinary CNA_FALSE, and a texture whose size does not match the frame is refused the same way, with no resize and no distinct reason. The driver probe and the camera enumeration stay separate questions, because a driver is not a camera. The canonical backend constructor becomes a second creation route, mirroring the system tray, and is the only way any verification tree reaches a frame or the refused state. The real camera is never opened by a test — on a machine that has one that switches on the user's webcam — a gap recorded rather than papered over, like the URL launcher's. DevicesSmoke.c covers both build states; green in all four trees (62/62) and under ASan+UBSan with leak detection on. This closes the whole devices module. |
The 325 media rows split by what each part needs to exist: the identities and standalone values
first, then the song that everything plays, then the library entities and their collections, then
the library that owns them, then the player that consumes them, and video last because it composes
the graphics surface as well. cna_c_api gained its cna_media link edge in CBIND-037C1 — the
module list stays exactly what the C API adapts.
| # | Rows | Task | Status | Acceptance criteria |
|---|---|---|---|---|
| CBIND-037C1 | 25 | Establish media identities, visualization and sources | ✅ | media.h and CnaCApiMedia.cpp map MediaState, MediaSourceType, VideoSoundtrackType, VisualizationData and MediaSource. Two decisions carry the slice. CNA_MediaSourceType keeps its canonical 0/4 gap rather than being renumbered into a dense range, so it deliberately has no MAXIMUM and consumers validate membership of the two defined values instead of an upper bound — the same rule that kept CNA_LOG_LEVEL_EXPERIMENT at 100. And the canonical source enumeration's ownership never crosses the ABI: MediaSource::GetAvailableMediaSources allocates its sources with new and hands back raw pointers its caller must free, so each C route enumerates, reads the one source it was asked about and destroys the whole list before returning; an index is a point-in-time value with nothing to release, and the sanitizer tree with leak detection is what proves it rather than a comment claiming it. ToString needs no route of its own because the canonical implementation returns the display name unchanged, and the media-source type name is addressed by index because the canonical member is an instance method on a type not constructible from outside the library. CNA_VisualizationData is a fixed 2,056-byte value rather than a handle, since both canonical buffers are fixed at 256 floats and the canonical type exposes them both as fields and through getters — one value is both. Strict-C MediaSmoke.c plus C and C++ ABI assertions; green in all four trees (55/55) and under ASan+UBSan with leak detection on. |
| CBIND-037C2 | 37 | Complete Song and SongCollection | ✅ | media.h grows an owned CNA_SongHandle (ObjectKind 72) and CNA_SongCollectionHandle (73). Several handles share one song: the resource is reference-counted, so releasing one handle never destroys a song a collection still holds — which is what lets cna_song_collection_create retain every song it was given, where the canonical collection merely stores non-owning pointers a released C handle would have dangled. Three canonical behaviors are preserved rather than tidied, and the first is a header-contradicts-implementation case like TouchPanel::ResetForTests: an omitted song name stays empty even though the constructor's own documentation claims it defaults to the file name; equality and the hash come from the file path, so two independently created songs over one file compare equal and hash equal — a deliberate CNA improvement over FNA's identity-based hash, kept rather than "fixed"; and getIsRated is not "rating is nonzero", because both tag formats reserve zero for unrated. ToString needs no route of its own (it returns the display name unchanged), a missing file surfaces as CNA_RESULT_IO through the canonical file-not-found exception, and a non-file URI scheme as CNA_RESULT_INVALID_STATE. Canonical collection disposal empties the collection, so its count drops to zero and every index is refused while the songs survive. Seven rows are not-applicable with reasons: the MediaLibrary friend declaration, and the collection's iterator pair with its two aliases. Re-partitioned: getAlbumProperty, getArtistProperty and getGenreProperty move to CBIND-037C3 (37 rows here, 105 there), because they return library-owned entities whose handles do not exist yet. MediaSmoke.c builds its fixture files through the storage API — the only portable way a strict-C17 test can obtain a real absolute path — with one non-ASCII UTF-8 file name. Green in all four trees (55/55) and under ASan+UBSan with leak detection on. |
| CBIND-037C3 | 117 | Complete the library catalog: MediaLibrary, albums, artists, genres, playlists | ✅ | media_library.h, CnaCApiMediaLibrary.cpp and MediaLibrarySmoke.c map MediaLibrary and the four entity families with their collections (ObjectKind 74–82). Re-partitioned on arrival: MediaLibrary moved here from CBIND-037C5 and its six picture rows moved to CBIND-037C4, because none of the entity types is constructible from outside the library — a slice that mapped them without it could not have produced a single testable object. The shape decision is that everything except the library is a borrowed view holding a reference to its library, so releasing the library handle first is safe and there is no parent-before-child rule to remember; the four structurally identical collection types therefore share one C shape rather than four. Album equality is not the name alone: names collide across artists, so the canonical comparison pairs name with artist. Optional entities — an album's artist and genre, a song's album, artist and genre — follow the availability-separate-from-the-answer rule. No stream crosses the ABI: the canonical art members hand back a caller-owned stream, so C reads it to the end and destroys it inside the call and the image crosses as bytes; the thumbnail is the same image, which is canonical rather than a C limitation. The sanitizer tree earned its keep here — it proved that MediaLibrary(MediaSource*) borrows its argument (it copies the kind and name into an object of its own) rather than adopting it, so the C route destroys every enumerated source before returning. Twenty-five rows are not-applicable: the Album::MediaLibrary friend declaration and the four collections' iterator pairs and aliases. The test points SDL's user-folder lookup at a generated fixture through XDG_CONFIG_HOME, so the scanned library is deterministic — two tag-only MP3 files sharing an artist, album and genre plus a folder cover whose exact bytes the art routes must return — instead of depending on whatever music the host holds, and no real user directory is read or written. Green in all four trees (56/56) and under ASan+UBSan with leak detection on. |
| CBIND-037C4 | 60 | Complete pictures, picture albums and the library's picture surface | ✅ | media_library.h grows CNA_PictureHandle, CNA_PictureAlbumHandle and their two collections (ObjectKind 83–86), plus the six MediaLibrary picture rows re-partitioned in from CBIND-037C3. Two shapes are new. The picture-album tree is the only tree in the media family, and the root's absent parent is what makes it walkable: cna_picture_album_get_parent reports availability rather than failing, so a caller climbs until the flag turns false; cna_media_library_get_root_picture_album answers the same way, because a device with no readable picture location has no tree at all. And a picture's date is the ABI's first point in time: durations elsewhere are 100-nanosecond ticks from zero, so the date uses the same tick counted from the Unix epoch — the canonical clock's own epoch — rather than inventing a second time unit. Everything else reuses shapes already settled: image and thumbnail bytes follow the album-art contract (the canonical caller-owned stream is read to its end and destroyed inside the call, so no stream enters the ABI, and the thumbnail is the same image), and the collections are the same six-route shape the music collections use. cna_media_library_save_picture_from_stream takes a storage stream handle, since a storage stream is the only byte source this ABI owns — the same decision content_readers.h made — borrowed for the call and left the caller's to close. Twelve rows are not-applicable: the two collections' iterator pairs and aliases. The CBIND-037C3 fixture is extended with a one-pixel BMP, so the picture side is as deterministic as the music side; the suite deletes the picture it saves so repeated runs start from the same state. Green in all four trees (56/56) and under ASan+UBSan with leak detection on. |
| CBIND-037C5 | 0 | Complete MediaLibrary | ✅ | Absorbed into CBIND-037C3 (music surface) and CBIND-037C4 (picture surface). MediaLibrary could not be a slice of its own in either direction: its members return the entity collections, and none of those entities can be obtained without it. |
| CBIND-037C6 | 44 | Complete MediaPlayer and MediaQueue | ✅ | media_player.h and CnaCApiMediaPlayer.cpp map the static MediaPlayer as free game-scoped routes and the queue as a view of one process-lifetime object (ObjectKind 87, registrations 88). Two canonical behaviors are preserved rather than tightened — the volume setter clamps instead of refusing, and the indexed Play overload is not range-checked — and two deviations are forced by ownership and documented: a queue entry crosses as an independently owned copy rather than a borrowed view, because the canonical queue destroys its entries on every clear (which every play route does) and a borrowed handle would dangle, and cna_media_queue_add appends a copy because the canonical Add adopts the pointer it is given, which C cannot do without leaving the caller a stale handle. Both copies carry the same file and name, so they compare equal to the original — and appending a copy is exactly what the canonical player itself does when it enqueues a song. Four rows are not-applicable with a stated limitation: the queue's constructor, destructor and move operations, since exactly one queue exists and C never constructs, moves or destroys it. The test asserts the playback transitions as a relationship, because whether play actually starts playing depends on the platform's ability to decode the fixture rather than on the C API — the paused/playing round trip is asserted when playback began and the no-op contract otherwise. Green in all four trees (57/57) and under ASan+UBSan with leak detection on. |
| CBIND-037C7 | 42 | Complete Video and VideoPlayer | ✅ | video.h and CnaCApiVideo.cpp map both types (ObjectKind 89–90) and close the media module. The slice's one hard problem is the frame texture, and it is solved by lifetime rather than by copying: the player owns and replaces its texture, so cna_video_player_get_texture hands back a borrowed CNA_Texture2DHandle that the C layer invalidates on the next call to that player — any later route, including another get_texture, releases it, so a stale frame fails with CNA_RESULT_INVALID_HANDLE instead of touching freed memory. The graphics device is reported as presence only, because a borrowed device handle is valid solely inside the callback that produced it. Three canonical behaviors are reported rather than corrected, and two of them were established by running the code rather than reading the header: an undecodable file leaves the metadata zeroed and, on play, leaves the player stopped with its video cleared, so get_video answers CNA_FALSE; and FromUriEXT does not parse URIs at all — unlike the song factory it forwards the text to the file constructor, so an http: string is just a missing path. VideoInfo gains a real producer (cna_video_get_info) instead of being exposed as a type nothing can fill. One row is not-applicable: the test-access friend declaration. The test writes its own non-video fixture rather than depending on another suite's files — a dependency that first showed up as a parallel-ctest failure. Green in all four trees (58/58) and under ASan+UBSan with leak detection on. media now records 276 implemented, 0 partial, 0 planned and 52 not applicable, closing parent CBIND-037C. |
The 665 gamer-services rows are the whole of what is left in the campaign, and they split by what
each part needs to exist. The identities need nothing at all and are split in two only because there
are two unrelated vocabularies of them -- the gamer/guide one and the avatar one. The exceptions are
firewall conversions rather than types, exactly as the audio exceptions were. Everything after that
needs a gamer to exist, so the gamer and its collections come before the surfaces that read one, and
the avatar surfaces come last because they compose the graphics module as well.
Every slice in this table must answer the same question CBIND-037D answered for a machine with no
sensor: on every verification tree there is no signed-in gamer and no live service, so the
truthful answer is usually "not signed in" or "not available". Report that as the real availability
-- an ordinary success with the flag clear where the canonical API answers a value, a documented
refusal where it throws -- rather than pretending a gamer exists. Check plan_gamer_services.md and
AUDIT.md for which parts of a type are genuinely implemented before assuming a route has something
behind it.
| # | Rows | Task | Status | Acceptance criteria |
|---|---|---|---|---|
| CBIND-037G1 | 108 | Establish the gamer and guide identities | ✅ | gamer_services.h gains ten fixed-width identities — GamerPresenceMode, NotificationPosition, GamerZone, LeaderboardKey, LeaderboardOutcome, MessageBoxIcon, ControllerSensitivity, GameDifficulty, GamerPrivilegeSetting and RacingCameraAngle — each a uint32_t with one macro per canonical value at its canonical ordinal and a _MAXIMUM. GamerPresenceMode is the largest identity in the whole inventory at sixty values and keeps CornflowerBlue: the framework's own joke is a presence mode a game may really set, so a C caller has to be able to name it. GamerIdentitiesSmoke.c writes every value of all ten out in canonical order and asserts each sits at its own index, which is stronger than spot-checking ordinals — it catches a value inserted or removed in the middle, the change that actually breaks the ABI, while a rename is caught by the compile. C and C++ ABI assertions pin each identity's width, endpoints and maximum separately. Nothing here needs a gamer, a service or a handle, so it is the one gamer-services surface fully exercised on every tree. Green in all four (71/71) and under ASan+UBSan. New docs/c-api/GAMER_SERVICES.md. |
| CBIND-037G2 | 133 | Establish the avatar identities | ✅ | gamer_services.h gains seven more identities — AvatarBone, AvatarAnimationPreset, AvatarEye, AvatarMouth, AvatarEyebrow, AvatarRendererState and AvatarBodyType — on the shape G1 settled. AvatarBone is the exception in the whole inventory: its numbering is sparse, fifty-five bones spread over ordinals 0 to 70 with gaps, and the gaps are preserved rather than renumbered, because a bone index is what an avatar animation stores and closing them would silently repoint every animation onto the wrong joint. Its test pins each bone against its exact canonical ordinal instead of its list position, and asserts the sequence is strictly ascending so a duplicate or a reordering fails too. The two *NamesEXT free functions become count/copy pairs over an identity that take no game handle and no thread affinity — pure value operations, the shape the static sample computations already use — and refuse an undefined identity at the boundary so the diagnostic names it. The test asserts the two answer different kinds of string: a clip name is the identity's own spelling, a body-type name is a content path. Green in all four trees (72/72) and under ASan+UBSan. |
| CBIND-037G3 | 30 | Convert the six gamer-services exceptions at the boundary | ✅ | All six become firewall arms in CnaCApiDetail.hpp — the type is not bound, the conversion is — and they answer four different results, because the differences are what a caller acts on. No gamer services at all and a title that needs updating are both NOT_SUPPORTED: nothing the caller supplies or retries changes either. An absent network, a privilege the gamer does not have and a guide that is already visible are INVALID_STATE, the shape a disconnected storage device already has. A network operation that failed while the network was available is PLATFORM — a native service failed and a retry may get past it. The slice turned up a cross-module fact worth keeping: the networking module's NetworkSessionJoinException derives from this module's NetworkException, so the two modules share one exception hierarchy and the compiler refused the first arm order outright. Both derived arms now sit before their common base, and BoundaryDetailTest.cpp asserts an absent network stays distinguishable from a network operation that failed while connected. Green in all four trees (72/72) and under ASan+UBSan. |
| CBIND-037G4 | 124 | Complete the gamer, its collections and its per-gamer surfaces | ✅ | gamer_services.h and a new CnaCApiGamers.cpp map the gamer base, the signed-in gamer, friends, the profile and the collections. Every cna_gamer_* route accepts either handle kind, because the canonical surface belongs to the base both derive from — the test drives it twice, once through each. The GamerCollection<T> question the plan flagged is answered implemented rather than partial: the template's rows are mapped once through the instantiation this ABI creates, a friend collection, and the collection keeps every gamer handle it holds alive because the canonical one stores pointers it does not own. Each canonical begin/end pair is one route that still runs the callback, since the canonical operation completes before Begin returns; the gamertag lookup and the partner token always answer NOT_SUPPORTED, which is the canonical refusal rather than a gap. Four canonical facts are reported rather than smoothed: a friend's presence is free text while a signed-in gamer's is a mode and a value; the signed-in collection's player-index lookup is positional, not a search; SetPresenceModeStringEXT stores nothing; and GetFriends answers an empty collection, a success rather than a refusal, because no friend service exists. cna_friend_gamer_create_ext and cna_friend_collection_create_ext map the canonical factories and are also what makes the friend surface reachable at all, following the sensors' test-backend precedent. 17 rows are recorded not-applicable: GamerAction (no operation object crosses this ABI), begin/end (a C++ iterator is not expressible in C) and every protected member (this ABI supplies no derived class). Five rows moved to CBIND-037G6 — the leaderboard writer, the game defaults and the three achievement reads — because each answers a type that slice maps. Green in all four trees (73/73) and under ASan+UBSan. |
| CBIND-037G5 | 58 | Complete the Guide, the dispatcher and the component | ✅ | The plan asked whether the guide's operations complete before Begin returns like every other pair in this ABI. They do not — the keyboard input and the message box are the first genuinely deferred operations here: they stay pending until the user answers, and only then does the completion callback run. Only one of each may be pending, a second start is refused without disturbing the first, and the end routes take no operation argument because the C layer keeps that one operation itself — which is also what stops the canonical newed operation leaking when a caller never reads the answer. Thirteen guide screens are no-ops: there is no UI behind compose message, friends, invites, gamer cards, the marketplace, messages, party, party sessions, player review, players, sign-in or achievements, and no notification system to delay — they validate their arguments and do nothing. The keyboard and the message box are real only because this ABI draws them, which is what the two _ext renderers and the readable pending state are for. Two canonical behaviors are asserted rather than assumed: a cancelled input carries no text at all, so the cancellation flag is what separates it from a confirmed empty string, and discarding is not completing — it runs no callback. The dispatcher and the component are free routes and one ordinary component handle: cna_gamer_services_component_create publishes the first canonical component this ABI has, so the components adapter grew a factory to keep the handle, the registry entry and the ownership bookkeeping identical. Strict-C GuideSmoke.c; green in all four trees (74/74) and under ASan+UBSan. |
| CBIND-037G6 | 129 | Complete achievements, leaderboards and property storage | ✅ | Split into three slices below, all complete; this row closes when all three do. The three families are unrelated to each other but not independent in order: a leaderboard entry's columns are a PropertyDictionary, so property storage has to land before leaderboards. |
| CBIND-037G6a | 35 | Complete achievements | ✅ | An achievement is an owned handle over a value: a snapshot for the numbers and flags, four count/copy pairs for the text, and one cna_achievement_equals behind both canonical operators — and equality is by value across every field, which is what makes a handle the collection answered comparable with one the caller built. The collection copies what it is given and both indexers answer a copy rather than a view, because the canonical reference points into storage a later insert or remove would invalidate and a value loses nothing by being copied; a released source handle therefore changes nothing in it. Removal here answers whether anything was found, unlike the gamer collection's, because the canonical operation does. begin/end are not-applicable for the settled reason. Two absences are kept distinct: the gamer picture is absent (a clear flag, an ordinary success) while the achievement picture is unimplemented (NOT_SUPPORTED), so a caller can tell "there is none" from "this runtime cannot". cna_signed_in_gamer_get_achievements is the one gamer-services read that finds real data anywhere: it answers what CBIND-037G4's award persisted, and each entry carries only a key, a flag and a timestamp because no catalog exists to supply more. Strict-C AchievementsSmoke.c; green in all four trees (75/75) and under ASan+UBSan. |
| CBIND-037G6b | 50 | Complete property storage and game defaults | ✅ | A variant map is the one shape a C ABI cannot carry directly, so what crosses is a typed family plus a kind query: cna_property_dictionary_try_get_value_kind_ext says which of nine kinds a slot holds, and the matching typed getter reads it. That pair replaces the canonical boxed indexers, Add and Values — those four have no C form, and neither do begin/end — and loses nothing that matters, since every value the canonical getters understand is one of the nine kinds. Every typed getter checks the kind at the boundary: a wrong-kind read is INVALID_STATE, which a caller can act on, rather than the generic internal failure the canonical unboxing would produce, and an unknown key is INVALID_ARGUMENT — a deliberately different answer. Keys are walked by index because the canonical key list and bulk copy both answer containers C cannot receive. One canonical contradiction is reported rather than corrected: the dictionary describes itself as read-only and is nonetheless writable. GameDefaults is the ordinary case beside it — a fixed value whose two colors are optional, with a flag beside each, because not having chosen a color differs from having chosen black. Strict-C GamerPropertiesSmoke.c; green in all four trees (76/76) and under ASan+UBSan. |
| CBIND-037G6c | 44 | Complete leaderboards | ✅ | An identity is a value with an inline key, so a caller builds one on the stack; a key that does not fit or has no terminator inside it is refused rather than truncated. The plan's own note was wrong and is corrected here: a read is not the deferred shape. LeaderboardAction::getIsCompletedProperty returns true unconditionally, so BeginRead does the whole read inline and Read's spin loop exits at once — each pair is one route that then invokes the callback, exactly like the gamer's reads. Every entry this ABI hands out is a copy, because the canonical entry list is answered by value; its columns are the exception, since the dictionary handle aliases the entry's own and keeps it alive. The rating-changed hook is not a subscription — one per entry, replaced on attach, alive as long as the entry. Two things are deliberately absent, each for a defect this slice surfaced. The LeaderboardWriter has no C form: it captures its owning gamer's address at construction and neither it nor Gamer re-points that on a copy — the canonical source says so outright — and every gamer this ABI publishes is a copy, so its writer's owner already dangles; binding it would hand a caller a route that crashes rather than writes a score, so its 3 rows are not-applicable. And the canonical synchronous Read leaks the operation it creates, unlike Gamer::GetProfile which deletes its own: these routes do the same work through the same two public halves and release it, which the ASan tree caught and now guards. Strict-C LeaderboardsSmoke.c; green in all four trees (77/77) and under ASan+UBSan. |
| CBIND-037G7 | 83 | Complete the avatar surfaces and close the module | ✅ | The expression and the appearance are fixed values, validated where they are used rather than where they are built. A description is one fixed size and any other length is refused, while validity is a separate question its first byte answers; its height is always zero and its body type always female, because the canonical format carries neither — asking for a male body still reports female, and the binding says so. A preset animation carries the whole skeleton but no timeline: its length is zero until a real clip is loaded, so advancing it moves nothing. The canonical animation interface has no separate C form, because a C caller cannot implement a C++ interface and this ABI's animation handle is the only thing that does — the renderer's draw route takes the handle exactly where the canonical one takes the interface. A renderer keeps its description alive, reads and writes transforms and lighting as sets of optional parts, and always refuses the bind pose, because its state is always unavailable on this runtime while the parent-bone hierarchy still reads. Strict-C AvatarsSmoke.c; green in all four trees (78/78) and under ASan+UBSan. This closes the gamer-services module and the whole CBIND-037 campaign: the inventory has no planned row left. |
The 104 content rows split once, at the boundary between the manager a C consumer actually drives
and the XNB reader pipeline that only C++ type readers can participate in. The manager side lands
first because the reader side needs its stream and type-reader contracts already settled.
| # | Rows | Task | Status | Acceptance criteria |
|---|---|---|---|---|
| CBIND-036B1 | 40 | Complete the content manager, manifest values and load failures | ✅ | content.h and CnaCApiContent.cpp map every ContentManager.hpp, ContentManifestEntry.hpp and ContentLoadException.hpp row: resolved asset path and normalized cache key count/copy, built-in loader registration, service-provider presence, graphics-device get/set validated by borrowed-handle re-validation and pointer identity, the manifest and .xnb reader-usage snapshots as fixed PODs plus count/indexed copy, and typed Load<Texture2D>, Load<TextureCube> and Load<SoundEffect> routes returning independently owned handles. System::IServiceProvider stays a hard ABI boundary, so the two service-provider constructors and getServiceProviderProperty remain documented partial; RegisterTypeReader<T>, RegisterCnjLoader<T>, CnjLoaderFn<T> and the log alias are not-applicable because C cannot name an arbitrary C++ type. ContentSmoke.c builds its own content root through the storage API so the manifest scan is deterministic, and runs green in all three trees (48/48). |
| CBIND-036B2 | 64 | Complete the XNB reader pipeline | ✅ | content_readers.h and CnaCApiContentReaders.cpp map every remaining content row. An owned CNA_ContentReaderHandle is built over an owned storage stream plus an optional manager handle, both borrowed through new adapter records so neither System::IO::Stream nor ContentManager crosses the ABI; the borrow blocks closing the stream, and destroying the reader closes it exactly as the canonical binary-reader base does. The reader exposes asset name/version/platform, all six fixed-value reads plus the bounding sphere, the type-reader-table and shared-resource passes, both bounds checks and a capacity-checked exact-byte read that consumes nothing when it refuses. An owned CNA_ContentTypeReaderHandle comes from the static registry or from the known-unsupported placeholder factory and carries the target type name, type version, version support, in-place-deserialization capability and initialization; the registry itself is three free functions because the canonical manager holds no state. Type erasure is where the mapping stops and that is recorded, not papered over: the two untyped read routes are partial because a type-erased C++ object has no C representation, and the typed ReadObject<T>/ReadRawObject<T>/ReadSharedResource<T>/ReadAsset<T>/ReadExternalReference<T> templates, ContentTypeReader<T>, LooseFileContentTypeReader<T>, AddTypeCreator and the protected/detail declarations are not-applicable. ContentLoadException gained a central boundary conversion to CNA_RESULT_IO. Strict-C ContentReaderSmoke.c builds a compiled-asset fixture through the storage API and runs green in all three trees (49/49). The content module now has no planned row left. |
The 317 rows owned by CBIND-035F are partitioned by dependency boundary. Value and identity
contracts land first, then device lifetime/state, then the collections, frame-control, binding and
draw routes that consume them; the renderer-neutral graphics-ext post-process family is last
because it builds on the completed effect and texture contracts.
| # | Rows | Task | Status | Acceptance criteria |
|---|---|---|---|---|
| CBIND-035F1 | 49 | Establish device values and identities | ✅ | graphics_device.h maps the complete Viewport header through a fixed 24-byte POD whose six public fields are its whole property set, plus construction, aspect ratio, bounds get/set, title-safe area, project/unproject and exact UTF-8 string count/copy. CNA_ClearOptions, CNA_GraphicsDeviceStatus and CNA_Unsupported3DGraphicsCallBehavior freeze their native ordinals, and the native ClearOptions/SpriteEffects operator overloads collapse to C's own bitwise operators on the fixed-width aliases. Source-side static assertions bind every identity to its native ordinal; strict-C GraphicsDeviceSmoke.c covers all three constructors, both zero-dimension aspect cases, depth-range scaling, clip-space corners, identity and perspective project/unproject round trips, exact strings, capacity atomicity and null arguments under HEADLESS and SDL_RENDERER, with C/C++ ABI layout assertions. |
| CBIND-035F2 | 51 | Complete device lifetime, state, events and service surface | ✅ | graphics_device.h maps device disposal state, status, adapter index, profile, scissor/viewport/blend-factor/multisample-mask/reference-stencil get and set, exact UTF-8 type name and an explicit NOT_SUPPORTED dispose result that names the game's ownership of the canonical device. All six events become owned subscriptions with fixed CNA_ResourceCreatedEventInfo/CNA_ResourceDestroyedEventInfo payloads; a created resource and a destroyed resource's tag are honestly reported as presence only because the canonical events expose a partially constructed object and caller-owned native state. Game destruction invalidates live subscriptions after the device raises Disposing, so a subscriber observes it and its handle stays releasable. The shared exception firewall converts DeviceLostException/DeviceNotResetException to INVALID_STATE and NoSuitableGraphicsDeviceException to NOT_SUPPORTED with their exact messages. Strict-C HEADLESS and SDL_RENDERER tests cover every route, real resource events, defaults, non-finite viewport rejection, capacity atomicity, stale handles and post-destruction release; the adapter test covers all three exception conversions and C/C++ assertions freeze both payload layouts. Three GraphicsDevice friend declarations and the four service-level IGraphicsDeviceService events are recorded as not applicable and partial respectively. |
| CBIND-035F3 | 8 | Complete texture and vertex-texture collections | ✅ | graphics_device.h maps TextureCollection and both device collection properties through stage-addressed slot operations: CNA_TEXTURE_COLLECTION_MAX_TEXTURES is asserted against the native constant, the indexing operator becomes a versioned CNA_TextureSlotInfo read, the call operator becomes a validated bind and RemoveDisposedTexture becomes an explicit unbind. No native vector, collection reference or raw Texture* crosses the ABI, and a slot filled by canonical CNA code reports as bound with an invalid handle. Strict-C HEADLESS and SDL_RENDERER tests cover both stages across all 16 slots, bind/read/unbind round trips, self-unbinding on texture destruction, render-target rejection where the backend binds one, and invalid stage/slot/structure/handle failures, with C/C++ ABI layout assertions. |
| CBIND-035F4 | 21 | Complete frame control and buffer binding | ✅ | graphics_device.h maps all remaining Clear overloads, Present, all four Reset overloads, both remaining GetBackBufferData windows and the complete vertex/index binding set. The reference and pointer adapter overloads collapse into one nullable-index route that preserves the renderer-private window handle; the nullable readback rectangle becomes an explicit flag plus value; and an element count smaller than the selected region is decided in C as BUFFER_TOO_SMALL rather than surfacing as a generic native failure. The four canonical index-buffer accessors collapse to one validated get/set pair, multi-binding application is atomic on rejection, and reads report the owning C handle or an invalid handle for a binding applied by canonical CNA code. Strict-C HEADLESS and SDL_RENDERER tests cover every route, non-finite and unknown-bit rejection, observed reset event pairs, untouched destination bytes and honest backend refusal, with C/C++ ABI layout assertions. |
| CBIND-035F5 | 49 | Complete draw submission and device extensions | ✅ | graphics_device.h maps all three buffered draw routes, the canonical topology vertex-count helper and all twenty-nine user-primitive overloads through two calls whose versioned CNA_UserPrimitives/CNA_UserIndices descriptors carry the vertex-source, declaration and index-width dimensions. Built-in vertex sources are converted before submission because those structures embed a polymorphic Color, and a raw stream always requires a declaration since the declaration-less canonical raw overloads read native objects. All CNAEXT device helpers are mapped; the resource-notification hooks and GetRenderer are documented as having no C route because each carries a native object, with their observable effects reachable through the event subscriptions, tracked-resource count and renderer queries. Draw routes and pipeline-state toggles refuse a backend without 3D support as NOT_SUPPORTED, and an assigned current effect is kept alive by the C API because the device stores a borrowed pointer. Strict-C HEADLESS and SDL_RENDERER tests cover every route, validation, capability-accurate refusal and lifetime, with C/C++ ABI layout assertions. This leaves no planned GraphicsDevice.hpp row. |
| CBIND-035F6 | 21 | Complete SpriteBatch text routes and occlusion queries | ✅ | graphics.h collapses all six DrawString overloads into one versioned CNA_SpriteTextCommand, because both native text types are copied before layout and the parameter shapes differ only in which transform fields stay at their defaults; CNA_SpriteMeshEXT maps DrawMeshEXT with converted colors and positions, since the native Color carries a vtable, and reports NOT_SUPPORTED on a renderer without mesh submission. Exact type-name count/copy is added, and both non-C constructors are documented: the default one produces a deviceless batch and the other takes a renderer-private object. graphics_device.h owns OcclusionQuery through a game-child handle with capability-gated creation, begin/end, completeness, pixel count and live-renderer state; the handle is a full graphics resource, so the generic cna_graphics_resource_* routes cover its protected disposal and type name. Strict-C HEADLESS and SDL_RENDERER tests cover every route, malformed and foreign-handle rejection, retained font atlases and honest backend refusal, with C/C++ ABI layout assertions. |
| CBIND-035F7 | 118 | Complete graphics-ext post-process and pipeline settings | ✅ | graphics_ext.h maps all seven extension identity enumerations at their native ordinals, both settings bags as fixed-layout CNA_PbrMaterial/CNA_RenderPipelineSettings PODs with canonical-default initializers, and the three post-process effects. CRTEffect and DepthEffect become ordinary owned CNA_EffectHandle values so the whole cna_effect_* contract covers their inherited ShaderEffect behavior; AsciiPostProcessEffect gets its own handle because it is not a shader Effect, and its two Draw overloads collapse into one nullable-rectangle call. Because the extended layer is an opt-in build option, every declaration exists in every build and the effect routes report NOT_SUPPORTED when it is absent, so no exported symbol changes shape with the option. Strict-C tests cover the complete unavailable contract under HEADLESS without the layer and, under SDL_RENDERER with it, real creation of all three effects, exact canonical defaults, native clamping, every mode identity, cross-type handle refusal, cell-size validation and a real ASCII draw with a measured glyph grid, plus C/C++ ABI layout assertions. This closes parent CBIND-035F: no planned CBIND-035 inventory row remains. |
| # | Task | Status | Acceptance criteria |
|---|---|---|---|
| CBIND-035B2a | Complete MathHelper | ✅ | Eight exact CNA_MATH_* constants and 15 fallible cna_math_* operations map every non-deleted public MathHelper row. The implementation delegates canonical interpolation/clamp/distance/angle/epsilon behavior and replaces the native signed-overflow-prone MSAA bit trick with a defined full-positive-int32 equivalent; negative nonsensical sample counts fail without output mutation. Strict-C tests call every operation across endpoints, NaN/infinity, epsilon, full-range MSAA and null/failure cases; C++23 freezes constant values. |
| CBIND-035B2b | Complete Vector2 | ✅ | vectors.h maps all 75 remaining Vector2 rows through 41 exported operations: three constructors, four constants, complete member/static/operator math, exact UTF-8 count/copy and single/bulk matrix, quaternion and normal transforms. Value/out-ref pairs intentionally share the C result-plus-output form; full/range vector overloads share a count/index/length descriptor with preflight validation and defined sequential aliasing. VectorSmoke.c calls every entry point and covers IEEE division, normalization, hashes/strings, overload-equivalent results, transforms, capacity/null/range atomicity under both backends and ASan+UBSan. |
| CBIND-035B2c | Complete Vector3 | ✅ | vectors.h maps all 87 remaining Vector3 rows through 50 exported operations: four constructors, eleven direction/value constants, complete member/static/operator math including cross products, exact UTF-8 count/copy and single/bulk matrix, quaternion and normal transforms. Value/out-ref pairs share the C result-plus-output form; full/range overloads use preflight-validated raw array ranges with defined sequential aliasing. VectorSmoke.c calls every entry point and covers IEEE division, normalization, hashes/strings, direction identities, transforms, capacity/null/range atomicity under both backends and ASan+UBSan. |
| CBIND-035B2d | Complete Vector4 | ✅ | vectors.h maps all 81 remaining Vector4 rows through 46 exported operations: five constructors, six constants, complete member/static/operator math, exact UTF-8 count/copy, typed Vector2/3/4 matrix and quaternion transforms, and validated Vector4 bulk ranges. Value/out-ref and full/range overloads collapse to the established output/range forms with defined IEEE and sequential-alias behavior. VectorSmoke.c calls every entry point under both backends and ASan+UBSan. This closes parent CBIND-035B2. |
| # | Task | Status | Acceptance criteria |
|---|---|---|---|
| CBIND-035B3a | Complete Quaternion | ✅ | quaternion.h maps all 50 remaining Quaternion rows through 28 exported operations: both constructors, identity, complete member/static/operator math, axis/matrix/yaw-pitch-roll factories, concatenation, inversion, Lerp/Slerp and exact UTF-8 count/copy. Value/out-ref overloads share the result-plus-output form. QuaternionSmoke.c calls every entry point and covers rotation identities, normalized interpolation, IEEE zero normalization, aliasing, exact strings and null/capacity failures under both backends and ASan+UBSan. |
| CBIND-035B3b | Complete Matrix and close parent B3 | ✅ | matrix.h maps all 98 remaining Matrix rows through 57 exported operations: both constructors, Identity, all seven directional/translation get/set properties, decomposition, determinant, equality/hash/string, every billboard/rotation/view/projection/scale/shadow/translation/reflection/world factory and complete arithmetic/transformation operators. Nullable pointers represent optional billboard directions. MatrixSmoke.c calls every entry point and covers row-major fields, property signs, decomposition success/failure outputs, projection rejection without output mutation, singular IEEE inversion and exact strings under both backends and ASan+UBSan. This closes parent CBIND-035B3. |
| # | Task | Status | Acceptance criteria |
|---|---|---|---|
| CBIND-035B4a | Complete Plane and Ray | ✅ | geometry.h maps all 42 remaining Plane/Ray rows through 31 operations: all constructors, dot/normalization/transforms, volume classification, equality/hash/string and box/sphere/plane/frustum ray intersection. Native optional distances become an explicit hit flag plus distance, with zero distance on miss. GeometrySmoke.c calls every entry point and covers classifications, matrix/quaternion transforms, hit/miss distances, frustum containment, exact strings and null/capacity failures under both backends and ASan+UBSan. |
| CBIND-035B4b | Complete BoundingBox | ✅ | geometry.h maps all 31 remaining BoundingBox rows through one corner-count constant and 20 operations: construction, all containment/intersection overloads, factories, merge, equality/hash/string and explicit optional ray distance. Corner copy uses a caller-capacity array, always reports the required count and performs no partial write. GeometrySmoke.c calls every entry point and covers canonical corner order, capacity atomicity, classifications, hit/miss distances, factories, exact strings and null/empty failures under both backends and ASan+UBSan. |
| CBIND-035B4c | Complete BoundingSphere | ✅ | geometry.h maps all 31 remaining BoundingSphere rows through 21 operations: construction, nonuniform matrix transformation, every containment/intersection overload, box/frustum/point factories, merge, equality/hash/string and explicit optional ray distance. Point arrays use checked counts and preserve outputs on rejection. GeometrySmoke.c calls every entry point and covers containment boundaries, hit/miss distances, touching spheres, all factories, merge, exact strings and null/empty failures under both backends and ASan+UBSan. |
| CBIND-035B4d | Complete BoundingFrustum and close parent B4 | ✅ | geometry.h maps all 31 remaining BoundingFrustum rows through one corner-count constant and 22 operations. The matrix remains the direct POD property; construction derives all six plane queries and eight canonical corners on demand. Value-equal frusta preserve same-value containment, caller-capacity copies are atomic and native boundary-origin ray NotImplementedException becomes CNA_RESULT_NOT_SUPPORTED without output mutation. GeometrySmoke.c calls every entry point under both backends and ASan+UBSan. This closes parent CBIND-035B4. |
| # | Task | Status | Acceptance criteria |
|---|---|---|---|
| CBIND-035B5a | Complete CurveKey value operations | ✅ | curve.h maps all 19 CurveKey rows through a fixed 20-byte CNA_CurveKey and 17 operations covering every constructor/property, clone, comparison, equality/operator and hash route. Unknown continuity values are rejected before output mutation; native IEEE/NaN comparison behavior is preserved. Strict C17 and C++23 assertions freeze size, alignment and every field offset, while CurveSmoke.c calls every entry point under both backends and ASan+UBSan. |
| CBIND-035B5b | Complete CurveKeyCollection ownership and mutation | ✅ | curve.h maps all 26 CurveKeyCollection rows through an owned, generation/type/thread-validated handle and 14 operations. Count/get and atomic destination-index copy replace native aliases, indexers and all iterator routes; add/set preserve native position ordering, while clear/clone/contains/index/remove map collection behavior without leaking std::vector. CurveSmoke.c calls every entry point and covers ordering, repositioning, clone independence, capacity atomicity, invalid keys/indices and invalid/stale/wrong-thread handles under both backends and ASan+UBSan. |
| CBIND-035B5c | Complete Curve evaluation and close parent B5 | ✅ | curve.h maps all 15 Curve rows through a generation/type/thread-validated owned handle and 14 operations. Both native key-reference properties collapse to an owned mutable collection-view handle that retains the curve; creation/destruction, constant state, both loop properties, deep clone, evaluation and every tangent overload delegate to the canonical implementation. CurveSmoke.c calls every entry point and covers all five loop modes, all tangent overloads, clone independence, retained-view lifetime, invalid enums/indices and invalid/stale/wrong-thread handles under both backends and ASan+UBSan. This closes parent CBIND-035B5. |
| # | Task | Status | Acceptance criteria |
|---|---|---|---|
| CBIND-035B6a | Complete Color value operations | ✅ | color.h maps all 25 previously planned non-constant Color rows through the existing four-byte CNA_Color, direct channel fields and 24 operations covering all constructors, packed value, exact/debug strings, conversions, equality/hash, Lerp, both premultiplication routes, multiplication/operators and packed-vector mutation. Integer premultiplication explicitly preserves FNA's unchecked Int32 product without C++ signed-overflow UB. ColorSmoke.c calls every entry point and covers ABI/order, clamp/truncation/wrap, exact strings, capacity atomicity and null failures under both backends and ASan+UBSan. |
| CBIND-035B6b | Complete named Color constants and close parent B6 | ✅ | named_colors.h maps all 141 public named colors to directly usable C17/C++23 CNA_COLOR_* value expressions built from exact RGBA channels. ColorSmoke.c passes every expression through the packed-value API and independently compares it with the canonical AABBGGRR literal, while public-header builds prove both language modes. This closes parent CBIND-035B6. |
| # | Task | Status | Acceptance criteria |
|---|---|---|---|
| CBIND-038 | Expand pure-C compatibility matrix | ✅ | tools/c-api/compatibility_matrix.json declares the toolchains, language modes and build configurations the ABI claims, and generate_compatibility_matrix.py both runs that matrix and generates docs/c-api/COMPATIBILITY.md from it. Each cell compiles every public header on its own — which is what proves a header is self-contained — plus the umbrella twice for its include guards: 60 translation units per cell, 1,380 across the 23 cells this machine has toolchains for. Two rules make the result honest rather than flattering: a toolchain that is installed is binding (present and rejecting a header fails the gate, required or optional), and a toolchain that is absent is skipped by name, never counted as agreement. Both are registered as build-free ctest gates (CApiCompatibilityMatrix, CApiHeaderCompatibility) beside CApiCoverageMatrix, with a CI workflow that installs the optional toolchains so their cells become evidence. The matrix found a real defect on its first run: CNA_PowerState was declared twice — once for a controller's power and once for the host's, same name and same six values — which C11 tolerates and C99 rejects. The duplicate is gone, devices.h now reuses the identity input_gamepad.h declares, and C99 is the floor rather than an unexamined claim. Verified to catch a regression by reinstating the duplicate: four C99 cells turn red while C11 and later stay green. Green in all four trees (78/78) and under ASan+UBSan. |
| CBIND-039 | Add ABI layout, export and compatibility gates | ✅ | tools/c-api/abi_baseline.json is a checked-in snapshot of what the ABI actually is, and generate_abi_baseline.py is what measures it: a generated probe reports every one of the 166 structs' size and alignment and every field's offset and size, 258 scalar typedefs' widths, 1,183 integer constants, 14 string constants and 141 named-color channel sets as the compiler really lays them out, then nm -D reads the shared object's 2,720 cna_* exports and the library is asked its own ABI version. The hand-written walls in AbiHeaderC.c/AbiHeaderCpp.cpp pin what each slice remembered to pin; this pins everything else, and the value is that a change arrives as a reviewable diff instead of a silently different binary. Differences are classified against docs/c-api/ABI_VERSIONING.md: an added struct, field, constant or export is an addition the evolution policy permits and the baseline is re-recorded; a moved or resized field, a changed size/alignment or constant value, a vanished export, or a library whose reported version disagrees with its headers is named individually as an ABI break. --library is optional, which splits the gate honestly in two: CApiAbiHeaderBaseline measures the header half with no build at all — so the ordinary build and a build-free CI job catch a moved field, which is where one is most likely to be introduced — while CApiAbiBaseline adds the two halves only a built library can answer, and what it cannot see it reports as skipped by name rather than passing over. Two defects the tool found in itself are worth recording: a \s-based macro regex made an include guard swallow the next line as its value, and CNA_PRESENTATION_PARAMETERS_TYPE_NAME, written across a line continuation, was therefore measured as a pointer — so the probe is now run twice and any value that differs between runs is refused as not a compile-time constant. Verified to catch both arms: swapping CNA_Point's two fields reports them as moved, and an added constant is classified as an addition. All four configurations export the same 2,720 symbols — the ABI surface does not vary with the renderer or with CNA_DEVICES, only the answers do — which the four green trees now prove rather than assume. Green in all four trees (78/78 plus both new gates), including the sanitized tree, which passes its own -fsanitize flags to the probe rather than being excluded. |
| CBIND-040 | Add safety and lifetime stress tests | ✅ | Run invalid-handle, double-release, stale-generation, shutdown-order, callback-unregister, UTF-8/buffer-boundary and high-volume create/release tests under ASan/UBSan where supported. Add focused fuzz targets for parser-like/buffer-facing APIs. Split into CBIND-040A (the stress suite) and CBIND-040B (the fuzz targets): the first is about what the ABI does when a caller gets it wrong, the second about what it does when nobody chose the input at all, and they share nothing but the sanitized tree. |
| CBIND-040A | Stress the handle, teardown and buffer contracts | ✅ | StressSmoke.c puts the lifetime rules under load instead of asserting them once: 4,096 create/destroy cycles keeping every handle ever issued, then requiring that no value was issued twice and that each dead one still refuses after the slots beneath it were recycled thousands of times; handles the registry never issued; a live handle carried into another family's route; thread affinity proved from a second thread and then proved not to have half-released anything; a capacity sweep over a copy route from zero past the exact length, checking at every step that the required count is reported, that a refusal writes not one byte, and that a success writes no terminator; the same sweep on the diagnostic itself, which must survive being read three times; and 20,000 parent/child churn cycles that are also the leak check. Three real defects, one of them a memory-safety bug. (1) A live game-event or game-window registration could outlive its game — cna_game_destroy guards owned resources but deliberately not subscriptions — and the later cna_game_unsubscribe then ran ~GameRegistration against a freed handler collection: a heap use-after-free, confirmed by the sanitized tree, with the source carrying a comment asserting the invariant it did not have. Fixed the way the graphics-device family already solved the same problem: live registrations are tracked and invalidated once the game has raised its disposal event, so the subscriber still observes the disposal and the later unsubscribe detaches nothing. Refusing the destroy was rejected as the fix — it would make CNA_GAME_EVENT_DISPOSED unobservable to anyone. (2) HANDLES.md claimed a zero handle answers CNA_RESULT_INVALID_ARGUMENT; every route has always answered CNA_RESULT_INVALID_HANDLE, and the tests have always expected it. The document was wrong, and zero is a handle value, not a missing argument — corrected, with the argument category's actual meaning spelled out. (3) The compatibility matrix declared the sanitized configuration as HEADLESS; it is and has been SOFTWARE, which is a factual error in a document whose whole purpose is evidence. Corrected, and the honest consequence recorded: SOFTWARE is the renderer exercised in both CNA_DEVICES states. Green in all four trees (79/79), including ASan+UBSan with leak detection. |
| CBIND-040B | Add fuzz targets for the parser-like surfaces | ✅ | The byte-facing surface of this ABI is short — ValidateStringView/CopyStringView behind every CNA_StringView, and ValidateBuffer/CheckedElementByteCount behind every array and count — so it is covered two ways rather than one. CApi_Utf8Oracle enumerates where enumeration is possible: every byte sequence of length one, two and three under both embedded-NUL policies, 16,843,008 cases, which is the entire space in which a UTF-8 scanner's mistakes live (truncation, overlong forms, surrogates, out-of-range lead bytes, stray continuations); four-byte sequences are 4.3 billion, so that sweep is structured instead — every lead byte crossed with the values where a decision changes — and longer strings come from a fixed seed, so a failure is reproducible rather than lucky. tests/fuzz/StringViewFuzz.cpp adds a libFuzzer entry point for what enumeration cannot reach. Both judge the answer, not the absence of a crash, against an oracle in tests/support/CApiFuzzOracle.hpp that is deliberately a different algorithm: the implementation matches byte ranges and never forms a code point, the oracle decodes the code point and applies the Unicode rules to the value; the implementation asks whether a product would overflow by dividing, the oracle forms the whole 128-bit product from 32-bit limbs and looks at it. An oracle mirroring the implementation would agree with its mistakes. Verified to catch a real disagreement rather than merely to run: inverting the surrogate rule makes the sweep fail naming ED A0 80 — U+D800 — and makes the fuzz target abort on the same bytes, proved with clang 19 and a real reproducer file. The fuzz target is not a ctest test, because it needs Clang and does not terminate; it is compiled by the normal build as an object library nobody links, so it cannot rot silently, and docs/c-api/FUZZING.md carries the command line. __int128 was the obvious oracle and is not standard C++ under the -pedantic wall these targets build with, hence the limb arithmetic. Green in all four trees (80/80), the sweep costing under a second natively and about six under ASan. This closes parent CBIND-040. |
| CBIND-041 | Publish C consumer documentation and examples | ✅ | The task's real content turned out to be a defect, not a document: find_package(CNA CONFIG) could not work, because the module installed CNACTargets.cmake and no CNAConfig.cmake beside it — every consumption instruction anyone might have written would have been aspirational. There is now a package: a config file, a version file whose version is read out of abi.h at configure time so find_package(CNA 0.1 CONFIG) cannot come to mean something different from cna_get_abi_version(), SameMinorVersion compatibility matching what ABI_VERSIONING.md promises for experimental 0.x, and a CNACApi install component so the smallest useful install is the C ABI rather than 113 MB of SDL and GoogleTest headers. modules/c-api/examples/c/hello_cna.c is the program a newcomer copies: one file, every call checked, doing in order the eight things a first CNA program must do — version check, game creation with callbacks, borrowing the device inside a callback, asking capabilities instead of assuming, the count/copy string idiom, reading the diagnostic after two deliberate mistakes (a null output is an argument failure, a dead handle is a handle failure, and arguments are checked first), drawing a texture, and tearing down children before the game. It builds at the C99 floor under -Wall -Wextra -Wpedantic -Werror. CApi_InstalledConsumer is what makes the documentation binding: it installs the component into a staging prefix, configures the example as a standalone project whose only knowledge of CNA is CMAKE_PREFIX_PATH, builds it and runs it, and requires the program's output lines so it cannot pass by exiting zero without reaching the graphics device. A failure at each step means a different defect — not a package, wrong headers or export, library will not load — and none of them is visible from inside the build tree. Two honest limitations are recorded rather than papered over in docs/c-api/CONSUMING.md: the library carries DT_NEEDED entries for SDL3 and FFmpeg that the package does not ship (its INSTALL_RPATH is now $ORIGIN, so a deployment that places them beside it works, and the gate passes -Wl,-rpath-link and LD_LIBRARY_PATH exactly as a consumer would have to), and CNA_C_API_STATIC names a future static configuration that does not exist — a static build would export every C++ symbol it archived and the ABI promise would be meaningless. The sanitized tree hands its own -fsanitize flags to the consumer rather than being excluded, the same accommodation CBIND-039 makes. Green in all four trees (81/81). |
| CBIND-042 | Define experimental release gate | ✅ | Require the B7 matrix, a real C application, documentation, installability, no unreviewed ABI break and a known-limitations matrix before publishing an experimental C ABI release. ABI 1.0 requires a later explicit release decision. Split into CBIND-042A (the known-limitations matrix, which the gate requires and which did not exist) and CBIND-042B (the gate itself). |
| CBIND-042A | Publish the known-limitations matrix | ✅ | docs/c-api/LIMITATIONS.md answers the question COVERAGE.md cannot. The inventory records all 6,415 public C++ declarations and what became of each; this collapses the ones that did not become a callable C route into the reasons behind them — 28 partially mapped symbols in 11 groups, each naming what the C route covers and what it leaves out, and 320 with no C form in 64 groups classified under 12 declared themes. It is generated from the same inventory plus tools/c-api/limitations.json, so it cannot drift, and it carries the limitations an inventory structurally cannot: the packaging gaps, the one-runtime-per-process rule, thread affinity, the 0.x status and the renderer boundary. Three rules are mechanically enforced, and the first run broke on two of them. (1) Every unmapped reason must fall under a declared theme — five reasons had no home, which is exactly how a limitations document acquires a silent "other" bucket. (2) A deferral may not name a task the plan records as finished — SpriteBatch::Begin's Effect and transform overloads still deferred to CBIND-035, closed, work never landed; and the four IGraphicsDeviceService events still deferred to CBIND-037, closed, work did land. The first was re-pointed at CBIND-044, which owns the decision to add them or record the omission as permanent; the second was simply wrong and is now implemented against cna_graphics_device_manager_subscribe, moving 4 symbols and making the snapshot 6,067 implemented / 28 partial. A third stale deferral, the signed-in-gamer collection, named its owner in prose rather than by id and was corrected too. (3) The counts come from the inventory, not from prose. Registered as the build-free ctest gate CApiLimitations beside the coverage and compatibility gates, with a CI workflow. All three failure modes verified by hand and the deferral one in CI: a stale document, a deferral to a closed task, and an unclassified reason each turn the check red. |
| CBIND-046 | Offer a static configuration | ✅ | Owner decision, 2026-08-16: build it. The objection that had blocked it was real and is not waved away: an archive carries every object it swallowed, so ar-ing the C API together with 25 CNA and Sharp Runtime archives would publish 68,120 global C++ symbols into a consumer's program and the ABI's claim — 2,720 cna_* names and nothing else — would stop meaning anything. tools/c-api/generate_static_archive.py finishes the job instead of skipping it: it reads the link line CMake already computed for the shared library, so the closure cannot drift from the one that produces the working .so; partially links all of it into one relocatable object (deduplicating archives, since CMake repeats them for cyclic dependencies and --whole-archive would otherwise pull every member twice); localizes every global that is not part of the ABI; and fails the build if any non-cna_* symbol survives. What does survive is measured and bounded: 83–95 depending on the tree, every one of them STB_GNU_UNIQUE — function-local statics in inline and template code, which objcopy refuses to localize precisely because their uniqueness is what makes them correct. They are mangled C++ names no C program can collide with, and because the gate is written as a property rather than a count, a symbol of any other binding fails the build in any configuration. The package installs the archive and a generated CNACStaticTargets.cmake, so a consumer writes target_link_libraries(app PRIVATE CNA::CApiStatic) and gets CNA_C_API_STATIC, the include path and the whole external link line for free; the same hello_cna.c is built both ways from the installed package and both are run, which is what stops the two halves drifting. -DCNA_C_API_BUILD_STATIC=OFF turns it off per tree because the archive is a few hundred megabytes in a debug build and is rewritten on every relink — and where it is off the consumer gate says so by name rather than testing half a package in silence. Three traps: CMake's find_program does not search when its result variable is already defined, so initializing it to "" silently finds nothing; the intermediates are each the size of the archive, so the tool deletes them rather than tripling the cost of every relink; and this repository's .gitignore opens with build*, which silently swallows a file named build_static_archive.py — hence generate_static_archive.py, which also matches the naming of every other tool in tools/c-api/. Green in all four trees (81/81), the static consumer carrying no dependency on libcna_c_api.so at all. |
| CBIND-045 | Ship the native libraries CNA builds | ✅ | Owner decision, 2026-08-16: the package carries them. The CNACApi component now installs libSDL3, libSDL3_image and libSDL3_mixer -- with their soname symlinks, because DT_NEEDED names libSDL3.so.0 and not the versioned file -- into the same directory as libcna_c_api.so, whose INSTALL_RPATH is already $ORIGIN. The result is a package that needs no environment variable of any kind: it links without -rpath-link and runs without LD_LIBRARY_PATH. That is not asserted but measured — CApi_InstalledConsumer now passes neither, so a regression fails a test instead of surprising a consumer, and the program was additionally run under env -i with every SDL library resolving out of the staged install. FFmpeg deliberately does not ship: libavcodec, libavformat, libavutil and libswresample come from the distribution, and copying a distribution's binaries here would take on their redistribution terms, freeze their soname against future security updates and drag in the transitive libraries they were linked against. It stays a system dependency, named in docs/c-api/CONSUMING.md. The release-gate criterion changed from an owner question into a measurement of the install rules and of what the consumer gate no longer needs to pass. Two traps worth remembering: CMake rejects FILES_MATCHING after any PATTERN, exclusions included; and one tree appeared to pass on a stale generated cmake_install.cmake because the reconfigure had failed with its output redirected to /dev/null — always check the configure's exit status before believing a green suite. Green in all four trees (81/81). |
| CBIND-042B | Define the experimental release gate | ✅ | The decision that everything above is enough, expressed as a declaration plus a checker rather than prose: each criterion names its mechanical evidence and its measured state, and the check fails when the recorded state and the measured state disagree in either direction — a criterion recorded as met that no longer is, and one recorded as blocked that has quietly become met. The two packaging questions in docs/c-api/LIMITATIONS.md marked open decision are the owner's to rule on and must block the release until they are ruled on. ABI 1.0 stays a separate, later decision. Done, and the verdict is now READY: both owner decisions were ruled on and implemented by CBIND-045 and CBIND-046, so all ten criteria are met and measured. tools/c-api/release_gate.json declares ten criteria, each naming its requirement, its mechanical evidence and its recorded state; check_release_gate.py measures every one of them on every run and docs/c-api/RELEASE_GATE.md publishes the verdict. Eight are met and measured, not asserted — the 23-cell compatibility matrix, the ABI baseline's 166 layouts and 2,720 exports, an inventory with zero planned rows, the limitations matrix, a real C application built from an installed prefix, the package config, thirteen documents, and the stress/oracle/fuzz evidence. The remaining two are owner decisions, and the gate's most important property is that it cannot be talked out of them: the current verdict is NOT READY, with the reason stated as "every mechanical criterion is met; what remains is 2 decisions that no implementer may make alone". The check fails in both directions — a criterion recorded as met that regressed, and, more importantly, one recorded as blocked that has quietly become met, which is the failure mode a release gate actually dies of because nobody re-reads a document that says "not yet". Both directions verified: adding a required document that does not exist turns documentation red, and ruling on the static-configuration question in limitations.json alone makes the gate refuse until release_gate.json is updated to match — a decision recorded in one place and not the other is not a decision. Registered as the build-free ctest gate CApiReleaseGate with a CI workflow that reproduces the second direction. Green in all four trees. |
next was merged into feature/binding on 2026-08-16. The merge is green — the C API suite passes
81/81 and every C API gate except the coverage matrix — but it reopened the coverage question,
and that is recorded here rather than papered over.
| # | Task | Status | Acceptance criteria |
|---|---|---|---|
| CBIND-047 | Reconcile the coverage matrix with the platform separation | ✅ | The merge grew the tracked public surface from 6,415 declarations in 414 headers to 7,742 in 444, leaving 1,303 unmapped rows: 1,196 in modules/platform, 52 in audio, 41 in core, and a dozen across runtime, devices, devices-ext and input. Until they are dispositioned the release gate reads not ready, and correctly so. The question the owner must settle first is whether CNA::Platform belongs in the C ABI's scope at all: it is an implementation substrate the C API sits on top of — a C caller reaches platform behaviour through the routes that use it, never through IPlatform — which is the same argument that keeps CNA::Internal::* out of the inventory by path. If it is out of scope, the fix is one scope rule and 1,196 rows disappear; if it is in scope, each family needs a mapping or a recorded limitation. Do not answer this by widening the inventory's exclusion list quietly — the exclusion is what the whole matrix's honesty rests on. Answered by the project owner on 2026-08-16: CNA::Platform is out of scope, and the exclusion is written where it can be read — a named EXCLUDED_MODULES rule carrying its owner, its date, its reason and its size, and reported in COVERAGE.md's own preamble rather than buried. The remaining 107 rows were then dispositioned one group at a time: CNA::Audio::Platform (52) is the audio module's own substrate and gets a theme of its own in LIMITATIONS.md; the sensor subsystem friendships, the INTERNAL_ window and touch hooks, the game's platform accessors and test peer, the empty haptic device, two std::function aliases and GetNativeWindowHandleEXT are all recorded not-applicable with their reasons. core-platform was re-pointed at CNA::TargetPlatform, since the rename took the old name for the new module while the published CNA_PLATFORM_* identities stayed exactly where they were. The inventory is 417 headers, 6,508 symbols, 6,080 implemented, 12 partial, 36 planned, 380 not applicable — and the 36 are not leftovers but a real surface worth binding, carried by CBIND-049. |
| CBIND-049 | Bind the renderer selection and fallback surface | ✅ | The 36 rows CBIND-047 deliberately left planned, because they are the one part of what next added that a C consumer genuinely wants: CNA::GraphicsRendererSelection (13) chooses a renderer at runtime and reports which one is active, which are available and whether the choice is still open; GraphicsRendererFallbackReason (5), GraphicsRendererFallbackRecord (4) and GraphicsRendererSelectionAccessEXT (5) say what fell back and why; three free functions name and parse renderer identities; CNA::Logger::Sink (3) lets a caller take the log; and isApplePlatform/isMobilePlatform/getCurrentPlatformName round out what cna_platform_get_current already answers. This is the runtime counterpart of the compile-time CNA_GRAPHICS_RENDERER the ABI already publishes, so the identities exist and only the routes are missing. Done, and the matrix is closed again: 6,111 implemented, 12 approved partial, 0 planned, 385 not applicable. Twenty-five new routes. GraphicsRendererSelection becomes cna_graphics_renderer_set_preferred_ext/_by_name_ext, _get_selected_ext, _get_active_ext, _get_is_latched_ext, the available count/copy/is-available trio, the fallback chain and switch, and the fallback history read by index; GraphicsRendererFallbackReason gets four identities at the canonical ordinals; GraphicsRendererFallbackRecord becomes a fixed struct without its message, because a string of unbounded length never goes in a fixed struct in this ABI and is read by the usual count/copy pair. Logger::SetSink becomes a function pointer plus an opaque context, with the line crossing as counted borrowed bytes. Three findings. GetActive() refuses before the latch -- "until something is created there is no honest answer to give" -- so the C route reports CNA_RESULT_INVALID_STATE and my first header text, which promised it would equal the selected renderer, was simply wrong. GraphicsRendererSelectionAccessEXT is recorded not-applicable, and the canonical header explains why better than I could: it is the inward direction, kept separate "so the two directions are not confusable" -- telling the selection which renderers are compiled in is a fact about the build, not a choice a caller may make. And the debugging cost one round to a trap this campaign already knows: an fprintf whose arguments both call a route and read its output is unsequenced in C, so my own diagnostic lied about which value was wrong. Green in all four trees, 81/81 and 88/88; the release gate reads ready again. |
| CBIND-048 | Restore the device test seams on the platform services | ✅ | The merge's one unresolved breakage, and it needs a design decision rather than a port. CnaCApiDevices.cpp implements four fake backends — TestMessageBoxBackend (30 lines), TestFileDialogBackend (47), TestTrayBackend (86) and TestCameraBackend (40) — against CNA::Devices::Detail::I{MessageBox,FileDialog,Tray,Camera}Backend, and the platform separation removed all four interfaces. They have no like-for-like replacement: tray, dialogs and message boxes are now services a platform hands out (IPlatform::GetTray() and siblings), and Camera no longer takes a backend at all — it opens whatever the selected platform's IPlatformCameraProvider reports. The seam moved from devices-ext down into the platform, so the published cna_*_set_test_backend_ext routes have nothing left to inject into. Three ways out, and the choice is the owner's: substitute a whole test IPlatform (faithful, and much larger than what it replaces); ask the platform campaign for a per-service test seam (smallest, but it is their contract to extend); or withdraw the injection routes and test devices only against a real platform (honest, and loses the hardware-free coverage those routes exist for). Owner decision, 2026-08-16: add the install hook and substitute a test platform — and the hook turned out to exist already: SetCurrentPlatform is public, so the platform contract needed no change at all. CnaCApiPlatformOverride is the one seam that replaces four: it forwards all 34 IPlatform methods to the real platform and substitutes only the services a test asked to fake, so installing a fake camera leaves the keyboard, the clipboard and the file system real. The four fakes became three, because the platform hands out one dialogs service where the C API had a message-box backend and a file-dialog backend; the tray and the camera each split into a provider and an instance, so tray destruction moved from an explicit Destroy() to the icon's destructor. Three findings worth keeping. The lifetime rule is the whole difficulty: a Game owns its platform and installs it on a stack, so the decorator must resolve what it wraps at install time and be uninstalled from the game-destroy path -- capturing the first platform ever seen outlives it, and leaving the decorator installed stops Game::UninstallPlatform re-aiming, because it only re-aims when the game is the one currently aimed at. The sanitizer's report was a consequence, not the cause: the heap-use-after-free in UninstallPlatform appeared because the test failed early, left its game in the registry, and the registry destroyed it at exit after Game.cpp's static platform stack had gone -- a latent static-destruction-order fault in next that any leaked game will hit. And the camera's semantics genuinely changed: Camera::getStateProperty can no longer produce Closed, because a device the platform did not open is never handed out, so cna_camera_set_test_state_ext now refuses CLOSED and NOT_SUPPORTED rather than accepting a state it cannot read back, and a fresh test camera reports OPENING. Green in all four trees, 81/81 and 88/88. |
| CBIND-050 | Pin every coverage rule to the symbols it was reviewed against | ✅ | The second merge of next (2026-08-17) showed that the coverage gate had been quietly lying, and by more than the merge itself. The merge added 176 public C++ declarations. The inventory flagged 55 of them as unmapped — and counted the other 121 as implemented, with test evidence, when not one C route existed for any of them. The cause is structural, not a slip: 73 of the 497 mapping rules match a whole header with qualified_name_regex: ".*", which is a reasonable way to say "this header's contract is bound in full" and a disastrous way to say it forever, because every declaration added to that header afterwards inherits the claim. Six such rules absorbed the merge: the PBR effects rule alone took 84 new members — ior, specularFactor, normalScale, occlusionStrength, alphaMode, alphaCutoff, doubleSided, the sRGB flags, the per-slot texture transforms — and grep for a single matching cna_pbr_effect_* route returns nothing at all. A rule now carries approved_symbols, the stable IDs an owner actually reviewed, and a symbol is covered only when its pattern and its ID match; anything else falls through to planned. That choice is deliberate over the cheaper count-based ratchet the owner was offered: a count that merely errors blocks regeneration and never says which symbols are new, while pinning always regenerates and lets the release gate's one existing criterion carry the failure. Approvals were seeded from the pre-merge tree rather than from the current one — seeding from the current one would have recorded the merge's 121 unreviewed rows as owner-approved, which is the very defect being fixed — and the proof the seeding is exact is that implemented came back at 6,111, the pre-merge figure to the symbol. --approve-rule-symbols is a separate mode on purpose: the routine --write a developer runs must not be able to widen a rule's authority. The inventory now reads 420 headers, 6,684 symbols, 6,111 implemented, 12 approved partial, 176 planned, 385 not applicable, coverage-closed is recorded not met and RELEASE_GATE.md reads not ready — all three of which are simply true. CBIND-051 binds the 176. |
| CBIND-051 | Bind the glTF import report and the KHR PBR material surface | ✅ | The 176 rows CBIND-050 exposed, which the owner decided on 2026-08-17 to bind in full rather than disposition. Split into 051A-051E. CNA::Internal::Graphics::ConfigureModelMaterialVariantsEXT is the one row to record not-applicable rather than bind. Each slice finished by re-approving only its own rule with --approve-rule-symbols --rule <id>. Done across 051A-051E: 175 rows bound, 1 recorded not applicable, coverage-closed met and the release gate ready. |
| CBIND-051A | Bind the KHR PBR material extensions | ✅ | The 84 rows on PbrEffect and SkinnedPbrEffect: index of refraction, specular factor and colour factor with their two maps, normal scale, occlusion strength, per-slot packed UV selectors and texture transforms, the base-colour/emissive/specular-colour sRGB flags, output sRGB encoding, and the alpha mode/cutoff/double-sided triple. Eighty-four C++ rows became twenty-four routes, and that ratio is the whole design. The existing surface already collapsed five separate map properties into one CNA_PbrTextureSlot, and both effects already share one CNA_EffectHandle family, so the two new specular maps become slots 5 and 6 rather than a second vocabulary — which also makes setSpecularMapEXTProperty and SetOwnedSpecularMapEXT land on the retaining route that already serves their five siblings. The per-slot UV selector and transform routes then cover all seven slots even though the canonical API keeps the specular pair as standalone properties beside an array of five; the C surface hides that asymmetry rather than reproducing it. CNA_TextureTransformEXT is a versioned POD with an _init and an equality route, and CNA_AlphaModeEXT is a four-identity typedef with a maximum. The sRGB flag is the one place the mapping deliberately refuses: only base colour, emissive and specular colour carry one, and asking about the normal, metallic-roughness, occlusion or scalar specular slot returns CNA_RESULT_INVALID_ARGUMENT rather than a false that would read as an answer — those slots are linear data by definition. One finding, and it was mine: the test asserted encodeOutputToSrgb defaults to false when the canonical default is true, so the first run failed on the assertion rather than on the route. The ABI change is purely additive — one struct, one scalar, seven constants, 26 exports, nothing renamed or relaid-out. Inventory 6,195 implemented, 92 planned; 81/81 and 88/88 in all four trees. |
| CBIND-051B | Bind the glTF import report and its diagnostics | ✅ | The 37 rows in GltfImportReportEXT.hpp: twelve scene counts, three derived getters, AnythingLost(), two identity enums, and a diagnostic list whose entries carry four strings and a magnitude. The report gets no handle of its own — it lives exactly as long as the Model that owns it, so it is read through CNA_ModelHandle and an index, which removes a whole lifetime question rather than answering it. The versioned CNA_GltfImportReportEXT carries the twelve counts and the four values the canonical report derives rather than stores, so a caller learns whether anything was lost without walking the list; the strings come back through the usual count/copy pairs, because a string of unbounded length never goes in a fixed structure in this ABI. Code and Message stay separately named routes rather than one call taking a string-kind identity: Code is the identity to branch on and Message may be reworded without an ABI break, and a surface that blurs them invites callers to match on the wrong one. The slice grew a writing half, and that was the finding. The read routes were only testable against an all-zero report, because nothing reachable from C could produce a populated one — the honest fix was to bind the setter too, as a counts-only setter that clears the list plus an append route taking borrowed string views. Its derived fields are refused on input rather than ignored: silently dropping a caller's warning_count would let them believe they had recorded something the next read contradicts. One test defect of my own: argument validation precedes the handle lookup, so a stale-handle assertion carrying a deliberately invalid descriptor was answered INVALID_ARGUMENT, exactly as it should have been. Twelve routes, three structs, two identity families; the ABI change is again purely additive. Inventory 6,232 implemented, 55 planned; 81/81 and 88/88 in all four trees. The Model::get/setGltfImportReportEXTProperty rows themselves belong to Model.hpp's rule and stay planned until CBIND-051C re-approves it. |
| CBIND-051C | Bind the mesh-part, clip target-space, morph tangent and value-type families | ✅ | Twenty rows across five headers, and the slice's one real decision was how to add a field to a published input struct — by not doing it. AnimationClipEXT::TargetSpace and MorphTargetDataEXT::TangentDeltas both look like they belong on CNA_AnimationClipEXTDescriptor and CNA_MorphTargetDeltaEXTDescriptor, and both descriptors are published without a size or version header, so growing either would move every field after it — a layout change, which docs/c-api/ABI_VERSIONING.md counts as a break costing a minor version even at experimental 0.1.0. Separate routes cost nothing and are purely additive, so that is what they got: per-clip target space on the owning CNA_SkinningDataHandle, and copy/replace routes for tangent deltas on the morph handle. The tangent deltas are three-component on purpose, mirroring the canonical type: glTF morphs the tangent direction, and handedness cannot be interpolated because blending +1 and −1 passes through 0, which is not a handedness. ModelMeshPart's six rows follow CBIND-051A's precedent a third time — one sampler state per CNA_PBR_TEXTURE_* slot, spanning the canonical five-entry and two-entry arrays with a single vocabulary — and the part's own topology becomes a validated CNA_PrimitiveType, refusing an undefined identity where the canonical setter would ignore an out-of-range slot. AlphaModeEXT and TextureTransformEXT needed no code at all: CBIND-051A had already published their C forms, and what was missing was the coverage rule saying so, which is exactly the gap CBIND-050's pinning is designed to make visible. Eight routes, one identity family; the ABI change is again purely additive. Inventory 6,252 implemented, 35 planned; 81/81 and 88/88 in all four trees. |
| CBIND-051D | Bind the model's cameras, skins, bounds and material variants | ✅ | The 28 rows in Model.hpp. Two ownership questions, answered by making them impossible rather than by documenting them. ModelSkinEXT::Meshes is a vector of raw ModelMesh*, so the C route names meshes by index into the model's own mesh collection: the model already owns those meshes, so an index cannot outlive what it points at, and an out-of-range one is refused instead of stored. ModelSkinEXT::Data borrows a SkinningData* the model does not own, so ModelResource now holds a strong reference beside each skin — without one, destroying the caller's handle would leave that pointer aimed at freed memory for the next reader to follow — and cna_model_create_skin_skeleton_handle_ext hands back a freshly owned handle rather than the one the caller passed in, which may have been destroyed since. The test proves exactly that: it destroys its skeleton handle, then reads the skin's bone count through a new one. Cameras follow CBIND-051B's clear/append shape with the name read by count/copy; the merged bounding sphere reports its std::nullopt as a has_value flag rather than an empty sphere that would read as a real one at the origin; material variants are read by index and selected with the canonical -1-restores-defaults rule, where the C++ std::out_of_range becomes CNA_RESULT_INVALID_ARGUMENT through the barrier. CreateInfinitePerspectiveFieldOfViewEXT becomes a matrix factory. ConfigureModelMaterialVariantsEXT is recorded not-applicable with a new limitations theme of its own: CNA::Internal is excluded by path everywhere else in the inventory and this entry point reaches it only because it is declared in a public header. Twenty-one routes, two structs; the ABI change is purely additive. Inventory 6,279 implemented, 7 planned; 81/81 and 88/88 in all four trees. |
| CBIND-051E | Bind the animation-player additions | ✅ | The last 7 rows. SkinningData's declared rig root -- a scene-node index and a name, both unset on models from older or non-glTF paths -- becomes four routes on the handle it belongs to. ModelAnimationsEXT becomes an owned handle whose clips are read by sorted index: the canonical type keeps them in an unordered_map, and a C consumer must not be handed an order that is really a hash-bucket accident. ApplyClipToBonesEXT is the interesting one. It throws for a joint-palette clip, and that refusal is the entire reason AnimationClipEXT carries a target space: palette indices applied to Model::Bones would pose the wrong bones with no symptom but wrong motion. So the C route lets the throw through as CNA_RESULT_INVALID_ARGUMENT rather than catching it into a success, and the test proves both halves -- the same clip refused, then accepted once it states scene-node space. ApplyBindPoseBoneTransformsEXT returns its posed-effect count as an output parameter. Fifteen routes, one handle kind; the ABI change is purely additive. The matrix is closed: 6,286 implemented, 12 approved partial, 0 planned, 386 not applicable, and RELEASE_GATE.md reads ready again. 81/81 and 88/88 in all four trees. |
| CBIND-052 | Reconcile the C ABI with the compiled-effect and IGL merges | ✅ | The third reopening, and the smallest so far: HEAD carries 128 commits the C API had not seen — the compiled Effect Framework campaign (plan_fx.md) and the IGL renderer — and the inventory grew by 9 symbols while 3 already-mapped ones changed shape, leaving 12 planned rows. Split into CBIND-052A (the identity families) and CBIND-052B (the Effect object graph). |
| CBIND-052A | Bind the renderer and capability identities, and gate the ABI against CNA's own enumerations | ✅ | Three planned rows — GraphicsRendererType::Igl, ::PixiJs and GraphicsCapability::CompiledEffects — and a fourth that no row reported, which is the finding. TINYGL had reached CNA::GraphicsRendererType in an earlier merge with no CNA_GRAPHICS_RENDERER_TINYGL constant existing at all, and the matrix recorded it implemented with test evidence: CBIND-050 seeded its approvals from the pre-merge tree, which was the right way to avoid blessing that merge's 121 unreviewed rows and which therefore also grandfathered the false claim the pre-merge tree already held. Seeding cannot distinguish the two, so the honest reading of CBIND-050 is that it stopped the bleeding and did not audit the wound. Why nothing caught it: cna_c_api was the one target in this module that did not compile with the strict warnings every one of its 59 test targets already had, and GCC had been reporting enumeration value 'TinyGL' not handled in switch on a stream nobody read. The library now builds with -Wall -Wextra -Werror, with two deliberate exclusions rather than a blanket suppression: -Wpedantic fires only inside Sharp Runtime's Decimal.hpp, on a sibling project's __int128 extension, and -Wmissing-field-initializers contradicts this ABI's own {sizeof(T), version} idiom. Turning it on cost three real fixes — a dead rectanglePointer in CnaCApiTexture.cpp whose live value is recomputed to point into the output structure, and [[maybe_unused]] on two genuinely configuration-dependent helpers (IsTransferTypeCompatible is SDL_RENDERER-only, ExtensionUnavailable is the #ifndef CNA_CNAEXT half). Both identity spaces now publish a _MAXIMUM, the convention 21 other identity families in this ABI already follow and the two most-extended ones lacked — which is why they drifted, since nothing could enumerate them. Three gates, each shown to fail on the exact defect by deleting PixiJs from one side: a consteval count of the renderers CNA::getGraphicsRendererName actually names, held against the C identity table's size; a second assertion tying CNA_GRAPHICS_RENDERER_MAXIMUM to that table; and an exhaustive no-default switch in each direction, so -Werror=switch now catches an appended renderer or capability. The tests walk the ranges instead of naming backends: every identity classifies and is answered by the selection surface, every capability's point query agrees with its flag bit, and both ranges are closed at each end. The slice also repaired what the merge had already broken, which is why it lands as one commit: EffectSmoke.c still required cna_effect_create_compiled to answer NOT_SUPPORTED when compiled effects are now a real format, so it asserts the three refusals decided before a renderer is consulted (empty and structurally invalid bytecode are bad arguments, MonoGame's MGFX container is a recognized format this constructor declines) and reads the new capability rather than a renderer name; and ContentReaderSmoke.c still required the EffectReader placeholder that the compiled-effect work replaced with a reader that really decodes, so the known-unsupported registry is now asserted empty — the negative that would catch an entry reappearing — with every reader-contract route driven through a placeholder the caller builds itself. Two coverage rules re-approved (+2 renderers, +1 capability); the implemented delta is +3, not +4, and that gap is exactly the TINYGL row that had been counted all along. The ABI change is purely additive: 6 constants, no renames, no relayouts. Inventory 6,286 implemented, 12 approved partial, 9 planned, 386 not applicable; RELEASE_GATE.md correctly reads not ready until CBIND-052B. 81/81 in all four trees, sanitizer included. |
| CBIND-052B | Bind the compiled-effect additions to the Effect object graph | ✅ | The 9 rows left, and the one that mattered was a re-approval, not a new route. Effect::Clone() and OnApply() stopped being pure virtual in the merge, which quietly invalidated the C adapter: CApiEffect overrode Clone() with "construct a fresh empty effect", correct while there was nothing to inherit and wrong the moment the base began cloning a compiled effect's runtime and copying its parameter values. A C caller cloning a compiled effect would have received an empty one, with no error anywhere. Both overrides are gone -- OnApply()'s base is the same no-op it was -- so what is left of the adapter is its two constructors. GetCompiledRuntimePtr() becomes cna_effect_get_is_compiled_ext: the runtime object is renderer-owned implementation a C caller can neither construct nor call into, so what crosses is the fact it can act on, and the clone contract is then asserted as a relationship between source and clone rather than as a constant. The three EffectParameter const overloads are re-approvals -- the routes that answer their non-const twins answer them, exactly as the EffectPass annotation pair was already approved -- because C has no second spelling of the same read. The two reflected constructors follow CBIND-051C a fourth time and become additive siblings, cna_effect_technique_create_reflected_ext and cna_effect_pass_create_indexed_ext, rather than growing a published signature; both new arguments read back, and the technique's default-pass flag is proved by pass count rather than by trusting it. One owner decision, ruled on 2026-08-17: EffectPass's passIndex was private with no accessor and unreachable from C -- Apply() forwards it only through an owner an ownerless C-created pass never has -- so the row could only have been a partial. The owner chose instead to add the public EffectPass::getIndexInternal() that EffectTechnique already had, which is the asymmetry the FX work left behind; that is one additive public C++ symbol outside modules/c-api/, with its own canonical GTest coverage, and it makes the row fully bound with the partial count unchanged at 12. Five routes, one canonical accessor; the ABI change is purely additive (5 exports, nothing renamed or relaid out). The matrix is closed: 6,296 implemented, 12 approved partial, 0 planned, 386 not applicable, and RELEASE_GATE.md reads ready again -- the gate having fired in both directions across this pair of slices, refusing a recorded-met criterion that had regressed and then refusing a recorded-not-met one that had quietly become met. One honest gap recorded rather than papered over: no tree this campaign builds advertises CNA_GRAPHICS_CAPABILITY_COMPILED_EFFECTS, so the accepting path of cna_effect_create_compiled is never taken and cna_effect_get_is_compiled_ext is proved only on its false branch. 81/81 in all four trees. |
| CBIND-053 | Fix the two defects the C template reported against a real desktop session | ✅ | Two symptoms reported from cna-c-template on 2026-08-18: a native Wayland client is impossible, and neither Escape nor the window's close button ends the game. Split into CBIND-053A (the SDL video-driver visibility) and CBIND-053B (the close request), with the keyboard half reproduced as not-a-defect — see CBIND-053B. |
| CBIND-053A | Say which video drivers the vendored SDL actually contains | ✅ | The report was accurate and the cause is a silent degradation in the dependency build, not in CNA: SDL's per-backend options (SDL_WAYLAND, SDL_X11, …) default ON and are requests, so an SDL configured on a machine without libwayland-dev/wayland-protocols/libdecor-0-dev drops the Wayland backend and finishes successfully. Nothing downstream notices, because SDL then picks x11 and a Wayland session runs through Xwayland. Confirmed by reading the generated SDL_build_config.h of both prebuilts on this machine: the template's own cache under build/cna-sdl-prebuilt-Linux-x86_64-GNU has x11, offscreen, dummy, while CNA's own .sdl-prebuilt-Linux-x86_64 has wayland, x11, kmsdrm, offscreen, dummy — the same source, the same options, a different set of packages present on the day each was built. Two fixes, both about visibility rather than about making Wayland appear: the configure now reads that generated header and prints the drivers it found, warning with the exact apt-get line and the cache directory to delete when Wayland is missing and the developer is on a Wayland session right now (WAYLAND_DISPLAY set), and staying at STATUS otherwise; and a failed video acquisition now names the drivers the linked SDL actually contains, so wayland not available reads …; this SDL build contains these video drivers: x11, offscreen, dummy instead of leaving the caller to grep the shared object for strings. Neither fails a build: an X11-only SDL is a perfectly good SDL. |
| CBIND-053B | Act on the window close request instead of relying on a synthesized quit | ✅ | The keyboard half did not reproduce and is not a defect. A probe built directly on the C ABI, run under Xvfb against the SDL_RENDERER tree, sees CNA_KEY_ESCAPE and exits when the key is held across a frame (xdotool keydown, 0.5 s, keyup → escape observed at frame 99); the same probe sees nothing when the key is tapped (xdotool key Escape, an instantaneous down+up). That is canonical: KeyboardState is a once-per-frame snapshot in CNA and in FNA alike — FNA's SDL2_FNAPlatform.PollEvents adds the key on down and removes it on up within the same batch — so a press and release that both land between two snapshots is invisible by construction, and a synthetic tap is the only realistic way to produce one. The close button, however, was a real gap: the mapper produces WindowEventKind::CloseRequested, Game::PollEvents had no case for it, and the golden transcript recorded RunApplication=true after one. It worked only because the windowing backend synthesizes a quit for the last window — synthesis that is skipped while a tray is active (CNA supports trays), for a non-topmost window, and whenever the backend's own opt-out is set. XNA's close button always ends the game, so the request is now acted on directly; Exit() is idempotent, so the usual case where the synthesized quit arrives in the same batch costs nothing. Golden updated, and the SDL3 parameter of GameEventSemanticsGoldenTest passes against it. The A/B that proves the handler rather than the synthesis is doing the work: with SDL_QUIT_ON_LAST_WINDOW_CLOSE=0 in the environment, so the backend synthesizes no quit at all, a real WM_DELETE_WINDOW still ends the probe at frame 101 of a 600-frame budget. Before this change that configuration could only have run to the budget. |
| CBIND-054 | Make the launch parameters enumerable | ✅ | The canonical value is a Dictionary<string, string> games enumerate, and every accessor here was keyed -- a count with no key list, which is enough to read a parameter the caller can already name and not enough to materialize the map. cna_game_launch_parameters_get_key_size/_copy_key add the index-addressed half, in the two-call shape microphones, adapters and manifest entries already use. The order is by name, ordinal, ascending, and deliberately not the canonical container's own: LaunchParameters derives from std::unordered_map, so its traversal order is unspecified and a single insertion may rehash and reorder every element -- an index into that would be meaningless between two calls. Sorting makes the sequence a function of the key set alone. The names are rebuilt per call rather than cached, because the container has no invalidation signal to cache against and a command line holds a handful of entries. |
| CBIND-055 | Give SpriteFont a glyph readback and a content loader | ✅ | A native font could be measured and never drawn: cna_sprite_font_measure_utf8 answers the size of a whole string, and placing a glyph needs its atlas rectangle, cropping offset and three kerning values, none of which were reachable -- XNA keeps those tables internal because SpriteBatch is their only reader, and CNA's SpriteBatch friendship reproduced that faithfully. Four CNAEXT accessors (getTextureEXT, getGlyphBoundsEXT, getCroppingEXT, getKerningEXT) open them, with their own GTest coverage, and cna_sprite_font_copy_glyphs returns exactly the CNA_SpriteFontGlyph array cna_sprite_font_create accepts -- the inverse of the constructor, so a font round-trips. cna_content_manager_load_sprite_font then removes .xnb/.cnj font parsing from the consumer entirely. It is the one loader with four parameters, because a SpriteFont is a font and the texture it draws from and both have to be nameable; both are owned, and the atlas refuses to be destroyed while the font lives, which is the ordering rule cna_sprite_font_create already imposes. |
| CBIND-056 | Let a caller register its own content type reader | ✅ | XNA's content pipeline is extensible by design and this ABI could only ever expose readers CNA was built with -- and the coverage matrix said so, in a mapping that read "registering a factory requires a callback that constructs a C++ reader object, which C cannot produce". That was true of the shape, not of the problem. cna_content_type_reader_manager_register takes a versioned callback table -- a per-file instance factory, a read callback, an optional per-instance destructor -- and an owned registration handle; the adapter wraps it in a canonical ContentTypeReaderBase. The read callback returns CNA_Result, unlike CNA_GameComponentCallbacks, which returns void: a component that fails has a next frame to recover in and a half-read asset does not, so a refusing read fails the load and the message carries the code the callback returned. It receives a callback-scoped borrowed CNA_ContentReaderHandle that answers every ordinary read route and refuses to be destroyed from inside the callback. The type a custom reader produces is CNA::Content::ForeignContentObjectEXT, one opaque pointer this ABI never dereferences and never frees, and cna_content_manager_load_foreign_ext is the route that reaches it -- proved on a real .xnb the smoke test assembles itself, header and type-reader table included. Two deliberate deviations, both documented: registration refuses a duplicate name where the canonical AddTypeCreator silently ignores one (a caller would otherwise hold a live handle whose factory is never called), and ContentTypeReaderManager::RemoveTypeCreatorEXT is added because a registration with an owner cannot be withdrawn by clearing the whole registry. One unrelated defect fell out of writing the test: an .xnb whose root reader produces a different type than the typed loader wants reached the exception barrier's catch-all as CNA_RESULT_INTERNAL -- std::bad_any_cast is not a runtime_error -- so an honest "this asset is not that type" was reported as a fault inside CNA. All four typed loaders now answer CNA_RESULT_IO with the real reason. |
| CBIND-057 | Let PreparingDeviceSettings change the settings | ✅ | The header recorded this as a canonical limitation, and it was one: EventHandler<T>::Raise delivers a const reference, so no subscriber in C++ or C could reach the argument's mutable accessor, and XNA's whole reason for this event -- overriding multisampling, back-buffer format or adapter before the device exists -- was unreachable. Fixed at the source rather than worked around: PreparingDeviceSettingsEventArgs holds the settings by pointer, so a CNAEXT getGraphicsDeviceInformationEXT() const hands back a mutable reference to a never-const object with no cast and no undefined behavior. cna_graphics_device_manager_subscribe_preparing_device_settings_ext forwards it. The published observation-only callback is left exactly as it was -- changing its const would be a source break for every consumer that wrote the handler with one -- so the two coexist, and the test subscribes both to prove it. A handler that corrupts the versioned structure is ignored rather than half-applied, because a partially applied configuration fails device creation for a reason with no visible connection to what was written. The mutator returns void deliberately: there is no failure here that device preparation could act on. |
| CBIND-058 | Reconcile the ABI with the merge, and correct the compiled-effect doc | ✅ | The fourth reopening, found the way [[cnabinding-campaign-closed]] says to find it -- except it was found after the slice began rather than before, which is the lesson. Two planned rows arrived with the merge: GraphicsDevice::GetShaderDialectEXT becomes cna_graphics_device_get_shader_dialect_ext over a new closed CNA_SHADER_DIALECT_* range with a _MAXIMUM and an exhaustive no-default switch, so an appended dialect is a compile error rather than a silent UNKNOWN; ShaderEffect::DeclareUniformBlockEXT becomes cna_shader_effect_declare_uniform_block_ext, taking CNA_StringView members because a view carries a length and not a terminator. Including the renderer contract header needed a scoped -Wunused-parameter suppression: it is written for the graphics module's warning settings, not for this library's -Wall -Wextra -Werror, and the suppression covers the include alone. And the item the C# binding called its single largest blocker was a stale doc comment, not a missing implementation. cna_effect_create_compiled said it answers CNA_RESULT_NOT_SUPPORTED "while native CNA bytecode loading is unavailable"; the compiled Effect Framework campaign made it real and CBIND-052A already fixed the test, leaving the header saying the opposite of what the code does. It now documents what is accepted (.fxb including the XNA 4 wrapper, and the Effect payload inside an XNB), what is refused by name (MGFX, HLSL source, GLSL/SPIR-V pairs), which renderers and build options make CNA_GRAPHICS_CAPABILITY_COMPILED_EFFECTS true, and every failure code with its cause. Honest gap, unchanged from CBIND-052B: no tree this campaign builds advertises that capability, so the accepting path is still exercised only by the shared compiled-effect conformance suite in a tree configured for it (-DCNA_GRAPHICS_RENDERER=OPENGLES3 -DCNA_EASYGL_COMPILED_EFFECTS=ON, or FNA3D, which has it always). ABI minor bumped to 0.2.0; every change in CBIND-054–CBIND-058 is additive, so a 0.1.0 consumer still links unchanged. Matrix closed again: 6,308 implemented, 12 approved partial, 0 planned, and RELEASE_GATE.md reads ready. 88/88 CApi tests in the SDL_RENDERER tree. |
| CBIND-059 | Give the buffers a window that indexes the buffer | ✅ | XNA's SetData(int offsetInBytes, …) had no form here at all: every transfer route's offset indexes the caller's array, so rewriting one slice of a large dynamic buffer -- the pattern particle systems and streaming terrain are built on -- meant rewriting all of it, and the C# binding threw NotSupportedException for a nonzero offset rather than writing to the wrong place. Added at the C++ layer, where the fix belongs: VertexBuffer::SetDataRawAtEXT/GetDataRawEXT and IndexBuffer::SetDataAtEXT, bound as cna_vertex_buffer_set_data_raw_at, cna_vertex_buffer_get_data_raw and cna_index_buffer_set_data_at. The deviation is about cost, not about result, and is documented in all four places: no renderer contract in this project takes a destination offset -- IVertexBufferRenderer::SetData(data, count, stride) replaces whole-buffer contents in every one of the renderer families -- so the window is composed on the CPU shadow and the whole buffer is re-uploaded. The bytes land exactly where XNA puts them; what a caller does not get is a smaller transfer. Threading an offset through the renderer contract instead would touch every renderer family for a benefit only two routes would use, and is the change to make when a backend actually offers one. Two consequences worth naming: the shadow is grown to the buffer's full capacity so bytes no upload ever wrote read as zero rather than as whatever a shorter earlier upload left; and the index form accepts no SetDataOptions other than None, because a windowed upload promises to keep the rest of the buffer and Discard promises the opposite. The raw readback closes the asymmetry the same request called the surprising part -- cna_vertex_buffer_set_data_raw could write an arbitrary stride and nothing could read one back, though the shadow held the bytes either way. |
| CBIND-060 | Write the three standing decisions into the headers | ✅ | Three requests that should stay unimplemented, each now saying so and why, because a header that explains an absence is worth as much as the route would have been -- and because the single most expensive thing in the consuming binding's work was a doc comment asserting a capability was impossible when it was not. Service registration (runtime_components.h): AddService stores an object under a type, and C can name neither the type nor author an object implementing the C++ interface a native consumer would call through; a token-taking route would satisfy neither side, since native code asking for IGraphicsDeviceService needs a vtable. The sanctioned place for a caller's own services is the void* context every callback here already carries. Handle identity (graphics_device.h): there is deliberately no route from a native object back to a handle, anywhere. A handle is a record this ABI created, not an identity the object carries; reversing it would need either a process-wide pointer-to-handle map that keeps every object alive forever and answers staleley after address reuse, or a C-shaped slot on every canonical graphics type. The consequence -- that get_texture reports bound with an invalid handle -- is what the bound flag is for, and the doc now says to cache what you bind and use the flag for the case a cache cannot cover. ResourceContentManager (content.h): every load through it fails, the placeholder is inherited from canonical CNA rather than introduced here, and the route stays published so a consumer reaching for it learns that from the header rather than from a failing load. |
| CBIND-061 | Bind ContentManager.Load<Effect> | ✅ | The half of the compiled-effect request CBIND-058 did not close. cna_effect_create_compiled takes bytes a caller already has; ContentManager.Load<Effect> is how an XNA game gets them, and it had no C form, so a consumer had to parse the XNB Effect payload itself even though CNA's own EffectReader decodes it. cna_content_manager_load_effect reads all three shapes CNA supports -- a compiled .xnb asset, a .cnj naming one of the five stock effects, and a .cnj carrying custom shader source -- and returns an ordinary effect handle. Declared in effects.h, not content.h, beside the rest of the effect surface: its return type is CNA_EffectHandle, and pulling the whole effect header into content.h for one typedef would have cost every content consumer that include; content.h carries a pointer to it where a reader would look. BorrowedContentManager grew a parentGame field so a resource can be created as the right game's child from a translation unit that does not own the manager's resource type. Only the compiled shape depends on CNA_GRAPHICS_CAPABILITY_COMPILED_EFFECTS, so the smoke fixture is a stock-effect descriptor -- which is also why the test accepts NOT_SUPPORTED as a correct answer on a renderer with no programmable pipeline, and only a third answer as a fault. 88/88 CApi tests. |
| CBIND-062 | Make the installed package's compatibility match what the contract promises | ✅ | Found by the ABI bump, in the one gate built to find it. ABI_VERSIONING.md has always said "a consumer must reject a different major and may require a minimum minor", and the package's version file said SameMinorVersion, which demands an exact minor and so cannot express "a minimum" at all. Nothing noticed for as long as the minor never moved. The moment 0.1 became 0.2, find_package(CNA 0.1 CONFIG) in modules/c-api/examples/c/ was refused by a library that is additively compatible with it in every respect, and CApi_InstalledConsumer failed in the SOFTWARE tree -- the SDL_RENDERER tree passed only because its staged install was still the pre-bump one, which is worth remembering about that gate. Fixed as SameMajorVersion, so a request for 0.1 accepts 0.1 and everything additive after it and still rejects a 1.x. The example deliberately keeps requesting the version it was written against: raising it with every additive minor would turn a compatibility test into a restatement of today's value. CONSUMING.md and ABI_VERSIONING.md updated to say the same thing the code now does. This is a packaging-policy change and the owner can reverse it -- the alternative reading is that 0.x minors may be breaking, in which case SameMinorVersion was right and the task instruction to bump the minor for additive changes is what needs revisiting. |
| CBIND-063 | Deliver initialize before load_content, as the header always said | ✅ | Reported from the C# binding's first real integration test, and it is a defect no counting assertion could have found. CGame::Initialize() called Game::Initialize() and then the initialize frame hook -- but the canonical Game::Initialize() ends by calling LoadContent() (Game.cpp:667), exactly as XNA's does, so a C consumer observed load_content → initialize → update. That is the reverse of this ABI's own header ("invoked once while the game initializes, before content loads") and of XNA, and it breaks the common shape where content loading reads a field initialization decided. One line: the hook now runs before the base, mirroring the canonical C++ idiom where a subclass does its own work and then calls base.Initialize(). Why it survived: every lifecycle assertion in the suite counted calls, and counting cannot see an order. RuntimeGameSmoke.c now records one letter per delivered event and asserts the sequence begins ilu; shown to fail on the old ordering by reverting the single line and rebuilding (exit 2), and to pass on the new one (exit 0). The header now spells out the whole first-frame order rather than one clause of it. 88/88 SDL_RENDERER, 81/81 SOFTWARE. |
| CBIND-064 | Make a gate read the export count that only prose carried | ✅ | The plan itself predicted this one: "these are prose claims and no gate reads them", written when the same three sentences were found saying 2,720 against a measured 2,838. They went stale again at the very next slice that added exports — CBIND-054–CBIND-063 took the library to 2,852 while ABI_VERSIONING.md, CONSUMING.md and limitations.json still said 2,838. Corrected, and tools/c-api/check_doc_export_counts.py now holds every such sentence against abi_baseline.json, wired as the build-free ctest gate CApiDocExportCounts. Shown to catch the real historical defect by reinstating 2,720 in CONSUMING.md: the gate fails naming the file, the line and both numbers. Two design choices worth keeping: the detector requires export vocabulary near the number, so COVERAGE.md's symbol and header totals — different measurements with their own generator — are not swept in; and finding zero claims is itself a failure, because a gate watching nothing passes silently and that is how the sentence rots next time. Also closed the bookkeeping this campaign left behind: CBIND-053's parent row still read 🔄 with both halves ✅, and all three status sections still described CBIND-052B as the last work. |
| CBIND-065 | Audit the ABI for correctness and coverage, and close what it found | ✅ | An owner-requested deep audit, deliberately aimed at what the existing gates do not check. Four mechanical sweeps came back clean and are worth recording as measurements rather than as impressions: declarations and exports match exactly (2,852 on both sides, nothing declared-but-missing, nothing exported-but-undeclared); the coverage matrix names no route and no test file that does not exist (509 distinct routes named across 506 rules, zero fabricated -- the CBIND-052A failure mode does not recur); all 126 versioned structs are size-validated and every reserved field is documented in one direction or the other; and a generated null/invalid sweep across 2,843 routes -- every callable one -- produced zero crashes and zero routes writing through a null output. The 49 that returned SUCCESS all take no out_ parameter at all: setters, show-routes and zero-length no-ops, where success is the right answer. The finding was coverage, not correctness. tools/c-api/check_route_test_coverage.py asks the question the coverage matrix structurally cannot -- which exported routes does no test source name? -- because that matrix credits a rule's test description to every symbol the rule covers. The answer was 78 routes with no caller, every one of them sitting behind a matrix row reading ✅ implemented with evidence: the entire gyroscope acquisition surface (a truncated copy of the accelerometer's, missing fourteen routes), 49 media-library routes including Genre::Songs and Artist::Songs, three of four XACT disposal subscriptions, the network session's host, and the streaming wave-bank constructor. All 78 are now called; the ratchet stands at 0 uncovered of 2,852 (100%) and fails the build if one arrives without a caller. Two real defects fell out of writing those tests. cna_wave_bank_create_streaming's header promised "the same answers as cna_wave_bank_create" and was wrong: the canonical WaveBank::InitStreaming swallows every parse failure, so a file that does not exist yields CNA_RESULT_SUCCESS and a live, empty bank where the non-streaming twin answers CNA_RESULT_IO. And the first reading of that was wrong, which is worth recording: it looked like a swallowed failure to be fixed, and WaveBank.cpp's own comment says otherwise -- FNA's streaming path never goes through the title container, it hands the path to the native audio layer, and a missing file does not throw there either. The behaviour is faithful; only the C header's claim was false. Fixed there, pinned in XactSmoke.c, and nothing is left open in modules/audio. And cna_invite_accepted_event_info_init stored a non-canonical CNA_Bool verbatim into a structure an event subscriber then reads -- now refused, because ABI_VERSIONING.md declares 0 and 1 the only valid values. |
| CBIND-066 | Measure the CNA_Bool input discipline | ✅ | Measured while closing CBIND-065: 94 routes take a CNA_Bool by value, and 65 of them do not visibly validate it. ABI_VERSIONING.md says "only CNA_FALSE (0) and CNA_TRUE (1) valid", and 29 routes enforce exactly that -- EffectTechniqueSmoke.c even asserts the refusal as documented behaviour. So the ABI is inconsistent with itself, and a caller cannot tell which kind of route it is holding. Worse than the inconsistency: an unvalidated byte does not fail uniformly. The implementation reads a CNA_Bool as != CNA_FALSE in 97 places and as == CNA_TRUE in 77, so a byte of 9 means true in one route and false in another -- the same out-of-contract value meaning opposite things inside one ABI; cna_invite_accepted_event_info_init stored it verbatim, so 9 meant neither. That one is fixed because the value escaped into an output structure another party reads. The measurement stood as an open decision for exactly one instruction: the owner chose fix everything, so CBIND-067 made all 94 uniform. The first count was wrong and the correction is the useful part: the detector matched the header's parameter names against implementation bodies that spell them differently, so it reported 77 unvalidated where the true number was 66 -- 24 routes validated themselves and four delegated to DeviceBooleanCommand, which validates for them. A tool that measures the wrong thing confidently is the same failure as a doc that claims the wrong thing confidently, and it was caught only by patching a route that already had the check. |
| CBIND-067 | Make every CNA_Bool input refuse a non-canonical byte | ✅ | The fix CBIND-066 sized. 66 routes gained the guard; 24 already had one and four delegate to DeviceBooleanCommand. One shared ValidateCanonicalBool replaces what would otherwise be 66 open-coded comparisons in two incompatible spellings. Placement was the whole difficulty and it took two attempts: the guard has to run after a route clears its out_ handle, because this ABI promises a refused creation leaves its output invalid and a guard ahead of that returns before the promise is kept -- EffectSmoke.c asserts exactly that pairing and failed the first patch, which is the second time in this audit an existing assertion caught an automated edit. The guarantee is held by a generated test rather than 94 hand-written assertions (generate_bool_contract_test.py -> BoolContractSmoke.c, plus CApiBoolContractCurrent so the generated file cannot go stale), because 94 assertions leave the next flag parameter exactly as uncovered as the 66 were. Shown to catch a regression by deleting one guard: the test names the route and the result it got. The generator errors on a by-value type it has no stand-in for rather than dropping that route, so coverage cannot shrink in silence, and a compiled-out surface answering NOT_SUPPORTED is accepted because refusing an argument to a route that does not exist in this build would be the wrong answer. ABI minor to 0.3.0 -- this is the first non-additive change in the campaign, and it is safe precisely because the behaviour it removes was never coherent: the same byte read as true in 97 places and false in 77. 92/92 SDL_RENDERER, 82/82 SOFTWARE. |
| CBIND-068 | Chain Update and Draw to the base, so components tick at all | ✅ | Reported from cna-cs, verified here against the source, and the same class as CBIND-063. CGame::Update and CGame::Draw were the only two overrides in that class that never called their base -- Initialize, BeginRun and OnExiting all do. Game::Update (Game.cpp:698) is what walks updateableComponents_ and then runs FrameworkDispatcher::Update(); Game::Draw (Game.cpp:678) does the same for the visible drawables. Neither ran, so a component added through cna_game_components_add was constructed, initialized by the add path, and then never ticked again, and the dispatcher that refills DynamicSoundEffectInstance buffers and raises MediaPlayer's song transitions never ran for any C consumer at all. Hook first, then base -- the same ordering CBIND-063 chose for Initialize and the shape XNA's own template has (base.Update(gameTime) last). The base pass is skipped once a callback failure is recorded, because that failure has already called Exit() and Invoke() and BeginDraw() both guard on the same flag. Why it survived is the same reason CBIND-063 did, one level down: every existing assertion drove a component directly through cna_game_component_update with a time the test supplied, which proves the callback table is wired and says nothing about whether the game ever calls it. The assertion that catches it is a component's own tick count across real frames, and it has to run outside a lifecycle callback -- the base pass happens after the consumer's handler returns, so an assertion inside that handler reads the previous frame's counts. Added to RuntimeComponentsSmoke.c with a disable-stops-the-ticks case beside it, and shown to fail on the old glue (exit 6: zero updates after a frame). The peer declined to work around it managed-side on the grounds that a fallback would double-update the moment this landed, which was the right call. |
| CBIND-069 | Audit the 381 not-applicable rows, and close the one that was a scope cut | ✅ | Prompted by cna-cs, which had just found its twelfth false "the C API cannot do this" claim on its own side and named the pattern: a scope cut reads exactly like a researched finding, and nothing in the prose distinguishes them. My coverage matrix carries the same hazard at scale -- 68 rules covering 381 symbols recorded not-applicable, each with a prose reason, in a machine-checked document that checks everything about those rows except whether the reason is true. CBIND-056 had already caught one (AddTypeCreator's "C cannot produce a factory"). Triaged all 68: 38 are structurally unanswerable (templates, iterators, operator overloading, C++ type identity, friendship declarations, move operations, destructors, deleted copies, documentation-only aliases) and the remaining 30 read as real decisions -- one did not. content-manager-template-extension-points said registering a .cnj factory "requires naming an arbitrary C++ type T and returning a C++ object of that type, neither of which exists in C", which stopped being true the moment CBIND-056 gave C a concrete type it can name: ForeignContentObjectEXT. cna_content_manager_register_cnj_loader_ext instantiates the canonical template at that carrier, so a descriptor's "type" reaches a C callback and cna_content_manager_load_foreign_ext -- unchanged -- then answers for it. That also closes the gap that route's own header documented, that only compiled .xnb assets could reach a registered reader. RegisterTypeReader stays without a C form and the row now says why it differs from the pair beside it: it takes a LooseFileContentTypeReader<T> subclass instance, so a caller would have to author a C++ class rather than supply a callback -- a reason that survives the question the other one failed. Row moved from not-applicable to partial with a recorded limitation. ABI 0.4.0, additively; the export-count gate from CBIND-064 caught the new symbol on its first run, which is what it was built for. |
| CBIND-070 | Split the render-target binding refusals: an unsupported slice is not a bad face | ✅ | The peer cna-cs reported RenderTargetBinding.ArraySlice as blocked upstream, and applying its own sharper test -- not is the stated reason true but does it force the conclusion -- to my own header found the defect was mine, in two places. CNA_RenderTargetBinding.array_slice was documented as "version one supports only zero", which says the limit is this struct's version and will lift in a later one. It is not: the canonical SetRenderTargets refuses a nonzero slice for a RenderTarget2D itself, so no struct version can lift it, and a binding consumer reading that doc would wait for a version that is never coming. Worse, the implementation folded slice and face into one condition answering CNA_RESULT_INVALID_ARGUMENT for both, so the case the canonical layer refuses with NotSupportedException reached C as a bad argument -- the wrong code, and it hid which of the two fields was actually at fault. Split into four distinct refusals across the two target kinds, each naming its own field: a 2D nonzero slice is now CNA_RESULT_NOT_SUPPORTED, matching how that exception maps everywhere else in this ABI; a 2D non-positive-X face stays INVALID_ARGUMENT because the field is meaningless for that kind; and for a cube target the slice is refused separately from an invalid face, meaningless rather than unsupported, because the face selects the subresource. Cube slice stays refused rather than ignored, so a caller who set it believing it meant something is told. GraphicsSurfaceSmoke.c asserts the three refusals and their codes; green in both trees. |
| CBIND-071 | Answer the peer's last two blocked upstream claims in the headers, both by the does it force the conclusion test | ✅ | cna-cs reported five routes as blocked on this side; three were already fixed and it had read stale headers (CBIND-057 for PreparingDeviceSettings, CBIND-060 for services), one was real (CBIND-070), and the last two are the interesting case: both stated reasons are factually true, and neither forces its conclusion. (1) An EffectParameter reached through a nested element or structure-member collection has no GraphicsDevice -- true, and irrelevant to the need behind it. A caller wanting the device in order to wrap the parameter's texture does not have to ask the parameter: cna_effect_parameter_get_value_texture returns a graphics-resource handle, and every texture kind resolves through ResolveGraphicsResource, so the handle answers cna_graphics_resource_get_graphics_device itself. No route added; the reasoning is recorded at the getter so the next reader does not re-derive it. (2) ContentManager.ServiceProvider is always null -- true, and not a C-layer property at all. Game builds its own manager with the argument-less constructor, serviceProvider_ is only ever stored and returned, and no CNA content path resolves a service through one, so a native game's manager reports CNA_FALSE too. The header said a manager "created through this API" always reports false, which invited exactly the inference the peer drew -- that the C-created manager was the weaker one. Reworded to say the field is inert on both sides. Documentation only; no ABI change. |
| CBIND-072 | The native window: a route named after one accessor that implemented another, and a refusal that had outlived its reason | ✅ | Applying the does it force the conclusion test to the 67 not-applicable coverage rules. Most are structurally unanswerable -- templates, iterators, friendship declarations, destructors, protected members with no derived class to hang them on -- but platform-shutdown-and-native-handle bundled two symbols under one reason, and for GameWindow::GetNativeWindowHandleEXT that reason was the native window handle is exactly the backend detail this ABI keeps out. True of the value; it does not force the conclusion, because the canonical NativeWindowHandle is already the portable tagged form -- an enum plus three void* and an XID, naming no platform type -- and it is the one escape hatch a language binding genuinely needs to host a CNA window in another toolkit. Pulling the thread found two further defects. cna_game_window_get_native_handle_ext is named after that accessor and implements a different one, getHandleProperty, and its header claimed the two answer the same pointer. Measured under Xvfb with SDL_RENDERER on a real X11 window: the native accessor answers system=X11, a real Display* and XID 2097205, while the property answers 0x55E1D63…, an SDL_Window* widened to an integer. Different values, different meanings -- and the platform header that mints the token says outright that new interop code should not use it. My first correction was wrong in the other direction: I assumed the property was inert and asserted it was always zero, which the SDL_RENDERER tree failed immediately. It is zero exactly for renderers that never create a window, which is what the header now says. Third, cna_graphics_device_get_native_window_handle refuses every call as unavailable at the stable C boundary -- true when written, false now, and left alone it is precisely the the C API cannot do this claim this campaign keeps finding. It still refuses, because a device may outlive the window it presents to, but now says where the answer lives. Added CNA_NativeWindowHandle, cna_native_window_handle_init and cna_game_window_get_native_window_ext; ABI 0.5.0, additively, symbol version node unchanged at CNA_C_API_0.1. RuntimeGameSmoke.c checks the versioned-structure refusal, the initialized empty state, the per-system field invariants, and that a reported windowing system implies a round-trip token -- the converse deliberately unasserted, since a dummy video driver reports HEADLESS while a platform window exists. Verified in both trees under both the dummy driver and a real X11 display. |
| CBIND-073 | Sweep the surviving not-applicable rows with the sharper test, and sharpen the one whose reason had gone weak | ✅ | CBIND-072 closed the row that failed does it force the conclusion; this is the rest of the sweep. Of the 67 rules, most are structurally unanswerable and were left alone. Six were checked against their strongest available counter-argument and all six hold. leaderboard-writer: verified Gamer's constructor really does capture this into leaderboardWriter_(this) with no custom copy operations, that FriendGamer::CreateInternal returns by value so the published gamer is a copy, and that LeaderboardWriter dereferences owner_ -- the route would crash rather than write a score, so the conclusion holds. (The underlying dangle is a canonical gamer-services defect that reaches C++ callers too; it is outside this plan's scope and is recorded here rather than fixed.) graphics-device-manager-protected: all five raise hooks do have subscribable C events, and the three device-selection hooks are a no-op, a default and a null check in CNA -- overriding them would influence nothing. cna-runtime-facade: the CNA::Runtime:: matches in the tree are a same-named namespace in test support, not the class, so nothing calls it still holds. audio-engine-instance-limit-decision: both producing methods really are below the private: at line 130. property-dictionary-untyped-surface: the count, key-at, kind and typed getters that replace the boxed surface all exist. media-queue-lifetime and the detached manager constructor hold on their own terms. One row needed rewording rather than reopening: content-reader-typed-templates justified itself with C can neither name T -- the exact sentence CBIND-069 disproved elsewhere once ForeignContentObjectEXT gave C a nameable type. The conclusion is still right, for a different reason now recorded: a typed read dispatches to the reader registered for the stream's compiled type, and C cannot register one of those, so a carrier-typed read would find no reader. Documentation only. |
| CBIND-074 | A fourth failure shape: a confident causal explanation attached to a true symptom | ✅ | cna-cs named a shape the earlier sweeps did not test for. CBIND-069 caught factually wrong, CBIND-072 caught true but not conclusive, and CBIND-073 caught outlived its premise; this fourth is the most durable of the four, because a stated cause answers the obvious follow-up question before anyone asks it and so stops the row being re-examined. Scanned every non-implemented rule for mechanism-asserting language and tested the eight that matched. game-protected-raise-hooks is the clean example: it justified ShowMissingRequirementMessage by the hook takes a C++ exception object, which C can neither author nor receive. True, and not what settles it -- nothing in the tree calls the hook, and its canonical body is a stub returning false. The old reason would have gone on reading as valid if CNA ever started invoking it, hiding a real notification behind a still-true sentence; the row now rests on the fact that would visibly change, and says to reopen if the stub goes. Three service-provider rows contradicted the header CBIND-071 had just corrected: they explained a C-created manager's null provider without saying a native manager's is null too, which is the inference the peer actually drew. content-manager-load-texture-partial still carried the C cannot name an arbitrary C++ type wording; the real constraint is that a template cannot be instantiated at a type chosen at run time, so the set of instantiations is fixed at compile time and the foreign route is how a caller's type joins it. Checked and left alone: audio-platform-substrate (the same substrate exclusion as CNA::Platform), the iterator rows (a genuine structural observation, not a speculative cause), and game-service-container-lookup, whose two services the runtime registers and no others is exactly right -- IGraphicsDeviceManager and IGraphicsDeviceService, both registered at GraphicsDeviceManager.cpp:566-567, and the C enum names two. Documentation only; no ABI change. |
| CBIND-075 | cna_shader_effect_create succeeded for text that cannot draw, and said nothing about it | ✅ | cna-cs reported that creating an effect from "this is not a shader" succeeds on SOFTWARE with CUSTOM_EFFECTS true, and offered two readings -- the renderer accepts source without compiling, or the compile is deferred to first use. Measured, and it is neither. The canonical ShaderEffect constructor compiles eagerly (ShaderEffect.cpp:22), and when the compile fails it prints to std::cerr and constructs the object anyway, leaving IsEffectValid() as the way to ask. The C route never asked. Probing both trees with the same nonsense text: SOFTWARE creates it and reports valid, because its CompileProgram accepts any non-empty text and sets compiled_ = true; SDL_RENDERER creates it and reports invalid, because it really compiles. So the verdict is genuinely renderer-dependent, cna_shader_effect_is_valid existed all along, and nothing in the header pointed at it -- while cna_content_manager_load_effect directly above documents a shader the renderer cannot compile among its failures, so a reader comparing the two would reasonably infer this route fails the same way. It does not. The empty case was worse than undocumented: both sources empty made SOFTWARE throw, which the exception barrier reported as CNA_RESULT_INTERNAL -- blaming CNA for the caller's input -- while SDL_RENDERER returned success and a handle for an effect with no source at all. Now refused identically as CNA_RESULT_INVALID_ARGUMENT before any renderer sees it. Documented rather than normalized: whether a renderer inspects source, since normalizing would mean parsing shader source in the ABI. cna_shader_effect_is_valid now says CNA_TRUE means nothing rejected this, weaker than this will draw, and CNA_FALSE is the strong answer. CUSTOM_EFFECTS now states it is a different capability from COMPILED_EFFECTS and that they differ in practice -- the peer had recorded custom shaders as blocked on the strength of the compiled route alone. ABI 0.6.0: the empty-source refusal changes an existing answer, so not additive. |
| CBIND-076 | A gate for the one export defect a count cannot see | ✅ | cna-cs reported a route as declared-but-not-exported. The specific claim was false -- it had the name from a doc comment's prose rather than the declaration, and the real route, cna_graphics_device_get_shader_dialect_ext, is exported by both trees -- but the method it named is a genuine hole here. CApiAbiBaseline compares the export list against a recorded baseline, so export drift is caught; nothing compared the headers against the exports. A header declaring a route the library never exports leaves both halves self-consistent, and the consumer finds out at its call site rather than at load, which is the worst place for it. CApiDocExportCounts cannot help: 2,855 declared and 2,855 exported are equally satisfied by a header declaring one route the library lacks while the library exports one no header declares. Cardinality is the wrong instrument; the set difference in both directions is the right one. check_declared_exports.py runs it, wired as CApiDeclaredExports beside the baseline gate under the same ELF/target guard. Current state is exact agreement, 2855 = 2855 with both differences empty, and the gate was verified against a planted declaration rather than trusted on a green first run. To be clear about what it caught: nothing. The reported symbol was never in these headers, so this gate would not have fired on that case either -- it closes a hole that was open, not one that had been exploited. A green gate introduced beside someone else's retraction is easy to misread later as having found the thing that prompted it. |
| CBIND-077 | Two adjacent effect routes that promise opposite things about the same input | ✅ | Follow-up from cna-cs, and the sharpest observation to come out of that exchange: cna_content_manager_load_effect and cna_shader_effect_create sit next to each other in effects.h and make opposite promises about the same input. The first documents a shader the renderer cannot compile among its failures; the second succeeds for source no renderer can run. CBIND-075 made the second one honest on its own terms, but honesty in isolation was not enough -- the peer read the contract across from the neighbour, which is what a reader of adjacent routes does, and nothing in either doc said the neighbour differed. Each now names the other and says why the difference is real rather than an inconsistency: load_effect owns the whole load including the compile, so it can answer for it; shader_effect_create hands source to a renderer that decides for itself. Also corrected the CBIND-076 row's record at the peer's request. Its gate found nothing -- the symbol reported as declared-but-not-exported was never in these headers, so the new gate would not have fired on that case either. It closes a hole that was open, not one that had been exploited, and a green gate introduced beside someone else's retraction is easy to misread later as having caught the thing that prompted it. Documentation only; no ABI change. |
| CBIND-078 | Merge origin/next, and bind the four public symbols it brought | ✅ | 39 commits, 743 files, almost all of it the glTF L7 pixel corpus and renderer work; zero file overlap with this branch, so the merge itself was textually clean. What it was not is finished: the coverage matrix reopened exactly as it does every time, and four new public symbols arrived with no rule, which the generator reports as planned -- a status that is not coverage. Three of the four are the C-side of the campaign next had just run. PbrEffect::VertexColorEnabledEXT and SkinnedPbrEffect::VertexColorEnabledEXT are what the whole GLTF-465 series was about, and without them a C consumer holding a PBR effect cannot state that a primitive carries vertex colour at all; bound as one route pair, since the resolved view carries whichever type the handle named and every other PBR route here already works that way -- and the skinned half is tested separately, because a per-type binding would be most likely to miss it. MorphTargetDataEXT::RecomputeFlatNormalsEXT and TriangleIndicesEXT are the flat-normal recomputation a primitive with no base normals needs; the index list follows the copy-with-capacity shape of the delta arrays beside it and refuses a count that is not a multiple of three, since any other count describes no triangle list. Six routes, ABI 0.7.0 additively. Two gates earned their keep on this merge without being asked to: CApiBoolContractCurrent failed the moment the new boolean setters existed and were absent from its generated suite, and CApiRouteTestCoverage would have failed had the routes shipped untested -- 2861/2861 named, declared and exported agreeing exactly. |
Every implemented public C entry point must receive all applicable coverage in the same task:
- Pure C compile test: includes the leaf and umbrella headers in the selected C standard; proves no C++/Sharp Runtime leakage.
- C link/runtime test: a C translation unit calls the shared C API through the documented library target, not a private C++ test helper.
- Native adapter test: validates C-to-C++ semantic conversion, exception firewall and cleanup behavior with the existing CNA test framework.
- Negative/lifetime test: validates null/invalid/stale/wrong-kind handles, buffers, UTF-8, bad enums, double release and shutdown ordering as applicable.
- Renderer test: HEADLESS supplies deterministic lifecycle/state control; any draw claim additionally needs a real supported renderer and a renderer-appropriate observable result.
- ABI regression test: protects externally visible sizes, offsets, numeric constants, symbols and ABI-version compatibility against accidental changes.
Before adding an API family or function, answer and document all of the following:
- What concrete C use case requires it?
- Which canonical CNA C++ operation defines its semantics and FNA behavior where it is XNA-facing?
- Does the signature contain only C-safe fixed-width/POD/handle/callback constructs?
- How do ownership, nullability, borrowed lifetime, parent lifetime and thread affinity work?
- How are strings, collections, buffers, paths, streams, callbacks and errors represented?
- Can all exceptions be caught and mapped without leaking implementation type names?
- What happens for unsupported renderer/platform capability?
- What C-only success, failure, lifetime and ABI tests will land with it?
- Does it expose an internal Sharp Runtime, STL, C++ or renderer-private concept? If so, redesign it.
- Is the ABI extension additive within its current ABI major? If not, stop for an explicit versioning decision.
The initial milestone is complete only when all selected B0–B4 rows are ✅ and the following statement is demonstrably true:
A C program, compiled as C and including only
<CNA/C/cna.h>, can create and run a small CNA game, receive lifecycle callbacks, clear a frame, upload a texture, submit a batched sprite draw, read a documented input snapshot, retrieve a UTF-8 error on failure, and release every owned resource without leaking or using a C++/Sharp Runtime ABI type.
This is an experimental C ABI foundation, not ABI 1.0 or a future language-specific binding.
The C API is not complete until CBIND-044 is ✅ and the machine-checked coverage matrix proves
that every public CNA API symbol has a documented C-native mapping and the required C-only tests.
The full surface must preserve the behavior of the canonical C++ implementation (and FNA/XNA where
applicable), including constants, overload-specific behavior, errors, lifetime and renderer
capability limits. A raw C++ type, exception, container, callback/delegate, stream, task or Sharp
Runtime value is never an acceptable substitute for a C mapping.
Snapshot (2026-08-19, after CBIND-064): 421 headers / 6,708 symbols —
6,312 implemented, 12 approved partial, 0 planned, 384 not applicable. ABI 0.2.0, 2,852
exported symbols.
Regenerate or verify with python3 tools/c-api/generate_coverage_inventory.py --write|--check.
The release gate reads ready.
| Scope | State |
|---|---|
CBIND-000–CBIND-034 |
✅ all |
CBIND-035 (math, geometry, textures, effects, models, device and draw submission) |
✅ closed by CBIND-035G |
CBIND-036 (storage, content, networking, fake-async) |
✅ closed by CBIND-036E5 |
CBIND-037A core, CBIND-037B the whole input module (gamepad, keyboard, mouse, cursor, text input, touch, haptics, joysticks, host devices) |
✅ |
CBIND-037C the whole media module (identities, songs, the library catalog, pictures, playback, video) |
✅ |
Six modules now have no planned row left: storage, content, net, core, input, media.
Nothing, as of CBIND-064 on 2026-08-19 — with the caveats below, which matter more than the
word "nothing". CBIND-053–CBIND-064 answered a review of the C ABI from the C#/.NET binding and
two defect reports from the C template, added eleven routes, wrote three standing refusals into the
headers, and moved the ABI to 0.2.0. The matrix is closed and RELEASE_GATE.md reads ready.
Expect the matrix to keep reopening, and do not read a closed one as a finished one. Four merges
have now each reopened it, and the pattern is stable: this plan's queue empties, the branch merges,
and the tracked surface grows. The standing work is not "close the matrix" but "reconcile after
each merge" — start every context by running python3 tools/c-api/generate_coverage_inventory.py --check, because a stale inventory is the first symptom every time. CBIND-058 found this the
hard way: it ran that check after starting a slice and discovered two symbols the merge had left
unbound.
One honest gap is open and is not a task here. No build tree this campaign uses advertises
CNA_GRAPHICS_CAPABILITY_COMPILED_EFFECTS, so the accepting path of cna_effect_create_compiled
and of cna_content_manager_load_effect's compiled shape is exercised only by the shared
compiled-effect conformance suite in a tree configured for it
(-DCNA_GRAPHICS_RENDERER=OPENGLES3 -DCNA_EASYGL_COMPILED_EFFECTS=ON, or FNA3D, which has it
always). Every refusal path is covered here; the acceptance is not, and no amount of work in this
plan changes that without such a tree.
One decision is reversible and belongs to the owner. CBIND-062 changed the installed CMake
package from SameMinorVersion to SameMajorVersion so it matches what ABI_VERSIONING.md
promises. The alternative reading is that 0.x minors may be breaking — in which case
SameMinorVersion was right and what needs revisiting is bumping the minor for purely additive
changes at all.
The table below is otherwise kept as the order the work was done in rather than as a queue:
| Order | Task | Status | Note |
|---|---|---|---|
| — | CBIND-038 pure-C compatibility matrix |
✅ | 23 cells, 1,380 translation units; found and fixed a duplicate CNA_PowerState that made C99 impossible |
| — | CBIND-039 ABI layout, export and compatibility gates |
✅ | abi_baseline.json records 166 structs, 258 scalars, 1,338 constants and 2,720 exports; two gates, one build-free |
| — | CBIND-040A stress the handle, teardown and buffer contracts |
✅ | found a heap use-after-free (a registration outliving its game) and two documentation defects |
| — | CBIND-040B fuzz targets for the parser-like surfaces |
✅ | 16.8M enumerated UTF-8 cases against an independent oracle, plus a libFuzzer target; closes CBIND-040 |
| — | CBIND-041 C consumer documentation and examples |
✅ | found that find_package(CNA CONFIG) could not work at all; there is now a package, an example and a gate that builds it from outside the tree |
| — | CBIND-042A publish the known-limitations matrix |
✅ | found three stale deferrals, one of which had silently become implemented |
| — | CBIND-042B define the experimental release gate |
✅ | ten criteria, eight measured met, two blocked on owner decisions; verdict is NOT READY and correctly so |
| — | CBIND-045 ship the native libraries CNA builds |
✅ | owner ruled yes on 2026-08-16; the package needs no environment variable at all |
| — | CBIND-046 offer a static configuration |
✅ | owner ruled yes on 2026-08-16; the archive publishes the same 2,720 names the shared library does, or the build fails |
| — | CBIND-044 close the public API coverage matrix |
✅ | closed 2026-08-16 with the owner's approval of the twelve remaining limitations |
| — | CBIND-052B bind the compiled-effect additions to the Effect object graph |
✅ | the 9 rows CBIND-052A left; closed 2026-08-17, and it found that the C adapter's Clone() override was silently dropping a compiled effect's runtime |
CBIND-043 is done — the matrix is a gate in both CTest and CI, so an unmapped public symbol now
fails a build rather than merely showing up in a report.
The mechanics — which files, which commands — are in The loop for one slice in the handoff below. These are the decisions that mechanics cannot make for you:
- Read the canonical
.cpp, not only the header. Several slices turned on behavior only the implementation reveals: a square clamp, an epsilon comparison, a silently dropped key, a deliberately no-op disposal, a hash that ignores half the fields. - Prefer the representation the ABI already has over a second spelling of the same data. The gamepad button set and directional pad reuse one mask; the thumbstick and trigger values are the two halves of a block the snapshot already carried, and an ABI assertion proves it.
- Collapse only what the canonical implementation itself collapses. Eleven named button getters become one route because each is the same masked test — not because eleven routes felt verbose.
- When C must differ, deviate deliberately and write it down, in the header, the coverage rule and here. When the canonical behavior is merely odd, preserve it and test it.
- Check the implemented delta equals the slice's row count after regenerating coverage. That check has already caught a rule silently claiming another slice's rows.
Both kinds are recorded rather than smoothed over; a new context should not "fix" them:
- Deliberate C-layer deviations: out-of-range keyboard keys, unchecked
NetworkSessionPropertiesindices, undefined text-input type hints, negative maximum touch counts, touch pressures outside zero through one, and touch appends past the fixed snapshot capacity are all refused instead of silently dropped, left undefined or quietly falling back to a different value; a gamepad snapshot carries one button mask, so a supplied directional pad is merged into it. - Re-partitions:
LocalNetworkGamerD→E; threeCreateoverloads E4→E2; a minimum gamer-services surface borrowed into E2/E3;GamePad::GetCapabilitiesB3→B1;Mouse::SetCursorB4b→B4c; the CNA::Input identity enumerations borrowed from B7 into B3, B4a and B4d.
The
exported ABI is still experimental 0.1.0: it contains the version/error substrate, the HEADLESS-
and SDL_RENDERER-tested C game lifecycle slice, callback-scoped graphics capability discovery and
owned Color Texture2D bulk transfer, batched textured-quad submission, expanded input POD
snapshots and
SDL pixel-verified backbuffer readback, not complete public CNA coverage. No language-specific
binding exists. B4 is complete; B5 now includes owned content-manager/root/cache control and Color
Texture2D loads, keyboard/mouse/gamepad/touch capture and a PCM16 SoundEffect/instance control
route with stable native playback-availability reporting and isolated success/unavailable-device
regressions. B5 is complete. B6 has a deterministic, reviewed 414-header/6,415-symbol baseline and
now maps the full CBIND-034 graphics family through C-native state/display PODs, adapter queries,
owned render targets and SpriteFonts. CBIND-035A establishes the public 3D value and identity ABI
without claiming its still-unimplemented operations. CBIND-035B1 completes the Point and Rectangle
operation families, and completed CBIND-035B2a–B2d cover stateless MathHelper plus all Vector2/3/4
rows. Completed CBIND-035B3a–B3b cover Quaternion and Matrix. The current snapshot is 1,442
implemented, 21 partial, 4,882 planned and 70 not applicable. CBIND-035B4a completes Plane and Ray,
CBIND-035B4b completes BoundingBox, CBIND-035B4c completes BoundingSphere and CBIND-035B4d
completes BoundingFrustum, closing parent CBIND-035B4. The current snapshot is 1,577 implemented,
21 partial, 4,747 planned and 70 not applicable. CBIND-035B5a completes all 19 CurveKey rows; the
current snapshot is 1,596 implemented, 21 partial, 4,728 planned and 70 not applicable.
CBIND-035B5b completes all 26 CurveKeyCollection rows; the current snapshot is 1,622 implemented,
21 partial, 4,702 planned and 70 not applicable. CBIND-035B5c completes all 15 Curve rows and closes
parent B5; the current snapshot is 1,637 implemented, 21 partial, 4,687 planned and 70 not
applicable. CBIND-035B6a completes the remaining 25 non-constant Color rows; the current snapshot
is 1,662 implemented, 21 partial, 4,662 planned and 70 not applicable, with CBIND-035B6b named
Color constants next. CBIND-035B6b completes all 141 named Color rows and closes parent B6; the
snapshot becomes 1,803 implemented, 21 partial, 4,521 planned and 70 not applicable. CBIND-035B7
completes all 132 remaining PackedVector/HalfTypeHelper/interface rows and closes parent B; the
current snapshot is 1,935 implemented, 21 partial, 4,389 planned and 70 not applicable, with
CBIND-035C texture, buffer and vertex-resource coverage next. CBIND-035C1 maps 104 built-in
vertex-value, VertexElement-operation and IVertexType rows through fixed PODs and type-tagged
operations; the snapshot is now 2,039 implemented, 21 partial, 4,285 planned and 70 not applicable,
with CBIND-035C2 vertex declarations and bindings next. CBIND-035C2 then maps its 14 rows through
owned declaration handles and a fixed binding descriptor; the snapshot is 2,053 implemented,
21 partial, 4,271 planned and 70 not applicable. CBIND-035C3 maps the 21-row common
GraphicsResource contract through generic validated handles, exact UTF-8 names/strings, C-owned
tag tokens, callback-scoped device identity and explicit disposal subscriptions; the snapshot is
now 2,074 implemented, 21 partial, 4,250 planned and 70 not applicable. CBIND-035C4 completes the
134 previously unfinished Texture/Texture2D rows and upgrades the two inherited partial Texture
properties through the generic typed transfer, image-memory/file and storage-safe handle contract;
the snapshot is now 2,210 implemented, 19 partial, 4,116 planned and 70 not applicable.
CBIND-035C5 then maps all 40 Texture3D/TextureCube rows through owned handles, explicit
volume/face/mip/region transfer descriptors and copied DDS input; the snapshot is now 2,250
implemented, 19 partial, 4,076 planned and 70 not applicable. CBIND-035C6 maps all 57
VertexBuffer/DynamicVertexBuffer rows through owned handles, copied declarations, typed/raw
transfers and ContentLost registration; the snapshot is now 2,307 implemented, 19 partial, 4,019
planned and 70 not applicable. CBIND-035C7 maps all 32 IndexBuffer/DynamicIndexBuffer rows through
owned handles, both index widths, caller-window transfers and ContentLost registration, closing
parent CBIND-035C; the snapshot is now 2,339 implemented, 19 partial, 3,987 planned and 70 not
applicable. CBIND-035D is partitioned into nine dependency-ordered slices; CBIND-035D1 maps all 17
EffectParameterClass/EffectParameterType rows through stable fixed-width identities, bringing the
snapshot to 2,356 implemented, 19 partial, 3,970 planned and 70 not applicable, with CBIND-035D2
effect annotations next. CBIND-035D2 maps all 30 annotation/collection rows through owned copied
handles, typed getters and count/index/name snapshot operations; the snapshot is now 2,386
implemented, 19 partial, 3,940 planned and 70 not applicable. CBIND-035D3 maps all 84
EffectParameter/Collection rows through stable mutable handles, tagged scalar/array/string/texture
operations and nested count/index/name/semantic views; the snapshot is now 2,470 implemented,
19 partial, 3,856 planned and 70 not applicable. CBIND-035D4 maps all 67 technique/pass/collection
rows through stable handles, identities, nested views and canonical Apply dispatch; the snapshot is
now 2,537 implemented, 19 partial, 3,789 planned and 70 not applicable. CBIND-035D5 maps its 68
callable Effect/EffectMaterial/ShaderEffect/SpriteEffect rows through owned game-child handles,
clones, current collections, exact strings, uniforms, textures and matrices; its two deleted copy
operations remain not applicable. The snapshot is now 2,605 implemented, 19 partial, 3,721 planned
and 70 not applicable at the end of CBIND-035D5.
CBIND-035D6 maps all 90 BasicEffect, DirectionalLight and effect-interface rows through reusable
matrix/fog/light operations, stable nested light handles, complete material state and retained
Texture2D assignments. The snapshot is now 2,695 implemented, 19 partial, 3,631 planned and 70 not
applicable at the end of CBIND-035D6.
CBIND-035D7 maps all 114 AlphaTestEffect, DualTextureEffect and EnvironmentMapEffect rows through
shared effect interfaces plus complete concrete state and clone-aware retained Texture2D/TextureCube
slots. The snapshot is now 2,809 implemented, 19 partial, 3,517 planned and 70 not applicable
at the end of CBIND-035D7.
CBIND-035D8 maps all 52 SkinnedEffect rows through complete material/lighting/fog/texture state,
the fixed 72-bone maximum and bounded copied palette operations. The snapshot is now 2,861
implemented, 19 partial, 3,465 planned and 70 not applicable.
CBIND-035D9 maps all 129 ColorMatrixEffect, PbrEffect and SkinnedPbrEffect extension rows through
finite fixed-layout color transforms, shared PBR material/interface operations, five clone-aware
retained texture slots and bounded 72-bone palette transfer. The snapshot is now 2,990
implemented, 19 partial, 3,336 planned and 70 not applicable; parent CBIND-035D is complete and
CBIND-035E model, mesh and animation coverage is next.
CBIND-035E is partitioned into seven dependency-ordered slices. CBIND-035E1 maps all 23
ModelBone/ModelBoneCollection rows through stable hierarchy nodes and live collection views with
cycle prevention and no dangling parent exposure. The snapshot is now 3,013 implemented,
19 partial, 3,313 planned and 70 not applicable. CBIND-035E2 maps all 28 ModelMeshPart and
ModelMeshPartCollection rows through stable shared parts, retained graphics-resource associations,
opaque C tags and count/index snapshot aliases. The snapshot is now 3,041 implemented, 19 partial,
3,285 planned and 70 not applicable. CBIND-035E3 maps all 38 ModelMesh, ModelMeshCollection and
ModelEffectCollection rows through owned game-child meshes, live part/effect views and retained
mesh snapshots with transitive lifetime. The snapshot is now 3,079 implemented, 19 partial,
3,247 planned and 70 not applicable. CBIND-035E4 maps all 14 Model rows through owned aggregate
handles, retained bone/mesh/root views, opaque tags, C-native owned-resource callbacks, bulk local/
absolute transforms and capability-gated Draw. The snapshot is now 3,093 implemented, 19 partial,
3,233 planned and 70 not applicable, with CBIND-035E5 morph-target extensions next.
CBIND-035E5 maps all 20 MorphTargetEXT rows through fixed copied descriptors, an owned validated
data handle, atomic nested-field copies, mutable weights/tracks, blend/evaluation operations and
retained ModelMeshPart upload. The snapshot is now 3,113 implemented, 19 partial, 3,213 planned
and 70 not applicable. CBIND-035E6 maps all 34 applicable SkinnedModelEXT rows through deep-copied
skeleton/clip descriptors, deterministic bulk access, native transform sampling and stable retained
GPU-resource sidecars; its deleted copy constructor and copy assignment remain the two established
not-applicable rows. The snapshot is now 3,147 implemented, 19 partial, 3,179 planned and 70 not
applicable, with CBIND-035E7 SkinningData and AnimationPlayer next.
CBIND-035E7 maps all 19 AnimationPlayer.hpp rows through owned copied SkinningData and retained AnimationPlayer handles, deterministic clip lookup, finite-seconds update controls and atomic local/world/skin transform copies. The snapshot is now 3,166 implemented, 19 partial, 3,160 planned and 70 not applicable; parent CBIND-035E is complete and CBIND-035F is next.
CBIND-035F is partitioned into seven dependency-ordered slices. CBIND-035F1 maps all 49 Viewport,
ClearOptions, GraphicsDeviceStatus, Unsupported3DGraphicsCallBehavior and SpriteEffects-operator
rows through a fixed 24-byte viewport POD with complete construction/property/transform/string
operations and fixed-width identities whose native ordinals are asserted at the adapter boundary.
The snapshot is now 3,215 implemented, 19 partial, 3,111 planned and 70 not applicable.
CBIND-035F2 then maps all 51 device lifetime, state, event, event-args, service and
device-exception rows through the borrowed device handle, owned event subscriptions with fixed
payload structures and a shared exception-firewall conversion. Its three GraphicsDevice friend
declarations become the first explicitly not-applicable rule rows and the four service-level
IGraphicsDeviceService events stay partial pending CBIND-037. The snapshot is now 3,259
implemented, 23 partial, 3,060 planned and 73 not applicable. CBIND-035F3 then maps all 8
TextureCollection and device texture-collection rows through stage-addressed slot reads, binds and
unbinds without exposing a native collection reference. The snapshot is now 3,267 implemented,
23 partial, 3,052 planned and 73 not applicable. CBIND-035F4 then maps all 21 clear, present,
reset, back-buffer-window and buffer-binding rows through versioned descriptors, a nullable adapter
index and caller-owned binding arrays. The snapshot is now 3,288 implemented, 23 partial, 3,031
planned and 73 not applicable. CBIND-035F5 then maps all 49 draw-submission and device-extension
rows through two descriptor-driven user-primitive calls, capability-gated buffered draws and the
complete CNAEXT helper set, closing every GraphicsDevice.hpp row. The snapshot is now 3,337
implemented, 23 partial, 2,982 planned and 73 not applicable. CBIND-035F6 then maps all 21
SpriteBatch text/mesh and OcclusionQuery rows through one text command covering every canonical
overload, a converted mesh descriptor and a capability-gated owned query handle. The snapshot is
now 3,358 implemented, 23 partial, 2,961 planned and 73 not applicable. CBIND-035F7 then maps all 118
graphics-ext rows through fixed identities, two settings-bag PODs and three owned post-process
effects whose routes report NOT_SUPPORTED when the opt-in extension layer is absent, so the
exported ABI never changes shape with the build option. The snapshot is now 3,476 implemented,
23 partial, 2,843 planned and 73 not applicable, and no planned CBIND-035 inventory row
remains; parent CBIND-035F is complete. CBIND-035G then adds the missing real-output evidence
through Draw3DSmoke.c: honest refusal on a backend without the 3D capability, and observable
pixel change through user, indexed, buffered and Model draw routes on the CPU-raster SOFTWARE
backend, with pixel readback treated as a capability separate from 3D. Parent CBIND-035 is
complete. CBIND-036A then closes the first CBIND-036 slice by mapping the whole 42-row storage
module: three strictly nested owned handle families, both canonical events, count/copy listings, a
C-native stream handle that never exposes System::IO::Stream, and the five fake-async Begin/End
pairs collapsed into single synchronous calls that still invoke the completion callback. Three
canonical failures gained boundary conversions -- std::filesystem::filesystem_error and
System::IO::IOException to CNA_RESULT_IO, StorageDeviceNotConnectedException to
CNA_RESULT_INVALID_STATE -- each proven in the adapter test. CBIND-036B1 then closes the
manager half of the content family (40 rows): resolved asset path and normalized cache key,
built-in loader registration, service-provider presence, graphics-device get/set, the manifest and
.xnb reader-usage snapshots as fixed PODs plus count/indexed copy, and typed Texture2D,
TextureCube and SoundEffect load routes. System::IServiceProvider stays a hard ABI boundary and
the Load<T>/RegisterTypeReader<T>/RegisterCnjLoader<T> templates are recorded as
inexpressible in C rather than given an invented untyped operation. The snapshot is now 3,545
implemented, 25 partial, 2,768 planned and 77 not applicable. CBIND-036B2 then closes the reader
half and with it parent CBIND-036B: an owned reader over an owned storage stream, an owned type
reader from the static registry or the known-unsupported placeholder factory, and the registry
itself. Type erasure is where the mapping stops, and that is recorded rather than papered over --
the two untyped read routes are partial because a type-erased C++ object has no C representation,
and every typed reader template, LooseFileContentTypeReader<T> and factory registration is
not-applicable. The snapshot is now 3,582 implemented, 28 partial, 2,704 planned and 101 not
applicable, with no planned content or storage row left. CBIND-036C then maps the 98 network
identity, value and packet rows: five enumerations at their canonical ordinals, the
quality-of-service value with both factories, an owned session-property list with an owned
enumerator, and owned packet buffers whose canonical color asymmetry is preserved and proved in
both directions. Two canonical gaps are closed on the C side rather than passed through -- the
unchecked Insert/RemoveAt indices and the enumerator's before-first dereference -- and the
join-failure conversion records the one payload a message cannot carry. The snapshot is now 3,665
implemented, 29 partial, 2,606 planned and 115 not applicable. CBIND-036D then maps the gamer,
machine and event-argument rows. Its boundary needed one correction: LocalNetworkGamer moved to
CBIND-036E because its receive and send paths dereference the owning session, so the slice is 47
rows rather than 65. The snapshot is now 3,711 implemented, 29 partial, 2,559 planned and 116 not
applicable. CBIND-036E is partitioned into five slices by what each part needs to exist, and
CBIND-036E1 closes the first: discovered sessions and their collection. The snapshot is now 3,728
implemented, 29 partial, 2,542 planned and 116 not applicable. CBIND-036E2 then maps the session
object itself. Two boundaries moved while implementing it: the three synchronous Create overloads
came here from CBIND-036E4, because none of the session's state is reachable without a session
object; and the minimum signed-in-gamer surface was borrowed from CBIND-037, because the canonical
session constructor selects its host from its local gamers and therefore cannot run with no gamer
signed in. The snapshot is now 3,792 implemented, 30 partial, 2,477 planned and 116 not applicable;
CBIND-036E3 then maps the ten session events as typed subscribe routes whose payload gamer handles
live only for the callback that receives them, and whose instance registrations hold a weak
reference so releasing one after its session is gone is a no-op. The snapshot is now 3,806
implemented, 30 partial, 2,463 planned and 116 not applicable. CBIND-036E4 then collapses every
canonical Begin/End pair into one synchronous C route that still invokes the completion
delegate, and maps discovery, join and the invited path alongside them. The snapshot is now 3,823
implemented, 30 partial, 2,446 planned and 116 not applicable, and LocalNetworkGamer is the only
net header with planned rows left. CBIND-036E5 closes it and with it parent CBIND-036: a local
gamer reuses the network-gamer handle and every route refuses a non-local one, all three receive and
all six send overloads are mapped, the sender comes back as a borrowed view, and three canonical
behaviors — the offset receive consuming its packet before rejecting the offset, the packet-reader
receive always reporting zero, and the declared-no-op voice and party-invite calls — are preserved
and asserted. The snapshot is now 3,841 implemented, 30 partial, 2,428 planned and 116 not
applicable, with no planned storage, content or net row left; CBIND-037 owns everything that
remains and is partitioned into seven module-sized slices. CBIND-037A closes the first of them, the
whole core module: one route per canonical logger static, the process-wide minimum level, the
compile-time platform, desktop operating system, renderer identity and renderer name, and both
backend classifications for any of the 46 public renderer identities. Two decisions are worth
recording. CNA::CNAException gained a central boundary conversion to CNA_RESULT_INVALID_STATE,
which is what makes the canonical non-desktop refusal of getCurrentDesktopOS observable in C
rather than collapsing into a generic internal failure. And the canonical log levels keep their
exact ordinals including the deliberate 100 for EXPERIMENT, so 6 is not an identity and is
refused. The snapshot is now 3,912 implemented, 30 partial, 2,356 planned and 117 not applicable,
with no planned core row left. CBIND-037B then splits the input module by device family and
opens it with CBIND-037B1: GamePadType at its canonical ordinals and the whole
GamePadCapabilities surface as one fixed 48-byte value with 35 directly readable and writable
flags, because every canonical property has both a getter and a setter. One boundary moved while
implementing it: GamePad::GetCapabilities came here from CBIND-037B3, because a capabilities
value with no producer cannot be tested against anything real. The snapshot is now 3,998
implemented, 30 partial, 2,270 planned and 117 not applicable. CBIND-037B2 then maps the five
gamepad value types onto the representations C already had rather than adding a second spelling of
the same numbers, and preserves three canonical behaviors worth naming: the thumbstick square clamp
and trigger clamp, the epsilon trigger comparison, and the directional pad's own hash weighting. It
also records one representational limit instead of hiding it — the C snapshot carries a single
button mask, so a supplied directional pad is merged into it, which is the relationship every state
CNA itself builds already has. The snapshot is now 4,063 implemented, 30 partial, 2,205 planned and
117 not applicable. CBIND-037B3 then maps the GamePad statics, keeping the canonical
availability-plus-answer shape of the sensor and touchpad queries instead of folding "no sensor"
into a failure, and borrowing the three CNA::Input identity enumerations three of those statics
return. The snapshot is now 4,108 implemented, 30 partial, 2,160 planned and 117 not applicable.
CBIND-037B4 splits again by device, and CBIND-037B4a closes the keyboard: the whole KeyboardState
value surface and every Keyboard static over the 256-slot bit field C already had, with the
canonical silent drop of an out-of-range key deliberately replaced by a refusal so nothing is lost
without the caller knowing. The snapshot is now 4,143 implemented, 30 partial, 2,125 planned and
117 not applicable. CBIND-037B4b then closes the mouse, including the static clicked event as an
owned registration that takes no game handle and a raise route that makes it observable without a
device. The snapshot is now 4,164 implemented, 30 partial, 2,104 planned and 117 not applicable.
CBIND-037B4c then closes the mouse cursor, whose stock singletons become borrowed views precisely
because their canonical disposal is a no-op, and records four honest not-applicable rows rather
than putting an SDL_Cursor* in the ABI. The snapshot is now 4,182 implemented, 30 partial, 2,082
planned and 121 not applicable. CBIND-037B4d closes text input and with it parent CBIND-037B4. This
is the first input family that is event-driven rather than sampled, so it is the first to carry
callbacks: all three canonical events become owned registrations sharing one release route, and the
INTERNAL_On* dispatchers become raise routes that make them observable without a keyboard. A
committed code unit is a uint16_t and an above-BMP code point arrives as two surrogate calls; the
composition and candidate payloads cross as CNA_StringViews borrowed only for the callback, so no
std::string or std::vector reaches the ABI. Writing its tests exposed something the earlier
input slices never had to face: a windowed backend publishes a real window into the canonical
static, so the family's behavior legitimately differs between trees. Rather than branching on the
renderer, the suite forces the unbound case to prove the null-guarded contract everywhere and then
restores what the backend really bound — which turned SDL_RENDERER into a genuine activate/
deactivate round trip instead of one more no-op assertion. The snapshot is now 4,209 implemented,
30 partial, 2,055 planned and 121 not applicable. CBIND-037B5 then closes touch and gestures by
extending what already existed rather than adding to the ABI: the whole TouchCollection mutation
surface operates in place on the fixed eight-slot snapshot the C API has carried since CBIND-025,
and only the gesture type and sample are genuinely new values. Its real work was archaeological.
Four canonical behaviors that a careless mapping would have quietly changed are now pinned by
tests: equality, the hash and the text are all blind to the pressure extension and the text carries
only the position; an empty gesture queue throws and so must refuse in C rather than hand back a
default sample; a raised touch event feeds gesture detection and never the snapshot, and is dropped
entirely until a display size is published; and ResetForTests clears the display metrics and the
window handle even though the canonical class comment says it leaves them alone. That last one is a
documentation-versus-implementation contradiction in the canonical header; the C contract follows
the implementation and states the discrepancy rather than repeating the comment. The snapshot is now
4,284 implemented, 30 partial, 1,975 planned and 126 not applicable. CBIND-037B6 then adds the
haptics family, the first input slice with no XNA counterpart at all and the first to produce an
owned handle rather than a value. Its central decision is that a closed device is an ordinary
object rather than an error: opening never fails for want of hardware, and every route on a device
with nothing behind it answers false, zero or -1 through its output. That is what makes a
force-feedback API testable on machines that have no force-feedback hardware — which is every
verification tree — and the closed path is asserted rather than skipped. Two representational
choices are recorded because a later reader would otherwise assume the obvious one: the custom
waveform travels beside the effect value rather than inside it, keeping that value a plain copyable
POD, and the device name is not part of the capability value, which is why the capability
comparison takes both names as arguments instead of silently comparing fewer fields than the
canonical operator does. The snapshot is now 4,407 implemented, 30 partial, 1,849 planned and 129
not applicable. CBIND-037B7a then completes the 54 raw-joystick rows and is the first input slice to
capture an owned snapshot handle rather than a value: four heterogeneous variable-length arrays
with no canonical maximum cannot honestly become a fixed POD, because any invented capacity
truncates real hardware and four separate queries would answer from four different instants. Two
things the plan had guessed wrong are corrected from the canonical source rather than assumed: the
POV hat is an ordinal identity, not a composable bit set, and the joystick facade returns plain
values, so only the snapshot and the two hot-plug registrations needed handle kinds (69 and 70). The
haptics closed-device contract carried over unchanged and is again the only path the verification
trees exercise. The snapshot is now 4,461 implemented, 30 partial, 1,795 planned and 129 not
applicable. CBIND-037B7b then completes the last 38 input rows — host sensors, device
enumeration, clipboard and power — and closes parents CBIND-037B7 and CBIND-037B, leaving the whole
input module mapped. Its three recorded decisions: the sensor reads leave the caller's reading
untouched when nothing answered, because that is what the canonical query does with its reference;
the device descriptor's identifier is 64-bit where the sensor and joystick ones are 32-bit, since a
touch-device identifier is natively that wide; and the clipboard setter reports that the request was
made rather than that the platform honored it, because the canonical setter returns nothing and this
ABI does not invent an outcome. The clipboard test therefore asserts a relationship and restores the
content it found. The snapshot is now 4,499 implemented, 30 partial, 1,757 planned and 129 not
applicable, with five modules fully mapped. CBIND-037C1 then opens the media module with its 25
identity, visualization and media-source rows, and adds the cna_media link edge the C API did not
have. Two decisions are recorded: the media-source identity keeps its canonical 0/4 gap rather than
being renumbered dense, so it has no maximum and consumers validate membership; and the canonical
source enumeration's new-ed pointers never cross the ABI — each route owns and destroys the list
it enumerated, which the sanitizer tree with leak detection proves. The snapshot is now 4,524
implemented, 30 partial, 1,732 planned and 129 not applicable. CBIND-037C2 then maps Song and
SongCollection. Its shape decision is reference-counted sharing: several handles may name one
song, which is what lets a collection retain the songs it was given where the canonical collection
only stores non-owning pointers. Three canonical behaviors are preserved rather than tidied — an
omitted name stays empty despite the constructor's own comment claiming otherwise, equality and the
hash come from the file path rather than handle identity, and IsRated is not "rating is nonzero" —
and three Song rows that return library-owned entities are re-partitioned into CBIND-037C3,
where those handles will exist. The snapshot is now 4,554 implemented, 30 partial, 1,695 planned and
136 not applicable. CBIND-037C3 then lands the whole music catalog — MediaLibrary plus albums,
artists, genres, playlists and their collections — after re-partitioning the library into this
slice, because none of the entity types is constructible from outside it. Everything except the
library is a borrowed view that keeps its library alive, so the library handle may be released
first. Two canonical facts were established by evidence rather than assumption: album equality pairs
the name with the artist because album names collide across artists, and MediaLibrary(MediaSource*)
borrows its argument rather than adopting it — a leak the sanitizer tree caught. The test builds
a deterministic fixture library by pointing SDL's user-folder lookup at a private directory, which
also keeps it from touching a real user's music. The snapshot is now 4,646 implemented, 30 partial,
1,578 planned and 161 not applicable. CBIND-037C4 then adds the picture surface, contributing two
shapes the family did not have: the picture-album tree, walkable because the root's absent
parent is an availability answer rather than a failure, and the ABI's first point in time — a
picture's date as 100-nanosecond ticks from the Unix epoch, reusing the existing tick rather than
inventing a second time unit. The stream-taking save overload accepts a storage stream handle, the
only byte source this ABI owns. The snapshot is now 4,694 implemented, 30 partial, 1,518 planned and
173 not applicable. CBIND-037C6 then maps playback: the static MediaPlayer as game-scoped free
routes and the queue as a view of one process-lifetime object. Its two deviations are both forced by
ownership — a queue entry crosses as an owned copy because the canonical queue destroys its entries
on every clear, and appending copies because the canonical Add adopts the pointer it is given —
and both copies compare equal to the original, which is exactly what the canonical player does when
it enqueues a song. The playback transitions are asserted as a relationship, because whether a play
call really starts playing depends on the platform's decoder rather than on the C API. The snapshot
is now 4,734 implemented, 30 partial, 1,474 planned and 177 not applicable. CBIND-037C7 then closes
the media module with video. Its frame texture is solved by lifetime rather than by copying — the C
layer invalidates the borrowed handle on the next call to that player, so a stale frame fails
deterministically — and three canonical behaviors are reported rather than corrected, two of them
discovered by running the code: an undecodable file leaves the player stopped with its video
cleared, and the URI factory does not parse URIs at all. The snapshot is now 4,775 implemented, 30
partial, 1,432 planned and 178 not applicable, with six modules fully mapped. CBIND-037D1 then
opens the devices module with the sensor reading values, settling the ABI's second point-in-time
form — ticks from 0001-01-01 plus a UTC offset, because that is the canonical runtime type's base —
and preserving three canonical quirks rather than tidying them: each reading constructor keeps its
own argument order, equality pairs values with the timestamp, and the text conversions carry only
part of each reading. The snapshot is now 4,844 implemented, 30 partial, 1,362 planned and 179 not
applicable. CBIND-037D2a then maps the two motion sensors that produce those readings, deciding
that a class-template base is repeated per sensor rather than modeled, that an event delivering
nothing but a reading hands over the reading, and that the canonical test-support surface is
mapped deliberately — it is the only way a machine with no motion sensors reaches the supported path
and the real dispatch chain. Three canonical behaviors are reported rather than smoothed: an
unsupported sensor refuses to answer its current value, a second disposal is refused where every
other disposable in this ABI is idempotent, and the disposed state has no query route because the
canonical flag is protected. The snapshot is now 4,904 implemented, 30 partial, 1,282 planned and
199 not applicable. CBIND-037D2b then closes the sensors with Compass, Motion and the three
event-argument types, which resolve three different ways by payload — a template wrapping one reading
is flattened into the callback, an argument carrying nothing becomes a payload-free callback, and the
legacy accelerometer argument carrying three separate components earns a value of its own. Both
sensors are unsupported on every verification platform, so the ABI supplies its own installable
backend to reach anything past that refusal, and three canonical limits are reported rather than
smoothed: the eleventh instance is refused, a running backend cannot be swapped, and the
north-referenced attitude answer is vacuously true before a backend starts. The snapshot is now 4,948
implemented, 30 partial, 1,236 planned and 201 not applicable. CBIND-037D3 then adds the vibration
controller and the whole CNA::Devices service set, settling how a compiled-out extension layer
looks from C — exported everywhere, refusing with NOT_SUPPORTED, probed with one route — and
answering the clipboard question the plan left open: the two canonical types are one platform
clipboard, so only the acceptance flag is new. The slice also settles what to do with routes no test
can complete: supply the backend where the canonical class has a seam, and record the one gap where
it does not. The snapshot is now 5,016 implemented, 30 partial, 1,167 planned and 202 not
applicable. CBIND-037D4 then closes the devices module with the camera, deciding that a frame
lands in a texture the caller owns and keeps rather than in a lent one, because that is what the
canonical signature says — and preserving the canonical refusal that a mismatched texture size looks
exactly like no frame at all. The snapshot is now 5,040 implemented, 30 partial, 1,143 planned and
202 not applicable, with devices and devices-ext fully mapped. CBIND-037E1 then opens the runtime
module with the component model, the slice where the ABI's direction reverses: a component is
behavior the caller supplies, and since C cannot implement a C++ interface it is a callback set this
ABI wraps in a derived object. That derivation is also the reason the canonical protected content
hooks are mapped here while a sensor's protected members were not. The service container is the one
canonical type C cannot fully have — keyed by C++ type identity, so lookup is a named-identity subset
and registration has no C form at all. The snapshot is now 5,106 implemented, 34 partial, 1,050
planned and 225 not applicable. CBIND-037E2 then adds the game's own state, frame control and
events, deciding that the five canonical frame hooks arrive as a second callback table rather
than as new members on the published one — appending was tried, and it leaves every positional
initializer a consumer has already written incomplete — and extending the callback-reentrancy rule to
the frame step. The snapshot is now 5,154 implemented, 34 partial, 993 planned and 234 not
applicable. CBIND-037E3 then adds the window, answering the one-per-game question the fourth time the
same way and finding the result code a window state change actually needs: a platform that refuses is
neither an argument fault nor an internal one. The snapshot is now 5,176 implemented, 34 partial, 958
planned and 247 not applicable. CBIND-037E4 then adds the graphics device manager, the one runtime
object a C caller creates, and finds two things worth more than the routes: the canonical game caches
a raw pointer to the graphics device service and never clears it, so a released manager must outlive
its handle; and the canonical device-settings event cannot change the settings at all, because its
handler receives a const reference. The snapshot is now 5,243 implemented, 34 partial, 878 planned
and 260 not applicable. CBIND-037E2b then adds the game's content manager,
settling the borrowed-handle contract the slice was held back for: the game owns the manager as a
value member, so C borrows it and the setter copies. The snapshot is now 5,246 implemented, 34
partial, 875 planned and 260 not applicable. CBIND-037E5 then closes the runtime module without
writing a route: the CNA::Runtime facade is declared and never defined, so its 11 rows are recorded
not-applicable and a new check fails if its symbols ever appear. The snapshot is now 5,246
implemented, 34 partial, 864 planned and 271 not applicable. CBIND-037F1 then opens the audio module
by completing sound effects: three more creation routes, the process-wide 3D-audio settings, the
static sample computations, and both audio exceptions converting in the firewall rather than in one
route. The snapshot is now 5,284 implemented, 32 partial, 816 planned and 283 not applicable. CBIND-037F2
then adds streaming and capture, deciding that a streaming instance shares the sound-effect-instance
handle kind rather than earning one of its own, and that a capture short read is an answer rather
than a failure. The snapshot is now 5,333 implemented, 32 partial, 767 planned and 283 not
applicable. CBIND-037F3 then adds 3D positioning, deciding that the emitter and the listener are
values rather than handles and that the array overload's one-listener limit is reported rather than
approximated, and moving RendererDetail to F4 because an AudioEngine is its only source. The
snapshot is now 5,357 implemented, 32 partial, 743 planned and 283 not applicable. CBIND-037F4 then
closes the audio module with the XACT family, deciding that the family is reachable because its
binary files can be authored by the test itself. The snapshot is now 5,431 implemented, 32 partial,
665 planned and 287 not applicable. CBIND-037G1 then opens the last module with the gamer and guide
identities. The snapshot is now 5,539 implemented, 32 partial, 557 planned and 287 not applicable.
CBIND-037G2 then adds the avatar identities, preserving the canonical skeleton's sparse bone
numbering rather than renumbering it. The snapshot is now 5,672 implemented, 32 partial, 424 planned
and 287 not applicable. CBIND-037G3 then converts the six gamer-services exceptions at the
boundary into four distinct results. The snapshot is now 5,702 implemented, 32 partial, 394 planned
and 287 not applicable. CBIND-037G4 then completes the gamer, its collections and its
per-gamer surfaces. The snapshot is now 5,808 implemented, 32 partial, 270 planned and 305 not
applicable. CBIND-037G5 then completes the guide, its dispatcher and its component,
and finds the first genuinely deferred operations in the ABI. The snapshot is now 5,866 implemented,
32 partial, 212 planned and 305 not applicable. CBIND-037G6a then completes achievements, the one gamer-services
surface that finds real persisted data. The snapshot is now 5,899 implemented, 32 partial, 177
planned and 307 not applicable. CBIND-037G6b then completes property storage and game defaults,
carrying a variant map across the ABI as a typed family plus a kind query. The snapshot is now 5,941
implemented, 32 partial, 127 planned and 315 not applicable. CBIND-037G6c then completes leaderboards, correcting the plan's own
note about the read's shape and declining to bind a writer whose owner pointer cannot survive the copy
every published gamer is. The snapshot is now 5,982 implemented, 32 partial, 83 planned and 318 not
applicable. CBIND-037G7 then completes the avatar surfaces and closes the campaign:
the snapshot is 6,063 implemented, 32 partial, 0 planned and 320 not applicable. Every
public/protected declaration the inventory tracks is now either mapped, partially mapped with the
subset named, or explicitly recorded as having no C form with the reason. CBIND-052A then reconciles
the branch with the compiled-effect and IGL merges: it binds the IGL and PIXIJS renderer
identities and the CompiledEffects capability, publishes a _MAXIMUM for both identity ranges so
they can be walked rather than remembered, and turns on the strict warnings the adapter library --
alone among this module's targets -- had never had. That last part is what found TINYGL: a
renderer with no C constant at all, recorded implemented because CBIND-050's approvals were seeded
from a tree that already believed it. The snapshot is 6,286 implemented, 12 approved partial, 9
planned and 386 not applicable, and those 9 -- the Effect object graph the compiled-effect work
reshaped -- are CBIND-052B's. CBIND-052B then closes them, and the row that mattered was a
re-approval rather than a route: Clone() and OnApply() stopped being pure virtual, which made the C
adapter's Clone() override silently drop a compiled effect's runtime and parameter values. The
snapshot is 6,296 implemented, 12 approved partial, 0 planned and 386 not applicable, and the
release gate reads ready.
Read Current status above first: it carries the snapshot, what is closed, and the ordered list of what remains. This section carries only what a fresh context cannot infer from the plan.
-
Branch:
feature/binding, at the same commit asnextandorigin/next.CBIND-052Ais the last task completed and it lands green: 81/81 in all four trees, sanitizer included, and every build-free gate (coverage,limitations,release_gate,compatibility,abi_baseline --checkheader half) passing. -
No task is open.
CBIND-064closed the last one on 2026-08-19, on branchbindingcin thecnabindingcworktree —cnabindingno longer exists, and neither do its four build trees (see the build-tree note). The next real work is whatever the next merge ofnextreopens — start by runningpython3 tools/c-api/generate_coverage_inventory.py --check, and read What remains above for why that is the standing first step rather than a formality. -
The published export count was stale in three documents —
ABI_VERSIONING.md,CONSUMING.mdandLIMITATIONS.mdall said 2,720 whereabi_baseline.jsonmeasured 2,838. Corrected on 2026-08-17 in its own commit, deliberately separate fromCBIND-052A/Bbecause it predates them. Nothing prevents it happening again: these are prose claims and no gate reads them, while every count in the same sentence's neighbourhood (COVERAGE.md's snapshot,LIMITATIONS.md's group counts,RELEASE_GATE.md's evidence) is generated. A checker that holds the three documents' export figure againstabi_baseline.jsonis the obvious fix and is not yet written. -
abi_baseline.jsonrecords noCNA_*_FLAG_*constant at all, because the tool reads simple integer constants and these are shift expressions;CNA_GRAPHICS_CAPABILITY_FLAG_COMPILED_EFFECTSis pinned by theAbiHeaderC.c/AbiHeaderCpp.cppassertion walls instead. Left as it stands: it is a tooling gap, not a wrong claim. -
Historical, kept because the reasoning still applies: the experimental release gate (
CBIND-042B) was built this way, and a later gate should be built the same way. Every input it needs now exists and should be pointed at rather than rebuilt:COMPATIBILITY.md(23 toolchain cells),abi_baseline.json(layouts and exports),hello_cnawithCApi_InstalledConsumer(a real C application built from outside the tree),FUZZING.md,COVERAGE.mdand nowLIMITATIONS.md. Build it the way this campaign builds every gate — a declaration, a checker,--checkin ctest and CI — and make it fail in both directions: a criterion recorded as met that no longer is, and one recorded as blocked that has quietly become met. The second direction is the one that matters here, because a release gate nobody re-reads is how a project ships something it decided not to ship.Two criteria are owner decisions and must block, both recorded as
open decisionindocs/c-api/LIMITATIONS.md: whether the package ships SDL3 and FFmpeg beside the library, and whether a static configuration is ever offered given that it would export every C++ symbol it archived. Do not decide those alone, and do not let the gate report ready while they stand. ABI 1.0 is explicitly a later, separate decision.Two things the last two gate tasks established that this one inherits: absence is skipped by name, presence is binding — a gate that cannot run somewhere says so rather than passing quietly; and a gate is not finished until it has been shown to fail on the defect it exists for.
CBIND-038proved itself by reinstating a duplicate typedef;CBIND-039by swapping two struct fields, in CI as well as by hand. -
The
CNA_DEVICESenvironment decision is done, not pending. The owner directed (2026-08-15) that the#ifdef CNA_DEVICEShalf ofdevices-extbe genuinely exercised rather than only ever tested compiled-out.cmake-build-binding-sdlrendererandcmake-build-binding-asanhave been reconfigured with-DCNA_DEVICES=ONand rebuilt;headlessandsoftwarestay OFF, so both states are covered, mirroring the existingCNA_CNAEXTsplit. No fifth build tree was added, and all four trees are green in that configuration. The routes themselves must stay exported in both states, following theCnaCApiGraphicsExt.cppprecedent of an#ifndeffallback that reports the feature as unavailable, so the ABI's symbol set never depends on a build option. -
Do not reopen a closed slice without a concrete demonstrated defect.
-
The four verification trees and the shared ccache are set up and warm; nothing needs configuring before the next slice. See Environment and disk hygiene below for what was cleaned up on 2026-08-15 and what must not be undone.
-
Check for a second Claude session before writing anything. See the trap entry below; this campaign lost work to it twice in one afternoon.
The C API lives entirely in modules/c-api/. A slice almost always edits exactly these, in this
order:
| File | Role |
|---|---|
include/CNA/C/<family>.h |
the public surface. One header per family — 55 today (sensors.h, video.h, media_player.h, media_library.h, media.h, input_devices.h, input_joystick.h, input_gamepad.h, input_keyboard.h, input_mouse.h, input_cursor.h, input_text.h, input_touch.h, input_haptics.h, net_sessions.h, storage.h, core_ext.h, …). Add a new one when the family is genuinely new; extend an existing one when it is not. |
include/CNA/C/cna.h |
the umbrella. Every new header must be added here or a strict-C consumer never sees it. |
src/CnaCApi<Family>.cpp |
the adapter — 45 files today. Routes go in extern "C" scope; helpers in an anonymous namespace above them. |
src/CnaCApiDetail.hpp |
shared substrate: the ObjectKind handle-kind enum (next free number is 91), the HandleRegistry, CallWithExceptionBarrier and its 18 exception arms, CopyStringView, Fail. A new handle kind or a new canonical exception conversion lands here. |
src/CnaCApi<Family>Detail.hpp |
cross-file borrow helpers, when one family's adapter must reach another's resource (CnaCApiGraphicsDetail.hpp exposes GetOwnedTexture2D, CnaCApiNetDetail.hpp exposes BorrowPacketReader, …). |
CMakeLists.txt |
the cna_c_api source list, and the per-test executable + add_test block (81 tests today). Since CBIND-052A the library itself also carries -Wall -Wextra -Werror, which the test targets always had and it never did; an exhaustive switch over a CNA enumeration is therefore a build gate now, not a suggestion. |
tests/pure_c/<Family>Smoke.c |
the strict-C17 behavior test. 55 files; prefer extending the family's existing one over adding a target — but a family with its own adapter file has earned its own test target, as haptics did. |
tests/pure_c/AbiHeaderC.c and tests/cpp/AbiHeaderCpp.cpp |
freeze every new identity value and every new struct size/alignment/offset. Both must compile — the surface has to be valid C17 and C++23. |
tests/cpp/BoundaryDetailTest.cpp |
only when a slice adds an exception-firewall arm; returns a distinct code per case. |
tools/c-api/coverage_mappings.json |
the rules that close inventory rows. |
docs/c-api/<FAMILY>.md + FEATURE_MATRIX.md + README.md |
the consumer-facing contract. |
The design contracts that already exist do not need restating in a new page — point at
docs/c-api/HANDLES.md, OWNERSHIP.md, STRINGS_AND_BUFFERS.md, ERRORS.md and
CALLBACKS_AND_THREADING.md.
Every closed slice follows these. A new slice that invents a different shape makes the ABI inconsistent, which is worse than the shape being slightly suboptimal.
- Identities are
typedef uint32_t CNA_Xxx;plus#define CNA_XXX_* UINT32_C(n)at the canonical ordinals — never renumbered into a dense range (CNA_LOG_LEVEL_EXPERIMENTis 100). Flag sets get aCNA_XXX_ALLmask and every route validates against it. - Values are fixed PODs. Anything that may grow carries
struct_size+struct_versionand an_initroute; a pure math pair (CNA_GamePadThumbSticks) does not. - Strings are the count/copy pair
cna_x_get_y_size+cna_x_copy_y: no terminator,out_bytesalways required and always written,CNA_RESULT_BUFFER_TOO_SMALLwith no partial write when the capacity is short. Input strings are a borrowedCNA_StringView, copied before use. - Handles are opaque, generation-checked and thread-affine. Owned vs borrowed is a deliberate choice per type: a borrowed view keeps its parent alive and blocks the parent's release.
_extsuffix marks a route with no XNA 4.0 counterpart — either the canonical member isCNAEXT, or the route exists only because C needs it. A whole header of CNA-namespace surface (core_ext.h) does not repeat the suffix on every route.- Every fallible route returns
CNA_Resultand runs insideCallWithExceptionBarrier. - Availability is separate from the answer. A canonical query that returns
booland fills an output reference becomes a route with anout_availableflag — "no sensor" is an ordinary answer, never a failure. - An identity is not a capability claim. Never branch a test or a route on which renderer is compiled in; probe the behavior.
- Preserve canonical quirks, record deliberate deviations. Both have precedent in the closed slices; what is not acceptable is silently smoothing one over.
cd /rv/data/development/github.com/openeggbert/cnabinding
export CCACHE_DIR=/media/robertvokac/claude/tmp/cna/ccache
B=/media/robertvokac/claude/tmp/cna/cmake-build-binding-headless
# 1. what does this slice own? (planned rows, by header)
python3 - <<'EOF'
import re
hdr=None
for line in open('docs/c-api/COVERAGE.md',encoding='utf-8'):
m=re.match(r'#### `(modules/[^`]+)`', line)
if m: hdr=m.group(1); continue
if line.startswith('| `CPP-') and '⬜' in line and '<Header>.hpp' in (hdr or ''):
print(line.split('|')[4].strip())
EOF
# 2. implement, then build just the library
nice -n 10 cmake --build $B --target cna_c_api -j3
# 3. probe a new struct layout before freezing it
gcc -std=c17 -I modules/c-api/include /path/to/probe.c -o /tmp/probe && /tmp/probe
# 4. coverage: regenerate, then CHECK THE DELTA equals the slice's row count
python3 tools/c-api/generate_coverage_inventory.py --write
python3 tools/c-api/generate_coverage_inventory.py --check
# 5. all four trees
for T in headless sdlrenderer software asan; do
D=/media/robertvokac/claude/tmp/cna/cmake-build-binding-$T
nice -n 10 make -C $D/modules/c-api -j3 || break
(cd $D && SDL_VIDEODRIVER=dummy ASAN_OPTIONS=detect_leaks=1 \
ctest --test-dir modules/c-api --output-on-failure -j3 | tail -3)
doneA slice is not finished until step 4's delta matches, all four trees are green, and
plan_binding.md / AUDIT.md / NEXT.md / the docs/c-api/ pages say what changed.
All four trees live under /media/robertvokac/claude/tmp/cna/ (off the repo, on the scratch
partition, sharing the project-wide CCACHE_DIR=/media/robertvokac/claude/tmp/cna/ccache).
Do not give the binding trees a cache of their own. They were pointed at a separate
tmp/ccache until 2026-08-15; it reached a 0.69% hit rate over 6,932 compilations because it
started cold and never saw the CNA and sharp-runtime objects the shared cache already holds.
It was deleted. Build only modules/c-api in
each — make -C <tree>/modules/c-api -j3 — never the default all target, which pulls in
unrelated modules and examples. Then ctest --test-dir modules/c-api. Cap parallelism at -j3.
| Tree | Configuration | Why it exists |
|---|---|---|
cmake-build-binding-headless |
HEADLESS, CNA_CNAEXT=OFF |
deterministic state; the no-extension-layer half |
cmake-build-binding-sdlrenderer |
SDL_RENDERER, CNA_CNAEXT=ON |
the extension-layer half; needs SDL_VIDEODRIVER=dummy |
cmake-build-binding-software |
SOFTWARE |
the only tree that can supply real 3D pixel evidence |
cmake-build-binding-asan |
SOFTWARE, CNA_CNAEXT=ON, CNA_SANITIZE=address,undefined |
verification only |
All four run the same 81 C API tests green. The sanitizer tree runs with
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 — stricter than the
detect_leaks=0 the CBIND-035B–E slices used; do not weaken it back. Every tree needs
-DCNA_BUILD_C_API=ON, which defaults to OFF: a freshly configured tree silently has no
modules/c-api build directory at all without it.
A slice that changes a canonical C++ header must also run the C++ suite, which these trees do
not build by default. CBIND-052B added one public accessor to modules/graphics and verified it
with cmake --build <tree> --target CnaTests -j3, then
SDL_VIDEODRIVER=dummy <tree>/CnaTests --gtest_filter='EffectPassTest.*' run from the repository
root, because the suite's fixtures are resolved relative to the working directory. The binary and
its object tree are ~1.1 GB, so they are deleted afterwards (rm -f <tree>/CnaTests && rm -rf <tree>/CMakeFiles/CnaTests.dir) and the C API suite re-run to confirm nothing else depended
on them. Deleting the .dir takes make's generated build.make with it, so the next attempt to
build that target fails with No rule to make target 'CMakeFiles/CnaTests.dir/build.make' until
cmake <tree> regenerates it -- run the reconfigure first rather than concluding the tree is
broken. Run the suite on a virtual display (Xvfb :95 -screen 0 1280x1024x24, then
env -u SDL_VIDEODRIVER DISPLAY=:95 <tree>/CnaTests) rather than under
SDL_VIDEODRIVER=dummy: the dummy driver skips a further handful of SDL3 platform and graphics
tests, and the repository already has a CNA_TEST_DISPLAY cache variable for the same purpose.
Displays :99 and :97 are usually already taken by other sessions -- pick a free one instead of
reusing theirs. Note the three GltfRenderer*Policy inventory audits that fail in a full run at this
commit: they are pre-existing, they predate this campaign's slices, and next's 09786205e already
fixes them on the other side of the merge.
Never branch a test on a renderer identity. Probe the capability or the actual result, so a new
backend needs no test edits. CApi_TextureSmoke, CApi_TextureVolumeSmoke and CApi_LifecycleSmoke
were rewritten once for exactly this reason.
--check only proves COVERAGE.md matches what the generator produces from the rules. It cannot
see a rule whose approved_symbols names something with no C route behind it — that is exactly how
TINYGL read implemented with test evidence for two merges. After CBIND-052B closed the matrix
on 2026-08-17 the closure was audited rather than assumed, in three ways worth repeating whenever
someone needs to trust a "0 planned":
- Count C identities against C++ enumerators, per family. Group the implemented
enum-valuerows by their enum, read theCNA_XXX_*glob out of each row's mapping text, and count the matching#defines inmodules/c-api/include. Exclude only_MAXIMUM,_ALLand_COUNT—_NONE,_UNKNOWNand_INVALIDare real enumerators and excluding them manufactures an off-by-one in a dozen families. Result: no family publishes fewer C constants than CNA declares enumerators. - Rely on the compiler for the outward direction. Since
CBIND-052Athe library builds with-Werror=switch, so any C++→C switch missing an enumerator fails the build. That is only worth anything if no such switch carries adefault:to swallow it — 29 defaulted switches in the adapter return aCNA_constant, and every one of them switches on a C identity, where a default is correct. Twostd::array<std::pair<CNA_…>>tables exist;RendererIdentitiescarries theconstevalcount gate, and the fallback-reason table is checked by (1). - Check that the routes the rules name exist. Extract every
cna_[a-z0-9_]+from themappingandteststext of everyimplementedrule and hold it againstabi_baseline.json's export list. Ignore trailing-underscore family prefixes (cna_vector3_) andcna_c_api_*test names. This found the campaign's two broken Doxygen cross-references —cna_gamepad_get_battery_level_extandcna_graphics_device_get_info, neither of which has ever existed — and after those were repaired all 484 named routes resolve.
None of the three is wired into a gate. They are cheap enough to re-run by hand, and (3) in particular is the one that would catch a rule citing a route that a later rename removed.
- A green coverage matrix is not evidence that a mapping exists.
CBIND-050pinned every rule to reviewed symbol IDs, seeded from the pre-merge tree so that merge's 121 unreviewed rows could not be blessed — the right call, and it also froze whatever the pre-merge tree already believed.TINYGLhad been sitting there asimplementedwith test evidence and noCNA_*constant anywhere. Seeding cannot tell a reviewed claim from an inherited one, so when a slice touches an identity family,grepthe C constant rather than trusting its row. - The four verification trees can be stale in a way that reads as green. Three of them were
built before the merge commit even existed and passed 81/81 on the old library; only the tree
actually rebuilt showed the two failures HEAD had introduced. Compare each tree's
libcna_c_api.somtime againstgit log -1 --format=%cibefore believing a suite, and never conclude "pre-existing" from a green run in a tree you did not rebuild. - Warning flags applied per target miss the target that matters.
cna_c_api_enable_strict_warningswas called on all 59 test executables and never oncna_c_apiitself, so the shipped library compiled without-Wall. GCC had been printingenumeration value 'TinyGL' not handled in switchthe whole time. When a helper exists to harden targets, check what it is not applied to. - Coverage rules on free operators need a
signature_regex.^CNA::Input::operator\|$with no signature also swallowedHapticFeatureEXT's five operators and claimed haptics coverage that does not exist. Caught only by comparing the implemented delta against the slice's row count — always do that comparison after regenerating. sharp-runtimeis a sibling checkout other sessions edit and commit to mid-build (/rv/data/development/github.com/openeggbert/sharp-runtime). A build failure inside it is very likely someone else's work in progress, not a regression here: rungit statusandgit login that tree before diagnosing, and never modify it from this task.- A windowed backend publishes a real window into the input statics.
GraphicsDevicecallsTextInputEXT::setWindowHandlePropertyandMouse::setWindowHandlePropertywhen it attaches or creates a window, so underSDL_RENDERERthe handle is not zero, while HEADLESS and SOFTWARE leave it at zero. A test that asserts the initial value is zero passes in three trees and fails in the fourth — which is the identity-versus-behavior rule biting from the other side, since the assumption is about the environment rather than the renderer name. The fix that also produced better evidence: force the unbound case to prove the null-guarded contract everywhere, then restore whatever the backend really bound and assert only the relationship between the answers (activation that took effect must be undone by stopping it). OnSDL_RENDERERthat is a real activate/deactivate round trip.TouchPanelhas the same window-handle property andCBIND-037B5did meet it again — any route that resets this state must put it back, because it is process-wide state the suite does not own. Expect the remainingCNA::Inputfamilies to have their own. - Out-parameter clobbering. Routes set
*out = CNA_INVALID_HANDLEbefore validating, so reusing one variable for an expected-failure call destroys a live handle. Hit three times; use a separate scratch variable for the failure case. Versioned output structures have the same problem in a nastier form:CBIND-037B5setstruct_version = 2on a liveCNA_GestureSampleto test the invalid-structure refusal and every later read of that variable then refused, which looks like a broken route rather than a broken test. Copy to a scratch value. - A canonical Doxygen comment can contradict its own implementation.
TouchPanel::ResetForTestsdocuments that the display size and orientation are left untouched; it clears them, plus the window handle, deliberately — a leaked display size silently corrupts another test's scaled touch coordinates. Read the.cpp, not just the header, before writing a C contract sentence, and when they disagree follow the behavior and say so in the C header. Assume other stale comments exist. - Two sources feed one snapshot.
TouchPanel::INTERNAL_onTouchEventfeeds gesture detection and the event-driven touch map;TouchPanel::SetFingerfeeds the slot array thatGetStateactually reports. A test that raises an event and then asserts the snapshot changed will fail while looking like a mapping bug. The raise path is also a silent no-op until a display size is published. Expect more of these splits in the remainingCNA::Inputfamilies. - Do not pipe a build into
grep … | head. SIGPIPE kills the build and leaves a target unlinked, which then shows up as a mysterious "Not Run" in ctest. Redirect to a log and grep the log. - A second Claude session on this working tree will silently destroy your edits. It happened
twice on 2026-08-15, and both times the first symptom was
git statuslisting files the session had not touched, or a file changing mtime mid-read. The damage: a duplicatedvalidate_text_input_familyblock appended toInputSnapshotsSmoke.c, and a read-modify-write that could have dropped the other session's concurrent write. It also produced a false test failure — asdlrendererred that looked like a mapping bug but was another session's mid-flight state. Detect it before writing:ListAgents, orps -eo pid,args | grep "[c]laude" | grep "resume .*cnabinding". Background sessions run with--permission-mode auto, survive their terminal being closed, and can respawn after being killed. If one is active, agree on who owns the slice before writing, and never build the same tree concurrently — a spuriousranlibfailure from exactly that is already on record. - Read the canonical
.cpp, not just the header. Several slices turned on behavior only the implementation reveals: a square clamp, an epsilon comparison, a silent drop, a no-op disposal, a hash that ignores half the fields.
Settled on 2026-08-15 after an audit; a future context should keep it this way rather than rediscover it.
- One shared ccache, not one per campaign.
CCACHE_DIR=/media/robertvokac/claude/tmp/cna/ccache(20 GB ceiling, ~31% hit rate across 21 build configurations). The binding trees briefly had their owntmp/ccache; it reached a 0.69% hit rate over 6,932 compilations because it started cold and never saw the CNA and sharp-runtime objects the shared cache already holds. It was deleted. Do not give these trees a private cache again. - Do not shrink the shared cache. It sits at 96.5% of its ceiling and still misses 69% of the time, which means it is undersized for this workload, not oversized. Shrinking it causes more compilation, which means more SSD writes — the opposite of what the build rules are protecting.
- Never build
allin a binding tree. Someone did, once, incmake-build-binding-headless: it left 56 stray executables and their object trees, 2.6 GB, none of which the C API loop ever links against. They were deleted; the tree went 3.7 GB → 1.1 GB with zero recompilation afterwards. Buildmake -C <tree>/modules/c-api -j3and nothing else. To check a tree for the same rot:find <tree> -maxdepth 1 \( -name 'cna_test_*' -o -name 'cna_demo_*' -o -name 'CnaTests' \). - The four binding trees are the only build directories this campaign owns. Everything else
under
/media/robertvokac/claude/tmp/cna/(cmake-build-multi,gltf-*,fna3d*,develop-opengles,next-*,software,sdlgpu, …) belongs to other checkouts and other sessions. Do not delete or build in them. - Session scratchpads and per-run build logs are disposable; write throwaway probes there, never a build tree.
analysis_binding.mdandanalysis_binding_sharp_runtime.mdare strictly read-only.- Only the C binding is in scope. Do not plan or implement C#, .NET, JavaScript, Rust, Python, Java, Zig, Go, Swift or any other language binding.
- One task, one commit. Stage explicit file names; never
git add -A. Do not push unless asked. - Build targets are
cna_c_apiplus the strict-C and C/C++ ABI targets; there is noCNAtarget in the configured build trees.