Repository navigation
Feat/larimar - #9
Merged
Merged
Conversation
Records the original Larimar vision verbatim (PROPOSAL.md) and adds the engineering plan that continues it with the Gyosho monorepo factored in (ARCHITECTURE.md). Central reframe: SumiC's 632-line frontend is the differentiator, its 229-line backend is not. Retarget codegen to emit .mat for matc rather than competing with Filament on per-backend translation, and keep the S2L language, the CPU/GPU parity guarantee, and Kantei-aware compilation. Kantei's Grade ladder becomes the backend selector: Filament covers Paper/Brush/Gold, the existing OpenMP renderer stays as Ink, which is below Filament's GLES 2.0 floor and is the tier the gallery and the embedded targets live on. Also reorders the phases so First Light (Pong on an abstracted engine) lands on the Ink backend before Filament is introduced, and records corrections on Filament's existing ECS and gltfio, the Dart hot-reload duplicate-entity trap, per-frame FFI cost, and the Phase 4 NPU claim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Carves the OS-oblivious boundary and ports Pong onto it, with no Filament and no Rust. Proves the API shape against a real game before the hardest dependency lands, rather than after. core/include/eshi/eshi.h is the entire public surface, pure C99: no platform, graphics API, or C++ type crosses it. One header serves three consumers unchanged — C++ hosts include it, Zig reaches it via @cImport, Dart will generate bindings from it in Phase 3. core/ implements a sparse-set SoA ECS with generational handles, a deterministic system scheduler, generic AABB collision with layers and an event queue, a fixed timestep with clamped catch-up, and a seeded RNG. render_ink.cpp is Kantei Grade 1: the CPU/OpenMP fullscreen material path, which lives below Filament's GLES 2.0 floor and is the tier the gallery and the embedded targets need. Non-Ink grades are rejected outright rather than silently downgraded. The fix that mattered for First Light: the uniform block is opaque to the engine. On feature/pong-engine, adding one game put a GameData struct into the shared math header and a fourth parameter onto every backend's renderFrame(). Here the game declares its own layout, the engine forwards a void*, and no backend signature names a game. examples/pong is a scene, a uniform block, three systems and a shader — no main(), no SDL, no IGame, and no ResolvePaddleBounce: the engine separates bodies and reflects velocity generically, and the game adds English and speed-up by reading collision events. Walls are static colliders rather than an if in the loop. Verified: 38 core checks pass; all 20 gallery examples still build and render; framebuffer digests are identical across runs at equal seed and differ across seeds. The encoded .mp4 bytes are not yet reproducible — that nondeterminism is in the shared FFmpeg/x264 path in encoder.h, not in the engine, and is recorded in the architecture doc as a Phase 4 prerequisite. zig build test links core/ alone — no SDL, no FFmpeg, not even libsumi — so the OS-oblivious rule is enforced by the build rather than by convention. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… 2-3 Filament is not needed to fill Paper and Brush for the fullscreen-material model, because eshi already had those backends. renderer_gl.h, renderer_metal.mm and renderer_gpu.cu all exposed the same renderFrame(pixels, stride, time) signature the Ink backend uses — which is why main.cpp could dispatch between them with nothing but #ifdefs. This ports the first two in behind a backend vtable, so Filament can later earn its place on what it uniquely adds (PBR, meshes, glTF, shadows) rather than on ground already covered. Backends are selected by Kantei grade through a registry; absent ones get a stub that reports unavailable, so eshi_grade_available() answers honestly in a build that lacks them and eshi_world_create() still refuses rather than silently downgrading. EshiMaterial now carries both a compiled-in cpu_shader for Ink and a source_path for the GPU tiers, which is what lets one material run on every grade — and which gives the GPU tiers shader hot-reload for free, since rebinding recompiles. Two things the port forced, both improvements. The GL backend had to be inverted: the original created its own hidden SDL window and called SDL_GL_GetProcAddress, which the core may not do. The host now owns the context and passes its loader through eshi_gl_set_proc_loader(), and the core declares its own GL typedefs rather than including a GL header — which is also what will let the Flutter embedder drive this backend with no SDL window in existence. And the shader transpiler, previously duplicated between the GL and Metal renderers, is now singular in core/src/render/transpile.cpp with the target-specific rules separated and commented. That consolidation is the prerequisite for writing the C++ shader subset down as a spec. Verified on an M4 Pro: examples/ripple.cpp is compiled in for Ink and handed to the GPU tiers as a source path — one file, both roles. At 480x270 through lossy H.264, Brush vs Ink is 53.4 dB PSNR and Paper vs Ink is 53.3 dB, well above the ~40 dB that reads as visually identical. Bit-exact CPU/GPU agreement is not achievable and was never the goal; this is the Ink-as-reference-oracle property working. 51 core checks pass and all 20 gallery examples still build. Known limit: pong stays Ink-only, since its shader takes a typed Uniforms& struct a textual transpiler cannot lower. The GPU tiers do support uniform blocks as a flat eshi_uniforms float array, so a GPU sidecar against that array would lift it — the pattern examples/gpu/ already uses. Also records what fluorite.game actually claims, so the comparison rests on their description rather than guesses, and names the two things a Filament-only engine structurally cannot have: a tier below Filament's hardware floor, and offline procedural video export. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
examples/pong/pong.gpu.cpp is the same material re-expressed against the flat eshi_uniforms float array, because a textual transpiler cannot lower pong.cpp's typed `const Uniforms&` parameter into a shader binding. Same pattern as examples/gpu/, for the same reason. pong.h now static_asserts the struct stays tightly packed, so a layout drift is a build failure rather than a garbled GPU frame. One constraint the transpiler imposes, recorded in the sidecar: every read of eshi_uniforms has to happen inside mainImage. GLSL exposes it as a global, but MSL binds it as a kernel argument threaded through mainImage's signature, so a helper function has no way to see it. fix(render/gl): flip the readback glReadPixels returns rows bottom-up while the engine's framebuffer is top-down, and the readback was copying straight through — so every GL render came out vertically mirrored against the CPU one. renderer_gl.h:323 has the same bug; it survived unnoticed because most of the gallery, and Pong, is close enough to vertically symmetric to hide it. This is what the Ink-as-oracle property is for. The first cross-tier pixel diff surfaced it in one run, having gone unseen for the life of the renderer. The staging buffer for the flip lives in the backend struct so the row reversal costs no per-frame allocation, and the memset over GlBackend was narrowed to the function table now that the struct holds a std::vector. Adds --dump DIR to the host, writing raw PPM frames with no codec in the path. Comparing tiers through a lossy encoder measures the encoder; this is the tool the conformance check actually needed. Measured on an M4 Pro at 320x180: pong over 120 frames and ripple over 60 frames both agree with Ink to a maximum of 1 LSB out of 255 on Paper and Brush alike, mean 0.0000. Note the --hash digests still differ across tiers, and should — that digest answers "is this tier deterministic", not "do two tiers agree". Different questions, different tools. 51 core checks pass; all 20 gallery examples still build. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Applies to the legacy shadertoy path the same fix the Larimar port needed. This is not dead code: USE_OPENGL is defined by scripts/build.arm64.bat, the Windows-on-ARM64 / Snapdragon build the README documents as using OpenGL. Neither the Makefile nor build.zig defines it, which is how the defects below survived — nothing anyone runs day to day compiles this file. The one-line readback got three things wrong at once: Orientation. glReadPixels returns rows bottom-up, while every consumer here is top-down — CpuRenderer writes row 0 from the largest fragCoord.y, and both Display and SimpleEncoder read it that way. Copying straight through mirrored every GPU frame vertically against the CPU renderer. Format. It asked for GL_RGB, three bytes per pixel, into buffers that are RGBA: Display uses SDL_PIXELFORMAT_RGBA32 and SimpleEncoder uses AV_PIX_FMT_RGBA. The short rows were then read as four-byte ones, skewing the image and shifting the channels. Stride. The destination pitch parameter was ignored outright. FFmpeg aligns linesize, so it is not always width * 4. Separately, readFile never stripped the glsl:: namespace qualifier, so any shader written as glsl::length(...) emitted invalid GLSL and failed to compile — examples/ripple.cpp among them. renderer_metal.mm has stripped it since it was written; this copy of the transpiler had drifted behind. That divergence is the concrete argument for the consolidation in core/src/render/transpile.cpp, which carries both rules in one place. Verified by building the legacy path with -DUSE_OPENGL and rendering the same shader through both renderers. examples/deepsea.cpp is vertically asymmetric, which ripple is not — a flipped ripple looks almost identical to an upright one, so it cannot test orientation. Against the CPU reference at frame 100, the GL output now scores mean |diff| 1.86 upright versus 52.99 against a vertically flipped reference, so the orientation is unambiguously correct. Still wrong and deliberately left alone: a failed shader compile is logged by checkShader() but does not stop construction, so the renderer prints "[GL] Ready." and proceeds to emit black frames. Changing that is an error-handling decision, not a bug fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GlRenderer logged shader failures and carried on regardless, so a shader it
could not build produced a silent video of black frames — and printed
"[GL] Ready." on the way. That is how the missing glsl:: strip fixed in the
previous commit stayed hidden.
Construction now throws on every failure it can detect, and main.cpp catches
it the same way it already catches MetalRenderer, leaving gpu_init false so
the existing CPU fallback takes over. No new mechanism: this is the contract
the Metal backend has always used, extended to the one that lacked it.
Four failures were previously unreported or fatal:
Fragment shader compilation was logged to stderr and ignored. Now throws,
naming the source file — the transpiled GLSL is generated rather than
authored, so a bare line number in the driver log points at text the user
has no way to open.
Vertex shader compilation was never checked at all.
Program link status was never checked at all.
A missing shader file called exit(1) from inside a header, killing the
process and denying the caller the CPU fallback it already knew how to do.
Now throws like the rest.
Framebuffer incompleteness was also logged and ignored; it throws too.
Because a destructor does not run for an object whose constructor threw, the
new fail() helper releases the GL context and hidden window before throwing.
Verified against examples/seascape.cpp, whose GLSL genuinely does not compile
("'&' : syntax error" at line 194 of the generated source). With --gpu it now
reports the reason, falls back, and renders: the output is bit-identical to a
plain CPU run, mean |diff| 0.0000 and max 0, with 172330 of 172800 pixels
non-black. examples/deepsea.cpp still initializes and runs on GL, so the good
path is unchanged, and the Metal and Larimar paths are untouched — 51 core
checks pass and all 20 gallery examples still build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
seascape and lunar failed to transpile, and chasing them turned up seven shaders failing across the two targets for nine distinct reasons. Every one is a construct that is valid C++ and has no direct equivalent in the target shading language — which is the actual content of the "C++ shader subset" that has never been written down. GLSL-only, all in the reference-parameter and C-syntax families: Reference parameters other than `vec4 &` were unhandled, so seascape, lunar, mario and rainforest emitted a bare `&`. These now map to `inout`, not `out`: mario's sprite helpers take `vec3& color` and composite onto what is already there, so `out` would make every sprite erase the background. A transpiler cannot assume write-before-read. `const T&` maps to `in` instead, and must be matched first — qualifying it `inout` while leaving the const yields `const inout vec4`, which GLSL rejects. `(void)x;` unused-parameter casts, C-style casts of both bare operands and parenthesised expressions, and C++ direct-initialization (`vec2 c(0,1);`) were all passed through verbatim. Metal-only: atan2f was mapped to atan, but MSL splits one- and two-argument arctangent into atan and atan2 — polar.cpp broke on it. It now maps to atan2, and file-scope two-argument overloads restore the GLSL spelling for shaders that call atan(y, x) directly, which no define can reach because the fix depends on arity. `mod` is GLSL's spelling and MSL has only fmod. Both targets: exp2f, log2f, tanhf, roundf and truncf were unmapped. A host-only declaration that opened a block had only its signature dropped, leaving the body orphaned at program scope. mario and tunnelwisp both define `extern "C" vec2 mainSound(...)` for the audio thread; the readers now track braces and take the body with the signature. Two more cases of the GL copy having drifted behind the Metal one, the same way the missing glsl:: strip had: the swizzle rewrite was a hardcoded list of eight spellings that silently missed lunar's .xz(), and the global-scope qualifier in tunnelwisp's ::tanhf was never stripped. Both now use the general form Metal has always used. This divergence is the argument for core/src/render/transpile.cpp holding one copy. Verified: all 19 gallery shaders compile on Metal, and seascape, lunar, mario, rainforest, polar, tunnelwisp, ripple, warp and voronoi all compile and run on the legacy GL path rather than falling back. Larimar cross-tier parity is unchanged at max 1 LSB, and 51 core checks pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
USE_OPENGL was defined only by scripts/build.arm64.bat, so neither the Makefile nor build.zig ever compiled renderer_gl.h. That is how a vertical flip, an RGB/RGBA mismatch, an ignored stride, silent shader-compile failures and a drawer of transpiler gaps all survived in it. This makes the path buildable from the normal build graph so it can be checked routinely. Off by default, deliberately. On macOS enabling it also takes precedence over Metal, because main.cpp's dispatch tries CUDA, then OpenGL, then Metal; and elsewhere it would add a libGL requirement to builds that are content on the CPU path today. The README says both, and says to turn it on when touching anything shared with the GPU backends — the transpiler above all, whose rules differ per target and are easy to fix on one while breaking the other. Larimar's own GL backend loses its -Dgl gate and is now always compiled. Unlike the legacy renderer it declares its own GL typedefs and resolves every entry point through the loader the host installs with eshi_gl_set_proc_loader(), so it links neither a GL library nor a windowing library. There was nothing for the option to save, and two flags a letter apart would only have invited confusion. Building the whole gallery this way turned up one more shader: aurora.cpp declares `float noise2(vec2)`, and noise1..noise4 are reserved GLSL built-ins returning genType, so it was a return-type redeclaration. Definition and call sites are renamed together, which keeps the shader self-consistent and costs nothing — the built-ins are deprecated, gone from core profiles, and return 0 on most drivers. Metal has no such names, so this is GLSL-only. Verified with all 19 gallery shaders compiling and running on the legacy GL path under -Dopengl=true, all 19 still compiling on Metal in the default build, 51 core checks passing, and Larimar unchanged. One measurement note for anyone repeating this: running the shaders in parallel makes six of them appear to fail with empty output. That is contention between twenty simultaneous GL contexts, not a shader defect — they pass sequentially. Check this corpus one at a time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
First CI in the repo, scoped to the gap that motivated it. USE_OPENGL was defined only by scripts/build.arm64.bat, so no routine build ever compiled renderer_gl.h, and it accumulated a vertical flip, an RGB/RGBA mismatch, an ignored stride, silent shader-compile failures and a drawer of transpiler gaps. The transpiler is the standing hazard: its rules differ per target, so a fix verified on Metal can silently break GLSL. Only building both catches that. Adds --validate to main.cpp: initialize the renderer, report which backend came up, exit without rendering. The CPU fallback is deliberately quiet at runtime — a shader the backend cannot build should still produce a video rather than black frames — which is right for users and useless for CI. This makes the same condition loud on demand, and avoids timing a 240-frame render to read one line of output. scripts/check_gpu_shaders.sh drives it over the gallery and is the actual test, so it runs locally as well as in CI. Two things it does deliberately: It requires the [validate] marker, not just a zero exit. A binary predating the flag ignores it, renders all 240 frames and exits 0 — which an exit-code-only check calls a pass. That is not hypothetical: it reported a false 19/19 against a stale zig-out during development, and the hardened check correctly failed 18 of those 19. It is sequential. Running the shaders concurrently makes unrelated ones report empty output and look like compile errors; twenty simultaneous GL contexts contend that badly. Six shaders "failed" that way and all passed one at a time. The Linux job supplies what a runner lacks: Xvfb for the X server SDL needs to create a context, and Mesa llvmpipe for GL 3.3 in software. Slow, and fine — the assertion is that each shader compiles and links on the GL backend, not that it renders quickly. A core-tests job runs alongside so a red GL job reads as "the GL path broke" rather than "something broke"; it installs no dependencies, which is itself the check that the OS-oblivious boundary in ARCHITECTURE.md §5 still holds. Verified locally: 19/19 on the GL build, 19/19 on Metal, the script exits 1 against a build with no GPU backend and against stale binaries, and `zig build test` passes with pkg-config pointed at a nonexistent path. The workflow YAML parses and the job graph is as intended, but it has not been executed on a runner — the Xvfb and llvmpipe steps are unproven until it runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…a macOS CI job SimpleEncoder now prefers VideoToolbox when it is present, routing H.264 and HEVC through the dedicated media engine instead of libx264. Measured on an M4 Pro at 1080p over 240 frames, that is 3.14s to 1.28s end to end — 2.4x — and it leaves the CPU cores for rendering, which matters most on the Ink tier where the renderer wants all of them. Quality measured against the raw rendered frames rather than against each other, using the host's --dump output as ground truth: libx264 46.3 dB PSNR, VideoToolbox 45.6 dB. The 0.7 dB is the usual hardware trade at equal bitrate and both are well above the ~40 dB that reads as visually identical. Comparing the two encoders to each other gives 26.6 dB and means nothing — that is two lossy encodes disagreeing, not either one being unfaithful. `--encoder auto|hw|sw` selects, and `--hevc` switches codec. auto is the default and falls back to software if VideoToolbox is absent or refuses a session, which it can do under virtualization; hw demands hardware and fails loudly; sw pins libx264. Use sw when output must be comparable across machines — hardware encoders make no bit-reproducibility guarantee across silicon or driver revisions, which forecloses the reproducible-export work tracked in ARCHITECTURE.md §7 on that path. Both hosts take the flag; the encoder needs no platform #ifdef, since the VideoToolbox encoders only exist in an Apple FFmpeg build and looking them up by name is enough. Colour range is now stated rather than inferred, which also silences a VideoToolbox warning about guessing it. The macOS job covers the other half of the transpiler. Its GLSL and MSL rules diverged repeatedly — a missing glsl:: strip, a hardcoded swizzle list, atan2f mapped to the wrong builtin — each compiling cleanly on one target while failing on the other, so neither job alone would have caught them. Two deliberate softenings there, both because a hosted macOS runner is virtualized. Metal is probed before the shader check runs, so "no GPU in the VM" is a notice and a skip rather than a red build that looks like a transpiler regression. And the encoder step asserts only that both paths produce a decodable stream, not that hardware was chosen, because falling back to software is the designed behaviour. The probe itself is hardened to fail when the binary is missing. Rehearsing it locally, an absent eshi made it report "no Metal" and skip silently — the same false-pass shape as the stale-binary hole closed in the previous commit. Verified locally: 19/19 shaders on Metal and on the GL path, 51 core checks, all four encoder modes producing valid streams at the right frame counts, and the workflow YAML parsing with the intended three jobs. The macOS and Linux jobs have still not run on a runner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 1b's opening move. core/src/render/filament.cpp implements the same backend vtable the Ink, GL and Metal tiers already satisfy, driving a headless offscreen Filament view, and Brush selects it in -Dfilament=true builds. Nothing about the C API moved to accommodate it: no eshi_* signature names Filament, which is the §2 rule that keeps the gallery alive through the transition, and the readback preserves the framebuffer contract so a Flutter hardware-texture host can later delete that copy without touching the ECS or the game. EshiMaterial grows a third representation. cpu_shader is compiled in for Ink and source_path is transpiled at runtime by the lightweight GPU tiers, but a packaged renderer wants neither — it wants a package built offline, so package_path joins them and the backend vtable's create() takes both. The asymmetry that follows is worth stating: source-backed tiers hot-reload by rebinding after a file change, package-backed tiers rebind a rebuilt package, and Ink cannot hot-reload at all because its shader is in the binary. The material API does not change across any of that. The entity layout moves from 24+8 to 17+8 bits. Not arbitrary: Filament's utils::Entity is GENERATION_SHIFT 17 / GENERATION_BITS 8 with a maximum index of 2^17-1, and matching it exactly is what lets §6.4's plan — adopt utils::Entity as *the* entity id rather than run a second ECS alongside Filament's — happen without a translation table. It costs a ceiling of 131071 simultaneous entities, which is not a ceiling this engine is near. zig build larimar -Dfilament=true compiles examples/pong/pong.mat with matc, installs the package at share/eshi/pong.filamat, and installs Filament's LICENSE alongside it, since Apache-2.0's attribution obligations are real even when the dependency is consumed as a build product. -Dfilament-path points at an already-installed distribution and -Dfilament-arch overrides the library directory when it is not the inferred arm64 or x86_64. It stays off by default, and that is a build-time judgement rather than a confidence one: the vendored checkout is source, so enabling it by default would put a multi-minute native dependency build in front of every ordinary Ink or Paper build. §6.8's preference for official prebuilt archives still stands and is still not done. eshi_world_backend_name() is new and small but earns its place — with two different implementations now reachable at Brush grade, "which renderer did I actually get" stopped being answerable from the grade alone. Still to do in this phase: mesh and renderable components, PBR, shadows, gltfio, and consuming prebuilts rather than building from source. What landed is a fullscreen material on Filament, which is the tier boundary proven, not the 3D scene. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ciler §6.6 and §6.7 built and proven, with no Dart anywhere near them. That ordering is the point: both are testable in C++ with no ffigen, no embedder and no Flutter toolchain in the way, so the FFI contract gets settled before the hardest dependency in the project has a vote on its shape. Building the acceptance test last is how you find out in month six that the API was wrong. The transport is a flat array of 32-bit words the world owns and a caller maps once — Dart wraps the same bytes in an Int32List and a Float32List through asTypedList, writes packed commands, and calls eshi_commands_flush() once per frame. Framing is [opcode:16 | payload_words:16] followed by payload; events travel the same framing in reverse. The length field bounds-checks a buffer another language wrote and lets a newer writer append payload words an older reader ignores. It deliberately does *not* license skipping unknown opcodes, which is the usual choice for a forward-compatible format and the wrong one here: a core that quietly drops an opcode its Dart package emits renders a scene that is wrong rather than one that is missing, and that surfaces as an art bug weeks later. Unknown opcode stops the flush with ESHI_ERR_UNSUPPORTED. Opcode compatibility is a version contract, not something to paper over at runtime. The reconciler is the part §6.6 warned was underestimated, and it was. A submission is a description of keyed nodes; the reconciler diffs it against the live ECS and emits create/update/destroy. Dropping a node destroys its entity, on SCENE_END and nowhere else — so a submission truncated mid-flight leaves the live scene alone instead of deleting whatever did not arrive, and a scene may span several flushes for the same reason. What was not obvious going in: idempotent is not sufficient. The first working version wrote every described component on every pass. It never duplicated anything and was still useless, because a reload mid-rally teleported the ball back to its spawn point. Retained mode has to write a component only when its *described* value changed, which means holding the previous description and diffing against it — exactly the role Flutter's Element tree plays between Widget and RenderObject. Described values are compared as raw words rather than decoded floats, because NaN != NaN would re-fire a value that never changed and -0.0 == 0.0 would hide one that did. Epoch gating covers the case that motivated all of this: a closure that outlived a reload and still holds a buffer will happily flush the scene it was built for, and without a gate that submission sweeps away everything the new code just created. An older epoch returns ESHI_ERR_STALE and sweeps nothing. The gate is scoped to scene topology — ESHI_CMD_INPUT is not part of the description and always applies, so a stale scene cannot also eat a frame of input. Pong is the proof rather than the tests being it. examples/pong/pong.cpp no longer calls eshi_entity_create at all; its scene is a description submitted through the command buffer, and pong::reload() re-submits at a fresh epoch. Digests at seeds 1, 42 and 99 are bit-identical to the imperative version this replaces (f3f0d79e16ec78b6, b9321cc6ecbe3e26, 657b25ed9010692e), so the reconciler reproduces the old scene exactly rather than approximating it. Cross-tier conformance is unchanged: Brush vs Ink over 120 frames is mean 0.0000/255, max 1 LSB. --reload N re-submits every N frames and must not move the digest. It does not, down to --reload 1 — the entire scene re-described on all 400 frames, framebuffer identical, entities=5 nodes=5 throughout. Built imperatively those same reloads would leave 2000 entities behind. What is absent from pong's description turned out to be the interesting part. The ball has a collider and a restitution but no transform and no velocity: those are simulation state owned by serve(), and declaring them would put the reconciler and the game in an argument over the ball every reload. Describe what the scene *is*, let systems own what it is *doing* — a rule easier to state now than to retrofit once widgets are writing scene descriptions. Every new test passed on the first run, so they were checked by sabotage rather than trusted: disabling change detection fails 2, breaking key lookup fails 13. One assertion was passing by comparing two nulls and is now guarded. 52 checks to 132. The CI gate got the same treatment and needed it. Comparing baseline against --reload only catches regressions that manifest differently under reload; a reconciler broken *uniformly* breaks both runs identically and sails through, which is exactly what a sabotaged key lookup did. It now also rejects a pong that scores 0-0 over 400 frames, since a scene that never resolved has nothing to keep invariant. Verified firing under both sabotages and passing clean. Known limitation, documented rather than left to be discovered: dropping a component from a node does not remove that component, because the core has no per-component removal yet. Drop the node instead. Remaining in this phase: ffigen bindings, the Flutter embedder, and the external-texture path (§6.11). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Complete PLAN Step 2 with ABI negotiation, versioned wire fixtures, hostile-input coverage, and sanitizer-backed release gates.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.