Skip to content

Commit da0a888

Browse files
wormeymanclaude
andauthored
Render Vulcanus terrain through the Rust engine (#225) (#305)
* Port Vulcanus resources and the 19-tile catalog (#225) Phase 5's expression chain stopped at elevation. This adds the two layers above it - `vulcanus_resources` and `tiles/vulcanus_catalog` - plus the `vulcanus_stack` that binds the whole Vulcanus graph, and `range_select_base` in `eval/math`, which the rock port will read from there rather than restate. **Every count agrees with the TypeScript, measured rather than assumed.** All 15 resource fields were scored on both sides against the same fixture with the same 1/256 capture-grid snap, and they match on the count AND on the worst residual to every printed digit. So the frozen numbers describe the distance both ports sit from the game, not a gap between them. The four starting spots are the load-bearing agreement: 1082, 974, 969 and 1049 of 1085 are the only counts `test/vulcanusResources.spec.ts` freezes rather than bounds, landed with #279, and this port reproduces all four without having seen them. Tile placement lands the same way - 374 of 381 at the forced surface seed and 368 of 381 at a real save's, both matching the TypeScript exactly, with the raw map seed scoring 37 as the control. That contrast is the point of the second test: without it, 368 of 381 would look like a strong result for a port that ignored the seed derivation entirely. The TypeScript asserts `agreement > 0.978` on the first of those. A bound that wide cannot see a change worth six tiles, so this side freezes the count - #162's pathology is exactly what the port exists to stop inheriting. Two things the anti-vacuity work caught rather than confirmed: - The all-zero field state has **no tie**, so the tie-break test built on it would have proven nothing. Its own assertion said so, and the state was rebuilt from expressions that genuinely collapse onto each other. - A numeric hook cannot reach the argmax, so `resolve_tile` carries `poison::index_result` rather than `f64_result` - the same hook Fulgora's argmax needed for the same reason. All four new tile tests were watched going red under `--features poison`. `engine.wasm` is byte-identical: nothing exported reaches these layers yet, so they are dead code in the wasm build. The render wiring is the next step. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A6kcDigVgZYxzCa5GNZeP9 * Render Vulcanus terrain through the engine, on a per-planet ABI (#225) The v1 request header was one fixed 104-byte struct with Fulgora's island sliders and four trig values baked into it. Vulcanus needs 31 more `f64` - three volcanism and temperature levers, eight resource-control values, and twenty trig values for its ten per-render bearings. v2 splits it instead of growing it: a 56-byte common prefix, then a block whose length the prefix declares. v1's `reserved` word becomes `params_bytes`, which is what its own comment said it was for, and its `ReservedNotZero` status becomes `BadParamsLength` - same slot, same code, same job of refusing a writer that disagrees with its own version word. **A Fulgora request is still exactly 104 bytes**, and Nauvis gets a third block in phase 6 with no further bump. Tier 3 holds both ways: - WASM against the TypeScript is byte-identical across four windows that vary width, height, origin and tiles-per-pixel independently. - WASM against the game's 1024x1024 PNG differs on **12,423 of 929,686** compared pixels - 98.664%, which is the TypeScript's own number to four decimal places, arrived at through a separate path. That is asserted as an EXACT count rather than the 2% bound `previewAgreement.spec.ts` uses, because byte-identity means it can be. The seed trap has its own arm: rendering at the raw map seed instead of `mapSeed + crc32("vulcanus")` scores an order of magnitude worse, so the derivation is load-bearing rather than incidental. **The round-trip fixture's independent checker was measured missing a real defect, and that is why it grew a third check.** `verify-wasm-request.py` is a third implementation - not the TypeScript writer, not the Rust reader - and it cannot reproduce the trig VALUES, because those are V8's `Math.sin` and a second libm is exactly the disagreement #270 measured. Checking each pair for unit norm caught a shifted block but **passed a planted swap of two bearings** - the failure that renders a plausible planet with its biomes rotated. It now also recovers each angle with `atan2` and checks it against the offset the game's Lua gives it. Seven planted breaks are caught, up from four. **The size tripwire fired at 138,642 bytes and was measured before it moved**, which is what its own comment asks for. Stubbing the Vulcanus dispatch arm dead-code-eliminates the whole chain: the ABI change alone is +2,072 bytes and the ported math is +52,399. Nothing unaccounted for got linked in, so the ceiling goes to 256 KB - the same 1.8x headroom 128 KB kept over phase 3. Only the bare `terrain` view routes to the module. Vulcanus's rock, cliff and resource overlays are still TypeScript, so the composite views keep the TypeScript path, and a test asserts that rather than assuming it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A6kcDigVgZYxzCa5GNZeP9 * Record phase 5's state and the ABI v2 split in CLAUDE.md Vulcanus now renders through the engine, so the paragraph saying its chain stops at elevation is out of date. Also records what the round-trip fixture's independent checker was measured missing - a property check is not a structural check, and a planted bearing swap is the cheap way to find out which one you wrote. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A6kcDigVgZYxzCa5GNZeP9 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ba8fbd8 commit da0a888

21 files changed

Lines changed: 4016 additions & 283 deletions

CLAUDE.md

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,15 +1327,37 @@ all 41 named fields against `oracle-fulgora-{shared,cells,elevation}` plus
13271327
`oracle-fulgora-tiles` - 5,057 tiles the game actually placed. Tier 2 folds all
13281328
42 fields at two slider settings. The CSP change (#222) has landed.
13291329

1330-
Part 2 added the boundary: `crates/fmw-wasm/src/abi.rs` (a 104-byte fixed
1331-
request header, little-endian, with a magic word, a version word and a reserved
1332-
word that is asserted zero), `render.rs`, and `src/noise/wasm/{request,engine}.ts`.
1333-
**Tier 3 is byte-identical RGBA** against `renderFulgoraLandMask` across four
1334-
windows that vary width, height, origin, tiles-per-pixel and both sliders
1335-
independently. `test/fixtures/wasm-request.v1.json` pins the encoding; it is
1330+
Part 2 added the boundary: `crates/fmw-wasm/src/abi.rs`, `render.rs`, and
1331+
`src/noise/wasm/{request,engine}.ts`. **Tier 3 is byte-identical RGBA** against
1332+
`renderFulgoraLandMask` across four windows that vary width, height, origin,
1333+
tiles-per-pixel and both sliders independently.
1334+
1335+
**The request layout is at ABI v2 and is now per-planet** (#225). v1 was one
1336+
fixed 104-byte struct with Fulgora's two island sliders and four trig values
1337+
baked into it; v2 is a 56-byte common prefix followed by a block whose length
1338+
the prefix declares. v1's `reserved` word became `params_bytes` - what its own
1339+
comment said it was for - and its `ReservedNotZero` status became
1340+
`BadParamsLength`. A Fulgora request is still exactly 104 bytes; a Vulcanus one
1341+
is 304, most of that being ten `(sin, cos)` pairs against Fulgora's two. Nauvis
1342+
gets a third block in phase 6 with no further version bump.
1343+
1344+
`test/fixtures/wasm-request.v2.json` pins the encoding for both planets. It is
13361345
declared under `notFixtures` because it is our own ABI rather than Factorio
1337-
ground truth, and its bytes were checked by an independent Python decode rather
1338-
than by re-running the writer under test.
1346+
ground truth, and its bytes were checked by
1347+
`test/fixtures/verify-wasm-request.py` - a third implementation, not the writer
1348+
under test - which is committed beside it so a future version is re-verified the
1349+
same way rather than regenerated from the encoder.
1350+
1351+
**That checker was measured MISSING a real defect, which is why it has three
1352+
trig checks and not one.** It cannot reproduce the trig VALUES, because those are
1353+
V8's `Math.sin` after an f32 narrowing and a second libm is exactly the
1354+
disagreement #270 measured. Checking each pair for `sin^2 + cos^2 = 1` catches a
1355+
shifted or half-shifted block - and **passed a planted swap of two bearings**,
1356+
which is the failure that renders a plausible planet with its biomes rotated. It
1357+
now also recovers each angle with `atan2` and checks it against the offset the
1358+
game's Lua gives it from the ashlands bearing. Seven planted breaks are caught,
1359+
up from four. A property check is not a structural check; this is the cheap way
1360+
to find out which one you wrote.
13391361

13401362
Errors return a **status code and do not trap**, because a trap would poison the
13411363
instance for every later request in that worker; a spec sends a bad magic and
@@ -1380,13 +1402,40 @@ been red whether or not the argmax had a control at all. `POISONED_TESTS` now
13801402
carries FULL test paths rather than bare `fixtures::` names, so a control can
13811403
live beside its op.
13821404

1383-
**Phase 5 (#225) ports Vulcanus, and its EXPRESSION chain down to elevation is
1384-
in.** Landed: `vulcanus_helpers`, `vulcanus_cracks`, `vulcanus_climate`,
1405+
**Phase 5 (#225) ports Vulcanus, and the planet now RENDERS through the
1406+
engine.** Landed: `vulcanus_helpers`, `vulcanus_cracks`, `vulcanus_climate`,
13851407
`vulcanus_spawn`, `vulcanus_biomes`, `vulcanus_elevation`, plus
1386-
`vulcanus_temperature` on the elevation module. `vulcanus_shared` needed no
1387-
port - it is `starting_spot_at_angle`, done in #279 - and `vulcanus_seed`
1388-
landed in phase 2. Still out: `vulcanus_resources`, `tiles/vulcanus_catalog`,
1389-
and the cliff, resource and rock stacks.
1408+
`vulcanus_temperature` on the elevation module; then `vulcanus_resources`,
1409+
`tiles/vulcanus_catalog`, `vulcanus_stack`, and the `terrain` render path
1410+
behind ABI v2. `vulcanus_shared` needed no port - it is
1411+
`starting_spot_at_angle`, done in #279 - and `vulcanus_seed` landed in phase 2.
1412+
Still out: the cliff, resource and rock OVERLAY stacks, so every composite view
1413+
keeps the TypeScript path and a test asserts that rather than assuming it.
1414+
1415+
**Tier 3 for Vulcanus** (`test/wasmVulcanusRenderParity.spec.ts`) is
1416+
byte-identical against the TypeScript across four windows, and **12,423 of
1417+
929,686** compared pixels against the game's own 1024x1024 PNG - 98.664%, which
1418+
is the TypeScript's own number to four decimal places, reached through a
1419+
separate path. It is asserted as an EXACT count where
1420+
`previewAgreement.spec.ts` uses a 2% bound, because byte-identity means it can
1421+
be.
1422+
1423+
**`vulcanus_stack` is TWO structs, and that is ownership rather than taste.**
1424+
`VulcanusBiomes`, `VulcanusElevation` and `VulcanusResources` all borrow the
1425+
layers beneath them, so one struct owning the whole graph would be
1426+
self-referential. `VulcanusBase` owns everything that owns its data; the biome
1427+
layer is a named local because two layers borrow it; `VulcanusStack` holds the
1428+
rest. Three lines of construction instead of one, and honest about it.
1429+
1430+
**Every one of the 20 frozen counts this phase added agrees with the
1431+
TypeScript**, measured on both sides against the same fixtures with the same
1432+
1/256 capture-grid snap - the same count AND the same worst residual to every
1433+
printed digit. The four starting spots are the load-bearing agreement: 1082,
1434+
974, 969 and 1049 of 1085 are the only counts `test/vulcanusResources.spec.ts`
1435+
freezes rather than bounds, and the port reproduced all four without having seen
1436+
them. Tile placement is 374 of 381 at the forced surface seed and 368 at a real
1437+
save's, both matching the TypeScript, with the raw map seed scoring 37 as the
1438+
control.
13901439

13911440
Tier 1 grades **24 named fields** across six fixtures. Every count was measured
13921441
again on the TypeScript side against the same fixture and all 24 agree, so they

crates/fmw-noise/src/eval/math.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,26 @@ pub fn max2(a: f64, b: f64) -> f64 {
169169
max(&[a, b])
170170
}
171171

172+
/// `range_select_base` (`core/prototypes/noise-functions.lua`): select a
173+
/// `from`-`to` range, at or above 0 inside it and descending outside when `min`
174+
/// is negative.
175+
///
176+
/// ```text
177+
/// clamp(min(input - from, to - input) / slope, min, max)
178+
/// ```
179+
///
180+
/// The TypeScript keeps this in `src/noise/rocks/rockCatalog.ts`, where it
181+
/// arrived first. It is a core noise function rather than a rock one - the
182+
/// Vulcanus tile catalog is its heaviest consumer - so it lives here, and the
183+
/// rock port will read it from here rather than restating it.
184+
///
185+
/// `min2` rather than `f64::min` for the reason [`min2`] records: this feeds an
186+
/// argmax whose tie-break is order-sensitive.
187+
#[must_use]
188+
pub fn range_select_base(input: f64, from: f64, to: f64, slope: f64, lo: f64, hi: f64) -> f64 {
189+
clamp(min2(input - from, to - input) / slope, lo, hi)
190+
}
191+
172192
/// Base-2 log (the DSL's `log2`).
173193
///
174194
/// Exact math, not `fast_approx::fast_log2`. The noise machine's own `log2` IS

crates/fmw-noise/src/expressions/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,7 @@ pub mod vulcanus_climate;
2020
pub mod vulcanus_cracks;
2121
pub mod vulcanus_elevation;
2222
pub mod vulcanus_helpers;
23+
pub mod vulcanus_resources;
2324
pub mod vulcanus_seed;
2425
pub mod vulcanus_spawn;
26+
pub mod vulcanus_stack;

0 commit comments

Comments
 (0)