River Run: cinematic render pipeline, examples-page card and playground port - #3886
Conversation
The PreToolUse hook loaded its jq script by a relative path, so every Bash call failed the moment the shell's cwd moved out of the repo root -- and because the hook gates Bash itself, cd-ing back was impossible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnf64QnUkgmffRqg58BRXk
The example ran, but it did not look like something to put on the site: the
player was a sphere with a rotor cross, the banks were a flat plane with an
axis-aligned checkerboard baked in by a floor()-quantized hash, there was no
sky or horizon at all, and the HUD was laid out in raw framebuffer pixels so
it came out half size on any HiDPI display.
Rendering is now a deferred-lit forward pass into an HDR MRT target followed
by a screen-space chain: SSAO, progressive-downsample bloom, screen-space sun
shafts, ACES tone mapping, per-section colour grade, vignette, grain and FXAA.
A single ortho shadow cascade replaces the fake planar blobs, so props shade
each other and the banks. Everything is held to the GLSL ES 3.00 / WebGL2
feature set, and the HDR attachment is probed at creation and falls back to
RGBA8, so the wasm build stays viable.
rr_shaders.das scene shading: sky, terrain, water, props, rotor, shadow
rr_postfx.das render targets and the post chain
rr_models.das helicopter, gunboat and jet welded from primitives into one
draw call each, part index baked into uv.x
rr_live.das live-command surface (status/reset/section/spawn/god mode)
Each of the ten sections is now a whole lighting environment -- sun colour and
elevation, sky, fog, exposure, grade -- crossfaded on section change, so the
run reads as flying through changing weather rather than a river tint swap.
The banks are sculpted from an analytic height field with a real shoreline
profile; gameplay shares the same terrain_height so props stand on the ground.
The HUD is rebuilt in design pixels against a 720p reference and scaled, with
a framed instrument cluster, life icons drawn as the player's own silhouette,
a section progress rail, and outlined type.
Also fixed, both found while iterating:
- Live reload aborted in Stream::push on a dead mutex. audio_initialized and
music_initialized were @LiVe, so they came back true after a reload, but
shutdown_audio() had already torn the audio system down and asch is not
@LiVe -- the new context drove a destroyed audio system and a strudel Stream
owned by the freed old heap. They describe live process state, not game
state, so they no longer persist.
- Reload emptied the world: the game never required live/decs_live, and init()
correctly skips re-spawning under is_reload().
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nnf64QnUkgmffRqg58BRXk
Input diagnostics, added while chasing "controls are disabled". They separate a game-logic problem from an activation one in a single query: `focused` is the GLFW window attribute, the rest are live glfwGetKey states. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnf64QnUkgmffRqg58BRXk
Three issues from playtesting, plus two reload bugs the work surfaced. geom_gen: gen_cone's SIDE triangles were wound backwards, disagreeing with both their own per-vertex normals and the correctly wound base cap in the same mesh. Under the usual CCW-front / cull-back setup the lit outer surface was culled and the unlit interior showed through. The old river_run hid this with a blanket glDisable(GL_CULL_FACE) around the obstacle pass, which the render rewrite dropped -- so the trees surfaced a latent generator bug. river_run is the only gen_cone caller in the tree. Terrain: the centre island of a split was smooth across x and stepped along y. emit_island computed the far-edge vertex heights from the NEAR slice's half width, so neighbouring slices disagreed about the height of the edge they share. Island vertices also took their normals from profile_normal, which evaluates bank_profile -- the wrong height field entirely. Normals are now emitted per surface at emit time instead of guessed by one after-the-fact pass. Explosions were a colour ramp on flying cubes. A blast is now six layers, each with its own lifetime, motion and blend mode: a white flash at the instant of the hit, fireballs that expand and cool, embers that arc under gravity and streak along their velocity, smoke that rises and outlives everything, solid debris that skids when it hits the water, and a shockwave ring racing outward. Smoke and fire draw as soft camera-facing billboards (vs_puff builds the quad in view space) rather than low-poly spheres, which is what removes the faceted silhouettes. The player's own death runs the same vocabulary at 2.2x. Also fixed: - A reload came back to a black screen. Neither the section environment (plain non-@LiVe globals) nor the geometry handles (GL objects in a context that was just destroyed) survive a reload, and a FULL reload additionally clears the @LiVe river. init() now rebuilds all three unconditionally. - cmd_fx_freeze / cmd_fx_step hold and hand-step the world so an effect can actually be looked at: a screenshot round-trip is far longer than a 75ms flash, and the player otherwise flies 100 units from the blast between one inspection command and the next. cmd_boom detonates on demand. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnf64QnUkgmffRqg58BRXk
…al reflections geom_gen: gen_cylinder was inverted on all three parts -- the side quads and BOTH caps -- so a cylinder rendered as its own interior wherever back faces are culled. Same class of defect as gen_cone last round; that pair is the whole circle-based family. The fuel depot's tank is the visible case here, but examples/opengl/07_hello_gen.das culls too and was equally wrong. Terrain: the centre island only ever tapered sideways, so it met open water at full crown height and left a ridge running down the river -- the seam that survived the previous fix. Height now takes min(lateral, longitudinal) distance, where the longitudinal term is the distance along y to the nearest end of the split run, so the crown rounds off in every direction. A run that continues past the generated river reports a large distance instead of tapering, otherwise the island would grow a moving wall at the far end as new segments scroll in. Shading: props can now mix in a sampled environment colour, which is what makes a surface read as metal rather than as painted plastic with a highlight on it. sky_env is a cheap probe -- the sky gradient and the sun, no cloud fbm or star field, plus a ground half so a cylinder does not mirror sky out of its underside. draw_prop takes a (reflectivity, head-on bias) pair with NONE / PAINT / METAL / CHROME presets, and anything in the model palette's metal slot reflects automatically, so the boats, jets and helicopters pick it up without their call sites opting in. The depot tank is chrome. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnf64QnUkgmffRqg58BRXk
…the scene The seam that survived two rounds of terrain fixes was never in the terrain. fs_water shades shallow-vs-deep from uv.x = distance to shore, and inside a split that was measured against the CHANNEL edges the geometry is built from -- so a whole narrow channel read as shallow, then flipped to deep the moment the per-segment `split` flag went false. One slice, one hard line straight across the river. Distance-to-shore is now a property of the water, not of the mesh it happens to be built from: the centre island shallows the water only in proportion to how much island is actually there. Its crown already tapers to nothing at the ends of a run, so the water stops pretending there is a shore mid-channel exactly as the island stops being one, and the transition is smooth in both directions. Pause no longer draws a full-screen scrim. Pausing is how you stop and look at something; dimming the frame hides the very thing you paused to inspect. Game over and the win screen keep theirs, because there the message IS what you are meant to be reading. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnf64QnUkgmffRqg58BRXk
Four things from playtesting. SOUND. The arcade character was the effect chain, not the notes: a raw sawtooth lead through jux(rev) + phaser is the chiptune signature. Cleanliness here is filtering the oscillator rather than replacing it -- a low-passed saw keeps the bite without the fizz -- plus a room to sit the voices in a space and chorus for width in place of the hard stereo flip. Variety comes from the pattern strings, now eight cycles long with mixed subdivisions and rests, so a melody takes eight bars to come round rather than four; iter() walks the arp through its own inversions. A low sustained pad under everything gives the other voices a floor. Chain DEPTH is a hard constraint: strudel's evaluator recurses per event, and a first attempt (off() duplication plus two sometimesby plus modulated filters, across five tracks) overflowed its stack on the first bar. The melodic variety is in the patterns, which cost nothing at eval time; the combinators are what have to stay shallow. Net result is lighter than before: 14% avg vs 24.5%. BANKS. They were a cone on a stick, repeated. Six flora variants now share one silhouette vocabulary -- conifer, broadleaf, dead, bush, boulder, reeds -- each welded into one mesh, with four pre-generated instances apiece so neighbours are never the same object. Conifer tiers are jittered and rolled, broadleaf crowns are a lumpy union of blobs rather than a ball, trunks taper and lean. Planting went from ~12 evenly scattered trees per section to ~90 in clumps, drawn from a distance-weighted mix: reeds at the waterline, bushes and boulders on the near bank, the tall silhouettes further back. One draw call per plant either way. PICKUPS. A chrome canister that takes the environment reflection, coloured end caps, a hot core, a per-type badge so what you are collecting reads before the colour does, plus two counter-rotating halo rings and a pool of light on the water beneath. DEBRIS. Explosion chunks were unit cubes, which read as flying dice. A shard is a cube whose eight corners are jittered -- corners, so the hull stays closed -- re-emitted with flat per-face normals. They now draw in the lit opaque pass with the metal reflection instead of as unlit alpha quads, because they are solid torn plating and were reading as paper. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnf64QnUkgmffRqg58BRXk
The card is wasm64-only, like the three showcase cards: the render path (deferred-lit pass plus the SSAO / bloom / sun-shaft chain) needs the compiled build to hold frame rate, so there is no interpreted fallback and no .das sources to stage. pages.yml builds it in the release-wasm loop, verifies the artifact, and stages it with the same "being rebuilt" placeholder rule the other always-listed cards use. Two source changes let ONE tree serve desktop and wasm, matching what arcanoid and boulder-dash already do: - live_api becomes `require ?dashv`. It needs dashv/libhv, which the wasm build neither has nor wants; `?` skips it there and keeps the REST API on desktop. - Music gates itself on audio_is_single_threaded(). The strudel player runs on its own worker, and the wasm audio backend is single-threaded, so there is no worker to host it -- the generated SFX carry the soundtrack there. Native is unaffected: it still reports a threaded backend and still plays music. Also drops a stray libhv log that had been committed in the game directory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnf64QnUkgmffRqg58BRXk
The GLSL emitter lowered `for (i in range(a, b))` to a while() whose increment was appended as the last statement of the BODY. A `continue` in that body jumps to the condition and skips the increment, so the loop never advances: it compiles clean, links clean, and then spins the GPU until the driver kills the context. On macOS that surfaces as "Caused GPU Hang Error" with no line of diagnostic pointing at the shader. The increments now ride in the loop header, where `continue` reaches them; a multi-source for puts all of them there, comma-separated. Verified in the emitted text, not the das source: nested range loops and the shadow-map PCF loops re-emit as `for ( ; i!=_for_range_vi.y; i++ )`, and River Run's SSAO pass -- which is what found this -- now runs its natural `continue` without hanging. The other two emitters are already correct and needed no change: dasMetal emits `for (T i = lo; i < hi; ++i)`, and dasSpirv branches `continue` to a structured loop's continue block, which is where its increment lives. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnf64QnUkgmffRqg58BRXk
External review round, verified against the tree. add_shafts_to_bloom passed cfg.shaft_strength through fs_up's p_params.x, which scales the SAMPLING OFFSET -- so the per-section shaft setting was changing the blur width and never the brightness, and a strength of zero would still have added the shaft buffer at full intensity. fs_up now takes radius in p_params.x and an output multiplier in p_params.y. The bloom upsample chain passes 1.0 for the multiplier and keeps its radius; the shaft composite passes a fixed radius and the strength as intensity, which is what the five section environments were always trying to set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnf64QnUkgmffRqg58BRXk
Same model as arcanoid. The playground interpreter cannot take the live/* + live_host stack -- static requires cannot be swapped at runtime -- so the port is a copy under web/examples/ui/samples with live_stub in their place. That is the one irreducible reason these ports exist as forks rather than as a flag. Four edits separate the copy from the real sources, all of them at the seam: rr_globals swaps ten live/* requires for one live_stub; rr_postfx drops live_host and live_vars, which leaves `@live` as inert metadata (there is no reload here to persist across); main drops rr_live, the REST command surface, which is a development tool with no role in a browser; and rr_audio gates music on is_standalone_exe() as well as the threading check. That last one is the music-and-threads question: the playground shares the renderer's runtime and cannot host the threaded strudel worker safely, and it does not report a single-threaded audio backend, so the threading check alone would not catch it. SFX carry the soundtrack there, as they do in the compiled wasm build. The card drops wasm64Only and gains a playground link; the sample is registered in data.json and in _interp.html's file list; pages.yml stages river_run's sources and HUD font alongside the other three games instead of as a wasm64-only card. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnf64QnUkgmffRqg58BRXk
…he card daspkg names the released card from package_name, so "river-run" emitted web/output64/examples/river-run/river-run.wasm while the workflow's verify step, its staging loop and the site card id all say river_run. The other three games hold dir == package_name == card id; this one now does too. Caught by building the card with emscripten locally rather than letting CI find it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnf64QnUkgmffRqg58BRXk
output/ and build/ cover the wasm32 playground dirs, but the same workflow also configures web/build64 and emits web/output64 for the example cards. Reproducing the pages build locally therefore left two untracked trees and a red preflight untracked gate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnf64QnUkgmffRqg58BRXk
There was a problem hiding this comment.
Pull request overview
This PR upgrades the “River Run” sample into a full cinematic render pipeline (deferred-lit forward + HDR MRT + post-FX chain), ships it both as a wasm64 examples-page card and as an interpreted web-playground port, and fixes a critical dasGlsl codegen bug where continue in a lowered range-loop could hang the GPU.
Changes:
- Fix dasGlsl range/dim
forlowering to emit a realfor ( ; cond ; i++ )header socontinueadvances the loop. - Rework River Run visuals (terrain/water continuity, shadowing, post-FX, HUD scaling, model composites) and address live-reload/audio issues.
- Wire River Run into the site examples list, interpreted fallback bundling, pages workflow staging/verification, and web build ignores.
Reviewed changes
Copilot reviewed 32 out of 33 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| web/examples/ui/samples/examples/river_run/rr_globals.das | New playground-port globals/constants/templates and environment setup for River Run. |
| web/examples/ui/samples/examples/river_run/main.das | New playground-port entrypoint: render loop, camera, lighting, post-FX, HUD pass. |
| web/examples/ui/samples/examples/river_run/live_stub.das | New shim replacing daslang-live host stack for web playground execution. |
| web/examples/ui/samples/examples/river_run/hud3d.das | New 2D instrument HUD layer (design-pixel scaling for HiDPI). |
| web/examples/ui/samples/examples/river_run/hud.das | New HUD text layer with outline rendering and design-space layout. |
| web/examples/ui/samples/data.json | Adds River Run to the web playground samples list and file bundle. |
| web/.gitignore | Ignores wasm64 pages build output/build trees alongside wasm32 ones. |
| site/files/examples.js | Adds River Run as a wasm card on the examples page (metadata/tags/poster). |
| site/examples/_interp.html | Adds River Run’s interpreted fallback bundle mapping. |
| site/examples.html | Updates SEO keywords to include River Run-related terms. |
| modules/dasGlsl/glsl/glsl_internal.das | Fixes GLSL emitter loop structure so continue can’t skip the increment. |
| modules/dasGlsl/glsl/geom_gen.das | Fixes cone/cylinder winding so they render correctly under back-face culling. |
| examples/games/river_run/rr_models.das | New composite-model generator (helicopter/jet/gunboat, shards, flora). |
| examples/games/river_run/rr_live.das | New REST-driven live-command surface for staging/inspection on desktop. |
| examples/games/river_run/rr_globals.das | Extends game globals and requires; adds section environments and reload persistence fixes. |
| examples/games/river_run/rr_audio.das | Fixes reload-safety of audio state and gates music on single-threaded backends. |
| examples/games/river_run/river.das | Rebuilds terrain/water geometry with analytic profiles, normals, and split continuity. |
| examples/games/river_run/main.das | Updates runtime loop to new render pipeline (shadow pass + post-FX + HUD overlay). |
| examples/games/river_run/hud3d.das | Reworks HUD geometry into design-space scalable instrument panels. |
| examples/games/river_run/hud.das | Reworks HUD text layout/scaling and adds outlined text rendering. |
| examples/games/river_run/.das_package | Aligns package_name with dir/id and documents wasm-vs-desktop compatibility strategy. |
| .github/workflows/pages.yml | Adds River Run to the wasm64 build/stage/verify loops for pages deploy. |
| .claude/settings.json | Makes the jq hook path robust by anchoring it to $CLAUDE_PROJECT_DIR. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…eploy broke A game shipped as a web card is written down in eight places, all by hand: the game directory, its package_name, the two deploy loops and the artifact-verify step, the examples-page card and its poster, the playground sample slug and its file list, and the interpreted fallback's copy of that same file list. The workflow that consumes them only runs on push to master, so any mismatch merges green and surfaces as a broken site. examples/games/REVIEW.das cross-checks all of it and runs per-PR in extended_checks. Each of its eight findings was verified by mutating the tree, running the gate, and restoring. The examples/ install block now excludes rule documents. They are the first ones under examples/, and examples/ installs wholesale, so without the exclusion the terminal install-manifest check would fatal on them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnf64QnUkgmffRqg58BRXk
The earlier commit fixed gen_cylinder's and gen_cone's index order, which was right for GenDirection.xy and yz and wrong for xz. GenDirection.xz maps the generated xy shape onto xz by swapping two axes, which is a mirror: determinant negative, so it reverses the orientation of every triangle it moves. No single index order can be correct for all three directions. The transform now reports its handedness and the four direction-taking generators reverse the winding back when it mirrored, so all three directions come out counter-clockwise seen from outside without a vertex or a UV moving. That also repairs gen_plane(xz) and gen_prism(xz), which have been inverted on master all along - gen_plane(xz) is what tank_game and arcanoid draw their ground with. tests/glsl/test_geom_gen_winding.das recomputes each triangle's winding from its vertices and compares it against that triangle's own normals, so it survives a retessellation. It is red on master's generator (7 of 15) and on the previous commit's (4 of 15). River Run's geo_plane_xz was built and never drawn; dropped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnf64QnUkgmffRqg58BRXk
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 37 out of 38 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
examples/games/river_run/river.das:442
island_normalcurrently callsterrain_heightfour times. Eachterrain_heightcall can end up scanningriver_segmentsviaisland_end_distance, which makes island normal generation disproportionately expensive during geometry rebuilds (especially becauseemit_islandcomputes normals for every vertex).
You can cut the expensive work roughly in half by sampling the segment + end-distance once for the two x-offset height samples (same wy), and only using terrain_height for the y-offset samples (different wy).
// Central-difference normal of the island crown, taken through terrain_height so
// it follows the same field the geometry and the gameplay both use.
def private island_normal(wx, wy : float) : float3 {
let e = 0.35
let hx1 = terrain_height(wx + e, wy)
Two Copilot findings, both accepted. The `continue` codegen fix had no test, and its regression mode is a GPU hang rather than a red lane. tests/glsl/test_for_continue_emission.das emits a shader whose range- and dim-for loops both carry a `continue`, then asserts the increment appears in a for-header and that no `while` survives - the property, not the emitter's choice of temporary names. Red on master's emitter, 3 of 5. The playground port drove itself with a blocking `while`, copied from the desktop entry point. All three other ported games use eval_main_loop, which lowers to requestAnimationFrame under emscripten; a blocking loop never gives the browser its event loop back. It also collected the GC a second time per frame, which live_end_frame already does. The desktop entry point keeps its blocking loop - that is what the other three do too, and the compiled card runs its main on a worker. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnf64QnUkgmffRqg58BRXk
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 38 out of 39 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
web/examples/ui/samples/examples/river_run/hud3d.das:280
- The comment says both culling and depth “stay off”, but this function re-enables GL_DEPTH_TEST. Since the text layer disables depth testing itself, this is misleading and the extra state toggle is unnecessary; either update the comment or keep depth disabled here.
examples/games/river_run/hud3d.das:280 - The comment says both culling and depth “stay off”, but this function re-enables GL_DEPTH_TEST. Since the text layer disables depth testing itself, this is misleading and the extra state toggle is unnecessary; either update the comment or keep depth disabled here.
// Culling and depth stay off: the text layer draws next, and its quads have
// no reliable winding.
glEnable(GL_DEPTH_TEST)
v_projection = saved_proj
v_view = saved_view
STYLE030 on the extended_checks lint lane. The local pre-push lint gate did not see it: that gate lints git diff origin/master...HEAD, so a file still untracked when it runs is invisible - it has to be committed first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnf64QnUkgmffRqg58BRXk
Shader codegen change: every GLSL shader in the tree re-emits its range loops.
continueinside one now advances the loop instead of hanging the GPU.Geometry change:
gen_plane,gen_cylinder,gen_coneandgen_prismnow wind the same way in all threeGenDirections. Anything drawing them under back-face culling is affected — see below.River Run ran, but it did not look like something to put on the site. The player was a sphere with a rotor cross. The banks were a flat plane with an axis-aligned checkerboard baked in by a
floor()-quantized hash. There was no sky and no horizon at all. The HUD was laid out in raw framebuffer pixels, so it came out half size on any HiDPI display.Rendering is now a deferred-lit forward pass into an HDR MRT target, followed by a screen-space chain: ambient occlusion, bloom, sun shafts, filmic tone mapping, a per-section colour grade, vignette, grain and FXAA. One ortho shadow cascade replaces the fake planar blobs, so props shade each other and the banks. Each of the ten sections is a whole lighting environment, crossfaded on section change, so the run reads as flying through changing weather. Everything is held to the GLSL ES 3.00 feature set, and the HDR attachment is probed at creation and falls back to RGBA8, so the WebAssembly build stays viable.
The game ships twice: as a compiled wasm64 card on the examples page, like the three showcase cards, and as an interpreted playground port so Safari and iOS get it too. The port is a copy of the eleven sources with the
live/*stack swapped forlive_stub, because static requires cannot be switched at runtime.Where to look:
rr_shaders.dasandrr_postfx.dasare the new render path;modules/dasGlsl/glsl/geom_gen.dasandmodules/dasGlsl/glsl/glsl_internal.dasare the two changes outside the example and the site wiring;river.dasholds the terrain and water fields, whose continuity across a river split took three passes to get right.The geometry winding fix
Every generator sets an outward per-vertex normal, and callers draw with
GL_CULL_FACE/GL_BACKunder a CCW front face, so each triangle has to be wound counter-clockwise seen from outside. Two independent defects had accumulated, and nothing checked either.gen_cylinderandgen_conepushed indices in the wrong order. Separately,GenDirection.xzmaps the generated xy shape onto xz by swapping two axes — a mirror, determinant negative — which reverses the orientation of every triangle it moves.yzcarries an extratm[2][0] = -1.that makes it a proper rotation;xznever got that, and the// todo: verifybesideyzis the fingerprint. So no single index order could be right for all three directions.The transform now reports its handedness and the four direction-taking generators reverse the winding back when it mirrored. Nothing moves — not a vertex, not a UV — only index order, and only for the direction that mirrors. That also repairs
gen_plane(xz)andgen_prism(xz), inverted on master all along.Measured with
tests/glsl/test_geom_gen_winding.das, which recomputes each triangle's winding from its vertices and compares it against that triangle's own normals, so it survives a retessellation:gen_cylindergen_conegen_planegen_prismgen_sphere,gen_cube(controls)The test is red on master's generator (7 of 15) and on the index fix alone (4 of 15).
Callers, and what changes for them. No caller needs editing.
examples/opengl/07_hello_gen.das—gen_cylinder(xz), cullsGL_BACK. Inside out on master; correct now.examples/daslive/tank_game—gen_plane(xz)+gen_cylinder(xz), cullsGL_BACK. The plane has been inside out on master all along; both correct now. Its planar-shadow pass inherits that cull state, and a shadow silhouette is covered either way for a convex shape, so it is unaffected.examples/games/arcanoid—gen_plane(xz), but it draws the ground inside aglDisable(GL_CULL_FACE)span, so winding never mattered there.examples/games/river_run—xythroughout, plus agen_plane(xz)that was built and never drawn; dropped.pacman,boulder-dash—gen_plane(xy), unaffected in either direction.Not verified on a GPU: the argument above is geometric and test-backed, not a screenshot of tank_game or 07_hello_gen running.
Validation, claims, ledger
Validation
The wasm64 card is verified end to end, not just compiled. With emscripten 5.0.7 (the pin this workflow already uses) the archives and the card build clean, and the card was played in a browser on real WebGL2 — ANGLE Metal on an M1 Max,
EXT_color_buffer_floatpresent so the HDR path is the one under test. Score, lives and fuel advance; the shadow cascade, water reflections, flora, bridges, depots and the transparent particle span all render; the river-split section shows no seam. Two defects came out of that run and are fixed here:package_namewasriver-runwhile the directory, the workflow's verify step, its staging loop and the site card id all sayriver_run. daspkg names the card frompackage_name, so CI would have looked for an artifact that was never emitted.web/.gitignorecovered the wasm32output/andbuild/trees but not theoutput64/andbuild64/ones the same workflow creates, so reproducing the pages build locally left two untracked trees and a red preflight untracked gate.Both were caught only by building the artifact rather than letting CI find it.
The gate that would have caught it
A game shipped as a web card is written down in eight places, all by hand: the game directory, its
package_name, the two deploy loops and the artifact-verify step, the examples-page card and its poster, the playground sample slug and its file list, and the interpreted fallback's copy of that same file list. The workflow that consumes them runs only on push to master, so a mismatch merges green and surfaces as a broken site.examples/games/REVIEW.dascross-checks all eight and runs per-PR inextended_checks. Each of its eight findings was verified the same way: mutate the tree, run the gate, confirm it fires, restore. It also found a second live gap — the port's file list is duplicated betweendata.jsonand_interp.htmlwith only a comment asking them to agree.examples/installs wholesale and these are the first rule documents under it, so the install block now excludes them; without that the terminal install-manifest check would fatal.Still CI-only
The playground port is exercised for the first time by CI. It could not be validated on this machine: a locally built
daslang_staticcannot resolve any embedded GL module —error[20605]: missing prerequisite 'glfw/glfw_boost'; file not found. The negative control says this is the local build and not the port: the shipped arcanoid sample and the stockgl_01_trianglesample fail identically here, and both run on the deployed playground. The embed flags are present in the link line and the module text is present in the wasm, so the fault is in path registration on this host, not in the workflow, the emsdk pin (three months of deploys have used 5.0.7) or this branch. Worth its own dig; it blocks local playground testing for every sample, not just this one.Music is verified running on the native threaded backend only. The single-threaded path is the branch wasm takes, and it is first exercised in CI.
The dupes gate could not run repo-wide: its corpus export fails on
examples/games/sequence/modules/das-cards/*, a locally installed daspkg dependency under a gitignoredmodules/directory, which does not compile in this build configuration and is untouched by this change. Detection was run scoped toexamples/games/river_runinstead.The external review round was run twice. The second round found one real defect, verified against the tree and fixed here: sun-shaft strength was reaching
fs_upas the filter radius rather than as an output intensity, so the per-section setting changed blur width and never brightness, and a strength of zero still added the shaft buffer at full intensity.The full preflight ran once, on the tip carrying the codegen fix; the fast gates were re-run after every commit since, and
tests/glslsweeps clean (116 tests).Claims — stated, not tested
continuenow advances a GLSL range loop is verified in the EMITTED text, not the das source: nested range loops and the shadow-map PCF loops re-emit asfor ( ; i!=_for_range_vi.y; i++ ), and the SSAO pass that found the bug runs its naturalcontinuewithout hanging. No test pins emitted GLSL, so a regression here would surface as a hung GPU in whichever shader next usescontinue, not as a red test.for (T i = lo; i less-than hi; ++i), and SPIR-V branchescontinueto a structured loop's continue block, which is where its increment lives. This is a read, not a probe.Stream::pushshortly after "reload complete".cmake_install.cmake, which carries the four EXCLUDE regexes, not from a full install run.Not done
build_*_richmusic builders are real duplication at ~0.80 similarity: each calls the same voice builders with different literal patterns. A per-section data table would collapse them. Left alone deliberately — it is a refactor of freshly tuned audio, and the result cannot be checked by ear from here.examples/graphics, outside the folder the checklist scopes.GenDirection.yzkeeps its// todo: verifycomment. Its transform is a proper rotation and its winding now tests clean, but no caller uses it, so the axis mapping itself is still unexercised.🤖 Generated with Claude Code