Add flame histogram receipt mode
This commit is contained in:
parent
3ae409817c
commit
d6aba37faf
6 changed files with 184 additions and 3 deletions
|
|
@ -45,6 +45,26 @@ This is not full flame parity. It is the first ruler on the desk.
|
|||
4. Lower proven flame definitions into GPU program rows. Only then should the
|
||||
live reservoir renderer use them as visual test scenes.
|
||||
|
||||
## Local Receipt
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
.\scripts\fractal-flame-receipt.ps1
|
||||
```
|
||||
|
||||
Default receipt:
|
||||
|
||||
- fixture: `linear-spherical-bubble.flame`
|
||||
- samples: `8192`
|
||||
- histogram: `64x64`
|
||||
- bounds: `-8,-8,8,8`
|
||||
- seed: `0x0BADC0DE`
|
||||
- expected checksum: `0xAEB1C81B`
|
||||
|
||||
This receipt is Aquarium's local CPU parser/evaluator checksum. It is not yet
|
||||
an external FLAM3/Apophysis renderer receipt.
|
||||
|
||||
## Sources
|
||||
|
||||
- Apophysis 7x repository: https://github.com/wanily/apophysis7x
|
||||
|
|
|
|||
19
scripts/fractal-flame-receipt.ps1
Normal file
19
scripts/fractal-flame-receipt.ps1
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
param(
|
||||
[string]$Flame = "tests\Aquarium.Engine.Fractal.Tests\Fixtures\Apophysis\linear-spherical-bubble.flame",
|
||||
[int]$Samples = 8192,
|
||||
[int]$BurnIn = 64,
|
||||
[string]$HistogramSize = "64x64",
|
||||
[string]$Bounds = "-8,-8,8,8",
|
||||
[string]$Seed = "0x0BADC0DE"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
dotnet run --project (Join-Path $repoRoot "tools\Aquarium.Fractal.Receipt\Aquarium.Fractal.Receipt.csproj") -c Release -- `
|
||||
--flame (Join-Path $repoRoot $Flame) `
|
||||
--histogram-samples $Samples `
|
||||
--histogram-burn-in $BurnIn `
|
||||
--histogram-size $HistogramSize `
|
||||
--histogram-bounds $Bounds `
|
||||
--seed $Seed `
|
||||
--out (Join-Path $repoRoot "artifacts\fractal-flame-receipts")
|
||||
|
|
@ -189,3 +189,4 @@
|
|||
{"timestamp":"2026-05-20T13:45:00+01:00","topic":"zyphos-fractal-surface-legibility","lesson":"The live Zyphos reservoir path was fast but visually incoherent because the GPU program packet discarded cube tile X/Y and the camera targeted parent pivots instead of selected domains. `AquariumPackedFractalIfsTransform.TileAddress` now preserves face/level/x/y, D3D12FractalReservoirCompute maps authored supports onto cube-sphere faces, D3D12FractalSplatRender projects tangent-frame surface patches from world space, and ZyphosCameraComposer targets the selected domain while keeping the parent as orbit anchor. The result still reads too abstractly; the next coherent renderer cut is a clustered/ray-query surface resolve for reservoir splats rather than more camera-facing billboard cosmetics.","files":["src/Aquarium.Engine.Contracts/Fractal/AquariumFractalContracts.cs","src/Aquarium.Engine.Fractal/Lod/FractalGpuProgramCompiler.cs","src/Aquarium.Engine/Render/Shaders/D3D12FractalReservoirCompute.hlsl","src/Aquarium.Engine/Render/Shaders/D3D12FractalSplatRender.hlsl","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Zyphos/ZyphosCameraComposer.cs","tests/Aquarium.Engine.Fractal.Tests/FractalGpuProgramCompilerTests.cs","tests/Aquarium.Engine.Fractal.Tests/ZyphosCameraComposerTests.cs"],"source":"user asked whether the current view looked like anything; answer was no, so the packet/domain/camera lies had to be cut before more rendering claims"}
|
||||
{"timestamp":"2026-05-20T14:05:00+01:00","topic":"apophysis-reference-first-fractal-parity","lesson":"Zyphos is too entangled to be the first correctness oracle for the fractal DSL. Aquarium now has a tiny Apophysis/FLAM3-shaped reference gate: `.flame` linear Sierpinski is parsed by an independent test helper, `.aquageo` expresses the same affine IFS through `affineifs`/`affinemap`, and tests compare exact fixed-transform sequence points plus chaos-game moment bounds. This follows the AquaSynth pattern: open-source/reference systems are external pressure and receipts, not runtime organs or copied code. Next parity gates should parse more `.flame` semantics and compare against FLAM3/Apophysis-compatible rendered histograms before feeding the GPU reservoir renderer.","files":["src/Aquarium.Engine.Fractal/Grammar/FractalAffineIfsDefinition.cs","src/Aquarium.Engine.Fractal/Grammar/FractalAffineIfsChaosGame.cs","src/Aquarium.Engine.Fractal/Grammar/FractalDslCompiler.cs","src/Aquarium.Engine.Fractal/Grammar/FractalOwnershipTree.cs","tests/Aquarium.Engine.Fractal.Tests/ApophysisReferenceParityTests.cs","tests/Aquarium.Engine.Fractal.Tests/Fixtures/Apophysis/linear-sierpinski.flame","tests/Aquarium.Engine.Fractal.Tests/Fixtures/Apophysis/linear-sierpinski.aquageo","research/rendering/fractal-reference-oracles.md"],"source":"user redirected from visual Zyphos debugging to reference-rendered IFS parity and suggested Apophysis 7x"}
|
||||
{"timestamp":"2026-05-20T14:20:00+01:00","topic":"flame-subset-parser-histogram-receipts","lesson":"The reference path now parses and evaluates a small Apophysis/FLAM3 flame subset in runtime code: xform weight, color, affine coefficients, and `linear`, `spherical`, and `bubble` variation weights. Tests cover parser fields, independent variation formulas, and a deterministic 64x64 histogram checksum for a mixed linear/spherical/bubble fixture. This is still not external renderer parity, but it gives Aquarium a stable CPU receipt before GPU lowering and keeps Zyphos out of the correctness loop.","files":["src/Aquarium.Engine.Fractal/Grammar/FractalFlameDefinition.cs","src/Aquarium.Engine.Fractal/Grammar/FractalFlameFileParser.cs","src/Aquarium.Engine.Fractal/Grammar/FractalFlameChaosGame.cs","src/Aquarium.Engine.Fractal/Grammar/FractalPointHistogram.cs","tests/Aquarium.Engine.Fractal.Tests/ApophysisReferenceParityTests.cs","tests/Aquarium.Engine.Fractal.Tests/Fixtures/Apophysis/linear-spherical-bubble.flame","research/rendering/fractal-reference-oracles.md"],"source":"continued after adopting Apophysis/FLAM3 as the fractal reference family"}
|
||||
{"timestamp":"2026-05-20T14:30:00+01:00","topic":"flame-histogram-receipt-cli","lesson":"The receipt tool now has a non-GPU `--flame` mode that loads a `.flame` fixture, runs FractalFlameChaosGame, bins points through FractalPointHistogramBuilder, and writes a JSON checksum receipt. `scripts/fractal-flame-receipt.ps1` runs the default mixed linear/spherical/bubble fixture with seed 0x0BADC0DE and emits checksum 0xAEB1C81B. This creates a cheap local receipt path before wiring external FLAM3/Apophysis renderer parity or GPU lowering.","files":["tools/Aquarium.Fractal.Receipt/Program.cs","scripts/fractal-flame-receipt.ps1","research/rendering/fractal-reference-oracles.md"],"source":"continued reference-first fractal pipeline build"}
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ renderer_state:
|
|||
- Zyphos camera composition targets the selected domain's tracked center while preserving the parent domain as orbit/composition anchor. Parent pivots remain part of navigation, but selected planetary/tile domains must not be pushed offscreen by aiming the camera at their parent.
|
||||
- Fractal reference parity starts before the renderer: FractalOwnershipTree can carry affine IFS definitions from `.aquageo`, FractalAffineIfsChaosGame generates deterministic point sets, and ApophysisReferenceParityTests compares a minimal Apophysis/FLAM3-style `.flame` linear Sierpinski fixture against an equivalent `.aquageo` candidate. Apophysis/FLAM3 are test oracles, not runtime code.
|
||||
- The Apophysis/FLAM3 reference subset now has a runtime parser/evaluator boundary: FractalFlameFileParser reads `.flame` xforms with weight, color, affine coefficients, and `linear`/`spherical`/`bubble` variation weights; FractalFlameChaosGame produces deterministic point sets; FractalPointHistogramBuilder produces fixed histogram checksums for parity receipts.
|
||||
- tools/Aquarium.Fractal.Receipt can now run a CPU flame histogram receipt with `--flame`; scripts/fractal-flame-receipt.ps1 emits the default linear/spherical/bubble fixture checksum 0xAEB1C81B under artifacts/fractal-flame-receipts. This is a local parser/evaluator receipt, not external renderer parity yet.
|
||||
- D3D12 capacity logging now reports active fractal reservoir resident MiB, splat count, visible debug splat count, updates/pass, and candidates/update so the GPU cache has a receipt in normal runtime logs, not just in the standalone benchmark.
|
||||
- The consolidated fractal brush architecture plan defines the intended pipeline: client intent -> domain roots -> semantic IFS brush grammar -> ownership tree -> node summaries -> contribution cache/residency scheduler -> backend packets -> renderer passes -> debug/evidence, with explicit module boundaries and mockable unit-test seams.
|
||||
- The Perfect Machine architecture lives in docs/perfect-machine-architecture.md. It is the current waterfall design authority for fast 2D SDF splats, 3D SDF splats, 2D-projected-to-3D surface splats, IFS `.aquageo` structural probes, Mimir-style camera/audio evidence candidates, ReSTIR/GRIS reservoir reuse, heuristic occupancy graph updates, resource-tier residency, and TAA guide-buffer integration.
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@
|
|||
"Zyphos camera composition must target the selected spatial domain's tracked center while keeping the parent as orbit/composition anchor. Looking directly at the parent made planetary mode aim at the binary midpoint and left Zyphos offscreen, which made renderer failures look like art failures.",
|
||||
"Do not use Zyphos as the first proof of fractal DSL correctness. Start with reference fractals. Apophysis/FLAM3-style `.flame` fixtures are the parity pressure for Aquarium `.aquageo`; GPL/open-source renderers stay external/test-only. The first gate is linear Sierpinski: exact fixed-xform sequence parity and chaos-game moment bounds before any GPU visualization claim.",
|
||||
"The local Apophysis/FLAM3 subset parser owns only a small proven flame shape: xform weight/color, affine coefficients, and `linear`, `spherical`, and `bubble` variation weights. `FractalFlameChaosGame` and `FractalPointHistogramBuilder` provide deterministic CPU receipts. Next step is external FLAM3/Apophysis rendered histogram parity before GPU lowering.",
|
||||
"Use `scripts/fractal-flame-receipt.ps1` for the local CPU flame receipt. The default mixed linear/spherical/bubble fixture uses 8192 samples, 64 burn-in, 64x64 bounds -8,-8,8,8, seed 0x0BADC0DE, and checksum 0xAEB1C81B. Treat this as parser/evaluator proof only; external FLAM3/Apophysis renderer parity is still the next gate.",
|
||||
"Zyphos' primary planetary terrain test path is now the fractal reservoir surface. The direct SDF planet and Umbros proxy shaders are removed from the render plan; only the parent star remains as a direct SDF body/light. If the planet looks wrong, fix the reservoir lowering/shading path instead of letting the old SDF terrain hide it.",
|
||||
"The GPU splat receipt harness lives at tools/Aquarium.Fractal.Receipt and scripts/fractal-splat-receipt.ps1. The live receipt allocates one 80-byte packed SDF splat UAV plus three distinct 64-byte reservoir UAVs for SDF envelopes, PBR material envelopes, and radiosity; those reservoirs are sampled in independent compute passes under a stochastic update budget. On the local NVIDIA GTX 1070 it kept 2,000,000 splats and 2,000,000 reservoirs per layer resident. The fallback hash-fractal receipt updated 50,000 entries per layer with 2 candidates/update at 4.471 ms/frame, 223.7 FPS equivalent. The authored precomposed selected-cut receipt used depth 48, 4 compact transforms, and 15,000 updates per layer at 4.908 ms/frame, 203.7 FPS equivalent, with full reservoir coverage in 133.3 frames. D3D12FractalSplatRender consumes the raw splat, SDF, PBR, and radiosity buffers through separate graphics SRVs for the visible debug subset. This proves compute-side GPU residency, budgeted temporal convergence, authored-program splat population decoupled from domain ancestry depth, and first layered GPU consumption; final terrain/material/radiosity shading is still future work.",
|
||||
"FractalProjectedSdfSplatCompiler is the Phase G 2D-projected-to-3D lowering seam: it converts resident SignedDistance2D page payload samples into compact 3D SDF splats through an explicit page-local Vector2 -> Vector3 mapping, surface normal, support radius, and splat budget. Zyphos caches a bounded first set with a flat local mapping until cube-sphere/torus mappers are added.",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Numerics;
|
|||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using Aquarium.Engine.Fractal;
|
||||
using Aquarium.Engine.Fractal.Grammar;
|
||||
using Aquarium.Engine.Fractal.Lod;
|
||||
using Vortice.D3DCompiler;
|
||||
using Vortice.Direct3D;
|
||||
|
|
@ -10,6 +11,12 @@ using Vortice.Direct3D12;
|
|||
using Vortice.DXGI;
|
||||
|
||||
var options = ReceiptOptions.Parse(args);
|
||||
if (options.FlamePath is not null)
|
||||
{
|
||||
RunFlameHistogramReceipt(options);
|
||||
return;
|
||||
}
|
||||
|
||||
var shaderSource = File.ReadAllText(options.ShaderPath);
|
||||
using var runner = new GpuFractalSplatReceiptRunner(shaderSource);
|
||||
var receipt = runner.Run(options);
|
||||
|
|
@ -41,6 +48,57 @@ Console.WriteLine($"cpu submit+wait ms/frame: {receipt.CpuSubmitAndWaitMilliseco
|
|||
Console.WriteLine($"readback checksum: 0x{receipt.ReadbackChecksum:X16}");
|
||||
Console.WriteLine($"receipt: {receiptPath}");
|
||||
|
||||
static void RunFlameHistogramReceipt(ReceiptOptions options)
|
||||
{
|
||||
var flameSource = File.ReadAllText(options.FlamePath!);
|
||||
var flame = FractalFlameFileParser.ParseFirst(flameSource, unchecked((int)options.Seed));
|
||||
var points = FractalFlameChaosGame.Generate(
|
||||
flame,
|
||||
options.HistogramSamples,
|
||||
options.HistogramBurnIn,
|
||||
new FractalXorShiftRandom(options.Seed));
|
||||
var histogram = FractalPointHistogramBuilder.Build(
|
||||
points,
|
||||
options.HistogramWidth,
|
||||
options.HistogramHeight,
|
||||
options.HistogramBounds);
|
||||
var checksum = histogram.Bins.Aggregate(2166136261u, (hash, value) => unchecked((hash ^ (uint)value) * 16777619u));
|
||||
var occupiedBins = histogram.Bins.Count(value => value > 0);
|
||||
var receipt = new FlameHistogramReceipt(
|
||||
Path.GetFullPath(options.FlamePath!),
|
||||
flame.Name,
|
||||
flame.Transforms.Count,
|
||||
options.HistogramSamples,
|
||||
options.HistogramBurnIn,
|
||||
options.HistogramWidth,
|
||||
options.HistogramHeight,
|
||||
[
|
||||
options.HistogramBounds.X,
|
||||
options.HistogramBounds.Y,
|
||||
options.HistogramBounds.Z,
|
||||
options.HistogramBounds.W,
|
||||
],
|
||||
histogram.HitCount,
|
||||
occupiedBins,
|
||||
checksum);
|
||||
|
||||
Directory.CreateDirectory(options.OutputDirectory);
|
||||
var receiptPath = Path.Combine(options.OutputDirectory, $"fractal-flame-histogram-receipt-{DateTimeOffset.Now:yyyyMMdd-HHmmss}.json");
|
||||
File.WriteAllText(receiptPath, JsonSerializer.Serialize(receipt, new JsonSerializerOptions { WriteIndented = true }));
|
||||
|
||||
Console.WriteLine("=== Aquarium Fractal Flame Histogram Receipt ===");
|
||||
Console.WriteLine($"flame: {receipt.FlameName}");
|
||||
Console.WriteLine($"source: {receipt.SourcePath}");
|
||||
Console.WriteLine($"xforms: {receipt.TransformCount:N0}");
|
||||
Console.WriteLine($"samples: {receipt.SampleCount:N0}");
|
||||
Console.WriteLine($"burn-in: {receipt.BurnIn:N0}");
|
||||
Console.WriteLine($"histogram: {receipt.Width}x{receipt.Height}");
|
||||
Console.WriteLine($"hits: {receipt.HitCount:N0}");
|
||||
Console.WriteLine($"occupied bins: {receipt.OccupiedBins:N0}");
|
||||
Console.WriteLine($"histogram checksum: 0x{receipt.Checksum:X8}");
|
||||
Console.WriteLine($"receipt: {receiptPath}");
|
||||
}
|
||||
|
||||
internal sealed class GpuFractalSplatReceiptRunner : IDisposable
|
||||
{
|
||||
private const int ThreadGroupSize = 256;
|
||||
|
|
@ -368,7 +426,13 @@ internal sealed record ReceiptOptions(
|
|||
int ProgramTransformCount,
|
||||
int ReadbackSplats,
|
||||
string ShaderPath,
|
||||
string OutputDirectory)
|
||||
string OutputDirectory,
|
||||
string? FlamePath,
|
||||
int HistogramSamples,
|
||||
int HistogramBurnIn,
|
||||
int HistogramWidth,
|
||||
int HistogramHeight,
|
||||
Vector4 HistogramBounds)
|
||||
{
|
||||
public static ReceiptOptions Parse(string[] args)
|
||||
{
|
||||
|
|
@ -383,7 +447,13 @@ internal sealed record ReceiptOptions(
|
|||
0,
|
||||
64,
|
||||
Path.Combine("src", "Aquarium.Engine", "Render", "Shaders", "D3D12FractalReservoirCompute.hlsl"),
|
||||
Path.Combine("artifacts", "fractal-splat-receipts"));
|
||||
Path.Combine("artifacts", "fractal-splat-receipts"),
|
||||
null,
|
||||
8192,
|
||||
64,
|
||||
64,
|
||||
64,
|
||||
new Vector4(-8.0f, -8.0f, 8.0f, 8.0f));
|
||||
for (var index = 0; index < args.Length; index++)
|
||||
{
|
||||
var arg = args[index];
|
||||
|
|
@ -394,17 +464,37 @@ internal sealed record ReceiptOptions(
|
|||
"--warmup" => options with { WarmupFrames = int.Parse(Next()) },
|
||||
"--frames" => options with { MeasuredFrames = int.Parse(Next()) },
|
||||
"--depth" => options with { Depth = int.Parse(Next()) },
|
||||
"--seed" => options with { Seed = Convert.ToUInt32(Next(), 0) },
|
||||
"--seed" => options with { Seed = ParseUInt32(Next()) },
|
||||
"--candidates" => options with { CandidatesPerPass = int.Parse(Next()) },
|
||||
"--reservoir-updates" => options with { ReservoirUpdatesPerPass = int.Parse(Next()) },
|
||||
"--program-transforms" => options with { ProgramTransformCount = int.Parse(Next()) },
|
||||
"--readback-splats" => options with { ReadbackSplats = int.Parse(Next()) },
|
||||
"--shader" => options with { ShaderPath = Next() },
|
||||
"--out" => options with { OutputDirectory = Next() },
|
||||
"--flame" => options with { FlamePath = Next() },
|
||||
"--histogram-samples" => options with { HistogramSamples = int.Parse(Next()) },
|
||||
"--histogram-burn-in" => options with { HistogramBurnIn = int.Parse(Next()) },
|
||||
"--histogram-size" => ParseHistogramSize(options, Next()),
|
||||
"--histogram-bounds" => ParseHistogramBounds(options, Next()),
|
||||
_ => throw new ArgumentException($"Unknown receipt option: {arg}"),
|
||||
};
|
||||
}
|
||||
|
||||
if (options.FlamePath is not null)
|
||||
{
|
||||
if (!File.Exists(options.FlamePath))
|
||||
{
|
||||
throw new FileNotFoundException("Receipt flame file was not found.", options.FlamePath);
|
||||
}
|
||||
|
||||
if (options.HistogramSamples <= 0 || options.HistogramBurnIn < 0 || options.HistogramWidth <= 0 || options.HistogramHeight <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(args), "Flame histogram samples and dimensions must be positive; burn-in must not be negative.");
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
if (!File.Exists(options.ShaderPath))
|
||||
{
|
||||
throw new FileNotFoundException("Receipt shader file was not found.", options.ShaderPath);
|
||||
|
|
@ -416,8 +506,57 @@ internal sealed record ReceiptOptions(
|
|||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
private static ReceiptOptions ParseHistogramSize(ReceiptOptions options, string value)
|
||||
{
|
||||
var parts = value.Split('x', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (parts.Length != 2)
|
||||
{
|
||||
throw new ArgumentException("Histogram size must be WIDTHxHEIGHT.");
|
||||
}
|
||||
|
||||
return options with { HistogramWidth = int.Parse(parts[0]), HistogramHeight = int.Parse(parts[1]) };
|
||||
}
|
||||
|
||||
private static ReceiptOptions ParseHistogramBounds(ReceiptOptions options, string value)
|
||||
{
|
||||
var parts = value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (parts.Length != 4)
|
||||
{
|
||||
throw new ArgumentException("Histogram bounds must be minX,minY,maxX,maxY.");
|
||||
}
|
||||
|
||||
return options with
|
||||
{
|
||||
HistogramBounds = new Vector4(
|
||||
float.Parse(parts[0], System.Globalization.CultureInfo.InvariantCulture),
|
||||
float.Parse(parts[1], System.Globalization.CultureInfo.InvariantCulture),
|
||||
float.Parse(parts[2], System.Globalization.CultureInfo.InvariantCulture),
|
||||
float.Parse(parts[3], System.Globalization.CultureInfo.InvariantCulture)),
|
||||
};
|
||||
}
|
||||
|
||||
private static uint ParseUInt32(string value)
|
||||
{
|
||||
return value.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
||||
? Convert.ToUInt32(value[2..], 16)
|
||||
: uint.Parse(value, System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record FlameHistogramReceipt(
|
||||
string SourcePath,
|
||||
string FlameName,
|
||||
int TransformCount,
|
||||
int SampleCount,
|
||||
int BurnIn,
|
||||
int Width,
|
||||
int Height,
|
||||
float[] Bounds,
|
||||
int HitCount,
|
||||
int OccupiedBins,
|
||||
uint Checksum);
|
||||
|
||||
internal sealed record GpuFractalSplatReceipt(
|
||||
string Adapter,
|
||||
int SplatCount,
|
||||
|
|
|
|||
Loading…
Reference in a new issue