fix(noise): multioctave's octave offset was an ALIAS - 1.17e-4 to 7.2e-7 (#162) - #166
Merged
Merged
Conversation
…e-7 (#162) `multioctave_noise`'s per-octave x shift was `-1774.83`, fitted against the oracle to a 2.5e-7 residual. The fit was right and degenerate: the basis lattice has period 256 per axis, and `17.17 - (-1774.83) == 1792 == 7*256`, so the scan landed on an alias seven periods out. The true constant is the double immediate `0x40312b851eb851ec`, exactly **17.17**. In f64 the two are literally interchangeable - bit-identical on all 266 oracle samples. In f32 they are not: the game rounds each octave's x to f32, and by octave 5 the alias sits at -8874 where an ulp is 1.1e-3, against 2.0e-6 for the true 85.85. That quantisation IS the "~1e-4 f32 floor" the notes recorded as irreducible, and the reason the error appeared to grow with distance. It also explains why five previous attempts to reproduce the game's f32 op order each made things 12x-27x worse: with the alias in place, f64 was accidentally compensating for a coordinate 100x too large. Neither fix works alone. f64 + alias (shipped) 1.170e-4 12/266 f32-exact f64 + true offset 1.170e-4 12/266 <- a literal no-op f32 + alias 1.427e-3 10/266 <- 12x regression f32 + true offset 7.153e-7 62/266 <- 164x The f32 model is read off `Noise::fastVectorMultioctaveNoise`, not the scalar `Noise::multioctaveNoise` the original notes disassembled. There are five multioctave entry points; `NoiseOperations::MultioctaveNoise::run` dispatches on the runtime global `Noise::vectorMultioctaveNoiseImplementationId`, which is non-zero on arm64, so the vector routine is the live path and the scalar overload is a fallback. That is the second op in this family where the oracle's entry point differs from the one first read. Also from that disassembly, and now ported: the sum accumulates in f32 one octave at a time; `output_scale` is folded into the starting amplitude rather than applied to the finished total; the RMS ratio is computed in f32 but its sqrt and the output_scale multiply happen in f64 and are rounded once; and a fractional octave count scales the frequency (not the amplitude) by `clamp(fastExp2(ceil(N)-N), 1, 1.99999)`. The remaining 7.2e-7 is NOT this composition - `octaves=1` alone carries 2.4e-7, and at P=0.5 that case is a single `basisNoise` call. `basisNoise` evaluates in f64 with no rounding at all; the game's kernel is f32 NEON. That is the next layer and is left alone here. Downstream, five pinned Vulcanus cliff statistics moved, every one toward the game: matched 18654->18657, wrong 693->691, surplus 1200->1199, missing 103->102, bracket-agreement 996->997 of 998 with the worst miss 6.7e-4->2.6e-5. Updated with dated banners in the notes rather than silently. Guards added: the spec now asserts all three pre-fix models still FAIL it, so neither half of the fix can silently regress and the tolerance cannot go vacuous. Verified by planting the old constant - the main assertion fails at 1.427e-3. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01KTCpQXFZ79UoVGunewFyL7
wormeyman
added a commit
that referenced
this pull request
Aug 5, 2026
…period out 17.17 - 256 = -238.83 exactly. The fitting note warned that a narrow U scan finds a 'false local min near -238.8', blamed near-collisions in the basis field, and advised scanning wide - which is precisely what walked seven more periods out to -1774.83 and caused #166. Every minimum either scan ever reported, in both files, was an alias of the truth: -238.83 here, and -4864 / -3840 (=-19*256 / -15*256) in the variable-persistence notes. Advice corrected to the opposite: scan narrow, take the smallest-magnitude representative. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01KTCpQXFZ79UoVGunewFyL7
wormeyman
added a commit
that referenced
this pull request
Aug 5, 2026
…- 1.8e-3 to 1.1e-5 (#162) (#167) * fix(noise): variable-persistence's octave shift was an alias of ZERO - 1.8e-3 to 1.1e-5 (#162) Second instance of the defect fixed in #166, found by taking that finding's own prediction seriously: `variablePersistenceMultioctaveNoise` carried a fitted per-octave x shift of `-7936`, and `-7936 == -31 * 256`. The basis lattice has period 256 per axis, so that names the same field as a shift of zero. The old comment's own "false minima near -4864 / -3840" are `-19*256` and `-15*256` - every candidate the scan surfaced was a multiple of 256, which is what a completely flat fit direction looks like from the inside. The independence checks (input-scale, seed, offset_x, persistence) could not discriminate either, because the period is independent of all of them. `VariablePersistenceMultioctaveNoise::run` settles it: the octave loop reloads the x and y offsets from the same two constant slots (+0xa2c, +0xa30) every iteration and has no counter-scaled term at all. There is no per-octave shift; octaves decorrelate through lacunarity alone. `OCTAVE_SHIFT` is deleted, not set to 0. Same pairing as #166 - neither half is an improvement alone, and here the mismatched combination is spectacularly bad: f64 + shift -7936 (shipped) 1.847e-3 61/266 f32-exact f64 + no shift 1.847e-3 61/266 <- a literal no-op f32 + shift -7936 3.629e-1 45/266 <- 196x WORSE than doing nothing f32 + no shift 1.144e-5 66/266 <- 161x better The third row is the sharpest illustration of why this class of bug hides: k*(-7936) at octave 5 lands near -39680, where an f32 ulp is ~3.9e-3. The remaining 1.1e-5 is not a modelling gap - it is basisNoise's f32 floor amplified by this op's 2^N * output_scale gain. `worst/gain` is 1.2e-7 to 2.4e-7 (one to two f32 ulps) in every one of the seven cases, including those with offset_x of 5000 and 40000. The spec now asserts that ratio per case as well as the absolute worst, so a future regression that scales with gain cannot hide inside a loose absolute bound. The spec's near-field/far-field split is gone with the shift that motivated it: it thresholded on a `maxNoiseX(...) < 500` computed from k*(-7936) and allowed the far domain 100x more error. With no shift, the offset_x=40000 case is no worse than the ones at the origin. Guards added mirroring #166: all three pre-fix models must still fail the tolerance. No downstream statistic moved; `pnpm run verify` exits 0 (1736 passed). Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01KTCpQXFZ79UoVGunewFyL7 * docs(noise): the plain op's '-238.8 false minimum' was the truth one period out 17.17 - 256 = -238.83 exactly. The fitting note warned that a narrow U scan finds a 'false local min near -238.8', blamed near-collisions in the basis field, and advised scanning wide - which is precisely what walked seven more periods out to -1774.83 and caused #166. Every minimum either scan ever reported, in both files, was an alias of the truth: -238.83 here, and -4864 / -3840 (=-19*256 / -15*256) in the variable-persistence notes. Advice corrected to the opposite: scan narrow, take the smallest-magnitude representative. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01KTCpQXFZ79UoVGunewFyL7 --------- Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
This was referenced Aug 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The finding
multioctave_noise's per-octave x shift was-1774.83, fitted against the oracle to a 2.5e-7 residual. That fit was right and degenerate: the basis lattice has period 256 per axis, andso the scan landed on an alias seven periods out. The true constant is the double immediate
0x40312b851eb851ecinNoise::fastVectorMultioctaveNoise- exactly17.17.In f64 the two are literally interchangeable (bit-identical on all 266 oracle samples). In f32 they are not, because the game rounds each octave's x to f32:
k*17.17k*-1774.83That quantisation is the "~1e-4 f32 floor" the notes recorded as irreducible, and the reason the error appeared to grow with distance from the origin.
Why five earlier attempts made it worse
With the alias in place, moving toward the game's real arithmetic is actively harmful - f64 was accidentally compensating for a coordinate 100x too large by carrying 100x more precision than the game does. Neither fix does anything alone:
Five entry points, and the oracle uses a different one
NoiseOperations::MultioctaveNoise::rundispatches on the runtime globalNoise::vectorMultioctaveNoiseImplementationId, non-zero on arm64, sofastVectorMultioctaveNoiseis the live path and the scalarNoise::multioctaveNoisethe original notes disassembled is a fallback. Second op in this family where that trap bites; the notes now say to check::run's dispatch first.Also ported from that read: f32 accumulation one octave at a time;
output_scalefolded into the starting amplitude; the RMS ratio in f32 but itssqrtand theoutput_scalemultiply in f64 rounded once; fractional octave counts scaling frequency byclamp(fastExp2(ceil(N)-N), 1, 1.99999).What is left, and it is not here
The remaining 7.2e-7 is not the composition:
octaves=1alone carries 2.4e-7, and at P=0.5 that case reduces to a singlebasisNoisecall.basisNoiseevaluates in f64 with no rounding anywhere; the game's kernel is f32 NEON. That is the next layer.variablePersistenceMultioctaveNoisecarries a fitted-7936 == -31*256, an alias of 0 - same signature, very likely the same defect. Untouched here.Downstream
Five pinned Vulcanus cliff statistics moved, every one toward the game: matched 18654→18657, wrong 693→691, surplus 1200→1199, missing 103→102, bracket agreement 996→997 of 998 with the worst miss 6.7e-4→2.6e-5. Updated with dated banners in the notes rather than silently.
Guards
The spec asserts all three pre-fix models still fail it, so neither half can silently regress and the tolerance cannot go vacuous. Confirmed by planting the old constant: the main assertion fails at 1.427e-3.
pnpm run verifyexits 0 (1732 passed).🤖 Generated with Claude Code
https://claude.ai/code/session_01KTCpQXFZ79UoVGunewFyL7