Skip to content

fix(noise): multioctave's octave offset was an ALIAS - 1.17e-4 to 7.2e-7 (#162) - #166

Merged
wormeyman merged 1 commit into
mainfrom
fix/multioctave-octave-offset-f32
Aug 5, 2026
Merged

fix(noise): multioctave's octave offset was an ALIAS - 1.17e-4 to 7.2e-7 (#162)#166
wormeyman merged 1 commit into
mainfrom
fix/multioctave-octave-offset-f32

Conversation

@wormeyman

Copy link
Copy Markdown
Owner

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, 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 in Noise::fastVectorMultioctaveNoise - exactly 17.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 true k*17.17 f32 ulp aliased k*-1774.83 f32 ulp
1 17.17 2.0e-6 -1774.83 2.1e-4
5 85.85 1.0e-5 -8874.15 1.1e-3

That 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:

variant worst f32-exact
f64 + alias (shipped) 1.170e-4 12/266
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)

Five entry points, and the oracle uses a different one

NoiseOperations::MultioctaveNoise::run dispatches on the runtime global Noise::vectorMultioctaveNoiseImplementationId, non-zero on arm64, so fastVectorMultioctaveNoise is the live path and the scalar Noise::multioctaveNoise the 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_scale folded into the starting amplitude; the RMS ratio in f32 but its sqrt and the output_scale multiply in f64 rounded once; fractional octave counts scaling frequency by clamp(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=1 alone carries 2.4e-7, and at P=0.5 that case reduces to a single basisNoise call. basisNoise evaluates in f64 with no rounding anywhere; the game's kernel is f32 NEON. That is the next layer.

variablePersistenceMultioctaveNoise carries 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 verify exits 0 (1732 passed).

🤖 Generated with Claude Code

https://claude.ai/code/session_01KTCpQXFZ79UoVGunewFyL7

…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
wormeyman merged commit e8fbb9b into main Aug 5, 2026
6 checks passed
@wormeyman
wormeyman deleted the fix/multioctave-octave-offset-f32 branch August 5, 2026 18:23
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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant