Skip to content

fix(noise): variable-persistence's octave shift was an alias of ZERO - 1.8e-3 to 1.1e-5 (#162) - #167

Merged
wormeyman merged 2 commits into
mainfrom
fix/varpersistence-octave-shift-f32
Aug 5, 2026
Merged

fix(noise): variable-persistence's octave shift was an alias of ZERO - 1.8e-3 to 1.1e-5 (#162)#167
wormeyman merged 2 commits into
mainfrom
fix/varpersistence-octave-shift-f32

Conversation

@wormeyman

Copy link
Copy Markdown
Owner

Second instance of the defect fixed in #166, found by taking that finding's own prediction seriously.

The finding

variablePersistenceMultioctaveNoise carried a fitted per-octave x shift of -7936. The basis lattice has period 256 per axis, and

-7936 = -31 * 256

so it names the same field as a shift of zero. The old comment's own warning about "false minima near -4864 / -3840" gives it away - those 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 the notes were proud of (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) on every iteration and has no counter-scaled term at all. Octaves decorrelate through lacunarity alone. OCTAVE_SHIFT is deleted, not set to 0.

Same pairing as #166, and here the mismatch is spectacular

variant worst f32-exact
f64 + shift -7936 (shipped) 1.847e-3 61/266
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

That 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. Anyone reproducing the game's arithmetic without first questioning the constant would conclude f32 was the wrong theory.

What is left is not a modelling gap

The remaining 1.1e-5 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 regression that scales with gain can't hide inside a loose absolute bound.

Spec changes

The near-field/far-field split is gone along with the shift that motivated it - it thresholded on 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 mirror #166: all three pre-fix models must still fail the tolerance.

No downstream statistic moved. pnpm run verify exits 0 (1736 passed).

🤖 Generated with Claude Code

https://claude.ai/code/session_01KTCpQXFZ79UoVGunewFyL7

wormeyman and others added 2 commits August 5, 2026 11:32
…- 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
…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
wormeyman merged commit 7dc1eb0 into main Aug 5, 2026
6 checks passed
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