Aquarium/tools/Aquarium.Fractal.Receipt/Program.cs

1335 lines
59 KiB
C#

using System.Diagnostics;
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;
using Vortice.Direct3D12;
using Vortice.DXGI;
var options = ReceiptOptions.Parse(args);
if (options.ReferencePpmPath is not null)
{
RunReferencePpmReceipt(options);
return;
}
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);
Directory.CreateDirectory(options.OutputDirectory);
var receiptStamp = DateTimeOffset.Now.ToString("yyyyMMdd-HHmmss");
var receiptPath = Path.Combine(options.OutputDirectory, $"fractal-gpu-splat-receipt-{receiptStamp}.json");
File.WriteAllText(receiptPath, JsonSerializer.Serialize(receipt, new JsonSerializerOptions { WriteIndented = true }));
var importReportPath = WriteProgramFlameImportReport(options, receiptStamp);
Console.WriteLine("=== Aquarium Perfect Machine GPU Splat Receipt ===");
Console.WriteLine($"adapter: {receipt.Adapter}");
Console.WriteLine($"splats: {receipt.SplatCount:N0}");
Console.WriteLine($"sdf reservoirs: {receipt.SdfReservoirCount:N0}");
Console.WriteLine($"pbr reservoirs: {receipt.PbrReservoirCount:N0}");
Console.WriteLine($"radiosity reservoirs: {receipt.RadiosityReservoirCount:N0}");
Console.WriteLine($"flame states: {receipt.FlameStateCount:N0}");
Console.WriteLine($"ifs program transforms: {receipt.ProgramTransformCount:N0}");
Console.WriteLine($"program mode: {DescribeProgramMode(receipt.ProgramMode)}");
Console.WriteLine($"warmup splat updates/frame: {receipt.WarmupSplatUpdatesPerFrame:N0}");
Console.WriteLine($"splat updates/frame: {receipt.SplatUpdatesPerFrame:N0}");
Console.WriteLine($"candidates/pass: {receipt.CandidatesPerPass}");
Console.WriteLine($"reservoir updates/pass/frame: {receipt.ReservoirUpdatesPerPass:N0}");
Console.WriteLine($"reservoir full-coverage frames: {receipt.ReservoirFullCoverageFrames:0.0}");
Console.WriteLine($"frames: {receipt.MeasuredFrames}");
Console.WriteLine("shader: D3D12 compute, independent GPU-resident SDF/PBR/radiosity reservoir passes");
Console.WriteLine($"packed bytes/splat: {receipt.BytesPerSplat}");
Console.WriteLine($"packed bytes/reservoir: {receipt.BytesPerReservoir}");
Console.WriteLine($"packed bytes/flame state: {receipt.BytesPerFlameState}");
Console.WriteLine($"gpu ms/frame total: {receipt.GpuMillisecondsPerFrame:0.000}");
Console.WriteLine($"gpu equivalent fps: {receipt.GpuEquivalentFps:0.0}");
Console.WriteLine($"gpu splats/sec: {receipt.GpuSplatsPerSecond:N0}");
Console.WriteLine($"gpu reservoir candidates/sec: {receipt.GpuReservoirCandidatesPerSecond:N0}");
Console.WriteLine($"cpu submit+wait ms/frame: {receipt.CpuSubmitAndWaitMillisecondsPerFrame:0.000}");
Console.WriteLine($"readback checksum: 0x{receipt.ReadbackChecksum:X16}");
if (importReportPath is not null)
{
var report = FractalFlameFileParser.ParseFirstWithReport(File.ReadAllText(options.ProgramFlamePath!), unchecked((int)options.Seed)).Report;
Console.WriteLine($"flame import: accepted {report.AcceptedFieldCount:N0}, approximated {report.ApproximatedFieldCount:N0}, ignored {report.IgnoredFieldCount:N0}, rejected {report.RejectedFieldCount:N0}");
Console.WriteLine($"flame import report: {importReportPath}");
}
if (receipt.VisualParity is not null)
{
Console.WriteLine($"visual parity reference: {receipt.VisualParity.ReferenceSource}");
Console.WriteLine($"visual parity candidate: {receipt.VisualParity.CandidateSource}");
Console.WriteLine($"visual parity density mass: {receipt.VisualParity.ComparedSamples:N0}/{receipt.VisualParity.ReferenceSamples:N0}");
Console.WriteLine($"visual parity views: {receipt.VisualParity.Views.Count:N0}");
Console.WriteLine($"visual parity bins: {receipt.VisualParity.Width}x{receipt.VisualParity.Height}");
Console.WriteLine($"visual parity occupancy overlap: {receipt.VisualParity.OccupancyOverlapPercent:0.00}%");
Console.WriteLine($"visual parity distribution score: {receipt.VisualParity.DistributionScorePercent:0.00}%");
Console.WriteLine($"visual parity l1 distance: {receipt.VisualParity.L1Distance:0.000000}");
Console.WriteLine($"visual parity rmse: {receipt.VisualParity.Rmse:0.000000}");
Console.WriteLine($"visual parity cosine: {receipt.VisualParity.CosineSimilarity:0.000000}");
foreach (var view in receipt.VisualParity.Views)
{
Console.WriteLine($"visual view {view.Name}: score {view.DistributionScorePercent:0.00}% / hits gpu {view.GpuHitCount:N0}, ref {view.ReferenceHitCount:N0} / starved bins {view.StarvedReferenceBins:N0} / under-mass {view.UnderrepresentedMassPercent:0.00}%");
}
}
Console.WriteLine($"receipt: {receiptPath}");
static string DescribeProgramMode(int mode)
{
return mode switch
{
1 => "precomposed selected cut",
2 => "flame 2D affine/variation program",
_ => "fallback hash IFS",
};
}
static void RunReferencePpmReceipt(ReceiptOptions options)
{
var ppmBytes = File.ReadAllBytes(options.ReferencePpmPath!);
var image = FractalPpmImageReceiptBuilder.Build(ppmBytes);
var receipt = new ReferencePpmReceipt(
Path.GetFullPath(options.ReferencePpmPath!),
image.Width,
image.Height,
image.MaxChannelValue,
image.PixelCount,
image.NonBlackPixelCount,
image.RgbChecksum,
image.LuminanceChecksum);
Directory.CreateDirectory(options.OutputDirectory);
var receiptPath = Path.Combine(options.OutputDirectory, $"fractal-reference-ppm-receipt-{DateTimeOffset.Now:yyyyMMdd-HHmmss}.json");
File.WriteAllText(receiptPath, JsonSerializer.Serialize(receipt, new JsonSerializerOptions { WriteIndented = true }));
Console.WriteLine("=== Aquarium External Flame Reference PPM Receipt ===");
Console.WriteLine($"source: {receipt.SourcePath}");
Console.WriteLine($"image: {receipt.Width}x{receipt.Height}");
Console.WriteLine($"pixels: {receipt.PixelCount:N0}");
Console.WriteLine($"non-black pixels: {receipt.NonBlackPixelCount:N0}");
Console.WriteLine($"rgb checksum: 0x{receipt.RgbChecksum:X16}");
Console.WriteLine($"luminance checksum: 0x{receipt.LuminanceChecksum:X16}");
Console.WriteLine($"receipt: {receiptPath}");
}
static void RunFlameHistogramReceipt(ReceiptOptions options)
{
var flameSource = File.ReadAllText(options.FlamePath!);
var parsed = FractalFlameFileParser.ParseFirstWithReport(flameSource, unchecked((int)options.Seed));
var flame = parsed.Definition;
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 stamp = DateTimeOffset.Now.ToString("yyyyMMdd-HHmmss");
var receiptPath = Path.Combine(options.OutputDirectory, $"fractal-flame-histogram-receipt-{stamp}.json");
File.WriteAllText(receiptPath, JsonSerializer.Serialize(receipt, new JsonSerializerOptions { WriteIndented = true }));
var importReportPath = Path.Combine(options.OutputDirectory, $"fractal-flame-import-report-{stamp}.json");
File.WriteAllText(importReportPath, JsonSerializer.Serialize(new FlameImportReportReceipt(Path.GetFullPath(options.FlamePath!), parsed.Report), 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($"flame import: accepted {parsed.Report.AcceptedFieldCount:N0}, approximated {parsed.Report.ApproximatedFieldCount:N0}, ignored {parsed.Report.IgnoredFieldCount:N0}, rejected {parsed.Report.RejectedFieldCount:N0}");
Console.WriteLine($"flame import report: {importReportPath}");
Console.WriteLine($"receipt: {receiptPath}");
}
static string? WriteProgramFlameImportReport(ReceiptOptions options, string receiptStamp)
{
if (options.ProgramFlamePath is null)
{
return null;
}
var parsed = FractalFlameFileParser.ParseFirstWithReport(File.ReadAllText(options.ProgramFlamePath), unchecked((int)options.Seed));
var reportPath = Path.Combine(options.OutputDirectory, $"fractal-flame-import-report-{receiptStamp}.json");
File.WriteAllText(reportPath, JsonSerializer.Serialize(new FlameImportReportReceipt(Path.GetFullPath(options.ProgramFlamePath), parsed.Report), new JsonSerializerOptions { WriteIndented = true }));
return reportPath;
}
internal sealed class GpuFractalSplatReceiptRunner : IDisposable
{
private const int ThreadGroupSize = 256;
private readonly ID3D12Device device;
private readonly ID3D12CommandQueue queue;
private readonly ID3D12CommandAllocator allocator;
private readonly ID3D12GraphicsCommandList commandList;
private readonly ID3D12Fence fence;
private readonly AutoResetEvent fenceEvent = new(false);
private readonly ID3D12RootSignature rootSignature;
private readonly ID3D12PipelineState splatPipelineState;
private readonly ID3D12PipelineState sdfPipelineState;
private readonly ID3D12PipelineState pbrPipelineState;
private readonly ID3D12PipelineState radiosityPipelineState;
private readonly string adapterName;
private ulong fenceValue;
public GpuFractalSplatReceiptRunner(string shaderSource)
{
ArgumentException.ThrowIfNullOrWhiteSpace(shaderSource);
device = D3D12.D3D12CreateDevice<ID3D12Device>(IntPtr.Zero, FeatureLevel.Level_11_0);
adapterName = ResolveAdapterName();
queue = device.CreateCommandQueue(new CommandQueueDescription(CommandListType.Direct));
allocator = device.CreateCommandAllocator(CommandListType.Direct);
commandList = device.CreateCommandList<ID3D12GraphicsCommandList>(0, CommandListType.Direct, allocator, null);
commandList.Close();
fence = device.CreateFence(0);
rootSignature = CreateRootSignature(device);
splatPipelineState = CreatePipelineState(device, rootSignature, shaderSource, "D3D12FractalSplatReceiptCS");
sdfPipelineState = CreatePipelineState(device, rootSignature, shaderSource, "D3D12SdfEnvelopeReservoirCS");
pbrPipelineState = CreatePipelineState(device, rootSignature, shaderSource, "D3D12PbrMaterialReservoirCS");
radiosityPipelineState = CreatePipelineState(device, rootSignature, shaderSource, "D3D12RadiosityReservoirCS");
}
public GpuFractalSplatReceipt Run(ReceiptOptions options)
{
var resolvedProgramTransforms = BuildReceiptProgram(options);
var resolvedProgramMode = resolvedProgramTransforms.Length > 0 ? options.ProgramMode : 0;
var splatStride = Marshal.SizeOf<AquariumPackedFractalSdfSplat3D>();
var reservoirStride = Marshal.SizeOf<AquariumPackedSdfEnvelopeReservoir>();
var flameStateStride = Marshal.SizeOf<AquariumPackedFractalFlameState>();
var framePlan = FractalGpuReservoirBudgetPlanner.Plan(
options.SplatCount,
options.ReservoirUpdatesPerPass,
options.CandidatesPerPass,
splatStride,
reservoirStride);
var splatBytes = checked((ulong)splatStride * (ulong)options.SplatCount);
var reservoirBytes = checked((ulong)reservoirStride * (ulong)options.SplatCount);
var flameStateBytes = checked((ulong)flameStateStride * (ulong)options.SplatCount);
using var splats = CreateUavBuffer(splatBytes, "Aquarium Fractal Receipt GPU Splat Buffer");
using var sdfReservoirs = CreateUavBuffer(reservoirBytes, "Aquarium Fractal Receipt SDF Reservoir Buffer");
using var pbrReservoirs = CreateUavBuffer(reservoirBytes, "Aquarium Fractal Receipt PBR Reservoir Buffer");
using var radiosityReservoirs = CreateUavBuffer(reservoirBytes, "Aquarium Fractal Receipt Radiosity Reservoir Buffer");
using var flameStates = CreateUavBuffer(flameStateBytes, "Aquarium Fractal Receipt Flame Iteration State Buffer");
using var programTransforms = CreateProgramTransformBuffer(resolvedProgramTransforms);
var readbackSplatBytes = (ulong)Math.Min(options.ReadbackSplats, options.SplatCount) * (ulong)splatStride;
var readbackReservoirBytes = (ulong)Math.Min(options.ReadbackSplats, options.SplatCount) * (ulong)reservoirStride;
var totalReadbackBytes = readbackSplatBytes + (readbackReservoirBytes * 3UL);
using var readback = device.CreateCommittedResource(HeapType.Readback, ResourceDescription.Buffer(Math.Max(totalReadbackBytes, 1)), ResourceStates.CopyDest, null);
using var queryHeap = device.CreateQueryHeap<ID3D12QueryHeap>(new QueryHeapDescription(QueryHeapType.Timestamp, 2u));
using var queryReadback = device.CreateCommittedResource(HeapType.Readback, ResourceDescription.Buffer(16), ResourceStates.CopyDest, null);
queue.GetTimestampFrequency(out var timestampFrequency);
var measuredGpuTicks = 0UL;
var measuredCpuTicks = 0L;
var measuredFrames = 0;
for (var frame = 0; frame < options.WarmupFrames + options.MeasuredFrames; frame++)
{
var splatDispatchCount = frame == 0
? options.SplatCount
: frame < options.WarmupFrames ? options.WarmupSplatUpdatesPerFrame : options.SplatUpdatesPerFrame;
allocator.Reset();
commandList.Reset(allocator, splatPipelineState);
commandList.SetComputeRootSignature(rootSignature);
BindConstants(options, frame, resolvedProgramTransforms.Length, resolvedProgramMode, splatDispatchCount);
commandList.SetComputeRootUnorderedAccessView(1, splats.GPUVirtualAddress);
commandList.SetComputeRootUnorderedAccessView(2, sdfReservoirs.GPUVirtualAddress);
commandList.SetComputeRootUnorderedAccessView(3, pbrReservoirs.GPUVirtualAddress);
commandList.SetComputeRootUnorderedAccessView(4, radiosityReservoirs.GPUVirtualAddress);
commandList.SetComputeRootUnorderedAccessView(5, flameStates.GPUVirtualAddress);
commandList.SetComputeRootShaderResourceView(6, programTransforms.GPUVirtualAddress);
commandList.EndQuery(queryHeap, QueryType.Timestamp, 0);
Dispatch(splatPipelineState, splatDispatchCount);
commandList.ResourceBarrier(ResourceBarrier.BarrierUnorderedAccessView(splats));
Dispatch(sdfPipelineState, options.ReservoirUpdatesPerPass);
Dispatch(pbrPipelineState, options.ReservoirUpdatesPerPass);
Dispatch(radiosityPipelineState, options.ReservoirUpdatesPerPass);
commandList.EndQuery(queryHeap, QueryType.Timestamp, 1);
commandList.ResolveQueryData(queryHeap, QueryType.Timestamp, 0, 2, queryReadback, 0);
if (frame == options.WarmupFrames + options.MeasuredFrames - 1 && totalReadbackBytes > 0)
{
CopyReceiptReadback(splats, sdfReservoirs, pbrReservoirs, radiosityReservoirs, readback, readbackSplatBytes, readbackReservoirBytes);
}
commandList.Close();
var cpuStart = Stopwatch.GetTimestamp();
queue.ExecuteCommandList(commandList);
WaitForGpu();
var cpuEnd = Stopwatch.GetTimestamp();
if (frame >= options.WarmupFrames)
{
measuredFrames++;
measuredCpuTicks += cpuEnd - cpuStart;
unsafe
{
var timestamps = (ulong*)queryReadback.Map<byte>(0);
measuredGpuTicks += timestamps[1] - timestamps[0];
queryReadback.Unmap(0);
}
}
}
var checksum = totalReadbackBytes == 0 ? 0UL : Checksum(readback, (int)totalReadbackBytes);
var visualParity = totalReadbackBytes == 0
? null
: BuildVisualParity(
options,
readback,
readbackSplatBytes,
readbackReservoirBytes,
reservoirStride,
(int)(readbackSplatBytes / (ulong)splatStride));
var gpuSeconds = measuredGpuTicks / (double)timestampFrequency;
var gpuMsPerFrame = gpuSeconds * 1000.0 / Math.Max(measuredFrames, 1);
var cpuMsPerFrame = measuredCpuTicks * 1000.0 / Stopwatch.Frequency / Math.Max(measuredFrames, 1);
var splatsPerSecond = options.SplatUpdatesPerFrame * (double)measuredFrames / Math.Max(gpuSeconds, 1.0e-12);
var reservoirCandidatesPerSecond = options.ReservoirUpdatesPerPass * 3.0 * options.CandidatesPerPass * measuredFrames / Math.Max(gpuSeconds, 1.0e-12);
return new GpuFractalSplatReceipt(
adapterName,
options.SplatCount,
options.SplatCount,
options.SplatCount,
options.SplatCount,
options.SplatCount,
resolvedProgramTransforms.Length,
resolvedProgramMode,
options.WarmupSplatUpdatesPerFrame,
options.SplatUpdatesPerFrame,
options.CandidatesPerPass,
options.ReservoirUpdatesPerPass,
framePlan.Passes[0].ExpectedFullCoverageFrames,
measuredFrames,
splatStride,
reservoirStride,
flameStateStride,
gpuMsPerFrame,
1000.0 / Math.Max(gpuMsPerFrame, 1.0e-12),
splatsPerSecond,
reservoirCandidatesPerSecond,
cpuMsPerFrame,
checksum,
visualParity);
void Dispatch(ID3D12PipelineState state, int elementCount)
{
commandList.SetPipelineState(state);
commandList.Dispatch((uint)((elementCount + ThreadGroupSize - 1) / ThreadGroupSize), 1, 1);
}
}
public void Dispose()
{
WaitForGpu();
radiosityPipelineState.Dispose();
pbrPipelineState.Dispose();
sdfPipelineState.Dispose();
splatPipelineState.Dispose();
rootSignature.Dispose();
fence.Dispose();
commandList.Dispose();
allocator.Dispose();
queue.Dispose();
device.Dispose();
fenceEvent.Dispose();
}
private ID3D12Resource CreateUavBuffer(ulong bytes, string name)
{
var resource = device.CreateCommittedResource(HeapType.Default, ResourceDescription.Buffer(bytes, ResourceFlags.AllowUnorderedAccess), ResourceStates.UnorderedAccess, null);
resource.Name = name;
return resource;
}
private ID3D12Resource CreateProgramTransformBuffer(AquariumPackedFractalIfsTransform[] transforms)
{
var stride = Marshal.SizeOf<AquariumPackedFractalIfsTransform>();
var bytes = (ulong)(Math.Max(transforms.Length, 1) * stride);
var upload = device.CreateCommittedResource(HeapType.Upload, ResourceDescription.Buffer(bytes), ResourceStates.GenericRead, null);
upload.Name = "Aquarium Fractal Receipt IFS Program Upload Buffer";
unsafe
{
var target = (AquariumPackedFractalIfsTransform*)upload.Map<byte>(0);
for (var index = 0; index < transforms.Length; index++)
{
target[index] = transforms[index];
}
upload.Unmap(0);
}
var resource = device.CreateCommittedResource(HeapType.Default, ResourceDescription.Buffer(bytes), ResourceStates.CopyDest, null);
resource.Name = "Aquarium Fractal Receipt IFS Program Buffer";
allocator.Reset();
commandList.Reset(allocator, null);
commandList.CopyBufferRegion(resource, 0, upload, 0, bytes);
commandList.ResourceBarrier(ResourceBarrier.BarrierTransition(resource, ResourceStates.CopyDest, ResourceStates.NonPixelShaderResource));
commandList.Close();
queue.ExecuteCommandList(commandList);
WaitForGpu();
upload.Dispose();
return resource;
}
private static AquariumPackedFractalIfsTransform[] BuildReceiptProgram(ReceiptOptions options)
{
if (options.ProgramFlamePath is not null)
{
var flame = FractalFlameFileParser.ParseFirst(File.ReadAllText(options.ProgramFlamePath), unchecked((int)options.Seed));
return FractalGpuProgramCompiler.CompileFlame2D(flame, Math.Max(options.ProgramTransformCount, 1));
}
return BuildSyntheticReceiptProgram(options.ProgramTransformCount);
}
private static AquariumPackedFractalIfsTransform[] BuildSyntheticReceiptProgram(int transformCount)
{
if (transformCount <= 0)
{
return [default];
}
var transforms = new AquariumPackedFractalIfsTransform[transformCount];
for (var index = 0; index < transforms.Length; index++)
{
var phase = MathF.Tau * index / Math.Max(transformCount, 1);
var branch = new Vector2(MathF.Cos(phase), MathF.Sin(phase));
var radius = 0.32f + (index % 5) * 0.035f;
var scale = 0.46f + (index % 3) * 0.035f;
var rotation = phase * 0.37f;
transforms[index] = new AquariumPackedFractalIfsTransform(
new Vector4(branch * 0.42f, scale, 0.18f + index * 0.013f),
new Vector4(radius, radius * 0.62f, rotation, 4.0f),
new Vector4((index + 1.0f) / (transformCount + 1.0f), index * 31 + 7, MathF.Cos(rotation), MathF.Sin(rotation)),
new Vector4(index % 6, 0.0f, 0.0f, 0.0f),
new Vector4(1.0f, 0.0f, 0.0f, 1.0f),
Vector4.Zero);
}
return transforms;
}
private void BindConstants(ReceiptOptions options, int frame, int programTransformCount, int programMode, int splatDispatchCount)
{
commandList.SetComputeRoot32BitConstant(0, (uint)options.SplatCount, 0);
commandList.SetComputeRoot32BitConstant(0, (uint)frame, 1);
commandList.SetComputeRoot32BitConstant(0, (uint)options.Depth, 2);
commandList.SetComputeRoot32BitConstant(0, options.Seed, 3);
commandList.SetComputeRoot32BitConstant(0, (uint)options.CandidatesPerPass, 4);
commandList.SetComputeRoot32BitConstant(0, (uint)options.ReservoirUpdatesPerPass, 5);
commandList.SetComputeRoot32BitConstant(0, (uint)programTransformCount, 6);
commandList.SetComputeRoot32BitConstant(0, (uint)programMode, 7);
commandList.SetComputeRoot32BitConstant(0, (uint)splatDispatchCount, 8);
}
private void CopyReceiptReadback(ID3D12Resource splats, ID3D12Resource sdf, ID3D12Resource pbr, ID3D12Resource radiosity, ID3D12Resource readback, ulong splatBytes, ulong reservoirBytes)
{
commandList.ResourceBarrier(ResourceBarrier.BarrierTransition(splats, ResourceStates.UnorderedAccess, ResourceStates.CopySource));
commandList.ResourceBarrier(ResourceBarrier.BarrierTransition(sdf, ResourceStates.UnorderedAccess, ResourceStates.CopySource));
commandList.ResourceBarrier(ResourceBarrier.BarrierTransition(pbr, ResourceStates.UnorderedAccess, ResourceStates.CopySource));
commandList.ResourceBarrier(ResourceBarrier.BarrierTransition(radiosity, ResourceStates.UnorderedAccess, ResourceStates.CopySource));
commandList.CopyBufferRegion(readback, 0, splats, 0, splatBytes);
commandList.CopyBufferRegion(readback, splatBytes, sdf, 0, reservoirBytes);
commandList.CopyBufferRegion(readback, splatBytes + reservoirBytes, pbr, 0, reservoirBytes);
commandList.CopyBufferRegion(readback, splatBytes + (reservoirBytes * 2), radiosity, 0, reservoirBytes);
commandList.ResourceBarrier(ResourceBarrier.BarrierTransition(splats, ResourceStates.CopySource, ResourceStates.UnorderedAccess));
commandList.ResourceBarrier(ResourceBarrier.BarrierTransition(sdf, ResourceStates.CopySource, ResourceStates.UnorderedAccess));
commandList.ResourceBarrier(ResourceBarrier.BarrierTransition(pbr, ResourceStates.CopySource, ResourceStates.UnorderedAccess));
commandList.ResourceBarrier(ResourceBarrier.BarrierTransition(radiosity, ResourceStates.CopySource, ResourceStates.UnorderedAccess));
}
private static unsafe VisualParityReceipt? BuildVisualParity(
ReceiptOptions options,
ID3D12Resource readback,
ulong readbackSplatBytes,
ulong readbackReservoirBytes,
int reservoirStride,
int readbackSplatCount)
{
if (!options.VisualParity || readbackSplatCount <= 0)
{
return null;
}
var readbackReservoirCount = Math.Min(
readbackSplatCount,
(int)(readbackReservoirBytes / (ulong)reservoirStride));
var views = options.VisualParityViews.Count > 0
? options.VisualParityViews
: [new VisualParityView("global", options.HistogramBounds)];
ReferenceDensityImage? externalReference = options.ReferenceDensityPpmPath is null
? null
: LoadReferenceDensityPpm(options.ReferenceDensityPpmPath, options.HistogramBounds);
var flameReferencePoints = externalReference is null
? BuildCpuFlameReferencePoints(options)
: null;
var viewReceipts = new VisualParityViewReceipt[views.Count];
var candidateHistograms = new FractalPointHistogram[views.Count];
var candidateSource = "Aquarium SDF envelope reservoir density";
var comparedSamples = 0;
var basePtr = readback.Map<byte>(0);
try
{
var reservoirs = (AquariumPackedSdfEnvelopeReservoir*)((byte*)basePtr + readbackSplatBytes);
for (var index = 0; index < views.Count; index++)
{
candidateHistograms[index] = BuildSdfEnvelopeDensityHistogram(
reservoirs,
readbackReservoirCount,
options.HistogramWidth,
options.HistogramHeight,
views[index].Bounds);
}
}
finally
{
readback.Unmap(0);
}
for (var index = 0; index < views.Count; index++)
{
var referenceHistogram = externalReference is null
? FractalPointHistogramBuilder.Build(flameReferencePoints!, options.HistogramWidth, options.HistogramHeight, views[index].Bounds)
: externalReference.Value.ToHistogram(options.HistogramWidth, options.HistogramHeight, views[index].Bounds);
viewReceipts[index] = BuildVisualParityView(
options,
views[index],
referenceHistogram,
candidateHistograms[index]);
comparedSamples = Math.Max(comparedSamples, candidateHistograms[index].HitCount);
}
var primary = viewReceipts[0];
return new VisualParityReceipt(
externalReference is null ? "Aquarium CPU flame oracle" : Path.GetFullPath(options.ReferenceDensityPpmPath!),
candidateSource,
comparedSamples,
externalReference?.TotalLuminance ?? options.VisualParityReferenceSamples,
options.HistogramWidth,
options.HistogramHeight,
primary.Bounds,
primary.ReferenceHitCount,
primary.GpuHitCount,
primary.ReferenceOccupiedBins,
primary.GpuOccupiedBins,
primary.SharedOccupiedBins,
primary.OccupancyOverlapPercent,
primary.L1Distance,
primary.Rmse,
primary.CosineSimilarity,
primary.DistributionScorePercent,
viewReceipts);
}
private static unsafe FractalPointHistogram BuildSdfEnvelopeDensityHistogram(
AquariumPackedSdfEnvelopeReservoir* reservoirs,
int count,
int width,
int height,
Vector4 bounds)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(width);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(height);
var bins = new int[width * height];
var spanX = MathF.Max(bounds.Z - bounds.X, 0.000001f);
var spanY = MathF.Max(bounds.W - bounds.Y, 0.000001f);
for (var index = 0; index < count; index++)
{
var reservoir = reservoirs[index];
var centerRadius = reservoir.CenterRadius;
var radiiFalloff = reservoir.RadiiFalloff;
if (!IsFinite(centerRadius) || !IsFinite(radiiFalloff) || reservoir.WeightTargetCount.Z <= 0.0f)
{
continue;
}
var centerX = centerRadius.X;
var centerY = centerRadius.Y;
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;
}
var minX = Math.Clamp((int)MathF.Floor(((centerX - radiusX) - bounds.X) / spanX * width), 0, width - 1);
var maxX = Math.Clamp((int)MathF.Ceiling(((centerX + radiusX) - bounds.X) / spanX * width), 0, width - 1);
var minY = Math.Clamp((int)MathF.Floor(((centerY - radiusY) - bounds.Y) / spanY * height), 0, height - 1);
var maxY = Math.Clamp((int)MathF.Ceiling(((centerY + radiusY) - bounds.Y) / spanY * height), 0, height - 1);
if (maxX < minX || maxY < minY)
{
continue;
}
var targetCount = MathF.Max(reservoir.WeightTargetCount.Y, 1.0f);
var confidence = Math.Clamp(reservoir.Validation.X, 0.0f, 1.0f);
var baseMass = Math.Clamp((int)MathF.Ceiling(targetCount * MathF.Max(confidence, 0.125f)), 1, 16);
for (var y = minY; y <= maxY; y++)
{
var worldY = bounds.Y + ((y + 0.5f) / height) * spanY;
var dy = (worldY - centerY) / radiusY;
for (var x = minX; x <= maxX; x++)
{
var worldX = bounds.X + ((x + 0.5f) / width) * spanX;
var dx = (worldX - centerX) / radiusX;
var q = (dx * dx) + (dy * dy);
if (q > 1.0f)
{
continue;
}
var shaped = 1.0f - q;
var mass = Math.Max(1, (int)MathF.Ceiling(baseMass * shaped * shaped));
var binIndex = (y * width) + x;
bins[binIndex] = bins[binIndex] > int.MaxValue - mass
? int.MaxValue
: bins[binIndex] + mass;
}
}
}
return new FractalPointHistogram(width, height, bounds, bins);
}
private static bool IsFinite(Vector4 value)
{
return float.IsFinite(value.X)
&& float.IsFinite(value.Y)
&& float.IsFinite(value.Z)
&& float.IsFinite(value.W);
}
private static VisualParityViewReceipt BuildVisualParityView(
ReceiptOptions options,
VisualParityView view,
FractalPointHistogram referenceHistogram,
FractalPointHistogram candidateHistogram)
{
WriteVisualParityImages(options, view.Name, referenceHistogram, candidateHistogram);
var metrics = CompareHistograms(referenceHistogram, candidateHistogram);
return new VisualParityViewReceipt(
view.Name,
[
view.Bounds.X,
view.Bounds.Y,
view.Bounds.Z,
view.Bounds.W,
],
referenceHistogram.HitCount,
candidateHistogram.HitCount,
metrics.ReferenceOccupiedBins,
metrics.GpuOccupiedBins,
metrics.SharedOccupiedBins,
metrics.StarvedReferenceBins,
metrics.OccupancyOverlapPercent,
metrics.UnderrepresentedMassPercent,
metrics.OversampledMassPercent,
metrics.L1Distance,
metrics.Rmse,
metrics.CosineSimilarity,
metrics.DistributionScorePercent);
}
private static Vector2[] BuildCpuFlameReferencePoints(ReceiptOptions options)
{
if (options.ProgramFlamePath is null)
{
throw new ArgumentException("Visual parity needs either --program-flame or --reference-density-ppm.");
}
var flame = FractalFlameFileParser.ParseFirst(File.ReadAllText(options.ProgramFlamePath), unchecked((int)options.Seed));
return FractalFlameChaosGame.Generate(
flame,
options.VisualParityReferenceSamples,
options.HistogramBurnIn,
new FractalXorShiftRandom(options.Seed));
}
private static ReferenceDensityImage LoadReferenceDensityPpm(string path, Vector4 bounds)
{
var bytes = File.ReadAllBytes(path);
var cursor = 0;
var magic = ReadPpmToken(bytes, ref cursor);
if (!magic.SequenceEqual("P6"u8))
{
throw new FormatException("External density reference must be a binary P6 PPM image.");
}
var width = ParsePositivePpmInt(ReadPpmToken(bytes, ref cursor), "width");
var height = ParsePositivePpmInt(ReadPpmToken(bytes, ref cursor), "height");
var max = ParsePositivePpmInt(ReadPpmToken(bytes, ref cursor), "max channel value");
if (max > 255)
{
throw new FormatException("External density reference must be 8-bit PPM.");
}
ConsumePpmWhitespace(bytes, ref cursor);
var expectedBytes = checked(width * height * 3);
if (bytes.Length - cursor < expectedBytes)
{
throw new FormatException("External density reference ended before all RGB pixels were present.");
}
var luminance = new int[width * height];
var total = 0L;
var pixels = bytes.AsSpan(cursor, expectedBytes);
for (var source = 0; source < pixels.Length; source += 3)
{
var r = pixels[source];
var g = pixels[source + 1];
var b = pixels[source + 2];
var value = ((r * 54) + (g * 183) + (b * 19)) >> 8;
var pixelIndex = source / 3;
luminance[pixelIndex] = value;
total += value;
}
return new ReferenceDensityImage(width, height, bounds, luminance, Math.Max(total, 1L));
}
private static ReadOnlySpan<byte> ReadPpmToken(ReadOnlySpan<byte> bytes, ref int cursor)
{
while (cursor < bytes.Length)
{
if (bytes[cursor] is (byte)' ' or (byte)'\t' or (byte)'\r' or (byte)'\n')
{
cursor++;
continue;
}
if (bytes[cursor] != (byte)'#')
{
break;
}
while (cursor < bytes.Length && bytes[cursor] != (byte)'\n')
{
cursor++;
}
}
var start = cursor;
while (cursor < bytes.Length && bytes[cursor] is not ((byte)' ' or (byte)'\t' or (byte)'\r' or (byte)'\n' or (byte)'#'))
{
cursor++;
}
if (cursor == start)
{
throw new FormatException("PPM header ended unexpectedly.");
}
return bytes.Slice(start, cursor - start);
}
private static void ConsumePpmWhitespace(ReadOnlySpan<byte> bytes, ref int cursor)
{
if (cursor >= bytes.Length || bytes[cursor] is not ((byte)' ' or (byte)'\t' or (byte)'\r' or (byte)'\n'))
{
throw new FormatException("PPM header is missing the pixel-data separator.");
}
cursor++;
}
private static int ParsePositivePpmInt(ReadOnlySpan<byte> token, string name)
{
var value = 0;
foreach (var digit in token)
{
if (digit is < (byte)'0' or > (byte)'9')
{
throw new FormatException($"PPM {name} is not an integer.");
}
value = checked((value * 10) + digit - (byte)'0');
}
return value > 0 ? value : throw new FormatException($"PPM {name} must be positive.");
}
private static void WriteVisualParityImages(
ReceiptOptions options,
string viewName,
FractalPointHistogram referenceHistogram,
FractalPointHistogram gpuHistogram)
{
if (options.VisualParityImageDirectory is null)
{
return;
}
Directory.CreateDirectory(options.VisualParityImageDirectory);
var safeViewName = SanitizeFileSegment(viewName);
var safePrefix = SanitizeFileSegment(options.VisualParityImagePrefix);
WriteHistogramPpm(
Path.Combine(options.VisualParityImageDirectory, $"{safePrefix}-{safeViewName}-reference.ppm"),
referenceHistogram,
new Vector3(0.45f, 0.86f, 1.0f));
WriteHistogramPpm(
Path.Combine(options.VisualParityImageDirectory, $"{safePrefix}-{safeViewName}-aquarium.ppm"),
gpuHistogram,
new Vector3(1.0f, 0.68f, 0.28f));
}
private static void WriteHistogramPpm(string path, FractalPointHistogram histogram, Vector3 color)
{
const int scale = 4;
var width = histogram.Width * scale;
var height = histogram.Height * scale;
var max = histogram.Bins.Max();
using var stream = File.Create(path);
using var writer = new StreamWriter(stream, System.Text.Encoding.ASCII, leaveOpen: true);
writer.Write($"P6\n{width} {height}\n255\n");
writer.Flush();
var row = new byte[width * 3];
for (var y = 0; y < height; y++)
{
Array.Clear(row);
var binY = histogram.Height - 1 - (y / scale);
for (var x = 0; x < width; x++)
{
var binX = x / scale;
var value = histogram.Bins[(binY * histogram.Width) + binX];
var normalized = max <= 0 ? 0.0 : Math.Sqrt(Math.Log(1.0 + value) / Math.Log(1.0 + max));
var offset = x * 3;
row[offset] = (byte)Math.Clamp((int)(normalized * color.X * 255.0), 0, 255);
row[offset + 1] = (byte)Math.Clamp((int)(normalized * color.Y * 255.0), 0, 255);
row[offset + 2] = (byte)Math.Clamp((int)(normalized * color.Z * 255.0), 0, 255);
}
stream.Write(row);
}
}
private static string SanitizeFileSegment(string value)
{
var chars = value.Select(ch => char.IsLetterOrDigit(ch) || ch is '-' or '_' ? ch : '-').ToArray();
return new string(chars).Trim('-') is { Length: > 0 } sanitized ? sanitized : "view";
}
private static HistogramComparison CompareHistograms(FractalPointHistogram reference, FractalPointHistogram candidate)
{
if (reference.Width != candidate.Width || reference.Height != candidate.Height)
{
throw new ArgumentException("Histogram dimensions must match.");
}
var referenceTotal = Math.Max(reference.HitCount, 1);
var candidateTotal = Math.Max(candidate.HitCount, 1);
var l1 = 0.0;
var squared = 0.0;
var dot = 0.0;
var referenceMagnitude = 0.0;
var candidateMagnitude = 0.0;
var referenceOccupied = 0;
var candidateOccupied = 0;
var sharedOccupied = 0;
var starvedReferenceBins = 0;
var underrepresentedMass = 0.0;
var oversampledMass = 0.0;
for (var index = 0; index < reference.Bins.Length; index++)
{
var referenceProbability = reference.Bins[index] / (double)referenceTotal;
var candidateProbability = candidate.Bins[index] / (double)candidateTotal;
var delta = referenceProbability - candidateProbability;
l1 += Math.Abs(delta);
underrepresentedMass += Math.Max(delta, 0.0);
oversampledMass += Math.Max(-delta, 0.0);
squared += delta * delta;
dot += referenceProbability * candidateProbability;
referenceMagnitude += referenceProbability * referenceProbability;
candidateMagnitude += candidateProbability * candidateProbability;
var referenceHas = reference.Bins[index] > 0;
var candidateHas = candidate.Bins[index] > 0;
referenceOccupied += referenceHas ? 1 : 0;
candidateOccupied += candidateHas ? 1 : 0;
sharedOccupied += referenceHas && candidateHas ? 1 : 0;
starvedReferenceBins += referenceHas && !candidateHas ? 1 : 0;
}
var occupiedUnion = Math.Max(referenceOccupied + candidateOccupied - sharedOccupied, 1);
var cosine = dot / Math.Max(Math.Sqrt(referenceMagnitude * candidateMagnitude), 1.0e-12);
var rmse = Math.Sqrt(squared / reference.Bins.Length);
return new HistogramComparison(
referenceOccupied,
candidateOccupied,
sharedOccupied,
starvedReferenceBins,
sharedOccupied * 100.0 / occupiedUnion,
underrepresentedMass * 100.0,
oversampledMass * 100.0,
l1,
rmse,
cosine,
Math.Max(0.0, (1.0 - (l1 * 0.5)) * 100.0));
}
private void WaitForGpu()
{
fenceValue++;
queue.Signal(fence, fenceValue);
if (fence.CompletedValue < fenceValue)
{
fence.SetEventOnCompletion(fenceValue, fenceEvent.SafeWaitHandle.DangerousGetHandle());
fenceEvent.WaitOne();
}
}
private string ResolveAdapterName()
{
using var factory = DXGI.CreateDXGIFactory1<IDXGIFactory6>();
for (var index = 0u; factory.EnumAdapterByGpuPreference(index, GpuPreference.HighPerformance, out IDXGIAdapter1? adapter).Success && adapter is not null; index++)
{
using (adapter)
{
var description = adapter.Description1;
if ((description.Flags & AdapterFlags.Software) == 0)
{
return description.Description;
}
}
}
return "default D3D12 adapter";
}
private static ID3D12RootSignature CreateRootSignature(ID3D12Device device)
{
var rootParameters = new[]
{
new RootParameter(new RootConstants(0, 0, 9), ShaderVisibility.All),
new RootParameter(RootParameterType.UnorderedAccessView, new RootDescriptor(0, 0), ShaderVisibility.All),
new RootParameter(RootParameterType.UnorderedAccessView, new RootDescriptor(1, 0), ShaderVisibility.All),
new RootParameter(RootParameterType.UnorderedAccessView, new RootDescriptor(2, 0), ShaderVisibility.All),
new RootParameter(RootParameterType.UnorderedAccessView, new RootDescriptor(3, 0), ShaderVisibility.All),
new RootParameter(RootParameterType.UnorderedAccessView, new RootDescriptor(4, 0), ShaderVisibility.All),
new RootParameter(RootParameterType.ShaderResourceView, new RootDescriptor(0, 0), ShaderVisibility.All),
};
var description = new RootSignatureDescription(RootSignatureFlags.None, rootParameters, []);
return device.CreateRootSignature(0, in description, RootSignatureVersion.Version1);
}
private static ID3D12PipelineState CreatePipelineState(ID3D12Device device, ID3D12RootSignature rootSignature, string shaderSource, string entry)
{
var shader = Compiler.Compile(shaderSource, entry, "D3D12FractalReservoirCompute.hlsl", "cs_5_0", ShaderFlags.OptimizationLevel3, EffectFlags.None);
return device.CreateComputePipelineState(new ComputePipelineStateDescription { RootSignature = rootSignature, ComputeShader = shader });
}
private static unsafe ulong Checksum(ID3D12Resource readback, int byteCount)
{
var data = (byte*)readback.Map<byte>(0);
var hash = 1469598103934665603UL;
for (var index = 0; index < byteCount; index++)
{
hash ^= data[index];
hash *= 1099511628211UL;
}
readback.Unmap(0);
return hash;
}
}
internal sealed record ReceiptOptions(
int SplatCount,
int WarmupSplatUpdatesPerFrame,
int SplatUpdatesPerFrame,
int WarmupFrames,
int MeasuredFrames,
int Depth,
uint Seed,
int CandidatesPerPass,
int ReservoirUpdatesPerPass,
int ProgramTransformCount,
int ProgramMode,
int ReadbackSplats,
string ShaderPath,
string OutputDirectory,
string? FlamePath,
string? ReferencePpmPath,
string? ReferenceDensityPpmPath,
string? ProgramFlamePath,
int HistogramSamples,
int HistogramBurnIn,
int HistogramWidth,
int HistogramHeight,
Vector4 HistogramBounds,
bool VisualParity,
int VisualParityReferenceSamples,
IReadOnlyList<VisualParityView> VisualParityViews,
string? VisualParityImageDirectory,
string VisualParityImagePrefix)
{
public static ReceiptOptions Parse(string[] args)
{
var options = new ReceiptOptions(
2_000_000,
2_000_000,
2_000_000,
30,
120,
8,
0xA17EA11u,
2,
50_000,
0,
0,
64,
Path.Combine("src", "Aquarium.Engine", "Render", "Shaders", "D3D12FractalReservoirCompute.hlsl"),
Path.Combine("artifacts", "fractal-splat-receipts"),
null,
null,
null,
null,
8192,
64,
64,
64,
new Vector4(-8.0f, -8.0f, 8.0f, 8.0f),
false,
1_000_000,
[],
null,
"visual-parity");
for (var index = 0; index < args.Length; index++)
{
var arg = args[index];
string Next() => index + 1 < args.Length ? args[++index] : throw new ArgumentException($"Missing value for {arg}.");
options = arg switch
{
"--splats" => options with { SplatCount = int.Parse(Next()) },
"--warmup-splat-updates" => options with { WarmupSplatUpdatesPerFrame = int.Parse(Next()) },
"--splat-updates" => options with { SplatUpdatesPerFrame = int.Parse(Next()) },
"--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 = 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()), ProgramMode = 1 },
"--program-flame" => options with { ProgramFlamePath = Next(), ProgramMode = 2 },
"--readback-splats" => options with { ReadbackSplats = int.Parse(Next()) },
"--shader" => options with { ShaderPath = Next() },
"--out" => options with { OutputDirectory = Next() },
"--flame" => options with { FlamePath = Next() },
"--reference-ppm" => options with { ReferencePpmPath = Next() },
"--reference-density-ppm" => options with { ReferenceDensityPpmPath = Next(), VisualParity = true },
"--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()),
"--visual-parity" => options with { VisualParity = true },
"--visual-parity-reference-samples" => options with { VisualParityReferenceSamples = int.Parse(Next()) },
"--visual-parity-view" => AddVisualParityView(options, Next()),
"--visual-parity-image-dir" => options with { VisualParityImageDirectory = Next() },
"--visual-parity-image-prefix" => options with { VisualParityImagePrefix = Next() },
_ => throw new ArgumentException($"Unknown receipt option: {arg}"),
};
}
if (options.ReferencePpmPath is not null)
{
if (!File.Exists(options.ReferencePpmPath))
{
throw new FileNotFoundException("Reference PPM file was not found.", options.ReferencePpmPath);
}
return options;
}
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 (options.ProgramFlamePath is not null && !File.Exists(options.ProgramFlamePath))
{
throw new FileNotFoundException("Receipt program flame file was not found.", options.ProgramFlamePath);
}
if (options.ReferenceDensityPpmPath is not null && !File.Exists(options.ReferenceDensityPpmPath))
{
throw new FileNotFoundException("External density reference PPM file was not found.", options.ReferenceDensityPpmPath);
}
options = options.ProgramFlamePath is not null && options.ProgramTransformCount <= 0
? options with { ProgramTransformCount = int.MaxValue, ProgramMode = 2 }
: options;
options = options.ProgramMode == 0 && options.ProgramTransformCount > 0
? options with { ProgramMode = 1 }
: options;
if (!File.Exists(options.ShaderPath))
{
throw new FileNotFoundException("Receipt shader file was not found.", options.ShaderPath);
}
if (options.VisualParity && options.ProgramFlamePath is null && options.ReferenceDensityPpmPath is null)
{
throw new ArgumentException("Visual parity requires --program-flame or --reference-density-ppm.");
}
if (options.SplatCount <= 0 || options.WarmupSplatUpdatesPerFrame <= 0 || options.WarmupSplatUpdatesPerFrame > options.SplatCount || options.SplatUpdatesPerFrame <= 0 || options.SplatUpdatesPerFrame > options.SplatCount || options.WarmupFrames < 0 || options.MeasuredFrames <= 0 || options.Depth <= 0 || options.CandidatesPerPass <= 0 || options.ReservoirUpdatesPerPass <= 0 || options.ReservoirUpdatesPerPass > options.SplatCount || options.ProgramTransformCount < 0 || options.ProgramMode < 0 || options.ProgramMode > 2 || options.ReadbackSplats < 0 || options.VisualParityReferenceSamples <= 0)
{
throw new ArgumentOutOfRangeException(nameof(args), "Splats, splat updates, frames, depth, candidates, and reservoir updates must be positive; splat/reservoir updates must not exceed splats; program transforms, warmup, and readback must not be negative.");
}
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)
{
return options with { HistogramBounds = ParseBounds(value, "Histogram bounds") };
}
private static ReceiptOptions AddVisualParityView(ReceiptOptions options, string value)
{
var separator = value.IndexOf(':', StringComparison.Ordinal);
if (separator <= 0 || separator == value.Length - 1)
{
throw new ArgumentException("Visual parity view must be name:minX,minY,maxX,maxY.");
}
var name = value[..separator];
var bounds = ParseBounds(value[(separator + 1)..], "Visual parity view bounds");
return options with { VisualParityViews = [.. options.VisualParityViews, new VisualParityView(name, bounds)] };
}
private static Vector4 ParseBounds(string value, string label)
{
var parts = value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length != 4)
{
throw new ArgumentException($"{label} must be minX,minY,maxX,maxY.");
}
return 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 ReferencePpmReceipt(
string SourcePath,
int Width,
int Height,
int MaxChannelValue,
int PixelCount,
int NonBlackPixelCount,
ulong RgbChecksum,
ulong LuminanceChecksum);
internal sealed record FlameImportReportReceipt(
string SourcePath,
FractalFlameImportReport Report);
internal readonly record struct VisualParityView(string Name, Vector4 Bounds);
internal readonly record struct ReferenceDensityImage(
int Width,
int Height,
Vector4 Bounds,
int[] Luminance,
long TotalLuminance)
{
public FractalPointHistogram ToHistogram(int width, int height, Vector4 viewBounds)
{
var bins = new int[width * height];
var sourceSpan = new Vector2(Bounds.Z - Bounds.X, Bounds.W - Bounds.Y);
var viewSpan = new Vector2(viewBounds.Z - viewBounds.X, viewBounds.W - viewBounds.Y);
for (var sourceY = 0; sourceY < Height; sourceY++)
{
var worldY = Bounds.Y + ((Height - 0.5f - sourceY) / Math.Max(Height, 1)) * sourceSpan.Y;
var v = (worldY - viewBounds.Y) / MathF.Max(viewSpan.Y, 0.000001f);
if (v < 0.0f || v >= 1.0f)
{
continue;
}
var targetY = Math.Clamp((int)(v * height), 0, height - 1);
for (var sourceX = 0; sourceX < Width; sourceX++)
{
var value = Luminance[(sourceY * Width) + sourceX];
if (value == 0)
{
continue;
}
var worldX = Bounds.X + ((sourceX + 0.5f) / Math.Max(Width, 1)) * sourceSpan.X;
var u = (worldX - viewBounds.X) / MathF.Max(viewSpan.X, 0.000001f);
if (u < 0.0f || u >= 1.0f)
{
continue;
}
var targetX = Math.Clamp((int)(u * width), 0, width - 1);
bins[(targetY * width) + targetX] += value;
}
}
return new FractalPointHistogram(width, height, viewBounds, bins);
}
}
internal sealed record VisualParityReceipt(
string ReferenceSource,
string CandidateSource,
int ComparedSamples,
long ReferenceSamples,
int Width,
int Height,
float[] Bounds,
int ReferenceHitCount,
int GpuHitCount,
int ReferenceOccupiedBins,
int GpuOccupiedBins,
int SharedOccupiedBins,
double OccupancyOverlapPercent,
double L1Distance,
double Rmse,
double CosineSimilarity,
double DistributionScorePercent,
IReadOnlyList<VisualParityViewReceipt> Views);
internal sealed record VisualParityViewReceipt(
string Name,
float[] Bounds,
int ReferenceHitCount,
int GpuHitCount,
int ReferenceOccupiedBins,
int GpuOccupiedBins,
int SharedOccupiedBins,
int StarvedReferenceBins,
double OccupancyOverlapPercent,
double UnderrepresentedMassPercent,
double OversampledMassPercent,
double L1Distance,
double Rmse,
double CosineSimilarity,
double DistributionScorePercent);
internal readonly record struct HistogramComparison(
int ReferenceOccupiedBins,
int GpuOccupiedBins,
int SharedOccupiedBins,
int StarvedReferenceBins,
double OccupancyOverlapPercent,
double UnderrepresentedMassPercent,
double OversampledMassPercent,
double L1Distance,
double Rmse,
double CosineSimilarity,
double DistributionScorePercent);
internal sealed record GpuFractalSplatReceipt(
string Adapter,
int SplatCount,
int SdfReservoirCount,
int PbrReservoirCount,
int RadiosityReservoirCount,
int FlameStateCount,
int ProgramTransformCount,
int ProgramMode,
int WarmupSplatUpdatesPerFrame,
int SplatUpdatesPerFrame,
int CandidatesPerPass,
int ReservoirUpdatesPerPass,
double ReservoirFullCoverageFrames,
int MeasuredFrames,
int BytesPerSplat,
int BytesPerReservoir,
int BytesPerFlameState,
double GpuMillisecondsPerFrame,
double GpuEquivalentFps,
double GpuSplatsPerSecond,
double GpuReservoirCandidatesPerSecond,
double CpuSubmitAndWaitMillisecondsPerFrame,
ulong ReadbackChecksum,
VisualParityReceipt? VisualParity);