Skip to content

River Run: cinematic render pipeline, examples-page card and playground port - #3886

Merged
borisbat merged 17 commits into
masterfrom
bbatkin/river-run-facelift
Aug 27, 2026
Merged

River Run: cinematic render pipeline, examples-page card and playground port#3886
borisbat merged 17 commits into
masterfrom
bbatkin/river-run-facelift

Conversation

@borisbat

@borisbat borisbat commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Shader codegen change: every GLSL shader in the tree re-emits its range loops. continue inside one now advances the loop instead of hanging the GPU.

Geometry change: gen_plane, gen_cylinder, gen_cone and gen_prism now wind the same way in all three GenDirections. 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 for live_stub, because static requires cannot be switched at runtime.

Where to look: rr_shaders.das and rr_postfx.das are the new render path; modules/dasGlsl/glsl/geom_gen.das and modules/dasGlsl/glsl/glsl_internal.das are the two changes outside the example and the site wiring; river.das holds 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_BACK under 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_cylinder and gen_cone pushed indices in the wrong order. Separately, GenDirection.xz maps the generated xy shape onto xz by swapping two axes — a mirror, determinant negative — which reverses the orientation of every triangle it moves. yz carries an extra tm[2][0] = -1. that makes it a proper rotation; xz never got that, and the // todo: verify beside yz is 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) and gen_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:

generator master after the index fix alone now
gen_cylinder xy, yz inverted xz inverted all correct
gen_cone self-inconsistent in all three xz inverted all correct
gen_plane xz inverted xz inverted all correct
gen_prism xz inverted xz inverted all correct
gen_sphere, gen_cube (controls) correct correct correct

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.dasgen_cylinder(xz), culls GL_BACK. Inside out on master; correct now.
  • examples/daslive/tank_gamegen_plane(xz) + gen_cylinder(xz), culls GL_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/arcanoidgen_plane(xz), but it draws the ground inside a glDisable(GL_CULL_FACE) span, so winding never mattered there.
  • examples/games/river_runxy throughout, plus a gen_plane(xz) that was built and never drawn; dropped.
  • pacman, boulder-dashgen_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_float present 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_name was river-run while the directory, the workflow's verify step, its staging loop and the site card id all say river_run. daspkg names the card from package_name, so CI would have looked for an artifact that was never emitted.
  • web/.gitignore covered the wasm32 output/ and build/ trees but not the output64/ and build64/ 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.das cross-checks all eight and runs per-PR in extended_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 between data.json and _interp.html with 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_static cannot 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 stock gl_01_triangle sample 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 gitignored modules/ directory, which does not compile in this build configuration and is untouched by this change. Detection was run scoped to examples/games/river_run instead.

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_up as 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/glsl sweeps clean (116 tests).

Claims — stated, not tested

  • The claim that continue now 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 as for ( ; i!=_for_range_vi.y; i++ ), and the SSAO pass that found the bug runs its natural continue without hanging. No test pins emitted GLSL, so a regression here would surface as a hung GPU in whichever shader next uses continue, not as a red test.
  • dasMetal and dasSpirv were read and need no matching fix: Metal already emits for (T i = lo; i less-than hi; ++i), and SPIR-V branches continue to a structured loop's continue block, which is where its increment lives. This is a read, not a probe.
  • The audio reload fix is verified by three consecutive reloads, including mid-run, not by a test. A regression would look like an abort in Stream::push shortly after "reload complete".
  • The install-manifest exclusion is verified from the generated cmake_install.cmake, which carries the four EXCLUDE regexes, not from a full install run.

Not done

  • The nine build_*_rich music 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.
  • Smoke density and explosion scale are tuned by eye against a boat at scale 1.0, depot 1.8, bridge 1.6 and player death 2.2. Those ratios are a judgement, not a measurement.
  • The card gate does not check the showcase cards (furier, path_tracer_lab, physarum_lab). They are built from examples/graphics, outside the folder the checklist scopes.
  • GenDirection.yz keeps its // todo: verify comment. 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

borisbat and others added 13 commits August 27, 2026 08:54
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
Copilot AI lite review requested due to automatic review settings August 27, 2026 17:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 for lowering to emit a real for ( ; cond ; i++ ) header so continue advances 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.

Comment thread modules/dasGlsl/glsl/glsl_internal.das
Comment thread examples/games/river_run/river.das
…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
Copilot AI review requested due to automatic review settings August 27, 2026 17:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 36 out of 37 changed files in this pull request and generated 1 comment.

Comment thread web/examples/ui/samples/examples/river_run/main.das
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
Copilot AI review requested due to automatic review settings August 27, 2026 17:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_normal currently calls terrain_height four times. Each terrain_height call can end up scanning river_segments via island_end_distance, which makes island normal generation disproportionately expensive during geometry rebuilds (especially because emit_island computes 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)

Comment thread modules/dasGlsl/glsl/geom_gen.das
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
Copilot AI review requested due to automatic review settings August 27, 2026 17:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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
Copilot AI review requested due to automatic review settings August 27, 2026 18:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 38 out of 39 changed files in this pull request and generated no new comments.

@borisbat
borisbat merged commit 17dedfd into master Aug 27, 2026
38 checks passed
@borisbat
borisbat deleted the bbatkin/river-run-facelift branch August 27, 2026 19:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants