Stop treating JWildfire normal as linear geometry

This commit is contained in:
Meta Vens 2026-05-21 01:51:52 +01:00
commit a1ffb39f01
7 changed files with 41 additions and 14 deletions

View file

@ -207,11 +207,25 @@ yet. With full SDF reservoir coverage on the local GTX 1070, the same
2,000,000 reservoir rows in all three layers. The contact sheet lives at
`artifacts/fractal-flame-jwildfire-reference/julian-disc-baby-sdf-density-contact.png`.
The remaining visible mismatch is now the right mismatch: Aquarium's SDF
reservoir density is sparse and shape-shifted relative to FARender's rendered
density. Fixing that belongs in dialect import, camera/render-filter parity,
target density weighting, and view-conditioned reservoir feedback, not in
pretending point occupancy is a rendered form.
The next correction removed another dialect lie. JWildfire's
`variationGroup normal="1"` is not the same thing as an explicit `linear`
variation in FARender. A normal-only diagnostic renders as a constant point in
FARender, while Aquarium had been adding affine linear geometry. The importer
now ignores JWildfire `normal` as variation-group metadata; explicit `linear`
still stays linear for Apophysis/FLAM3-style fixtures. The SDF-density receipt
also gives subpixel envelopes a minimum one-bin reconstruction footprint so the
comparison does not quietly collapse back into point occupancy.
After those fixes, the `julian-disc-baby` receipt scored `31.36%` against the
FARender PPM at `256x256` bounds `-2,-2,2,2`, with `41.692 ms/frame` while
refreshing all 2,000,000 reservoir rows in all three layers. The contact sheet
lives at
`artifacts/fractal-flame-jwildfire-reference/julian-disc-baby-normal-cut-area-contact.png`.
The remaining visible mismatch is now narrower: Aquarium draws the same family
of form, but its density is still too compact and differently weighted. Fixing
that belongs in exact JWildfire variation/camera parity, target density
weighting, and view-conditioned reservoir feedback.
The next oracle step is to compare Aquarium through a matching density image
resolve, not raw point occupancy.

View file

@ -74,7 +74,8 @@ public static class FractalFlameFileParser
? new Vector2(post[4], post[5])
: new Vector2(post[2], post[5]);
var variations = new FractalFlameVariationWeights(
ParseOptionalFloat(variationSource, "linear", 0.0f) + ParseOptionalFloat(variationSource, "normal", 0.0f),
ParseOptionalFloat(variationSource, "linear", 0.0f)
+ (isJWildfireDialect ? 0.0f : ParseOptionalFloat(variationSource, "normal", 0.0f)),
ParseOptionalFloat(variationSource, "spherical", 0.0f),
ParseOptionalFloat(variationSource, "bubble", 0.0f),
ParseOptionalFloat(variationSource, "disc", 0.0f) + ParseOptionalFloat(variationSource, "jwf_disc", 0.0f),
@ -82,7 +83,7 @@ public static class FractalFlameFileParser
ParseOptionalFloat(variationSource, "julian_power", ParseOptionalFloat(variationSource, "jwf_julian_power", 2.0f)),
ParseOptionalFloat(variationSource, "julian_dist", ParseOptionalFloat(variationSource, "jwf_julian_dist", 1.0f)),
ParseOptionalFloat(variationSource, "gaussian_blur", 0.0f) + ParseOptionalFloat(variationSource, "jwf_gaussian_blur", 0.0f));
ClassifyVariationFields(variationSource, accepted, approximated, ignored, rejected);
ClassifyVariationFields(variationSource, isJWildfireDialect, accepted, approximated, ignored, rejected);
ClassifyXformFields(element, variationGroup, accepted, ignored, rejected);
var transformName = ((string?)element.Attribute("name")) ?? $"xform/{index:0000}";
@ -155,6 +156,7 @@ public static class FractalFlameFileParser
private static void ClassifyVariationFields(
XElement variationSource,
bool isJWildfireDialect,
List<string> accepted,
List<string> approximated,
List<string> ignored,
@ -185,7 +187,15 @@ public static class FractalFlameFileParser
if (name is "normal")
{
approximated.Add("variation:normal -> linear");
if (isJWildfireDialect)
{
ignored.Add("variationGroup:normal");
}
else
{
approximated.Add("variation:normal -> linear");
}
continue;
}

View file

@ -194,3 +194,4 @@
{"lesson":"The flame parity gate now has an external image-density reference instead of only Aquarium CPU self-agreement. tools/Aquarium.Fractal.Receipt accepts --reference-density-ppm, loads a binary PPM rendered by JWildfire/FARender, converts luminance into the reference histogram, and compares Aquarium GPU splat density against it. scripts/new-jwildfire-baby-fixture.ps1 derives a smaller FARender-compatible julian-disc-baby.flame from FARender selftest while preserving boilerplate FARender requires. First GTX 1070 receipt: FARender 768x768 quality 2000 reference, Aquarium 2M splats/states, 8 acquisition frames, 50k steady splat updates/frame, depth 8, 1.145 ms/frame, but only 15.33% external density score in 256x256 bounds -2..2; a 128x128 bounds sweep peaked at 32.95% for -4..4 while Aquarium-vs-CPU scored 89.94%. This proves the next failure is dialect/camera/render filtering, not GPU sampling count.","timestamp":"2026-05-21T01:05:00+01:00","files":["tools/Aquarium.Fractal.Receipt/Program.cs","scripts/fractal-splat-receipt.ps1","scripts/new-jwildfire-baby-fixture.ps1","research/rendering/fractal-reference-oracles.md","state/map.yaml","state/memory.json"],"source":"user asked to ditch the shitty oracle and get proper ground truth that gives Aquarium useful exercise","topic":"external-flame-density-oracle"}
{"lesson":"The external FARender density oracle exposed two concrete JWildfire import/math bugs: Aquarium transposed JWildfire coefs off-diagonal terms even though FARender applies a,b,c,d,e,f as x=a*x+b*y+c and y=d*x+e*y+f, with JWildfire XML effectively a b d e c f; Aquarium also used atan2(y,x) for disc while FARender jwf_disc uses atan2(x,y)/PI. Fixing both moved the julian-disc-baby external-density receipt from 15.33% to 26.87% at 256x256 bounds -2..2. The remaining visible spray is raw splat occupancy versus FARender camera/filter/gamma density, not proof that the GPU sampler is simply exploding.","timestamp":"2026-05-21T01:18:00+01:00","files":["src/Aquarium.Engine.Fractal/Grammar/FractalFlameFileParser.cs","src/Aquarium.Engine.Fractal/Grammar/FractalFlameDefinition.cs","src/Aquarium.Engine/Render/Shaders/D3D12FractalReservoirCompute.hlsl","tests/Aquarium.Engine.Fractal.Tests/ApophysisReferenceParityTests.cs","research/rendering/fractal-reference-oracles.md","state/map.yaml","state/memory.json"],"source":"user asked why Aquarium explodes while the reference stays bounded","topic":"jwildfire-flame-import-coordinate-lies"}
{"timestamp":"2026-05-21T01:32:00+01:00","topic":"sdf-reservoir-density-parity","lesson":"The external flame parity candidate now comes from Aquarium's SDF envelope reservoir layer, not raw splat-center occupancy. tools/Aquarium.Fractal.Receipt stamps populated AquariumPackedSdfEnvelopeReservoir rows into shaped density histograms and reports the candidate source explicitly. PBR and radiosity remain separate reservoir layers: PBR should score appearance later, while radiance is out of this gate for now. With full 2M-row reservoir refresh on the GTX 1070, julian-disc-baby scored 22.14% against the FARender PPM at 256x256 bounds -2..2 and cost 42.053 ms/frame; the contact sheet shows a sparse, shape-shifted Aquarium SDF density rather than a valid rendered flame. The next work is dialect/camera/filter parity plus target-density and view-conditioned reservoir feedback, not raw occupancy scoring.","files":["tools/Aquarium.Fractal.Receipt/Program.cs","research/rendering/fractal-reference-oracles.md","state/map.yaml","state/memory.json"],"source":"user corrected the metric: SDF reservoirs express form density, PBR reservoirs express surface appearance, and raw occupancy is the wrong comparison"}
{"timestamp":"2026-05-21T01:52:00+01:00","topic":"jwildfire-normal-is-not-linear","lesson":"JWildfire FARender does not treat `variationGroup normal=\"1\"` as an explicit linear variation. A normal-only FARender diagnostic collapsed to a constant point, proving Aquarium's `normal -> linear` approximation was inventing geometry. The importer now ignores JWildfire `normal` as variation-group metadata while preserving explicit `linear` for Apophysis/FLAM3 fixtures. The SDF-density receipt also enforces a minimum one-bin reconstruction footprint for subpixel envelopes so density parity no longer degenerates into point occupancy. The julian-disc-baby external score moved from 22.14% to 31.36% with full 2M-row reservoir refresh; remaining mismatch is compactness/weighting, not the previous fake affine contribution.","files":["src/Aquarium.Engine.Fractal/Grammar/FractalFlameFileParser.cs","tests/Aquarium.Engine.Fractal.Tests/ApophysisReferenceParityTests.cs","tools/Aquarium.Fractal.Receipt/Program.cs","research/rendering/fractal-reference-oracles.md","state/map.yaml","state/memory.json"],"source":"continued flame external parity push after discovering FARender normal-only behavior"}

View file

@ -154,7 +154,7 @@ renderer_state:
- View-conditioned visual parity now scores named camera windows instead of trusting one global histogram. For the depth-16 flame receipt, global -8..8 scores 97.09%, mid -1..1 scores 91.93%, close -0.5..0.5 scores 88.97%, and micro -0.25..0.25 scores 82.05% with 2,656 starved reference bins and 17.95% underrepresented mass. This is the current hard loss: the reservoir is not yet spending work based on camera-conditioned underrepresentation.
- High-reference flame contact sheets should use at least 20M CPU oracle samples for visual judgment. The current artifact is artifacts/fractal-contact-sheet-hq/flame-parity-contact-sheet-hq.png, comparing 20M CPU oracle histograms against 250k Aquarium GPU readback samples for global frame 1/10 and zoom frame 1/10. The 1M-sample oracle was too noisy and should be treated as a scalar smoke check only.
- External flame visual references are now reproducible from ignored local artifacts. scripts/install-flame-reference-tools.ps1 hydrates JWildfire 9.00 plus Temurin JRE 17 under artifacts/reference-tools; scripts/fractal-flame-jwildfire-reference.ps1 prefers JWildfire's bundled FARenderJWF GPU renderer for explicit PNG output. Local GTX 1070 smoke rendered FARender's selftest.flame at 1024x1024 quality 2000 in 7.69 sec to artifacts/fractal-flame-jwildfire-reference/selftest-farender-20260520-233638.png. JWildfire/FARender is a visual reference artifact lane, not Aquarium runtime code.
- tools/Aquarium.Fractal.Receipt now has an external density oracle: --reference-density-ppm compares Aquarium GPU SDF envelope reservoir density against luminance density from a binary PPM rendered by an external flame renderer. scripts/new-jwildfire-baby-fixture.ps1 derives a smaller FARender-compatible julian-disc-baby.flame from JWildfire's selftest by reducing julian power and muting gaussian/wfield amounts while preserving FARender boilerplate. First GTX 1070 external-density receipt against FARender's 768x768 quality-2000 baby render scored only 15.33% when Aquarium still binned raw splat centers; after fixing JWildfire coefs packing and FARender disc angle convention the raw-center score reached 26.87%. The current SDF-reservoir-density receipt, with full 2M-row reservoir refresh, scores 22.14% at 42.053 ms/frame in 256x256 bounds -2..2. Aquarium still scores about 89.94% against its own CPU slice, so the remaining external gap is dialect/camera/render-filter semantics, target density weighting, and view-conditioned reservoir feedback, not GPU sampling count.
- tools/Aquarium.Fractal.Receipt now has an external density oracle: --reference-density-ppm compares Aquarium GPU SDF envelope reservoir density against luminance density from a binary PPM rendered by an external flame renderer. scripts/new-jwildfire-baby-fixture.ps1 derives a smaller FARender-compatible julian-disc-baby.flame from JWildfire's selftest by reducing julian power and muting gaussian/wfield amounts while preserving FARender boilerplate. First GTX 1070 external-density receipt against FARender's 768x768 quality-2000 baby render scored only 15.33% when Aquarium still binned raw splat centers; after fixing JWildfire coefs packing and FARender disc angle convention the raw-center score reached 26.87%. Switching to SDF-reservoir density with full 2M-row reservoir refresh scored 22.14% at 42.053 ms/frame. Cutting the false JWildfire `normal` -> `linear` import and enforcing a one-bin SDF density reconstruction footprint moved the current score to 31.36% at 41.692 ms/frame in 256x256 bounds -2..2. Aquarium still scores about 89.94% against its own CPU slice, so the remaining external gap is exact JWildfire variation/camera/filter semantics, target density weighting, and view-conditioned reservoir feedback, not GPU sampling count.
- JWildfire selftest flame is the current honest faceplant target. After adding a first dialect slice for JWildfire coefficient layout, variationGroup, disc, julian, gaussian_blur, and post transforms, the depth-64 GTX 1070 receipt against Aquarium's CPU interpretation of artifacts/reference-tools/j-wildfire-9.00/lib/FARenderJWF/selftest.flame reaches 84.19% score in bounds -8..8 and 88.01% in bounds -128..128, but costs about 129 ms/frame. This is CPU/GPU slice parity, not external JWildfire visual parity. It proves the remaining hard work is richer dialect semantics plus persistent GPU iteration-state reservoirs; brute replaying deep flame iteration from zero per splat is not the final hot path.
- FractalFlameFileParser now has ParseFirstWithReport for import accountability. Receipt runs emit fractal-flame-import-report-*.json beside flame histogram/GPU receipts. The JWildfire selftest report currently records 15 accepted fields, 3 approximations, 45 ignored render/presentation fields, and 11 rejected geometry/sampling fields: wfield_* weighting-field attributes. Successful XML parsing is not successful translation.
- FractalFlameIterationState and FractalFlameIterationStepper define the persistent flame iteration invariant before GPU lowering: advancing a sample in chunks must match a one-shot advance from the same state. This is a parity/mock boundary for the future GPU state buffer, not a runtime CPU renderer.

View file

@ -112,7 +112,7 @@
"Global flame parity is not enough. View-conditioned parity now scores named viewport bounds and reports GPU/reference hits, starved reference bins, and underrepresented mass. First depth-16 GTX 1070 receipt with 250k GPU readback splats vs 1M CPU samples: global -8..8 scores 97.09%, mid -1..1 scores 91.93%, close -0.5..0.5 scores 88.97%, micro -0.25..0.25 scores 82.05% with 2,656 starved bins and 17.95% underrepresented mass. This proves the current reservoir is fast but not view-adaptive; next work needs camera-conditioned target weights and underrepresented-region feedback.",
"Do not use the quick 1M-sample flame oracle image for visual judgment; it is too noisy. The high-reference contact sheet uses 20,000,000 CPU oracle samples and 250,000 Aquarium GPU readback splats per panel at artifacts/fractal-contact-sheet-hq/flame-parity-contact-sheet-hq.png. It makes the zoom failure legible: the CPU oracle smooths out while Aquarium remains under-sampled after zoom because the current reservoir is not view-adaptive.",
"Aquarium's CPU flame histogram is a math oracle, not visual ground truth. The visual reference ladder is: Chaotica as gold market-quality reference when available/imported, JWildfire/FARender as scriptable visual reference, FLAM3/Apophysis as portable baseline compatibility, Aquarium CPU as deterministic unit-test/debug oracle only. On 2026-05-20 Chaotica and flam3-render were not on PATH, but scripts/install-flame-reference-tools.ps1 hydrated JWildfire 9.00 plus Temurin JRE 17 under ignored artifacts, and scripts/fractal-flame-jwildfire-reference.ps1 rendered JWildfire's bundled FARenderJWF selftest flame at 1024x1024 quality 2000 in 7.69 sec on the GTX 1070. JWildfire/FARender is an external visual reference artifact lane, not Aquarium runtime code. Do not claim best-in-market flame parity until imported Chaotica renders or fully matched JWildfire flame dialect metrics are wired into residual scoring.",
"The useful flame oracle is now external density, not Aquarium CPU self-agreement. tools/Aquarium.Fractal.Receipt accepts --reference-density-ppm to compare Aquarium GPU SDF envelope reservoir density against luminance density from a binary PPM rendered by JWildfire/FARender. scripts/new-jwildfire-baby-fixture.ps1 derives a smaller FARender-compatible julian-disc-baby.flame from the bundled selftest while preserving required FARender boilerplate. First external receipt scored 15.33% in 256x256 bounds -2..2 while Aquarium still binned raw centers; after fixing JWildfire coefs packing (`a b d e c f`) and FARender's disc angle convention (`atan2(x,y)`), the raw-center score reached 26.87%. With the candidate switched to full SDF-reservoir density, the same target scores 22.14% at 42.053 ms/frame with all 2M reservoir rows refreshed. Aquarium-vs-CPU still scores about 89.94%, so the live failure is dialect/camera/render-filter semantics, target density weighting, and view-conditioned reservoir feedback, not sample count.",
"The useful flame oracle is now external density, not Aquarium CPU self-agreement. tools/Aquarium.Fractal.Receipt accepts --reference-density-ppm to compare Aquarium GPU SDF envelope reservoir density against luminance density from a binary PPM rendered by JWildfire/FARender. scripts/new-jwildfire-baby-fixture.ps1 derives a smaller FARender-compatible julian-disc-baby.flame from the bundled selftest while preserving required FARender boilerplate. First external receipt scored 15.33% in 256x256 bounds -2..2 while Aquarium still binned raw centers; after fixing JWildfire coefs packing (`a b d e c f`) and FARender's disc angle convention (`atan2(x,y)`), the raw-center score reached 26.87%. With the candidate switched to full SDF-reservoir density, the same target scored 22.14% at 42.053 ms/frame with all 2M reservoir rows refreshed. Cutting the false JWildfire `normal` -> `linear` import and resolving subpixel SDF envelopes as at least one histogram bin moved the score to 31.36% at 41.692 ms/frame. Aquarium-vs-CPU still scores about 89.94%, so the live failure is exact JWildfire variation/camera/filter semantics, target density weighting, and view-conditioned reservoir feedback, not sample count.",
"JWildfire selftest is now the flame renderer faceplant target. The current dialect slice supports JWildfire coefficient layout, variationGroup, disc, julian, gaussian_blur, and post transforms. After post lowering, the depth-64 GTX 1070 receipt scores 84.19% against Aquarium's CPU slice oracle in bounds -8..8 and 88.01% in bounds -128..128, but costs about 129 ms/frame. This is not external JWildfire visual parity; it is CPU/GPU agreement on the supported slice. Aquarium still needs richer dialect semantics, palette/density/render-filter parity, weighting-field handling, and persistent per-sample GPU iteration-state reservoirs instead of replaying deep chaos-game ancestry from zero for every splat.",
"Flame imports must report translation loss. FractalFlameFileParser.ParseFirstWithReport returns accepted, approximated, ignored, and rejected fields per transform, and receipt runs write fractal-flame-import-report-*.json. The JWildfire selftest report currently shows 15 accepted fields, 3 approximations, 45 ignored render/presentation fields, and 11 rejected geometry/sampling fields: wfield_* weighting-field attributes. Treat successful XML parsing as insufficient until rejected geometry fields are either implemented as DSL/operator nodes or explicitly accepted as out-of-scope diagnostics.",
"Persistent flame iteration now has both a CPU parity contract and a GPU realization. FractalFlameIterationState plus FractalFlameIterationStepper prove chunked CPU advancement; AquariumPackedFractalFlameState mirrors the point/support/material/RNG/step packet in D3D12FractalReservoirCompute as a UAV so flame splats advance over frames. Keep the acquisition/maintenance split explicit in receipts and renderer budgets.",

View file

@ -122,7 +122,7 @@ public sealed class ApophysisReferenceParityTests
Assert.Equal(2, result.Report.TransformCount);
Assert.Equal(new Vector4(1.0f, 0.0f, 0.0f, 1.0f), flame.Transforms[0].Matrix);
Assert.Equal(new Vector2(-0.1f, 0.2f), flame.Transforms[0].Translation);
Assert.Equal(1.0f, flame.Transforms[0].Variations.Linear);
Assert.Equal(0.0f, flame.Transforms[0].Variations.Linear);
Assert.Equal(0.5f, flame.Transforms[0].Variations.Julian);
Assert.Equal(5.0f, flame.Transforms[0].Variations.JulianPower);
Assert.Equal(1.25f, flame.Transforms[0].Variations.JulianDist);
@ -130,7 +130,7 @@ public sealed class ApophysisReferenceParityTests
Assert.Equal(new Vector4(0.9f, 0.0f, 0.0f, 0.9f), flame.Transforms[0].PostMatrix);
Assert.Equal(new Vector4(0.25f, -0.75f, 0.75f, 0.25f), flame.Transforms[1].Matrix);
Assert.Equal(0.8f, flame.Transforms[1].Variations.Disc);
Assert.Contains("variation:normal -> linear", result.Report.Transforms[0].ApproximatedFields);
Assert.Contains("variationGroup:normal", result.Report.Transforms[0].IgnoredFields);
Assert.Contains("variation:jwf_gaussian_blur -> stochastic gaussian_blur", result.Report.Transforms[0].ApproximatedFields);
Assert.Contains("post", result.Report.Transforms[0].AcceptedFields);
Assert.Empty(result.Report.Transforms[0].RejectedFields);

View file

@ -586,8 +586,10 @@ internal sealed class GpuFractalSplatReceiptRunner : IDisposable
var centerX = centerRadius.X;
var centerY = centerRadius.Y;
var radiusX = MathF.Max(MathF.Abs(radiiFalloff.X), MathF.Abs(centerRadius.W));
var radiusY = MathF.Max(MathF.Abs(radiiFalloff.Y), MathF.Abs(centerRadius.W));
var pixelRadiusX = spanX / width;
var pixelRadiusY = spanY / height;
var radiusX = MathF.Max(MathF.Max(MathF.Abs(radiiFalloff.X), MathF.Abs(centerRadius.W)), pixelRadiusX);
var radiusY = MathF.Max(MathF.Max(MathF.Abs(radiiFalloff.Y), MathF.Abs(centerRadius.W)), pixelRadiusY);
if (radiusX <= 0.0f || radiusY <= 0.0f)
{
continue;