From a1ffb39f01cc02ca7500fc5db2d8baccd9c81dcd Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 21 May 2026 01:51:52 +0100 Subject: [PATCH] Stop treating JWildfire normal as linear geometry --- .../rendering/fractal-reference-oracles.md | 24 +++++++++++++++---- .../Grammar/FractalFlameFileParser.cs | 16 ++++++++++--- state/evidence.jsonl | 1 + state/map.yaml | 2 +- state/memory.json | 2 +- .../ApophysisReferenceParityTests.cs | 4 ++-- tools/Aquarium.Fractal.Receipt/Program.cs | 6 +++-- 7 files changed, 41 insertions(+), 14 deletions(-) diff --git a/research/rendering/fractal-reference-oracles.md b/research/rendering/fractal-reference-oracles.md index 49e3fcc..7110719 100644 --- a/research/rendering/fractal-reference-oracles.md +++ b/research/rendering/fractal-reference-oracles.md @@ -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. diff --git a/src/Aquarium.Engine.Fractal/Grammar/FractalFlameFileParser.cs b/src/Aquarium.Engine.Fractal/Grammar/FractalFlameFileParser.cs index 87211a6..b607301 100644 --- a/src/Aquarium.Engine.Fractal/Grammar/FractalFlameFileParser.cs +++ b/src/Aquarium.Engine.Fractal/Grammar/FractalFlameFileParser.cs @@ -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 accepted, List approximated, List 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; } diff --git a/state/evidence.jsonl b/state/evidence.jsonl index e937586..9aa5991 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -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"} diff --git a/state/map.yaml b/state/map.yaml index b0a6a03..ea19d9d 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -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. diff --git a/state/memory.json b/state/memory.json index 2009723..62b9ed6 100644 --- a/state/memory.json +++ b/state/memory.json @@ -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.", diff --git a/tests/Aquarium.Engine.Fractal.Tests/ApophysisReferenceParityTests.cs b/tests/Aquarium.Engine.Fractal.Tests/ApophysisReferenceParityTests.cs index 0607b95..3ccc168 100644 --- a/tests/Aquarium.Engine.Fractal.Tests/ApophysisReferenceParityTests.cs +++ b/tests/Aquarium.Engine.Fractal.Tests/ApophysisReferenceParityTests.cs @@ -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); diff --git a/tools/Aquarium.Fractal.Receipt/Program.cs b/tools/Aquarium.Fractal.Receipt/Program.cs index ea383c8..cf05f76 100644 --- a/tools/Aquarium.Fractal.Receipt/Program.cs +++ b/tools/Aquarium.Fractal.Receipt/Program.cs @@ -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;