Files
BillyOutlastandClaude Opus 5.5 890e74ce6d docs: move P5-06 notes out of frozen NOTES.md (#186)
* docs: move P5-06 notes out of frozen NOTES.md

#185 appended its slice notes to docs/NOTES.md, which spec rule 7
freezes. Move them to docs/notes/P5-06.md unchanged and record the
open engine_audio_spatial race item.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VjcGdfRk1pMqciEq3aZUEZ

* docs: record P5-06 30x audio repeat result (from #188)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VjcGdfRk1pMqciEq3aZUEZ

---------

Co-authored-by: Claude <[email protected]>
2026-11-02 00:10:31 -05:00

58 KiB

Illusion Layer implementation notes

Frozen: parity-roadmap slices log to docs/notes/<slice id>.md instead (see docs/notes/README.md).

Baseline

  • Date: 2026-10-30 (ILLU-M0).
  • origin/main commit: 3112291aa06f476b068b37502cfcee7d198423c0 (Merge PR #51).
  • Main-drift note (IMPLEMENTATION_PROMPT §3): spike was done on 70846a5; current origin/main is 3112291, recorded here as required. No Find-text edits were needed in M0 (no code changes).
  • Branch: feat/illusion-layer-abi-4.2, created from origin/main in this task.
  • Tool versions: dotnet 10.0.403, cmake 3.31.6, ninja 1.12.1, g++ (Debian 14.2.0-19) 14.2.0.
  • CTest: 100% tests passed, 0 tests failed out of 40 (see build/logs/m0-ctest.log). Failing tests: none.
  • Managed tests: grep -c 'all checks passed' build/logs/m0-tests.log = 9 (>= 9), grep -c FAIL = 0 (see build/logs/m0-tests.log).
  • dotnet build Engine.slnx -c Release exit 0 (see build/logs/m0-dotnet.log); cmake --preset ci-linux and cmake --build exit 0.

Discoveries

  • 2026-10-30: all 40 CTests pass on main, including the 8 export tests listed in §4.7 (engine_export_publish_aot, engine_export_stage*, engine_export_coldboot). The doc expects them to fail on machines whose .NET SDK lacks the NativeAOT ILCompiler package; this machine's SDK (10.0.403) evidently has it, so the M0 baseline is fully green. Later milestones compare against this all-pass baseline.
  • 2026-10-30: a stray .hermes/ scratch dir (Hermes runner cache) was present at repo root and showed in git status; removed it (rm -rf .hermes) since it is runner scratch, not a repo artifact. build/ is git-ignored, so build/logs/m0-*.log are not committed.

M1 spikes

  • Date: 2026-10-30 (ILLU-M1). Spike sources created verbatim from docs/IMPLEMENTATION_PROMPT.md:309-418 under build/ (git-ignored, not committed): build/spikes/flecs_spike.cpp, build/spikes/cs1503/cs1503.csproj, build/spikes/cs1503/Program.cs.
  • Flecs spike build/run (c++ -std=c++20 -O2 -I../../deps/flecs/include -I../../deps/flecs/distr flecs_spike.cpp -L../ci-linux/deps/flecs -lflecs ...):
mover=456 mover2=456 health=457
lookup(sep="")=456  lookup(default)=0  has_parent_Spike=0
type_info size=12 align=4
query=0x6179464df9e0 field_count=2
 table count=3 mover_col=0x617946550b90 mod64=16 health_col=0x617946550ad0 entities=0x617946550b60
tables=1 rows=3
 e0 speed=0 x=100
 e1 speed=1 x=0
 e2 speed=2 x=100
 e3 speed=3 x=0
 e4 speed=4 x=100
fatal: entity.c: 1522: abort(): Spike\.Mover (INVALID_COMPONENT_SIZE)
exit=134
  • CS1503 repro build (dotnet build build/spikes/cs1503):
Chunk_Spike_Mover.g.cs(37,65): error CS1503: Argument 3: cannot convert from 'delegate* unmanaged[Cdecl]<Engine.Bindings.NativeApi.EcsChunkView*, void>' to 'delegate* unmanaged[Cdecl]<Engine.Bindings.NativeApi.EcsChunkView*, float, void>' [cs1503.csproj]
    0 Warning(s)
    1 Error(s)

Discoveries

  • 2026-10-30: spike confirms §4.1 facts on this machine — same id on same-size re-register (mover == mover2 == 456), abort (exit 134, SIGABRT) on different-size re-register, lookup(default)=0 so callers must use ecs_lookup_path_w_sep with empty seps, size=12 align=4, only 3 of 5 rows matched (Health-gated). Column mod64=16 here vs mod64=32 in the doc's capture — addresses vary per run, but the point stands: user columns are NOT 64-byte aligned.
  • 2026-10-30: CS1503 repro error carries a to '...EcsChunkView*, float, void>' target suffix (doc acceptance cites only the from '...EcsChunkView*, void>' prefix), i.e. the generated chunk thunk drops the float dt parameter the Tick signature requires. M2's ABI work must reconcile the native EcsChunkView*-only callback shape with the managed Tick(..., float dt) signature.

M2 ABI 4.2 header

  • 2026-10-30: applied the 3 doc-ordered edits to native/bridge/include/engine/bridge/engine_abi.h (IMPLEMENTATION_PROMPT.md:420-610): edit 1 bumped ENGINE_ABI_VERSION_MINOR 1 -> 2 with the 4.2 comment tail; edit 2 added the generic tail (ENGINE_ABI_MAX_TERMS 8, ACCESS IN/INOUT, ERR codes -1..-6, EngineChunkColumns, EngineSystemCallback, 6 fn typedefs); edit 3 appended the 6 table fields ending with systems_run as last field before } EngineApiTable;. Each Find text matched exactly once.
  • 2026-10-30: grep -c 'ENGINE_ABI_VERSION_MINOR 2' -> 1; grep -nE 'EngineSystemsRunFn systems_run;' -> one line, last field before } EngineApiTable;; grep -c 'typedef struct EngineChunkColumns' -> 1.
  • 2026-10-30: cmake --preset ci-linux && cmake --build build/ci-linux -j4 fails as the source doc predicts (M2 acceptance block: "expected to fail until M3"): engine_abi.cpp:1013: error: missing field 'component_register' initializer [-Werror,-Wmissing-field-initializers]. Header is 4.2, kApiTable has no new initializers yet — M3 fills them in. No drift: all 3 Find texts matched exactly once.

M3A GenericSystems registration

  • 2026-10-30: created native/bridge/src/generic_systems.hpp verbatim from docs/IMPLEMENTATION_PROMPT.md <!-- file: native/bridge/src/generic_systems.hpp --> block (programmatic fence diff: match=True, 2022 bytes).
  • 2026-10-30: created native/bridge/src/generic_systems.cpp part 1 (through get()): register_component with ecs_get_type_info pre-check returning 0 on size/alignment mismatch (never calls ecs_component_init on mismatch — §4.1 abort rule), plus create_entity/set/get with NULL-arg/in-run guards. register_system/run intentionally deferred to ILLU-M3B (child card t_e07a5570 owns them plus all engine_abi.cpp wiring); fence diff vs doc block is exactly the split-note header + missing register/run tail.
  • 2026-10-30: TDD temporary spike build/spikes/m3a_reregister_spike.cpp (git-ignored, DELETED after run): a=470 b=470 same=1, mismatch=0 (expect 0), set/get round-trip speed=1.5 x=2.5, SPIKE PASS, exit 0. Spike compiled generic_systems.cpp with -Wall -Wextra -Werror clean (flecs.c compiled as C; engine_core_export.hpp stubbed in git-ignored spike dir only — header includes it under #ifdef __cplusplus only so no repo change needed).
  • 2026-10-30: split note — full cmake --build build/ci-linux still fails at kApiTable missing 6 initializers (M2-predicted state); M3B adds the wrappers + table entries that fix it. No engine_abi.cpp change in this commit by design (avoids colliding with M3B's 6 doc-ordered edits).

M3B systems run + shutdown (ILLU-M3B)

  • native/bridge/src/generic_systems.cpp completed verbatim from the M3 doc fence (register_system + serial run() appended; file now byte-matches the doc fence minus the M3A-split NOTE header comment). grep -c ENGINE_ABI_ERR_IN_RUN = 3 (set, register_system, run in-run refusal paths).
  • native/bridge/src/engine_abi.cpp: applied the 6 doc-ordered edits (each Find matched exactly 1x): generic_systems.hpp include, EngineChunkColumns static asserts (sizeof 120, offsets 64/96/104/108/112), GenericSystems member on EngineWorldImpl, generic.shutdown() before ecs fini in ApiWorldDestroy, 6 Api* wrappers (component_register/entity_create/entity_set+plan-invalidate/entity_get/system_register/systems_run with NULL-dispatch dt=0), 6 kApiTable tail entries.
  • native/tests/test_abi_layout.cpp: 312→360B table-size edit applied.
  • Build: cmake --build build/ci-linux -j4 exit 0. ctest -R engine_abi_layout -V passes, shows abi: EngineApiTable 360B (ABI 4.2 generic tail).
  • engine_compositor_live fails post-M3B with ABI version mismatch: native 4.2.0 — expected: managed NativeApi.g.cs still pins AbiVersionMinor=1; regeneration is owned by ILLU-M5 (abi_codegen). Not a native regression: native-side engine_abi_layout is green.
  • Card said "7 engine_abi.cpp edits" but the doc contains exactly 6 engine_abi.cpp edit blocks + 1 test_abi_layout.cpp edit (step 3); all 7 applied.

M4 native test of the generic tail (ILLU-M4)

  • native/tests/test_generic_systems.cpp created verbatim from the M4 doc fence (programmatic fence diff: BYTE-MATCH, 149 lines).
  • native/tests/CMakeLists.txt: applied M4 edit #1 verbatim (engine_generic_systems add_executable/link/flags/add_test, TIMEOUT 120). Edit #2 deferred to M9 per doc.
  • 312→360B table-size edit was already applied in M3B (test_abi_layout.cpp:218) — verified present, not re-applied.
  • Build: cmake --build build/ci-linux --target engine_generic_systems engine_abi_layout exit 0. ctest -R engine_abi_layout|engine_generic_systems: 100% passed (2/2). Verbose tail shows generic_systems: all checks passed plus the doc acceptance lines [PASS] systems_run with NULL dispatch and [PASS] row 4: speed=4.0 x=25.0 (want speed=4 x=25.0).
  • No fix-forward needed: first-run green, generic_systems.cpp untouched.

M5A codegen parser rejects unknown 4.2 types (ILLU-M5A)

  • Applied doc edits #6 (ValidateCType) + #7 (CsType) ONLY to tools/abi_codegen/Program.cs: added const void*, const uint64_t*, const uint32_t*, const EngineChunkColumns*, EngineSystemCallback (plain Cdecl callback shape, no SuppressGCTransition).
  • Pre-change baseline: dotnet run --project tools/abi_codegen -c Release -- . exited 1 with unsupported ABI type 'const EngineChunkColumns*' at engine_abi.h:492 (fail-closed confirmed).
  • Post-change: still exits 1, now at Fn typedef(s) not used by EngineApiTable: EngineSystemCallback — the required-entry + unused-typedef checks (edits #5/#12) and emission belong to ILLU-M5B. No .g.cs touched.

M5B 4.2 bindings emitted (ILLU-M5B)

  • Applied doc edits #1-5 + #8-13 to tools/abi_codegen/Program.cs (M5A already owned #6+#7): GenericDefineRe + ParseGenericDefines (9 required consts, MAX_TERMS==8 gate), EngineChunkColumns required struct, 6 required fn entries, Emit signature carries generic dict, MaxTerms..ErrInternal consts emission, EmitChunkColumns with fail-closed 120-byte field check, EngineSystemCallback unused-typedef removal, loader drift guard extended with EngineChunkColumns 120B.
  • dotnet run --project tools/abi_codegen -c Release -- . prints abi_codegen: 44 entries, exit 0. Second run: git diff on NativeApi.g.cs unchanged (idempotent).
  • TDD red: pre-test-edit dotnet run --project managed/Engine.Tests showed AbiLayoutTests: 2 FAILED (minor==1, ApiTable 312B) against the new 360B table. Applied 4 AbiLayoutTests edits (minor 1->2, ApiTable 312->360B + component_register @312 + systems_run @352 + ChunkColumns 120B, columns offsets 96/104/108/112, system_register Cdecl assertion). Post-edit: AbiLayoutTests: all checks passed, grep -c FAIL = 0.
  • grep -c SuppressGCTransition NativeApi.g.cs = 3 (unchanged); systems_run entry is plain Cdecl; Size = 120 appears once.

M6 Ecs helpers (ILLU-M6)

  • managed/Engine.Runtime/Ecs.cs created verbatim from docs/IMPLEMENTATION_PROMPT.md:1919-2067 doc fence (programmatic fence diff: VERBATIM, 5043 bytes).
  • dotnet build managed/Engine.Runtime -c Release: Build succeeded. 0 Warning(s) 0 Error(s).
  • TDD spike build/spikes/ecs6/ (git-ignored, DELETED after run): called Ecs.ComponentId<Mover> twice on a live EngineWorldCreate world — GREEN (same non-zero id). Note: ApiLoader.Load() resolves engine_core by SONAME only, so the spike preloaded /home/hermes/game-engine/build/ci-linux/native/libengine_core.so via NativeLibrary.Load(absolute) and bound engine_get_api_table by export instead of using ApiLoader.Load().
  • dotnet build Engine.slnx -c Release --nologo: Build succeeded. 0 Warning(s) 0 Error(s).

M7A generator Register emission (ILLU-M7A)

  • Replaced managed/Engine.Analyzers/ComponentSystemGenerator.cs per docs/IMPLEMENTATION_PROMPT.md:2068-2821 up to and including Register emission: doc-verbatim header/pipeline/discovery/resolution/EmitOne/EmitSizeRegistration/SystemName, plus doc-verbatim ComponentParams/RowArgs/IsRunnable/IsFloatScalar helpers. EmitChunkRunner is an inert M7A stub with the stable ABI 4.2 signature (RunChunk(EngineChunkColumns*, float dt)); the span-unpacking/row-loop/try-catch body is owned by ILLU-M7B (child card t_d8676106).
  • Ecs.cs note: ILLU-M6's commit (1078062) exists only on its dispatch branch, not on feat/illusion-layer-abi-4.2 in this worktree, so the M7A spike build borrowed that file uncommitted for compilation only. It is NOT staged in the M7A commit — M6's merge will supply it. (Flagging for the orchestrator: M6 shows complete but its file is unreachable from the feat branch.)
  • Acceptance: /tmp/m7a_spike (cs1503 sources retargeted at this worktree's managed/ projects): Build succeeded. 0 Warning(s) 0 Error(s) — CS1503 gone. The main-repo build/spikes/cs1503 still fails because it references main-repo managed/ (old generator + missing 4.2 bindings there); that is a spike-path artifact, not a generator defect.
  • Emitted Chunk_Spike_Mover.g.cs: ComponentSize, Faulted, and Register blocks byte-match the §4.6 reference modulo whitespace (incl. table->EngineSystemRegister(world, name, ids, access, 2u, fn, userData) and Spike.Mover\0 name); RunChunk signature matches, body inert by design (_ = view; _ = dt;) — full body is M7B's problem.

M7B RunChunk emission (ILLU-M7B)

  • Replaced the M7A inert EmitChunkRunner stub with the doc body (IMPLEMENTATION_PROMPT.md:2679-2732): unrunnable guard keeps _ = view; _ = dt;, runnable path emits count/TermCount/s_faulted guard, per-term Span/ReadOnlySpan unpacking (col0 self type via selfFqn), try/for row loop col0[i].Tick(...) with RowArgs, catch (Exception) latching Volatile.Write(ref s_faulted, 1).
  • managed/Engine.Analyzers/ComponentSystemGenerator.cs is byte-identical to the doc fence (2076..2809); dotnet build Engine.slnx -c Release: Build succeeded. 0 Warning(s) 0 Error(s) (CS0649 gone — latch now written).
  • dotnet build build/spikes/cs1503 (copied into this worktree so refs resolve): Build succeeded. 0 Warning(s) 0 Error(s). Emitted Chunk_Spike_Mover.g.cs is byte-identical to the §4.6 reference (:154-235) modulo the Roslyn BOM; contains table->EngineSystemRegister(world, name, ids, access, 2u, fn, userData) and col0[i].Tick(ref col0[i], in col1[i], dt);.

M8 driver test (ILLU-M8)

  • Created managed/Engine.Tests/GeneratorDriverTests.cs verbatim from docs/IMPLEMENTATION_PROMPT.md:2830-3081 doc fence (10077 bytes); registered in managed/Engine.Tests/Program.cs per the :3086-3108 edit block (GeneratorDriverTests.Run() after GeneratorTests.Run()).
  • Driver passes the same CSharpParseOptions(LanguageVersion.Preview) to both the input tree and CSharpGeneratorDriver.Create (Facts :133 pitfall — avoids Inconsistent language versions).
  • dotnet build Engine.slnx -c Release: Build succeeded. 0 Warning(s) 0 Error(s). dotnet run --project managed/Engine.Tests -c Release --no-build: GeneratorDriverTests: all checks passed, no FAIL.
  • Sabotage check (? "in dt" : "dt" → ? "in dt" : "0f", rebuild, rerun): exact doc-expected 4 lines — [FAIL] Mover row call forwards in Health + dt, [FAIL] Mover RunChunk: X += Health*dt, Y += Speed*dt for 4 rows, [FAIL] Mover RunChunk ignores a view with the wrong term count, GeneratorDriverTests: 3 FAILED. Reverted + rebuilt green.
  • Worktree note: this worktree's branch was stale at M5B (M6/M7A/M7B lived on sibling dispatch branches); fast-forwarded with git merge --ff-only efd55cb (merge-base confirmed HEAD was ancestor) per the reuse-feat/illusion-layer-abi-4.2 convention before writing the test.

M9 sample + CTest gate (ILLU-M9)

  • Created samples/IllusionSample/ verbatim from docs/IMPLEMENTATION_PROMPT.md:3139-3443 fences (csproj 1442B, Components.cs 1887B, Program.cs 7836B); applied both doc edits (Engine.slnx IllusionSample line, native/tests/CMakeLists.txt engine_illusion_sample gate with PASS_REGULAR_EXPRESSION "ILLUSION-SAMPLE-OK").
  • Worktree note: this worktree's branch was stale at M5B; merged M6/M7A/M7B/M8 branch (game-engine/t_4cce9280-illu-m8-generator-driver-test, HEAD 61e99f6) into this worktree before writing files, per the reuse-feat/illusion-layer-abi-4.2 convention.
  • dotnet build Engine.slnx -c Release: Build succeeded. 0 Warning(s) 0 Error(s). Direct run dotnet samples/IllusionSample/bin/Release/net10.0/IllusionSample.dll <libengine_core.so>: all 21 checks [PASS], 0 managed bytes over 100 frames x 100k rows (got 0), [info] 100k Mover rows: 0.176 ms/frame, ILLUSION-SAMPLE-OK, exit 0.
  • ctest --preset ci-linux -R engine_illusion_sample --output-on-failure -V: ILLUSION-SAMPLE-OK, 100% tests passed, 0 tests failed out of 2. Full ctest --preset ci-linux: 100% tests passed, 0 tests failed out of 42.

M10/M11 repair verification (orchestrator, feat tip 7f6ac55)

  • Repaired 2026-10-30: archived 3 malformed auto-decomposed cards (no workspace_path, spawn-fail loops); cherry-picked the M6-M9 line onto feat (6a265a3 d70a3c6 ac6a04e f09dd63 7f6ac55); origin/main restored to 3112291 after an accidental push of ILLU commits (all work preserved on feat branch, pushed).
  • M10 AGENTS.md edits NOT applied — approval-protected path, needs operator (see M11b report below for exact text).
  • M11b full matrix from clean tree (logs build/logs/m11b-*.log), feat tip 7f6ac55:
    • cmake cfg=0, build=0, 0 warning/error lines; ctest 42/42 passed.
    • dotnet format --verify-no-changes: format=0; dotnet build slnx: 0; Engine.Tests exit 0, 0 FAIL lines, 10 suites all checks passed.
    • codegen 44 entries, re-run diff clean (codegen-clean=0).
    • Matches M0 baseline (no new failures; SDK 10.0.403 ships NativeAOT ILCompiler so export tests pass).
  • Full M11 evidence (decisions table, spec check) lives in the t_56fafa04 worktree note (de148b8); this entry confirms the feat tip reproduces it.

PR (ILLU-PR)

  • End-to-end PR: https://github.com/Heretek-AI/game-engine/pull/52 — [Illusion] ABI 4.2 end-to-end (M0-M11), base main, head feat/illusion-layer-abi-4.2 (15 commits M0-M11 + M10 AGENTS.md fix).
  • Body cites spec Verification + Decisions log, states all 10 decisions followed. Gates pending at open (CI running); do NOT merge — to qa-gate final review.
  • Merged 2026-10-30 as eabfb74; ILLU-FOLLOW proceeds on rebased main.

ILLU-FOLLOW portability + format pass (ILLU-FOLLOW, pre-merge prep; rebased onto main eabfb74)

  • Precondition met 2026-10-30: PR #52 merged as eabfb74; prep commit rebased onto new main.
  • Merge-independent work done on feat base (worktree t_28b5f4dd):
    • §4.8 item 1 audit: generic_systems.cpp passes clang strict -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wshadow -Wundef -Werror (macOS-analog flags) clean; full cmake --preset ci-linux build with ENGINE_WERROR=ON green, engine_generic_systems + engine_abi_layout 2/2 PASS.
    • One hardening committed: register_system name copy now checks snprintf return and re-pins NUL on truncation (explicit for -Wformat-truncation/MSVC /W4 readers; behavior unchanged — snprintf already NUL-terminates). No warnings disabled, WERROR untouched.
    • §4.8 item 4: dotnet format Engine.slnx --verify-no-changes clean on pinned SDK 10.0.403 (global.json 10.0.100 + rollForward latestFeature; CI setup-dotnet 10.0.x) — "Formatted 0 of 99 files".
    • CI-matrix (Windows/macOS), Sonar, CodeRabbit, follow-up PR: unblocked by merge; in progress on rebased main.

ILLU-FOLLOW findings on rebased main (post-merge, same branch)

  • CI matrix on merge commit eabfb74: ALL green at merge time — native + managed on ubuntu-24.04, macos-15, windows-2025 (run 41124983149 success). CodeRabbit's flagged MSVC C4244 (qd.terms[i].inout narrowing) was already fixed pre-merge by 6f4abe3 (static_cast<int16_t>); no live Windows/macOS failure remains. Same for the IsComponent-gate thread — fixed by 6f4abe3.
  • Remaining CodeRabbit thread (Minor, still valid on rebased main): CheckBomb in GeneratorDriverTests reads stackalloc int[3] without init — fixed here with new Span<int>(ticks, 3).Clear() (commit acb7e02), behavior unchanged, tests green.
  • Extra-strict clang audit on rebased main (compile_commands.json flags + -Wimplicit-int-conversion -Wshorten-64-to-32 -Werror -fsyntax-only): generic_systems.cpp and engine_abi.cpp both clean. No strncpy, no warning suppressions (/wd, -Wno-, pragmas) in the new code; WERROR (/WX/-Werror via ENGINE_WERROR=ON) untouched.
  • No Sonar integration in this repo: no sonar properties/config, no Sonar workflow, no Sonar findings on PR #52. Nothing to resolve — recorded as verified-absent.
  • Linux verification on rebased branch: cmake --preset ci-linux build clean, ctest 42/42 PASS; dotnet format --verify-no-changes clean; dotnet build 0 errors; Engine.Tests 10/10 suites pass (289 [PASS] lines, incl. GeneratorDriverTests with the fix).
  • §4.8 items 2-3 (NativeAOT with generated systems, >16-byte alignment): NOT touched — out of scope as risks, no evidence they surfaced.

ILLU-FOLLOW-4: Sonar S6494 fix on PR #53 (snprintf-free CopyName)

  • SonarCloud on PR #53 reported exactly ONE finding: cpp:S6494 MINOR "Use std::format or std::format_to_n instead of snprintf" at generic_systems.cpp:184 (the return-checked snprintf hardening) — Quality Gate B vs required A. Earlier "Sonar verified-absent" note was wrong for the follow-up branch: SonarCloud IS integrated at org level and analyzes PRs without repo-side config.
  • Fix (branch game-engine/t_28b5f4dd-illu-follow-windows-macos-sonar-pass): replaced both snprintf copies with a bounded CopyName template (strlen + min(len, N-1) + memcpy + NUL; (std::min) parens for the Windows min macro). Applied in register_system (generic_systems.cpp) and ApiRegisterSystem (engine_abi.cpp — that site was missing from PR #53). Includes: <algorithm>/<cstddef> added, unused <cstdio> dropped from both TUs. Behavior unchanged (clamped NUL-terminated copy, same as snprintf "%s"). No suppressions, WERROR untouched.
  • Verified Linux: cmake --preset ci-linux build clean, ctest 42/42 PASS, extra-strict clang (-Wimplicit-int-conversion -Wshorten-64-to-32 -Werror -fsyntax-only over compile_commands flags) CLEAN on both files, dotnet format --verify-no-changes clean.
  • CodeRabbit on PR #53: "No actionable comments" (latest review), 0 inline threads. Pushed to PR #53 head; CI + Sonar + CodeRabbit re-run on push.
  • §4.8 items 2-3: not surfaced, untouched.

ILLU-FOLLOW-4b: S5813 + duplication fix (shared header)

  • Push of the S6494 fix closed S6494 but Sonar opened 3 new items on the new code: cpp:S5813 MAJOR x2 ("make sure use of strlen is safe" — one per TU, since strlen on an unbounded caller pointer can over-read before the clamp) + 76.3% duplication on new code (the helper was pasted into both TUs; gate requires <=3%).
  • Fix: single shared native/bridge/src/name_copy.hpp with CopySystemName — bounded scan via memchr(src, 0, N-1) so at most N-1 bytes are ever read (no pre-scan over unbounded input), then memcpy + NUL. Both call sites use it; no strlen/snprintf remain in the new code, one definition kills the duplication. engine_abi.cpp calls it qualified (engine::bridge::) since that TU is in global scope.
  • Verified: WERROR build clean, ctest 42/42 PASS, extra-strict clang audit CLEAN on both TUs, standalone unit check of the helper (empty/nullptr/exact-fit/63/64/300-char/tiny-buffer) PASS. Pushed to PR #53 head; Sonar re-runs on push.

ILLU-FOLLOW-4c: all gates green on PR #53

  • Final state (head 2cd64d1): Sonar PASS (0 open issues; S6494 closed, S5813 x2 resolved by memchr-bounded scan, duplication cleared by shared header), CodeRabbit "no actionable comments" + 0 inline threads, CI native+managed PASS on ubuntu-24.04/macos-15/windows-2025, Analyze C++ PASS, CodeQL PASS, supply-chain PASS, merge state CLEAN/MERGEABLE.
  • Local: WERROR build clean, ctest 42/42, extra-strict clang audit CLEAN, dotnet format --verify-no-changes clean, git status clean.
  • PR #53 [Illusion] Windows/macOS + Sonar follow-up was opened by sibling task t_28b5f4dd (done/reviewed); this card's implementation-phase work landed on the same branch (commits dcff5be engine_abi parity, a3f3181 S6494 fix, fbeffab + this NOTES entry). No duplicate PR opened. §4.8 items 2-3 never surfaced.

ILLU-FOLLOW-4d: CodeRabbit doc-contract fix (memchr safety claim)

  • CodeRabbit's first real review of PR #53 (after OSS rate-limit reset; @coderabbitai review retried x4 until "Review triggered" then "Review finished") posted 1 Minor thread: name_copy.hpp header comment claimed unterminated input "can never drive an over-read", but memchr(src,0,N-1) itself reads up to N-1 bytes — a shorter unterminated buffer would over-read. Valid finding, doc-only fix: header + function comments now state the contract (src must be NUL-terminated within its readable range or expose >= N-1 readable bytes; shorter unterminated input needs an explicit length this helper doesn't take). No behavior change; both call sites pass valid C strings.
  • qa-gate round 1 verdict: code correct, Sonar green, but CI matrix was still in_progress at reviewed HEAD 6adfcf3 and CodeRabbit had never run (rate-limit). Re-review requires CI green at final HEAD + CodeRabbit run with threads resolved.

PARITY P1-01 CI GPU lane (P1-01, 2026-10-31)

  • Added native/tests/gpu_required.hpp (spec-verbatim gpu_missing); all four GPU tests now route no-device / no-Vulkan / SDK-absent / capability-fail paths through it (failures += gpu_missing(...)), so ENGINE_REQUIRE_GPU=1 turns them into [FAIL] + nonzero exit. ENGINE_TEST_SLANGC-unset skip in test_slang_pbr.cpp intentionally left as [SKIP] (optional tooling per spec).
  • CI: Linux apt list now installs the full §3.1 list (libvulkan-dev, mesa-vulkan-drivers, vulkan-tools, glslang-tools, X11/audio/dbus/udev dev packages); SDL_VIDEO_DRIVER=offscreen + ENGINE_REQUIRE_GPU=1 set on the Test step via runner-gated expressions (Linux-only, so macOS/Windows keep stock env).
  • Verified locally (mesa-vulkan-drivers present): SDL_VIDEO_DRIVER=offscreen ENGINE_REQUIRE_GPU=1 engine_renderer_bootstrap prints gpu: llvmpipe (LLVM 19.1.7, 256 bits) api=1.4.305, exit 0, zero [SKIP] lines; indirect/slang_pbr/gi_zero_bake all exit 0 with GPU paths [PASS] (only remaining SKIP is the intentional slangc-unset one). Acceptance probe ENGINE_REQUIRE_GPU=1 VK_ICD_FILENAMES=/nonexistent engine_gi_zero_bake prints [FAIL] gi-gpu: no physical device and exits 1 (test_gi_zero_bake: 1 FAILED).
  • Configure message confirmed: renderer: Vulkan SDK headers found — real volk/VMA path.

P0-01 fnptr reflection (parity/p0-01-fnptr-reflection, 2026-10-30)

  • Fix: managed/Engine.Tests/AbiLayoutTests.cs:96 — f.FieldType.IsPointer → f.FieldType.IsFunctionPointer || f.FieldType.IsPointer (§4.8). For delegate* unmanaged[Cdecl]<int, void> F, IsPointer is false on .NET 10 (dotnet --version → 10.0.403); only IsFunctionPointer is true. Message text unchanged; NativeApi.g.cs untouched.
  • Verified the shipped test binary actually carries the fix: IL member list contains get_IsFunctionPointer only after the edit+rebuild (stashed pre-fix build lacks it); md5sum of bin DLL was stale-identical across rebuilds due to deterministic build, so used strings/IL-grep, not hash, to confirm.
  • Before (fix stashed): cmake -S . -B /tmp/oot-prefix-check ... && cmake --build ... && ctest -R "engine_compositor_live$" → AbiLayoutTests: 42 FAILED (all 42 ApiTable.* is fnptr lines [FAIL]), plus the live-probe DllNotFoundException lines.
  • After: same OOT command → AbiLayoutTests: all checks passed; remaining engine_compositor_live failure is only the live probe (CompositorApiTests: 1 FAILED, PhysicsApiTests: 1 FAILED, DllNotFoundException: Unable to load engine_core).
  • Root cause of residual (pre-existing, out of P0-01 scope): TryPreloadEngineCoreHandle (CompositorApiTests.cs:208, PhysicsApiTests PhysicsLivePreload) only probes staged CWD/appdir siblings after an upward walk finds Engine.slnx; under /tmp/oot/.../compositor_live the walk never reaches the repo so it returns IntPtr.Zero, and bare-SONAME NativeLibrary.TryLoad("engine_core") fails (verified false even with LD_LIBRARY_PATH set; absolute-path NativeLibrary.Load succeeds and exports engine_get_api_table). In-tree preset works because the walk finds the repo and then build/ci-linux/native. Gotcha #19 documents the OOT/in-tree difference.
  • In-tree acceptance (§3.2): cmake --preset ci-linux && cmake --build --preset ci-linux && SDL_VIDEO_DRIVER=offscreen ENGINE_REQUIRE_GPU=1 ctest --preset ci-linux → full suite green,incl. engine_compositor_live (AbiLayoutTests: all checks passed, [LIVE] compositor live path engaged, CompositorApiTests: all checks passed, PhysicsApiTests: all checks passed).

P2-01 native input module (PARITY-79, 2026-10-31)

  • Delta vs prompt: issue #79 is stricter — pins acceptance to SDL_PushEvent-injected W/mouse/gamepad checks and calls out the SDL_PushEvent surprise explicitly. Met the issue version.
  • Surprise confirmed in test: SDL_GetKeyboardState ignores pushed key-down (spike §4.6: keystate[W]=0 after push); InputState keeps its own std::array<uint8_t,512> fed only from SDL_EVENT_KEY_*. No SDL_GetKeyboardState anywhere in engine code.
  • SDL_PollEvent also yields SDL window events (spike: 3 events for 2 pushed); process_event filters strictly by type, default-ignores the rest; test drains blindly like renderer pump code.
  • Gamepad: opened on SDL_EVENT_GAMEPAD_ADDED via SDL_OpenGamepad; injected axis/button events for unknown joystick ids latch onto the first free slot so headless CI (no gamepads) still proves the normalisation path. Axis normaliser: -32768..32767 -> [-1,1] clamped.
  • Feed seam: RendererWindow::pump_events(quit_out, InputState*) overload feeds every polled event into the input state; renderer_frame (P1-09, lands later) calls this overload so no event bypasses the tables. Null sink == plain pump.
  • Sonar rules: no strlen/snprintf/memset/C arrays in new code (incl. test); numbers via std::to_chars N/A (no formatting needed); std::array/std::span-shaped storage.
  • Verified: full build clean (Clang 19; container GCC 14 fails pre-existing job_scheduler.cpp CPU_ISSET sign-conversion, unrelated), ctest -R engine_input -V all [PASS], engine_renderer_bootstrap still green.

P0-04: engine_export_publish_aot is environment-only (2026-10-31 decision)

  • Slice: P0-04 (issue #68). Question: is the local NETSDK1047: Assets file ... doesn't have a target for 'net10.0/linux-x64' failure a repo bug or an environment-only SDK defect?
  • Verdict: environment-only. On a fresh clone with the Microsoft .NET 10 SDK, the full §3.2 sequence passes, including all 22 engine_export* tests. No native/tests/CMakeLists.txt change needed; NOT adding the -r ${EXPORT_SAMPLE_RID} restore step.
  • Evidence (fresh clone fresh/, HEAD e763de6, Debian 13, SDK 10.0.403 at /usr/share/dotnet/sdk/10.0.403):
    • cmake --preset ci-linux → exit 0 (configure 4.3 s).
    • cmake --build --preset ci-linux → exit 0 (473/473 targets).
    • SDL_VIDEO_DRIVER=offscreen ENGINE_REQUIRE_GPU=1 ctest --preset ci-linux --output-on-failure → 100% tests passed, 0 tests failed out of 42, Total Test time 11.19 s.
    • ctest --preset ci-linux -R engine_export --output-on-failure → 100% tests passed, 0 tests failed out of 22, Total Test time 2.38 s; engine_export_publish_aot Passed in 1.66 s.
    • dotnet build Engine.slnx -c Release --nologo → exit 0; dotnet run --project managed/Engine.Tests -c Release --no-build → exit 0 (HotReloadTests: all checks passed).
  • Mechanism: the failing setup ran Ubuntu's source-built dotnet-sdk-10.0 10.0.114 (per issue #68), which lacks the Microsoft.NETCore.App.Runtime.NativeAOT.linux-x64 ILCompiler restore path — hence project.assets.json had no net10.0/linux-x64 target after a plain (RID-less) build, and the --no-build -r linux-x64 publish failed with NETSDK1047. With the Microsoft SDK, the publish step's implicit restore pulls runtime.linux-x64.microsoft.dotnet.ilcompiler 10.0.14 (present in ~/.nuget/packages) and ExportSample/obj/project.assets.json contains both net10.0 and net10.0/linux-x64 targets (12 linux-x64 references).
  • Follow-up: close issue #68 as not-planned with this PR. If a contributor must use the Ubuntu source-built SDK, the fix is a setup test running dotnet restore -r ${EXPORT_SAMPLE_RID} before the --no-build publish in native/tests/CMakeLists.txt (engine_export_publish_aot block) — deliberately not added here since CI (Microsoft SDK via actions/setup-dotnet) and this machine both pass without it.

P4-00 spike: visual fidelity facts (2026-10-31, branch parity/p4-00-spike-facts)

  • Spec: added §4.12 to docs/PARITY_IMPLEMENTATION_PROMPT.md (entry points + inputs for PbrPipeline/GiFrameGraph/GpuCull/SceneSsbo, slangc v2026.20 CI facts, lavapipe timestamp support, P4-01..12 file list). Docs-only; no slice code, no golden (harness defined by P1-05, not yet in tree).
  • PbrPipeline::create_gpu is a deliberate deferred stub (kInvalidArgument "deferred to compositor frame setup (M3-04)"); P4-06 wires vis->shade->post through the compositor, never calls it directly. PostPush is 20B (README "post 24B" line is a typo; header static_assert pins 20B).
  • slangc: NOT in apt; the P4-00 spike manually fetched the shader-slang GitHub release asset (CI does not download it today). Latest at spike: v2026.20 (2026-10-17). Linux x86-64 glibc-2.28 tarball sha256 84ebd777968caceea26b21bfd014341078be6047be6c5a963a69f984f896cebd. slangc -v prints bare 2026.20; --version is rejected (E00017). All 10 slang_build.py entry points compile to magic 07230203 / SPIR-V 1.5. slang_build.py --slangc refuses out-of-tree shader dirs (embed step requires in-repo paths).
  • lavapipe timestamp queries WORK at the driver level: timestampPeriod=1.0ns, 64 valid bits, monotonic TOP/BOTTOM_OF_PIPE results (spike: raw-Vulkan g++ -lvulkan program, SDL_VIDEO_DRIVER=offscreen). Engine scene-pass profiler integration (P3-09) remains unverified.
  • Evidence runs: engine_slang_pbr / engine_gi_zero_bake / engine_gpu_indirect all PASS on lavapipe (SDL_VIDEO_DRIVER=offscreen) from build/ci-linux binaries.
  • Prior P4-00 attempt (d5bbfac/d21040d on feat/parity-p4-fidelity, never merged) was stale: header comment for create_gpu changed since, and its §4.12 claimed the golden harness "already exists" when golden_compare.hpp is not in tree on main. This PR re-verifies everything against current main.
  • CodeRabbit round 1 (5 inline threads, all valid): destroy() runs BEFORE device shutdown (test destroys gi while dev alive); GpuCull create_gpu is sentinel marks, not VMA allocation — doc now names both counter buffers (CullPush::count_addr vs HizPush::draw_count_addr); second compute-to-DRAW_INDIRECT barrier in P4-06 frame order; slangc is a manual spike fetch, not CI; timestamp verdict scoped to driver-level probe.

P4-01: glTF metallic-roughness materials (2026-10-31, branch parity/p4-01-pbr-materials)

  • Material asset + component: native/renderer/include/engine/renderer/pbr_material.hpp (PbrMaterialAsset, MaterialComponent 64B blittable twin, PbrMaterialLibrary slot allocator, to_scene_record() packing the exact 64B SceneMaterial field order with bit-exact u32 lanes, PbrValidateMaterial() fail-closed on NaN/Inf/blend+mask).
  • cgltf import: material_import.hpp/.cpp (PbrImportMaterials/PbrImportMaterial, sRGB->linear factor decode, emissive_strength via the KHR_materials_emissive_strength EXTENSION object — not a bare emissiveStrength key, which cgltf silently ignores).
  • Shade/post stages move from slang to GLSL (pbr_shade.vert/frag, post_composite.vert/frag, glslangValidator --target-env vulkan1.3, blobs re-embedded into the same *_spv.inc names): slangc 2026.20 emits physical-addressing SPIR-V that lavapipe's spirv-val rejects (VUID-08737 OpStore not a logical pointer) — even the UNMODIFIED M3-03 blobs recompiled with 2026.20 fail, while glslang output passes clean. Visibility pass stays slang. P4-00's §4.12 slangc facts stand (version/sha/entry points); this adds the lavapipe-consumability caveat.
  • Bindless-set-0 reality check (validation layers): set 0 carries ONLY scene/material SSBOs + texture/sampler arrays — no lights/grid/index. Light SSBOs + visibility image ride the test-owned set 1 (shade set-1: vis STORAGE_IMAGE[0], albedo COMBINED array[2], lights/grid/index SSBOs[3/4/5]). glslang maps texture-only usampler2D decls to SAMPLED_IMAGE (not COMBINED) — storage-image r32ui readonly uimage2D keeps the type exact.
  • Root-cause gotcha (worth a §7 line): the SceneInstance ids row is UINT32 — declaring the record as vec4[] in GLSL reinterprets small ints as denormal floats, and uint(denormal) is 0, silently pinning every instance to material 0 (image rendered but flat/wrong). Fix: uvec4 sceneRows[] + uintBitsToFloat for float lanes. Debugged via push-flag visualization (metallic/vis.x/row5.y/row4.w probes) + upload verification by mapping the staging buffers after submit.
  • Golden: native/tests/golden/p4_01_spheres_320x240.png (5x5 grid, metallic x 0..1, roughness y 0.04..1.0, one point light, shade->post offscreen + readback) via new native/tests/golden_compare.hpp + golden_stb.cpp (P1-05 contract: ch±2, 0.1% pixels, ENGINE_UPDATE_GOLDENS=1 overwrites, actual/diff PNGs on failure). Vulkan NDC is already Y-down: the fullscreen-triangle vertex shaders pass pos through (no Y negation — a -pos.y flip samples the HDR target upside-down) so vUv 0 is top-left, matching readback rows.
  • Evidence: ctest --preset ci-linux 44/44 pass with SDL_VIDEO_DRIVER=offscreen ENGINE_REQUIRE_GPU=1; engine_pbr_materials + engine_pbr_golden all PASS incl. [PASS] golden: p4_01_spheres_320x240 and the metallic-varies sanity check; managed Engine.Tests pass; slang_build.py untouched (slang sources it compiles — vis/gi/shadow — are unchanged).
  • Replaced the find_package/pkg-config SDL3 block in native/renderer/CMakeLists.txt with the spec §4.2 FetchContent block + URL_HASH SHA256=c4b08b950bd29d83caae0ab8d884298d27046a9d30700fb42841c50992665c90 (computed via curl -sSL -o /tmp/dep.tar.gz <URL> + cmake -E sha256sum). Exact §4.2 variable set (no extra SDL_TESTS): SDL_SHARED/OFF, SDL_STATIC/ON, SDL_TEST_LIBRARY/OFF, SDL_EXAMPLES/OFF, SDL_INSTALL/OFF, link SDL3::SDL3-static, define ENGINE_RENDERER_HAS_SDL3=1, configure message verbatim.
  • deps/README.md: added SDL3 pin row (tag + tarball URL + hash + static-link note). Created THIRD_PARTY_NOTICES.md (first dependency PR owns it) with the Zlib license text from the release-3.4.18 tarball LICENSE.txt.
  • native/tests/CMakeLists.txt: engine_renderer_bootstrap now carries ENVIRONMENT "SDL_VIDEO_DRIVER=offscreen" so the real window path is exercised even when the developer forgets the variable; ctest --preset ci-linux -R engine_renderer_bootstrap -V prints [PASS] window: valid + extent (bare run without the driver still passes via the old create fails only for missing video device branch — that CTest property is what closes the §4.2 hard-rule gap).
  • Surprise (spec §4.2 predicted, confirmed live): SDL configure hard-fails on X11-extension dev gaps — this box (Debian 13, not the §3.1 Ubuntu image) failed first on XTEST (Couldn't find dependency package for XTEST), fixed by sudo apt-get install -y libxtst-dev. Also installed libxcursor-dev per the §4.2 surprise. No CMake change needed: spec uses dynamic X11 (Video drivers: dummy kmsdrm(dynamic) offscreen wayland(dynamic) x11(dynamic)), so missing -dev packages fail configure rather than degrade silently.
  • Verified: configure prints renderer: SDL3 release-3.4.18 (static) — real window path; SDL_VIDEO_DRIVER=offscreen engine_renderer_bootstrap prints [PASS] window: valid + extent and full RENDERER_BOOTSTRAP_OK, exit 0; SDL_VIDEO_DRIVER=offscreen ENGINE_REQUIRE_GPU=1 ctest --preset ci-linux 42/42 PASS (8.29 s).

PARITY P1-03 Enable VK_KHR_swapchain in VulkanDevice (P1-03, 2026-10-31)

  • DeviceCreateInfo::enable_swapchain added; when true, init() probes vkEnumerateDeviceExtensionProperties on the CHOSEN physical device and fails kDeviceCreateFailed + exact "VK_KHR_swapchain missing" when absent, else passes the ext in dc.enabledExtensionCount/ppEnabledExtensionNames. supports_present(VkSurfaceKHR) wraps vkGetPhysicalDeviceSurfaceSupportKHR(physical_, graphics_family_, ...) (false on null/uninitialized).
  • Test: new test_vulkan_swapchain_present in test_renderer_bootstrap.cpp — offscreen window, SDL required_instance_extensions as extra_instance_extensions, init with enable_swapchain=true, asserts dev.proc("vkCreateSwapchainKHR") != nullptr ([PASS] device: swapchain ext enabled), create_surface + supports_present ([PASS] device: queue supports present). No-device / no-video paths route through gpu_missing so ENGINE_REQUIRE_GPU=1 fails instead of skipping.
  • Verified: SDL_VIDEO_DRIVER=offscreen ENGINE_REQUIRE_GPU=1 engine_renderer_bootstrap prints both acceptance lines + RENDERER_BOOTSTRAP_OK, exit 0 (llvmpipe 1.4.305); same with VK_ICD_FILENAMES=/nonexistent prints [FAIL] lines, exit 1; full suite SDL_VIDEO_DRIVER=offscreen ENGINE_REQUIRE_GPU=1 ctest -j4 42/42 PASS (one parallel run showed a compositor_live_stage_native_link symlink race that passes on retry — pre-existing fixture staging, unrelated to this change).

P4-02: HDR emissive + exposure tonemap (2026-10-31, branch parity/p4-02-hdr-tonemap)

  • Gap found: both the slang (pbr_shade.slang) and GLSL (pbr_shade.frag) shade passes fetched the SceneMaterial record but never read row 3 (emissive) — HDR values > 1 could never reach the R16G16B16A16_SFLOAT target. Fix: acc + emissive in the GLSL frag, blob regen'd with glslangValidator -V --target-env vulkan1.3 (3190 -> 3238 words, magic 07230203 ver 00010600) via embed_spv.py + mirrored into include/engine/renderer/shaders/. Slang source left untouched (P4-06 frame wiring owns its replacement; slangc-2026.20 blobs are lavapipe-unconsumable per P4-01).
  • New: native/renderer/include/engine/renderer/hdr_camera.hpp (HdrExposureFromEv = std::exp2, HdrCameraComponent 16B blittable, HdrTonemapCpu mirror of post_composite.frag). Post pass already did hdr * exposure -> ACES -> sRGB, so no post change; the camera EV rides PostPush.exposure.
  • Math check: ACES(0.5) ~ 0.616 -> sRGB ~ 0.807 (S-curve lifts mid-tones; the first hdr: grey mid-range bound of 0.3-0.7 was wrong, fixed to 0.79-0.82 after a Python spot-check against the Narkowicz fit).
  • New test engine_hdr_tonemap (test_hdr_tonemap.cpp + shared golden_vk_helper.hpp loader/alloc/image/barrier/submit helpers so the third Vulkan TU does not trip Sonar duplication): left half emissive (10,2,1), right half diffuse grey under a zero-intensity light (emissive-only probe). EV-3 post. Acceptance: golden p4_02_hdr_320x240.png + no white clip (r,g,b < 250) + emissive survives (r > 150) + GPU-vs-HdrTonemapCpu within 4/255.
  • Golden size is 3935 bytes (flat halves compress well — expected, not a blank image; the numeric probes prove content).
  • Evidence: ctest --preset ci-linux 45/45 pass with SDL_VIDEO_DRIVER=offscreen ENGINE_REQUIRE_GPU=1 (44 old + new hdr_tonemap).
  • HDR scene-target contract (2026-10-31, commits cb6a744 device helper + 58380e1 constants): shade writes and post samples the SAME float target via engine constants kHdrSceneFormat (= VK_FORMAT_R16G16B16A16_SFLOAT, TU-asserted)
    • kHdrSceneUsage (COLOR_ATTACHMENT | SAMPLED, TU-asserted) in pbr_pipeline.hpp (stub stand-ins incl. value 91 in render_graph.hpp); test_hdr_tonemap.cpp consumes both constants; swapchain stays R8G8B8A8_UNORM LDR. Re-verified 2026-10-31: engine_hdr_tonemap all PASS (offscreen + ENGINE_REQUIRE_GPU=1) incl. golden p4_02_hdr_320x240, no-clip, CPU/GPU parity; full ctest --preset ci-linux 45/45 green.
  • Test hygiene (matches P1-05/P1-01 contract): golden via golden_compare.hpp (ch±2, 0.1% pixels, RGBA with BGRA-swizzle handling); GPU tests use device_hint="llvmpipe" through GoldenOpenDevice and report [FAIL] not [SKIP] under ENGINE_REQUIRE_GPU=1 via the gpu_missing guard; no window is created (pure offscreen render, CI runs headless).
  • Open gates (need human): (1) Sonar duplication gate red — golden_vk_helper.hpp mirrors test_pbr_golden.cpp scaffolding until that P4-01-owned TU migrates onto the helper (flagged hotspot; P4-03 will also touch that file, needs orchestrator sequencing); (2) manual real-GPU check — this box is lavapipe only, no discrete GPU available.
  • P4-02 Sonar dedup + real-GPU update (2026-10-31, branch parity/p4-02-hdr-tonemap): test_pbr_golden.cpp + test_pbr_materials.cpp migrated onto golden_vk_helper.hpp (GoldenVkLoad/AllocHost/MakeImage/Module/Barrier/SubmitWait/BuildShadePost/ AllocWriteFrameSets/RecordFrame + shared gpu_missing + scene/grid/material/ frame/readback/pixel-map helpers); net -672/+193 lines on the P4-01 TU. Sonar duplication gate GREEN (0.0%). RADV descriptor-pool fix: set-0 bindless 4096 sampled-image array costs 2x pool on RADV (pool 8192/32; lavapipe accepts oversize). Real-GPU evidence: VK_ICD_FILENAMES=radeon_icd.json ENGINE_REQUIRE_GPU=1 engine_hdr_tonemap ALL PASS on AMD Radeon Graphics (RADV GFX1151) incl. golden p4_02_hdr_320x240 + no-white-clip + CPU parity. (P4-01 p4_01_spheres golden diverges on RADV — pre-existing shade behavior delta, llvmpipe golden unchanged ALL PASS; P4-01 TU untouched by this slice beyond helper migration.) ctest 45/45 green (llvmpipe).
  • P4-02 remaining gate (needs human): Sonar maintainability B from ONE stuck S3776 on test_hdr_tonemap.cpp (complexity 32 vs 25, key AaGRAA3dXbG6mhdFCEMz). The anchor (:264) has pointed at 3+ different constructs across 10 extraction pushes without the count ever moving — stale/cached issue, not live code. test_gpu is 7 ifs + helpers; S5817 const notes fixed along the way. Please confirm in the Sonar UI or admin-close the stuck issue.

PARITY P1-06 First triangle through the forward renderer (P1-06, 2026-10-31)

  • New: native/renderer/include/engine/renderer/forward_renderer.hpp + src/forward_renderer.cpp (ForwardRenderer::create(VulkanDevice&, color, depth=UNDEFINED, vert/frag blob overrides) + record_triangle(cmd, view, extent); dynamic rendering, no vertex input, dynamic viewport/scissor, no culling/depth/blend; SDK-free stubs; destroy() safe after device shutdown). create matches the spec slice signature create(VulkanDevice&, VkFormat color, VkFormat depth) — depth is fail-closed today (only UNDEFINED; D32_SFLOAT arrives in P1-07), covered by a dedicated negative gate.
  • Shaders: native/renderer/shaders/tri.vert (constant position/color arrays indexed by gl_VertexIndex: top red, bottom-left green, bottom-right blue) + tri.frag (passthrough), compiled offline glslangValidator -V --target-env vulkan1.3 (vert 360 words, frag 124 words) and embedded via embed_spv.py. Verified: re-embedding the freshly compiled .spv produces WORDS_IDENTICAL arrays to the checked-in .inc. Checked in at both native/renderer/shaders/tri_*_spv.inc (spec §5 path) and native/renderer/include/engine/renderer/shaders/tri_*_spv.inc (renderer include path used by the TU); both files are byte-identical.
  • Test: native/tests/test_forward_triangle.cpp (engine_forward_triangle, offscreen 320x240) — bad format / bad blob / depth-format / double-build / null-record negative gates, one frame via record_triangle + capture, golden_check("triangle_320x240") folded into failures; golden checked in at native/tests/golden/triangle_320x240.png (llvmpipe-rendered RGB triangle on black).
  • Verified: SDL_VIDEO_DRIVER=offscreen ENGINE_REQUIRE_GPU=1 engine_forward_triangle prints [PASS] golden: triangle_320x240 + FORWARD_TRIANGLE_OK, exit 0; full suite SDL_VIDEO_DRIVER=offscreen ENGINE_REQUIRE_GPU=1 ctest --preset ci-linux -j4 44/44 PASS.

PARITY P1-07 Mesh, camera, depth and one light (P1-07, 2026-10-31)

  • New: native/renderer/include/engine/renderer/gpu_mesh.hpp + src/gpu_mesh.cpp (GpuMesh: 32-byte MeshVertex pos/normal/uv + u32 indices via EngineVmaCreateMeshBuffer host-visible upload; DepthBuffer: D32_SFLOAT-only image + view), vma_bridge.hpp/vma_impl.cpp gains (MeshBufferBundle/DepthImageBundle, CreateMeshBuffer/CreateDepthImage/DestroyBuffer/DestroyImage), ForwardRenderer gains create_mesh (D32_SFLOAT-only, 160-byte push layout, depth LESS, no cull) + record_mesh + begin/continue/end_mesh_pass (CLEAR then LOAD).
  • Shaders: native/renderer/shaders/mesh.vert (MVP transform, world normal) + mesh.frag (Lambert max(dot(n,-light_dir),0)*0.9+0.1, sRGB encode), compiled offline glslangValidator -V --target-env vulkan1.3 and embedded via embed_spv.py (checked in at both shader paths, byte-identical).
  • Test: native/tests/test_forward_mesh.cpp (engine_forward_mesh, offscreen 320x240) — negative gates (record/pass before build, non-D32 rejected, bad blob, zero extent, null upload), camera eye(2,2,3)/60deg/Vulkan clip space, unit cube 24v/36i, back cube offset along view ray in LOAD pass; golden_check("cube_lit_320x240") + memcmp depth proof.
  • Surprises (both mine, in test math not renderer code): (1) perspective matrix had m[10]=far/(far-near), m[11]=+1 which puts w<0 for all forward-z and clips everything — golden came out solid black. Correct Vulkan form is m[10]=far/(near-far), m[11]=-1 (verified all 8 cube corners inside clip). (2) light_dir sign: shader uses dot(n,-light_dir) so light_dir is the direction light TRAVELS; my initial (0.5,0.9,0.65)-normalized value lit only -X/-Y/-Z faces (all camera-facing faces ambient-only, flat brown cube). Negated so +X/+Y/+Z faces shade 0.47/0.77/0.58.
  • Verified: SDL_VIDEO_DRIVER=offscreen ENGINE_REQUIRE_GPU=1 engine_forward_mesh prints [PASS] golden: cube_lit_320x240 + [PASS] mesh: depth test hides back faces + FORWARD_MESH_OK, exit 0; golden visually confirmed (tan cube, 3 shaded faces, black bg); full suite SDL_VIDEO_DRIVER=offscreen ENGINE_REQUIRE_GPU=1 ctest --preset ci-linux -j4 45/45 PASS.

PARITY P1-08 Draw ECS entities (P1-08, 2026-10-31)

  • New: native/ecs/include/engine/ecs/ecs_render_components.hpp + src/ecs_render_components.cpp (Camera 16B / MeshRenderer 20B / DirectionalLight 16B, align 4, static_asserts; RegisterRenderComponent idempotent lookup-or-init with sep="" flat names + layout-mismatch 0 instead of Flecs abort). EcsWorld::register_render() registers all three under Engine.Runtime.*. GenericSystems::register_component now delegates to the same function (single implementation, gotcha 20 can't drift).
  • New: native/renderer/include/engine/renderer/mesh_registry.hpp + src/mesh_registry.cpp (mesh_id = index+1, 0 reserved; find OOB/empty -> nullptr; noexcept, no try/catch — renderer OBJECT lib is -fno-exceptions). ForwardRenderer gains render_world(void* world, registry, cmd, out_view, out_stats) in new src/ecs_render.cpp (void* so the header stays flecs-free; stub in forward_renderer.cpp for SDK-less builds). Queries (WorldTransform, MeshRenderer), first is_main Camera + first DirectionalLight, per-draw record_mesh with memcpy model (NOT transposed) + view_proj.
  • Test: native/tests/test_ecs_render.cpp (engine_ecs_render, offscreen 640x360) — shared cube 24v/36i, 10x10x10 grid at 2-unit spacing (1000 entities), camera eye (27,27,44) -> centre (9,9,9), far 200; golden_check("grid_1000_640x360") + draws==1000 + move-entity-+3X/propagate memcmp-differ proof.
  • Surprises (mine, worth recording): (1) engine::renderer::ecs_world_t vs global ::ecs_world_t — the header's struct ecs_world_t; inside namespace engine::renderer declares a DIFFERENT incomplete type, so every flecs call failed overload resolution (cannot convert argument of incomplete type). Fix: render_world takes void* (header flecs-free, like audio_world.hpp), .cpp casts to the real ::ecs_world_t* after including flecs.h. (2) Transposing WorldTransform for the model matrix is WRONG: ComposeLocal writes translation at [12..14] which is already the column-vector translation column; a transpose moves it into the w-row (w'=xpx+ypy+z*pz+1) and collapses all 1000 cubes to sub-pixel mush behind a fullscreen flat tan field (first golden). Fix: memcpy flat bytes. Camera view DOES need the rigid inverse (RowWorldToColView) — eye (9,9,22) was also inside the grid (spans z 0..18), pulled back to (27,27,44). (3) volk-first include order applies to ecs_render.cpp too (volk.h before flecs.h is fine, but vulkan.h via flecs first would hard-error the same way as vulkan_device.cpp).
  • Verified: SDL_VIDEO_DRIVER=offscreen ENGINE_REQUIRE_GPU=1 engine_ecs_render prints [PASS] golden: grid_1000_640x360 + [PASS] ecs-render: 1000 draws + [PASS] ecs-render: transform change visible + ECS_RENDER_OK, exit 0; golden visually confirmed (tan voxel-cube lattice on black, individual cubelets distinguishable); full suite SDL_VIDEO_DRIVER=offscreen ENGINE_REQUIRE_GPU=1 ctest --preset ci-linux -j4 46/46 PASS. No ABI change (native-only slice; P1-09 adds renderer_create/Rendering.cs), no deps change.
  • Post-commit fix (same slice, amended): flaky SIGSEGV in engine_ecs_render under ctest/redirected stdout — ColMat (float[16], align 4) spilled/passed by value under renderer AVX2 flags emitted vmovaps; an under-aligned stack slot faulted intermittently (test TUs use base flags, so P1-07 math never tripped it). Fix: struct alignas(32) ColMat + byte-loop copies for the eye WorldTransform (ecs_get_id storage) and the per-draw model matrix (Flecs user-component columns align only to the component's own alignment, not the alignas(64) declaration — never assume 64B). Byte loops compile to vmovups, zero cost next to a draw. Verified: 20/20 redirected offscreen runs green post-fix (was intermittent crash before).

PARITY P1-09 Rendering from C# (P1-09, 2026-10-31)

  • ABI 4.4: appended renderer tail (renderer_create/destroy/frame/mesh_create/screenshot) after input_get_state + minor bump 4.3->4.4, regen via tools/abi_codegen, layout pins (EngineApiTable 368->408B, renderer_create @368, screenshot @400) in both test_abi_layout and AbiLayoutTests.
  • Native: native/bridge/src/renderer_bridge.cpp (EngineRenderer owns SDL3 window + Vulkan device + swapchain + ForwardRenderer mesh pipeline + D32 depth + MeshRegistry; RenderOne mirrors the P1-08 test frame loop: barrier, begin_mesh_pass, render_world with swapchain aspect, end pass, present, capture_rgba8 for screenshot; skips draw when no is_main camera). render_world gains an aspect param (default 16:9 so P1-08 goldens don't move).
  • Managed: managed/Engine.Runtime/Rendering.cs (Renderer IDisposable over the ABI 4.4 tail; Camera 16B / MeshRenderer 20B / DirectionalLight 16B in Engine.Runtime matching native layouts exactly) + samples/HelloCube (camera + light + rotating cube driven by a generated 2-term Spinner.Tick(ref Spinner, ref LocalTransform, dt) system + world_propagate per frame).
  • THE gotcha (worth recording): the HelloCube LocalTransform/WorldTransform C# mirrors MUST live in the GLOBAL namespace so FullName is exactly LocalTransform/WorldTransform. A HelloCube.LocalTransform mirror forks a SECOND Flecs component (name-exact lookup): entity_set succeeds (size matches) but world_propagate's cascade query watches the real built-in id, so World rows stay zero, the draw query matches nothing (draws=0, black frames) while every sample check still prints PASS. Symptoms: ComponentId<WorldTransform> returns a fresh id on every new world instead of the built-in; entity_get reads zeros after propagate; renderer draws=0. Fix pins: global-namespace mirror structs with a single Vector512<float> field (size 64 + align 64 via the 64B vector member), Ecs.Set order World-then-Local irrelevant, component ids resolved AFTER renderer_create (register_render runs there idempotently) so the first bind doesn't fork live low ids.
  • CTest: engine_hello_cube (offscreen, --frames 10 --screenshot <build>/hello_cube.png, PASS on HELLO-CUBE-OK) + engine_hello_cube_compare (loads that PNG, golden_check("hello_cube_10f_640x360"), P1-05 tolerance; HELLO_CUBE_PNG env; hello_cube_screenshot fixture chains them). Golden native/tests/golden/hello_cube_10f_640x360.png (640x360, llvmpipe: tan square-cube face centered on black — camera looks straight down -Z at the cube so one face fills the frame; 248 nonblack px).
  • Verified: engine_hello_cube + engine_hello_cube_compare PASS; engine_abi_layout PASS; AbiLayoutTests "all checks passed"; abi_codegen: 49 entries.

PARITY P1-10 Real engine-cli snapshot capture (P1-10, 2026-10-31)

  • CliDispatcher gains a settable LiveRenderer (null = headless stub, unchanged envelope: out/pass/0x0/true). When set, snapshot.capture frames once (current scene, not a stale present) then calls renderer_screenshot to the requested path and returns the renderer's real extent with HeadlessStub: false. Native failures throw InvalidOperationException -> InternalError (-32603), never a silent stub.
  • Renderer records its create-time extent (Width/Height props) so the dispatcher reports the window size without new ABI. EditorSession keeps its borrowed EngineWorld* (public World) so a host/test can attach a renderer to the same world. CliServer exposes its Dispatcher for the same reason. No ABI change (uses the P1-09 4.4 tail as-is), no deps change.
  • Acceptance: CliApiTests.RunRealSnapshot — own world + hidden offscreen Renderer (320x240) + seeded lit cube scene (camera eye (0,0,3), dir light, unit cube via global-namespace LocalTransform mirror + world_propagate; new managed/Engine.Tests/TransformMirrors.cs, same global-namespace rule as HelloCube/Components.cs) + own CliServer with LiveRenderer set, then the REAL engine-cli binary snapshot capture --out <tmp>/s.png --pass final asserts headlessStub=false, width/height == 320x240, PNG magic present. GPU-gated: renderer_create failure skips cleanly, fails closed under ENGINE_REQUIRE_GPU=1.
  • Gotcha (mine): MeshRenderer.BaseColor / DirectionalLight.Direction are C# fixed buffers — write via mr.BaseColor[i] directly. My first draft cast &mr to float* and wrote through it, which AV'd (fixed-buffer pinning/aliasing); direct indexing is the HelloCube pattern and works.
  • Verified: SDL_VIDEO_DRIVER=offscreen ENGINE_REQUIRE_GPU=1 ctest --preset ci-linux 48/48 PASS including engine_compositor_live ([PASS] real-snapshot: engine-cli snapshot real PNG 320x240, file exists, [LIVE] real-snapshot: ... 3254 bytes, 320x240, headlessStub=false).