Commit 73da5c9
* Port the Vulcanus cliff stack to Rust, graded end to end (#225)
The engine-generic cliff machinery plus Vulcanus's two cliff fields and its
ore rejection. Nothing renders through this yet - the ABI and the render path
are the next commit - so `engine.wasm` is unchanged apart from two panic-location
line numbers (see below).
What landed:
- `cliffs/catalog.rs` - the placement grid, the 20 placing codes, the code ->
orientation bijection, and the per-orientation collision boxes. The boxes ship
as quantised literals with `rotbb_box` kept live beside them and a test
asserting the two agree bit-for-bit, so the render path does no floating-point
rounding for them while the derivation stays checkable against the Lua.
- `cliffs/placement.rs` - `crossesCliff`, the per-chunk `fixImpossibleCells`
sweep including its self-set retry flag, the `cliff_smoothing` knot model, and
the chunk-structured enumeration that keeps worker tiling byte-identical.
- `cliffs/connections.rs` - `Cliff::updateConnections` and `onDestroy`. Not on
any render path; it is the model #84's investigation is scored with, ported so
that investigation can run against the engine too.
- `cliffs/vulcanus_fields.rs` - `cliffiness_basic` and the cliff-channel
elevation, plus the lava tile gate.
- `cliffs/vulcanus_ore_rejection.rs` and `resources/vulcanus_catalog.rs` - the
ORE -> CLIFF removal and the solid-ore footprint it asks about.
Tier 1, against the game's own cliff entities across three regions:
| arm | game | ours | matched | orientation |
| --- | ---: | ---: | ---: | ---: |
| lava only | 1569 | 1570 | 1525 | 1492 |
| shipping | 1569 | 1547 | 1525 | 1504 |
All 24 of those numbers were measured on the TypeScript side with the same two
arms against the same fixture and agree exactly, so they describe the distance
both ports sit from the game rather than a gap between them. Because the
orientation column agrees too, the ports produce the same cell CODES and not
merely the same positions. The lava-only rows also reproduce the figures
`vulcanusCliffEntities.spec.ts` publishes in its own header.
The ore rule removes 23 cells, none of them a cliff the game kept, and turns 12
wrong orientations right - wrong orientations go 33 -> 21, which is exactly what
`renderVulcanusCliffs.ts` records having measured, reached here through a
separate implementation.
`cliffiness` is exact at all 12,675 captured corners. The fixture's `elevation`
column is the TILE channel, so grading `cliff_elevation` against it is a
category error worth 60.6 tiles - that is issue #83, and the test now asserts
the two grids DISAGREE at 2,519 of the corners rather than leaving it a comment.
Three poison hooks, because three ops here can be wrong independently:
`crossing_result` for the tri-state crossing, `sweep_order` for a pass with no
value to bend at all, and the existing `f64_result` / `bool_result`. Under
poison the crossing hook moves every edge in the lattice, so the end-to-end test
would be red whether or not the sweep had a control - hence its own test in
`POISONED_TESTS`.
`engine.wasm` changes by exactly two bytes and they are both a
`core::panic::Location` line number - the `RefCell` borrow sites in
`vulcanus_resources.rs`, 427 -> 436 and 469 -> 478, because `OreRegions` added
nine lines above them. Every wasm section keeps its exact size and no code byte
moves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DUQvbMXKFerAcSJrYt1MXj
* Render Vulcanus cliffs through the engine, on a per-planet ABI field (#225)
The `cliffs` view now dispatches to WebAssembly. `rocks`, `resources` and `all`
still take the TypeScript path, and the parity spec asserts that rather than
assuming it.
**ABI: the Vulcanus block grows 248 -> 280 bytes with NO version bump.** That is
the per-planet split working rather than a shortcut - the prefix declares its own
block length, `BadParamsLength` refuses a writer that disagrees, and Fulgora's
request did not move a byte. A version bump is for a change to the COMMON prefix,
which every planet reads.
The new field is the cliff `cell_query_box`, four f64, and it is **sent rather
than derived**. The halo is asymmetric, its two directions CROSS - a mark
reaching far backwards has to be caught from ahead of the tile - and it needs the
FULL image's geometry, which the prefix does not carry and only the tiled
renderer knows. Keeping the arithmetic on the TypeScript side keeps it in the one
place `test/tiledEquality.spec.ts` already guards.
`cliffs` is sent as ONE request rather than two because the overlay has nothing
to draw on its own and the two passes share the whole field DAG below the tile
argmax; splitting it would build that chain twice.
Tier 3, in `test/wasmVulcanusRenderParity.spec.ts`:
- byte-identical against the TypeScript across the same four windows terrain
uses, which vary every geometry field independently;
- cliff pixels painted per window, frozen per window rather than bounded, and
counted only where the overlay actually CHANGED the terrain render - a cliff
pixel that was already that colour proves nothing;
- tiled equals whole THROUGH THE ENGINE, with a no-halo arm that must differ.
That arm runs at 8 tiles/px on purpose: at 1 tile/px the 4px block sits on a
4px lattice and a 32px seam is a multiple of 4, so blocks never straddle and
the test would pass with the halo doing nothing.
`test/fixtures/verify-wasm-request.py` - the third implementation, not the writer
under test - grew five planted breaks for the new field, all confirmed caught by
its per-edge value check. Its two structural checks (four distinct edges, not
inverted) constrain the FIXTURE rather than catching a break, and the file says
so rather than claiming credit for the five.
**The benchmark both layers' own docs asked for.** `vulcanus_resources` and
`vulcanus_biomes` each said "nothing on the render path reaches this layer yet,
so it is correct-first by choice" and named the measurement to take when that
stopped being true. The ore rejection now reaches both. Measured at 256x256,
1 tile/px, min of 5 after a warm pass, three runs agreeing:
| arm | terrain | cliffs | overlay |
| --- | ---: | ---: | ---: |
| TypeScript | 33.10 us/px | 42.41 us/px | 1.28x |
| WASM | 8.64 us/px | 9.52 us/px | 1.10x |
The un-memoized chain costs proportionally LESS here than the memoized one does
there, because the cliff pass walks a 4-tile lattice rather than every pixel - a
few thousand evaluations against the terrain sweep's 65,536. Read the RATIOS and
not the microseconds: those absolutes are from inside vitest, where the
TypeScript arm pays #267's transform and the WASM arm does not, and
`vulcanus-cliffs-NOTES.md` measures the same TypeScript terrain view at
12.68 us/px outside it. A ratio between the arms would be measuring the harness.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DUQvbMXKFerAcSJrYt1MXj
* Grade the ported connection model against the game (#225)
`cliffs::connections` landed with unit tests and no measurement against
anything. It is on no render path - it is the model #84's investigation is
scored with - so nothing was checking that 445 lines of `Cliff::updateConnections`
and `onDestroy` reproduce the behaviour they transcribe.
This runs the same three arms `test/cliffConnections.spec.ts` runs, over the same
fixture, scored on ORIENTATION against the game's own cliffs:
| model | matched | wrong | surplus | missing |
| --- | ---: | ---: | ---: | ---: |
| `reject_at_crossing_stage` (ships) | 1504 | 21 | 22 | 6 |
| `applyCliffs`, lava + ore | 1508 | 18 | 22 | 5 |
| `applyCliffs`, no cascade | 1500 | 25 | 22 | 6 |
All twelve numbers were written from the TypeScript spec's own header BEFORE
running, and all twelve matched on the first run - so `destroy_end`,
`is_cliff_connected`, the `onDestroy` cascade and the chunk-border gate are
graded rather than asserted.
The no-cascade row is what makes the middle row mean something: without it
"the apply stage is better" would not distinguish the cascade from the
re-staging. The relations are asserted as well as the counts, so the claim
survives a re-measure that moves every row.
**It is the most expensive test in the crate and `verify:rust` is no longer the
cheapest job in the workflow.** Measured: 33s normally, 93s under poison, taking
the script from a few seconds to 1m50s. Poison is the expensive half because
`crossing_result` turns every lattice edge into a crossing, so far more cells
place and the cascade recurses over a dense set. Recorded at the POISONED_TESTS
entry and in CLAUDE.md, whose "19s, the cheapest job" line expired here. Still
far under the 300s+ test shards, so it does not move the gate wall.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DUQvbMXKFerAcSJrYt1MXj
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent da0a888 commit 73da5c9
26 files changed
Lines changed: 4025 additions & 74 deletions
File tree
- crates
- fmw-noise/src
- cliffs
- expressions
- resources
- fmw-wasm/src
- scripts
- src/noise
- preview
- wasm
- test
- fixtures
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
555 | 555 | | |
556 | 556 | | |
557 | 557 | | |
558 | | - | |
559 | | - | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
560 | 573 | | |
561 | 574 | | |
562 | 575 | | |
| |||
1409 | 1422 | | |
1410 | 1423 | | |
1411 | 1424 | | |
1412 | | - | |
1413 | | - | |
| 1425 | + | |
| 1426 | + | |
| 1427 | + | |
| 1428 | + | |
| 1429 | + | |
| 1430 | + | |
| 1431 | + | |
| 1432 | + | |
| 1433 | + | |
| 1434 | + | |
| 1435 | + | |
| 1436 | + | |
| 1437 | + | |
| 1438 | + | |
| 1439 | + | |
| 1440 | + | |
| 1441 | + | |
| 1442 | + | |
| 1443 | + | |
| 1444 | + | |
1414 | 1445 | | |
1415 | 1446 | | |
1416 | | - | |
1417 | | - | |
1418 | | - | |
1419 | | - | |
1420 | | - | |
1421 | | - | |
| 1447 | + | |
| 1448 | + | |
| 1449 | + | |
| 1450 | + | |
| 1451 | + | |
| 1452 | + | |
| 1453 | + | |
| 1454 | + | |
| 1455 | + | |
| 1456 | + | |
| 1457 | + | |
| 1458 | + | |
| 1459 | + | |
| 1460 | + | |
| 1461 | + | |
| 1462 | + | |
| 1463 | + | |
| 1464 | + | |
| 1465 | + | |
| 1466 | + | |
| 1467 | + | |
| 1468 | + | |
| 1469 | + | |
| 1470 | + | |
| 1471 | + | |
| 1472 | + | |
| 1473 | + | |
| 1474 | + | |
| 1475 | + | |
| 1476 | + | |
| 1477 | + | |
| 1478 | + | |
| 1479 | + | |
| 1480 | + | |
| 1481 | + | |
| 1482 | + | |
| 1483 | + | |
| 1484 | + | |
| 1485 | + | |
| 1486 | + | |
| 1487 | + | |
| 1488 | + | |
| 1489 | + | |
| 1490 | + | |
| 1491 | + | |
| 1492 | + | |
| 1493 | + | |
| 1494 | + | |
| 1495 | + | |
| 1496 | + | |
| 1497 | + | |
| 1498 | + | |
| 1499 | + | |
| 1500 | + | |
| 1501 | + | |
| 1502 | + | |
| 1503 | + | |
| 1504 | + | |
| 1505 | + | |
| 1506 | + | |
| 1507 | + | |
1422 | 1508 | | |
1423 | 1509 | | |
1424 | 1510 | | |
| |||
1931 | 2017 | | |
1932 | 2018 | | |
1933 | 2019 | | |
| 2020 | + | |
| 2021 | + | |
| 2022 | + | |
| 2023 | + | |
| 2024 | + | |
| 2025 | + | |
| 2026 | + | |
| 2027 | + | |
| 2028 | + | |
| 2029 | + | |
| 2030 | + | |
| 2031 | + | |
| 2032 | + | |
| 2033 | + | |
| 2034 | + | |
| 2035 | + | |
| 2036 | + | |
| 2037 | + | |
| 2038 | + | |
| 2039 | + | |
| 2040 | + | |
1934 | 2041 | | |
1935 | 2042 | | |
1936 | 2043 | | |
| |||
0 commit comments