Commit 57d3fb3
* port nauvis_shared to Rust (#226)
Phase 6's first layer: `nauvis_hills`, `nauvis_hills_cliff_level`,
`nauvis_plateaus`, `nauvis_bridge_billows`, `forest_path_billows` and the
domain-warped `nauvis_hills_offset` / `nauvis_cliff_ringbreak` pair, which
`elevation_nauvis`, `aux_nauvis`, `moisture_nauvis` and the cliff field all
read.
Tier 1 grades all four captured fields against
`oracle-cliff-offset-raw.seed123456.json` at two seeds, snapped onto the
1/256 capture grid, scored by exact f32 match count:
| field | seed 123456 | seed 777771 |
| ---------------- | ----------- | ----------- |
| `rawX` | 30/38 | 36/38 |
| `rawY` | 30/38 | 30/38 |
| `hillsOffset` | 29/38 | 31/38 |
| `cliffRingbreak` | 29/38 | 31/38 |
Every one of the eight was measured on the TypeScript side against the same
fixture with the same snap and agrees exactly, so they describe the distance
BOTH ports sit from the game rather than a gap between them.
The two warp seeds are hashed NAMES - `crc32("nauvis_offset_x"/"_y")` - and
are pinned by their own assertion rather than graded through the fields above
them, because a wrong constant seeds a different permutation table and
produces a plausible warp field rather than a broken one.
Two things measured rather than assumed:
- The origin is a genuine FIXED POINT of the warp. `basis_noise` returns
exactly 0 at a lattice corner and (0, 0) is one for both warp fields, so the
displacement is 0 and `cliff_ringbreak(0, 0)` is exactly 0. Pinned, because
it is the one input where a warp wired to the wrong coordinate would agree
with the right one.
- The layer's `poison::f64_result` hook is a BACKSTOP, not the control its
tier-1 test trips. Deleting it leaves that test red under `--features
poison` at 5 of 30 on `rawX`, because every field here composes
`basis_noise` and inherits its hook. Recorded at the hook so its presence is
not read as evidence of an independent control - no test in the crate could
give it one.
`engine.wasm` is unchanged: nothing in the wasm crate reaches this module yet,
so it is dead-code-eliminated.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C1zu6dg6X7VJcfm2qMsMXw
* port amplitude_corrected_multioctave_noise, and fix the persistence operand's width (#226)
`elevation_lakes` and `elevation_nauvis` both read this wrapper, so phase 6
cannot skip it. `fixtures.rs` used to say it was "deliberately NOT ported yet:
porting it now would mean porting a known-wrong model and enshrining its
wrongness in a Rust assertion" - that line is now the opposite of the port's
standing rule, which is to reproduce the TypeScript faithfully so tier 2 stays
honest and to fix a model in a change graded on its own.
It lands at 81/152 with worst 1.788139e-7, both measured on the TypeScript
side against the same fixture. The count is FROZEN rather than bounded, which
is strictly stronger than what the TypeScript asserts (`worst < 2.5e-7`, no
count): a change that moved 30 positions inside that bound would pass there
and fail here.
## The port scored BETTER than the TypeScript, and finding out why moved a
## shipped op
The first working version scored 89/152 at worst 5.960e-8. The cause was not
the wrapper: `variable_persistence_multioctave_noise` took `persistence` as an
**f32** and the TypeScript multiplies its f32 accumulator by an un-narrowed
JavaScript number - `f32(acc * persistence)`.
No fixture in the tree could see that. `oracle-variable-persistence-multioctave`'s
captured `persistenceField` is `0.35 + 0.25 * basis_noise{...}` evaluated in
the noise machine, so all 38 values are exactly f32 and both widths score
266/266 with worst 0. It is the same shape as #191 and #309 - a narrowing the
fixtures agree on because they only ever offer values already on the grid.
`oracle-multioctave-wrappers`'s amplitude-corrected cases DO discriminate,
because they pass the program constant `0.7` straight in and 0.7 is not
f32-exact:
| persistence operand | exact | worst |
| ----------------------------- | ------- | --------- |
| f64 (the TypeScript, and now this) | 81/152 | 1.788e-7 |
| f32 (what shipped here) | 89/152 | 5.960e-8 |
The better-scoring form is NOT the one taken. 89 is an improvement and not a
full exact count, so the greedy-accept rule rejects it as a model change, and
adopting it would put a divergence into every Nauvis elevation value with
nothing to grade it. It is a real finding for #254 all the same: that issue
records the 81/152 as unexplained, and this names one term in it, worth 8
points and still 63 short. Neither form is the game's.
## Two harness compensations removed
- `checksum_variable_persistence` crossed the ABI as an f32, so
`test/wasmMultioctaveParity.spec.ts` narrowed its own value with
`Math.fround` before comparing - making the two sides agree by construction
on exactly the term that differed. Both now use f64. Two of that spec's
cases (0.62, 0.9) are not f32-exact, so the comparison grades the operand
width instead of hiding it: planting the f32 narrowing back into the Rust
turns it red at `p=0.62`, checked rather than claimed.
- `p ** octaves` is `powf`, not `powi`. `powi` disagrees with V8 by one ULP at
0.7^4, 0.7^6 and 0.7^8, and one ULP there flips the f32 rounding of the
octave gain, which moves every point in the case.
`the_geometric_series_matches_v8s_pow_bit_for_bit` pins the raw bits at all
nine (persistence, octaves) pairs this port can reach.
`engine.wasm` 178,531 -> 178,533 bytes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C1zu6dg6X7VJcfm2qMsMXw
* port elevation_lakes and elevation_island to Rust (#226)
`make_0_12like_lakes` plus `finish_elevation` - the two non-Nauvis map types.
`elevation_island` gets no module of its own because it is not a different
expression: it is this tree with `bias = -1000` and the segmentation
quartered, applied by `ElevationLakesParams::to_island`.
Tier 1, snapped onto the capture grid and scored by exact f32 match count:
| tree | exact | worst |
| ------------------ | ----- | ----------- |
| `elevation_lakes` | 21/26 | 3.814697e-6 |
| `elevation_island` | 19/26 | 1.525879e-5 |
Both measured on the TypeScript side against the same fixtures with the same
snap and identical to every printed digit. The counts are FROZEN where
`test/elevationLakes.spec.ts` and `elevationIsland.spec.ts` assert bounds, so
the 4 far-field misses that are #255 stay visible instead of sitting inside a
tolerance.
## The two `20`s are different constants that coincide
Branch 1 of `make_0_12like_lakes` is `bias + varPers1`; branch 2 carries its
own literal `20`. At `elevation_lakes` the bias is 20 as well, so a port that
confused them would agree with the game at every position the lakes fixture
offers. `elevation_island` is what separates them, which is why its fixture is
graded here rather than treated as a duplicate.
## Three auxiliary counts, all frozen at what was measured
Each replaced an assertion that was too strong, and each turned out to state
something about the expression:
- island vs lakes differ at 232 of 256 grid points, not all of them.
`finish_elevation` is a `min` of four terms and only one reads the lakes
branch, so where a starting-lake term is already smallest the bias cannot
show. Both halves are frozen - "everything differs" is false, and "something
differs" would pass for a port that dropped `bias` and got its difference
from the quartered segmentation alone.
- the persistence field saturates its LOWER clamp 430 times and its upper
clamp never. That is the expression, not the grid: `amplitude` is 0.5, so
the corrected field spans about +/-0.25 and `+ 0.3` lands near [0.05, 0.55],
a long way under 0.9.
- `starting_lake_positions: None` derives the game's real lake and an empty
vector means no lake at all. Those are different trees near spawn, and that
is the whole near-spawn band of the fixture.
No poison hook on this layer, for the reason planted at `nauvis_shared`: every
value in the tree composes `basis_noise` and there is no path to any of them
that avoids it, so a hook here could not be given an independent control.
Both tier-1 tests are in `POISONED_TESTS` and both go red, checked.
## Adding an UNREACHABLE module moved `engine.wasm`, and it is not line numbers
178,533 -> 178,479 bytes, 54 SMALLER, with the first differing byte at offset
1,323 and everything after it shifted. Nothing in the wasm crate calls this
module, and the only other change to a non-test file is the `pub mod` line.
That is a third fingerprint to add to the two in CLAUDE.md, and it is not the
panic-location one: no section kept its size and the delta is not a line
count. The sufficient explanation is inlining - `elevation_lakes` is a new
caller of `var_pers_eval`, `quick_sum_octaves` and `min`, all of which the
reachable code also calls, so the cost heuristics for those can change and
re-emit the paths that DO ship.
Checked rather than assumed, both ways: each source rebuilds to its own hash
reproducibly, and all 55 wasm parity tests pass - including tier 3's
byte-identical renders for both planets. So the diff is codegen with no
behavioural effect.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C1zu6dg6X7VJcfm2qMsMXw
* port elevation_nauvis to Rust (#226)
`elevation_nauvis_function(nauvis_hills_plateaus)` - the bridge/macro/detail
mix lifted by the shared layer's plateaus, then cut by the starting-lake term
- plus `elevation_nauvis_no_cliff`, which is the same tree with
`added_cliff_elevation` forced to zero and which `cliff_elevation_nauvis`
depends on.
Tier 1, snapped and scored by exact f32 match count:
| tree | exact | worst |
| --------------------------------- | ----- | ----------- |
| `elevation_nauvis` | 8/26 | 3.852844e-4 |
| `..._no_cliff`, seed 123456 | 6/26 | 3.833771e-4 |
| `..._no_cliff`, seed 777771 | 4/26 | 3.089905e-4 |
All three measured on the TypeScript side against the same fixtures with the
same snap and identical to every printed digit.
8 of 26 is the weakest count in the Nauvis port so far, and it is inherited
rather than new: this tree stacks the shared layer, an amplitude-corrected
persistence field and a variable-persistence detail stack, so it carries every
unported narrowing underneath it at once - #254's 81/152 sits directly in its
persistence term. `test/elevationNauvis.spec.ts` bounds the same quantity at
4e-4 and reports no count at all.
## A game-against-game control for the cliff-elevation flag
Both tier-1 tests call the same struct, so a port that ignored
`with_cliff_elevation` would score against two genuinely different fixtures
and neither test would say the flag was read. The two fixtures sample exactly
the same 26 positions (asserted), so their columns can be compared directly:
the GAME's own two columns differ at 17 of 26, and our two trees differ at the
same 17. Both halves frozen - "all 26 differ" is false, because the outer
`min` against `starting_lake` masks the term near spawn, and "some differ"
would pass for a port that read the flag and got the term wrong.
That test stays GREEN under `--features poison`, which is correct and was
checked rather than predicted: it is a relational assertion, so a perturbation
applies to both sides and cancels. `poison.rs` now records it beside the two
earlier ones of that shape.
## Three traps written down where they can be seen
- `starting_island` is the ONE node that reads the raw
`control:water:frequency` rather than `nauvis_segmentation_multiplier`. The
two differ by 1.5 at every setting including the default, and both names are
in scope at the call site.
- the persistence field runs at `nauvis_seg / 2` while the detail stack it
drives runs at `nauvis_seg / 14`. Swapping them produces a plausible field.
- `nauvis_macro` is `macro_a * max(0, macro_b)`, half-wave rectified. Dropping
the `max` doubles the term's support; frozen at 3,192 of 6,400 grid points
where the rectifier zeroes it.
Unlike `elevation_lakes`, BOTH persistence clamp bounds bite here (1,452 and
1,020 of 3,600) - the window is 0.15 wide against that tree's 0.8, so the same
+/-0.25 field saturates on each side.
`engine.wasm` is unchanged from the previous commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C1zu6dg6X7VJcfm2qMsMXw
* port aux, moisture and temperature to Rust (#226)
The three climate expressions. With an elevation they are what the tile
catalog argmaxes over, so all three have to land before any Nauvis tile can be
resolved.
Tier 1, snapped and scored by exact f32 match count:
| expression | exact | worst |
| --------------------------------- | ----- | ----------- |
| `aux` (= `aux_nauvis`) | 14/26 | 5.960464e-8 |
| `moisture` (= `moisture_nauvis`) | 18/26 | 5.960464e-8 |
| `temperature` (= `temperature_basic`) | **26/26** | **0** |
All three match `test/aux.spec.ts`, `moisture.spec.ts` and
`temperature.spec.ts` exactly.
`temperature` is BIT-EXACT, and asserted as such rather than bounded. It is
the shallowest expression in the Nauvis port - one `quick_multioctave_noise`
and a clamp, with no composed layer beneath it - so it is the one field here
that reaches the game exactly. Read the other two beside it: both are one
`nauvis_plateaus` away from it, and both land at one f32 ULP.
## One module for three expressions, because `aux.rs` cannot exist on Windows
`aux` is a reserved device name there, so a file by that name cannot be
checked out at all. The port otherwise mirrors `src/noise/expressions/` 1:1;
this is the one place it cannot. `temperature_basic` is not Nauvis-specific
either - Nauvis is just the only planet in this port that reaches it - so if a
second one does, move it rather than duplicating it.
## Three properties no oracle fixture can grade
- **`moisture`'s starting-area levers are degenerate at their defaults.**
`slider_to_linear(1, -0.5, 0.5)` is 0, so the whole blend collapses to
`base_bias` everywhere and the fixture exercises none of it. Asserted as a
recorded property, with a moved slider proving the blend reaches the output
near spawn and provably does not far away.
- **The cutout is a `min` of three fields**, and a `min` of three is
indistinguishable from a `min` of two wherever the third never wins. Frozen
at the measured split: bridge billows win 541, hills 993, forest-path
billows 966 of 2,500.
- **The biases are user controls no fixture moves off zero.** They shift the
field by exactly the bias at 1,501 of 1,600 grid points; the other 99 are a
clamp saturating, which is why the identity is stated narrowly instead of as
`min(base + bias, hi)`. That distinction was found by the test failing, not
by reading the code.
`engine.wasm` is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C1zu6dg6X7VJcfm2qMsMXw
* add tier 2 for the Nauvis expression core (#226)
`NauvisStack` builds every Nauvis expression from one `NauvisCtx`, and carries
the tier-2 field selector over the 16 named fields: the shared layer's nine,
the four elevation trees, and the three climate expressions.
`checksum_nauvis` folds one of them over a swept grid;
`test/wasmNauvisParity.spec.ts` folds the same field through the shipped
TypeScript and compares by strict bit equality.
It lands NOW rather than at the end of phase 6 because it has a shelf life:
it compares Rust against TypeScript and #227 deletes the TypeScript, so it can
only ever be written while both exist.
## The off-grid sweep is load-bearing, and that was planted
Every tier-3 window uses a binary origin and step, so all of its coordinates
are f32-exact and a narrowing difference is invisible there by construction -
which is how #309 survived three shipped PRs. This spec's steps are not binary
fractions: 1,430 of its 1,452 sampled positions have at least one coordinate
off the f32 grid, frozen, with two tier-3-shaped windows asserted at 0 as the
control.
Planting a pure coordinate narrowing in `hills_offset_raw_x` -
`f64::from(x as f32) * scale` for `x * scale`, a no-op wherever `x` is already
f32-exact:
| tier | result |
| ------------------------------------------------- | ------ |
| 1 (`oracle-cliff-offset-raw`, all coords f32-exact) | GREEN |
| 2 (this spec) | RED at `hillsOffsetRawX` |
So this is not a second opinion on what tier 1 covers; it is the only thing in
the gate that can see that class of change on Nauvis.
## Two departures from the Vulcanus shape, both deliberate
- **The parameters cross as ARGUMENTS, not as a request.** Vulcanus reads a
request so the module builds its stack through the same `render::vulcanus_*`
helpers the renderer uses, putting a mis-wired bearing inside the
comparison. Nauvis has no render path yet, so there is no shared wiring to
enclose, and a request block would be an ABI commitment made before the
renderer that must live with it exists. Recorded at the export so it gets
revisited when the render path lands.
- **No trig crosses the boundary**, and for once that is free rather than
arranged: Nauvis reaches no transcendental at all, so #270's hazard does not
apply. `moisture` calls `slider_to_linear`, which is f32 per operation and
graded by its own tier-1 test. Noted so a future field that reaches `pow`,
`log2`, `sin` or `cos` gets the value passed in instead.
`NauvisStack` is one struct, not two: nothing in this chain borrows a layer
beneath it, because each expression derives its own `NauvisShared` - which is
what the TypeScript does, and is bit-identical since the layer is a pure
function of `(seed0, segmentation)`.
Three cargo tests guard the selector itself: every field index must produce a
distinct sweep (a `match` arm pointing at the wrong accessor yields a valid
checksum for a field nobody named), the catch-all must start exactly where
`FIELD_COUNT` says, and each of the eleven controls must reach the expression
it names. The last found two masking effects worth recording - `water_level`
is invisible inside the starting island, and the starting-area moisture
frequency does nothing at a distance where both settings saturate its region
clamp.
`engine.wasm` 178,479 -> 199,147 bytes. Full gate green: 2,159 tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C1zu6dg6X7VJcfm2qMsMXw
* record phase 6's expression core in CLAUDE.md (#226)
The phase-by-phase record, kept incrementally the way phases 3-5 were. Covers
what landed and what has not, the nine frozen tier-1 counts with the reading
that makes them useful (`temperature` is bit-exact because nothing composes
beneath it; `elevation_nauvis` is weakest because everything does), the
persistence-width finding and why the better-scoring form was rejected, the
two harness compensations removed with it, tier 2's off-grid sweep and the
plant that proves it load-bearing, the `aux.rs`-on-Windows constraint, the
third `engine.wasm` diff fingerprint, and why no phase-6 layer carries its own
poison hook.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C1zu6dg6X7VJcfm2qMsMXw
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 4123136 commit 57d3fb3
15 files changed
Lines changed: 3189 additions & 16 deletions
File tree
- crates
- fmw-noise/src
- expressions
- fmw-wasm/src
- scripts
- src/noise/wasm
- test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2464 | 2464 | | |
2465 | 2465 | | |
2466 | 2466 | | |
| 2467 | + | |
| 2468 | + | |
| 2469 | + | |
| 2470 | + | |
| 2471 | + | |
| 2472 | + | |
| 2473 | + | |
| 2474 | + | |
| 2475 | + | |
| 2476 | + | |
| 2477 | + | |
| 2478 | + | |
| 2479 | + | |
| 2480 | + | |
| 2481 | + | |
| 2482 | + | |
| 2483 | + | |
| 2484 | + | |
| 2485 | + | |
| 2486 | + | |
| 2487 | + | |
| 2488 | + | |
| 2489 | + | |
| 2490 | + | |
| 2491 | + | |
| 2492 | + | |
| 2493 | + | |
| 2494 | + | |
| 2495 | + | |
| 2496 | + | |
| 2497 | + | |
| 2498 | + | |
| 2499 | + | |
| 2500 | + | |
| 2501 | + | |
| 2502 | + | |
| 2503 | + | |
| 2504 | + | |
| 2505 | + | |
| 2506 | + | |
| 2507 | + | |
| 2508 | + | |
| 2509 | + | |
| 2510 | + | |
| 2511 | + | |
| 2512 | + | |
| 2513 | + | |
| 2514 | + | |
| 2515 | + | |
| 2516 | + | |
| 2517 | + | |
| 2518 | + | |
| 2519 | + | |
| 2520 | + | |
| 2521 | + | |
| 2522 | + | |
| 2523 | + | |
| 2524 | + | |
| 2525 | + | |
| 2526 | + | |
| 2527 | + | |
| 2528 | + | |
| 2529 | + | |
| 2530 | + | |
| 2531 | + | |
| 2532 | + | |
| 2533 | + | |
| 2534 | + | |
| 2535 | + | |
| 2536 | + | |
| 2537 | + | |
| 2538 | + | |
| 2539 | + | |
| 2540 | + | |
| 2541 | + | |
| 2542 | + | |
| 2543 | + | |
| 2544 | + | |
| 2545 | + | |
| 2546 | + | |
| 2547 | + | |
| 2548 | + | |
| 2549 | + | |
| 2550 | + | |
| 2551 | + | |
| 2552 | + | |
| 2553 | + | |
| 2554 | + | |
| 2555 | + | |
| 2556 | + | |
| 2557 | + | |
| 2558 | + | |
| 2559 | + | |
| 2560 | + | |
| 2561 | + | |
| 2562 | + | |
| 2563 | + | |
| 2564 | + | |
| 2565 | + | |
| 2566 | + | |
| 2567 | + | |
| 2568 | + | |
| 2569 | + | |
| 2570 | + | |
| 2571 | + | |
| 2572 | + | |
| 2573 | + | |
| 2574 | + | |
| 2575 | + | |
| 2576 | + | |
| 2577 | + | |
| 2578 | + | |
2467 | 2579 | | |
2468 | 2580 | | |
2469 | 2581 | | |
| |||
0 commit comments