Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 107 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1202,7 +1202,7 @@ Field labels carry in-game tooltip text via `FInfo` (an `info` prop on
`EnemyValueRow`, an `info:` entry in `controlCatalog.ts` for the enemy-base
autoplace rows).

### The Rust/WASM noise engine (`crates/`) - phases 1-3 expressions landed
### The Rust/WASM noise engine (`crates/`) - phases 1-4 landed, phase 5 in progress

A Cargo workspace at the repository root, landed empty on purpose (#219) so the
gate was proven green on `main` before any port code depended on it. Two crates:
Expand Down Expand Up @@ -1294,6 +1294,112 @@ been red whether or not the argmax had a control at all. `POISONED_TESTS` now
carries FULL test paths rather than bare `fixtures::` names, so a control can
live beside its op.

**Phase 5 (#225) ports Vulcanus, and its EXPRESSION chain down to elevation is
in.** Landed: `vulcanus_helpers`, `vulcanus_cracks`, `vulcanus_climate`,
`vulcanus_spawn`, `vulcanus_biomes`, `vulcanus_elevation`, plus
`vulcanus_temperature` on the elevation module. `vulcanus_shared` needed no
port - it is `starting_spot_at_angle`, done in #279 - and `vulcanus_seed`
landed in phase 2. Still out: `vulcanus_resources`, `tiles/vulcanus_catalog`,
and the cliff, resource and rock stacks.

Tier 1 grades **24 named fields** across six fixtures. Every count was measured
again on the TypeScript side against the same fixture and all 24 agree, so they
are the distance BOTH ports sit from the game rather than a gap between them.

**Four things this phase measured that are worth more than the counts:**

- **A second, independent fixture pointed at #269, and #269 has since landed.**
`hairline_cracks` is the shallowest expression in its layer - a bare `plasma`,
nothing composed on top - so its weakness could not come from the crack file.
`plasma` subtracts two `basis_noise_expr` results, and that adapter returned
the un-narrowed f64 product. Fixed in `df3e39e`, and this branch re-scored
against it:

| field | before | after | exposed? |
| -------------------- | ------- | ----------- | ----------------------------- |
| `hairlineCracks` | 3/61 | **2/61** | directly, at output scale 0.6 |
| `floodCracksA` | 15/61 | 15/61 | no |
| `floodCracksB` | 40/61 | 40/61 | no |
| `floodPaths` | 10/61 | 10/61 | no |
| `floodBasaltsFunc` | 8/61 | **9/61** | via `hairline_cracks` |
| `mountainPlasma` | 7/38 | **11/38** | directly, at 125 and 625 |
| `elev` / `elevation` | 113/434 | **115/434** | directly, at 250 and 150 |

**Two corrections came out of that, and both are worth more than the counts.**

First, **exposure is transitive.** `fixtures.rs` predicted the four flood
fields would not move, on the grounds that eleven of the layer's twelve DIRECT
`basis_noise_expr` calls sit at power-of-two output scales. Three held.
`floodBasaltsFunc` did not, because it READS `hairline_cracks` -
`+ 0.3 * min(0.5, hairline_cracks)`, right there in the layer's own verbatim
transcription. The three that held are exactly the three that never touch it.
Count composition, not call sites.

Second, **`hairlineCracks` went DOWN, 3 to 2.** That is not evidence against
the fix: the primitive is graded 196/196 against the game at five output
scales. It is the both-directions movement #273 measured. These chains carry
other unported narrowings, so correcting one term shifts values slightly and a
position that happened to land exactly right can stop doing so. A count
falling by one at 61 positions says the field is still wrong for reasons this
change does not address.

- **A clamp flatters a count, and here it is measurable.** The three clamped
biomes score 403, 402 and 408 of 434 against their own unclamped sources at
128, 107 and 127 - the same quantity, times 2, clamped. Nothing improved
between them: the clamp saturates at 0 or 1 over most of the map and a
saturated position is exact for free. Read `*_biome_full` as the port's score
and `*_biome` as what the consumer needs. Same effect in `starting_area`
(371 of 410) against the unclamped `ashlands_start` (61) feeding it.
- **The oracle cannot see elevation's `-500` clamp**, and that was checked
rather than assumed. `vulcanus_elevation` is `max(-500, elev)` and the
captured `elev` bottoms out at **-58.77**, so the two columns are the same
field at all 434 positions - 0 of 434 differ - and a port that dropped the
`max` would score 115 either way. Both are graded anyway; the clamp's real
test lives in the module, constructing the case the fixture does not.
- **A discrete output scores like one.** `mountain_volcano_spots` at 359 of 434
is the highest UNCLAMPED count in the Vulcanus port, because it is dominated
by which single candidate survives per region - a choice a sub-ULP error
almost never changes. The same property `voronoi_cell_id` has.

**`detailNoise` is the reading to carry out of this phase.** It has the
SMALLEST residual of its three helper fields (7.778e-5) and the FEWEST exact
matches (**1 of 38**), where `mountainPlasma` has 2.815e-3 and 11 of 38. A field
can be uniformly close and almost never right, which is the argument for
counting matches rather than bounding error, stated in one number.

Read elevation's worst residual of 1.332e-1 against its scale before reacting:
the field spans -58 to +1024, so that is ~1.3e-4 relative, the same order as
every layer above it. An absolute bound would need re-tuning per field for
scale alone - a third reason not to use one.

**`vulcanus_biomes` keeps a REAL cache, and it is the only layer that does.**
Every other ported layer evaluates top to bottom into locals, because every read
is at the same `(x, y)`. `raw_spots` is not: it reads selected spots from up to
four neighbouring regions, which is genuine cross-position state. The region
cache is a `RefCell<BTreeMap>` so `eval` can stay `&self` while the density and
favorability closures handed to `select_spots` borrow it. `BTreeMap` rather than
`HashMap` deliberately - nothing iterates it today, but a determinism-critical
port should not carry a container whose iteration order is unspecified.

`volcano_area` is evaluated at every spot candidate and pulls the whole
pre-volcano chain at that candidate; the TypeScript memoizes those and the port
recomputes them. **Nothing on the render path reaches this layer yet**, so it is
correct-first on purpose. If it ever joins a per-pixel render that is the first
measurement to take - `multioctave_noise`'s own docs record what happened last
time a per-call rebuild went unmeasured, which was 20x.

**The mountains pre-volcano split is load-bearing.** `mountain_volcano_spots`
depends on the mountains biome and the mountains biome folds the volcano field
back in; the Lua breaks that with a PRE-volcano stage that `volcano_area` reads.
Collapsing the two is an infinite recursion, which announces itself - reading
`volcano_area` off the POST-volcano raw does not.

**`cliff_elevation` is a separate entry point, not a convenience.**
`multisample`'s offsets are in the CONSUMING program's grid units, so the cliff
generator's 4-tile lattice moves the field 16 tiles for a `dx` of 4 (#83). The
tile and terrain channels pass 1; cliffs pass 4; both go through one code path
with the grid as a parameter.

**Tier 3 now covers both preview PNGs**, which is what #224's gate asks for.
`test/wasmFulgoraRenderParity.spec.ts` renders through the real boundary and
compares against the images Factorio itself produced:
Expand Down
2 changes: 1 addition & 1 deletion crates/fmw-noise/src/expressions/fulgora_elevation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
use crate::eval::math::slider_rescale;
use crate::eval::math::{lerp_f32, max2, min2};
use crate::expressions::fulgora_cells::CellFields;
use crate::expressions::fulgora_shared::Prepared;
use crate::expressions::fulgora_shared::{FulgoraCtx, SharedFields};
use crate::multioctave_noise::MultioctaveParams;
use crate::multioctave_noise::Prepared;
use crate::poison;

/// `seed1` for each multioctave call: `crc32` of the Lua's string seed.
Expand Down
3 changes: 2 additions & 1 deletion crates/fmw-noise/src/expressions/fulgora_roads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@

use crate::eval::math::lerp;
use crate::expressions::fulgora_cells::CellFields;
use crate::expressions::fulgora_shared::{FulgoraCtx, Prepared, SharedFields};
use crate::expressions::fulgora_shared::{FulgoraCtx, SharedFields};
use crate::multioctave_noise::MultioctaveParams;
use crate::multioctave_noise::Prepared;
use crate::voronoi_noise::{Voronoi, VoronoiDistanceType, VoronoiParams};

/// `seed1` values, computed with CRC32 over the UTF-8 bytes of the name in the
Expand Down
3 changes: 2 additions & 1 deletion crates/fmw-noise/src/expressions/fulgora_ruins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ use crate::eval::math::max2;
use crate::expressions::fulgora_cells::CellFields;
use crate::expressions::fulgora_masks::MaskFields;
use crate::expressions::fulgora_roads::RoadFields;
use crate::expressions::fulgora_shared::{FulgoraCtx, Prepared};
use crate::expressions::fulgora_shared::FulgoraCtx;
use crate::multioctave_noise::MultioctaveParams;
use crate::multioctave_noise::Prepared;

const SEED1_RUINS_WALLS: u32 = 2_307_136_174; // crc32("fulgora_ruins_walls") = 0x89841AAE
const SEED1_RUINS_PAVING: u32 = 3_946_133_559; // crc32("fulgora_ruins_paving") = 0xEB353837
Expand Down
37 changes: 1 addition & 36 deletions crates/fmw-noise/src/expressions/fulgora_shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@
//! evaluated, checked field by field, which is what makes the substitution
//! legitimate; a field that read a neighbour would need the cache back.

use crate::basis_noise::{tables_from_seed, BasisNoiseTables};
use crate::eval::math::{clamp, slider_to_linear};
use crate::expressions::starting_spot_at_angle::{starting_spot_at_angle, AngleTrig, StartingSpot};
use crate::multioctave_noise::{octave_terms, sum_octaves, MultioctaveParams, OctaveTerms};
use crate::multioctave_noise::{MultioctaveParams, Prepared};
use crate::poison;

/// `seed1` for `fulgora_wobble_x`: `crc32(utf8("fulgora_wobble_x"))`.
Expand Down Expand Up @@ -84,40 +83,6 @@ pub struct SharedFields {
pub starting_vault_mask: f64,
}

/// One multioctave call with its seed tables and octave terms already derived.
///
/// **This is the shape every renderer needs, and its absence was a measured
/// bug.** `multioctave_noise(x, y, &params)` re-derives both on every call, and
/// `tables_from_seed` runs a PRNG over three 256-byte tables; Fulgora's chain
/// makes eight such calls per pixel. Building them per point measured **1.15x**
/// against the TypeScript, which builds them once in a closure. Hoisting is
/// what the ratio in phase 3's pull request is.
///
/// Results are identical either way, so nothing in tiers 1 to 3 could see it.
///
/// No `Debug` or `Clone`: `OctaveTerms` has neither, and deriving them here
/// would mean giving them to derived state whose shape is an implementation
/// detail.
pub struct Prepared {
terms: OctaveTerms,
tables: BasisNoiseTables,
}

impl Prepared {
#[must_use]
pub fn new(params: &MultioctaveParams) -> Self {
Self {
terms: octave_terms(params),
tables: tables_from_seed(params.seed0, params.seed1),
}
}

#[must_use]
pub fn eval(&self, x: f64, y: f64) -> f32 {
sum_octaves(x, y, &self.terms, &self.tables)
}
}

/// The per-render constants of Fulgora's shared layer.
pub struct FulgoraShared {
/// `fulgora_grid` - the Voronoi cell size in tiles.
Expand Down
6 changes: 6 additions & 0 deletions crates/fmw-noise/src/expressions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,10 @@ pub mod fulgora_scrap;
pub mod fulgora_shared;
pub mod fulgora_stack;
pub mod starting_spot_at_angle;
pub mod vulcanus_biomes;
pub mod vulcanus_climate;
pub mod vulcanus_cracks;
pub mod vulcanus_elevation;
pub mod vulcanus_helpers;
pub mod vulcanus_seed;
pub mod vulcanus_spawn;
Loading