Move reservoir history update to compute

This commit is contained in:
Meta Vens 2026-05-28 22:18:18 +01:00
commit 4d0fcba656
6 changed files with 192 additions and 79 deletions

View file

@ -576,10 +576,12 @@ w: invalidation code
```
Current state: SDF, temporal Gaussian, and TubeField producers write current
scene reservoir guide data and shared candidate rows. Reservoir presentation
resolve reads current candidates plus ping-ponged structured reservoir-history
rows, folds confidence and domain validity into validation, and writes the next
structured history rows. Pixel-history MRTs are no longer the temporal owner.
scene reservoir guide data and shared candidate rows. The shared reservoir
history update compute pass reads current candidates plus ping-ponged structured
reservoir-history rows, folds confidence and domain validity into validation,
writes the next structured history rows, and emits the resolved HDR field
texture consumed by bloom and presentation. Pixel-history MRTs and the
presentation shader are no longer temporal owners.
### Phase C: Fractal Probe Pipeline

View file

@ -292,13 +292,14 @@ w: invalidation code
```
The first live producers are SDF surfaces, temporal Gaussian splats, and
TubeField candidates, but the schema is not surface-only. Resolve reads current
candidate rows and previous structured reservoir-history rows, folds confidence
and domain validity into validation, then writes the next structured reservoir
history. This resolve is the first reservoir resolve implementation, not an
independent TAA owner. Future ReSTIR/GRIS passes should extend the producer side
of this schema with explicit Form/Appearance/Transport fields rather than
packing more reservoir folklore into scene-control channels.
TubeField candidates, but the schema is not surface-only. The shared reservoir
history update compute pass reads current candidate rows and previous structured
reservoir-history rows, folds confidence and domain validity into validation,
writes the next structured reservoir history, and emits the resolved HDR field
texture. Presentation samples that resolved texture; it does not write reservoir
history. Future ReSTIR/GRIS passes should extend the producer side of this
schema with explicit Form/Appearance/Transport fields rather than packing more
reservoir folklore into scene-control channels.
## Implementation Roadmap
@ -318,8 +319,9 @@ packing more reservoir folklore into scene-control channels.
7. Weave reservoir confidence and temporal detail into structured reservoir
history rows so reservoir resolve can distinguish stable reused evidence
from fresh stochastic noise. The current implementation keeps four
resolver-owned rows per pixel and no longer stores previous reservoir
validity in pixel-history MRTs.
resolver-owned rows per pixel, updates them in a shared compute pass, emits
a resolved HDR field texture for bloom/presentation, and no longer stores
previous reservoir validity in pixel-history MRTs.
8. Add Mimir-facing candidate adapters only after the fractal path proves the
contract: modality features are candidates, not a second reservoir system.
9. Port the pure core to HLSL and add CPU/GPU parity fixtures.

View file

@ -85,6 +85,7 @@ public sealed class D3D12Renderer : IAquariumRenderer
private const int RootFractalRadiosityReservoirSrv = 17;
private const int RootBlueNoise = 18;
private const int RootFieldReservoirCandidates = 19;
private const int RootReservoirResolvedOutput = 20;
private const int RootFusionFrameConstants = 0;
private const int RootFusionSeeds = 1;
private const int RootFusionSensorCameras = 2;
@ -204,6 +205,7 @@ public sealed class D3D12Renderer : IAquariumRenderer
private ID3D12PipelineState? bloomDownsamplePipelineState;
private ID3D12PipelineState? bloomBlurHorizontalPipelineState;
private ID3D12PipelineState? bloomBlurVerticalPipelineState;
private ID3D12PipelineState? reservoirHistoryUpdatePipelineState;
private ID3D12PipelineState? resolvePipelineState;
private readonly ID3D12Fence fence;
private readonly AutoResetEvent fenceEvent = new(false);
@ -231,6 +233,7 @@ public sealed class D3D12Renderer : IAquariumRenderer
private D3D12RenderTarget sceneCandidateMetadataRenderTarget;
private D3D12RenderTarget sceneCandidateControlRenderTarget;
private D3D12RenderTarget sceneCandidateReservoirGuideRenderTarget;
private D3D12RenderTarget reservoirResolvedRenderTarget;
private readonly Dictionary<string, D3D12RenderTarget> graphRenderTargets = new(StringComparer.Ordinal);
private D3D12TrackedResource sceneDepthTarget;
private D3D12DescriptorSlot sceneDepthStencilView;
@ -433,6 +436,7 @@ public sealed class D3D12Renderer : IAquariumRenderer
sceneCandidateMetadataRenderTarget = CreateSceneAuxiliaryRenderTarget("scene-candidate-metadata-target", "Aquarium D3D12 Scene Candidate Metadata Target");
sceneCandidateControlRenderTarget = CreateSceneAuxiliaryRenderTarget("scene-candidate-control-target", "Aquarium D3D12 Scene Candidate Control Target");
sceneCandidateReservoirGuideRenderTarget = CreateSceneAuxiliaryRenderTarget("scene-candidate-reservoir-guide-target", "Aquarium D3D12 Scene Candidate Reservoir Guide Target");
reservoirResolvedRenderTarget = CreateReservoirResolvedRenderTarget();
sceneDepthStencilView = depthStencilViewArena.Allocate();
sceneDepthTarget = CreateSceneDepthTarget(sceneDepthStencilView);
CreateBloomRenderTargets();
@ -981,6 +985,10 @@ public sealed class D3D12Renderer : IAquariumRenderer
sceneCandidateControlRenderTarget.CreateShaderResourceView(device, frameResources.SceneCandidateControlDescriptor);
frameResources.SceneCandidateReservoirGuideDescriptor = frameResources.TransientShaderDescriptors.Allocate();
sceneCandidateReservoirGuideRenderTarget.CreateShaderResourceView(device, frameResources.SceneCandidateReservoirGuideDescriptor);
frameResources.ReservoirResolvedDescriptor = frameResources.TransientShaderDescriptors.Allocate();
reservoirResolvedRenderTarget.CreateShaderResourceView(device, frameResources.ReservoirResolvedDescriptor);
frameResources.ReservoirResolvedUnorderedAccessDescriptor = frameResources.TransientShaderDescriptors.Allocate();
reservoirResolvedRenderTarget.CreateUnorderedAccessView(device, frameResources.ReservoirResolvedUnorderedAccessDescriptor);
frameResources.FieldReservoirCandidateUnorderedAccessDescriptor = frameResources.TransientShaderDescriptors.Allocate();
fieldReservoirCandidateBuffer.CreateUnorderedAccessView(device, frameResources.FieldReservoirCandidateUnorderedAccessDescriptor);
frameResources.FieldReservoirLockUnorderedAccessDescriptor = frameResources.TransientShaderDescriptors.Allocate();
@ -1244,6 +1252,7 @@ public sealed class D3D12Renderer : IAquariumRenderer
sceneCandidateControlRenderTarget.Dispose();
sceneCandidateMetadataRenderTarget.Dispose();
sceneCandidateRenderTarget.Dispose();
reservoirResolvedRenderTarget.Dispose();
sceneDepthTarget.Dispose();
heightFieldRenderTarget.Dispose();
for (var index = 0; index < frames.Length; index++)
@ -1871,6 +1880,8 @@ public sealed class D3D12Renderer : IAquariumRenderer
bloomBlurHorizontal.Name = "Aquarium D3D12 Bloom Blur Horizontal Pipeline";
var bloomBlurVertical = CreateBloomBlurVerticalPipelineState(paths.Post);
bloomBlurVertical.Name = "Aquarium D3D12 Bloom Blur Vertical Pipeline";
var reservoirHistoryUpdate = CreateReservoirHistoryUpdatePipelineState(paths.Post);
reservoirHistoryUpdate.Name = "Aquarium D3D12 Reservoir History Update Pipeline";
var resolve = CreateResolvePipelineState(paths.Post);
resolve.Name = "Aquarium D3D12 Resolve Pipeline";
@ -1901,6 +1912,7 @@ public sealed class D3D12Renderer : IAquariumRenderer
bloomDownsample,
bloomBlurHorizontal,
bloomBlurVertical,
reservoirHistoryUpdate,
resolve);
}
@ -1925,6 +1937,7 @@ public sealed class D3D12Renderer : IAquariumRenderer
resourceRegistry.RemoveRenderTarget("scene-candidate-metadata-target");
resourceRegistry.RemoveRenderTarget("scene-candidate-control-target");
resourceRegistry.RemoveRenderTarget("scene-candidate-reservoir-guide-target");
resourceRegistry.RemoveRenderTarget("reservoir-resolved-target");
resourceRegistry.RemoveResource("scene-depth-target");
resourceRegistry.RemoveRenderTarget("height-field-target");
DisposeBloomRenderTargets();
@ -1937,6 +1950,7 @@ public sealed class D3D12Renderer : IAquariumRenderer
sceneCandidateMetadataRenderTarget.Dispose();
sceneCandidateControlRenderTarget.Dispose();
sceneCandidateReservoirGuideRenderTarget.Dispose();
reservoirResolvedRenderTarget.Dispose();
fieldReservoirCandidateBuffer.Dispose();
fieldReservoirLockBuffer.Dispose();
DisposeFieldReservoirHistoryBuffers();
@ -1971,6 +1985,7 @@ public sealed class D3D12Renderer : IAquariumRenderer
sceneCandidateMetadataRenderTarget = CreateSceneAuxiliaryRenderTarget("scene-candidate-metadata-target", "Aquarium D3D12 Scene Candidate Metadata Target");
sceneCandidateControlRenderTarget = CreateSceneAuxiliaryRenderTarget("scene-candidate-control-target", "Aquarium D3D12 Scene Candidate Control Target");
sceneCandidateReservoirGuideRenderTarget = CreateSceneAuxiliaryRenderTarget("scene-candidate-reservoir-guide-target", "Aquarium D3D12 Scene Candidate Reservoir Guide Target");
reservoirResolvedRenderTarget = CreateReservoirResolvedRenderTarget();
DisposeTubeFieldRestirBuffers();
CreateTubeFieldRestirBuffers();
CreateFieldReservoirBuffers();
@ -2017,6 +2032,23 @@ public sealed class D3D12Renderer : IAquariumRenderer
return target;
}
private D3D12RenderTarget CreateReservoirResolvedRenderTarget()
{
var target = new D3D12RenderTarget(
device,
width,
height,
SceneHdrFormat,
renderTargetViewArena.Allocate(),
null,
null,
true,
new Color4(0.0f, 0.0f, 0.0f, 1.0f),
"Aquarium D3D12 Reservoir Resolved Target");
resourceRegistry.Add("reservoir-resolved-target", target);
return target;
}
private void CreateFieldReservoirBuffers()
{
fieldReservoirCandidateBuffer = CreateFieldReservoirCandidateBuffer();
@ -2520,7 +2552,8 @@ public sealed class D3D12Renderer : IAquariumRenderer
}
ResolveFieldReservoir(context.CommandList, frameResources);
RenderBloom(context.CommandList, frameResources);
UpdateReservoirHistory(context.CommandList, frameResources);
RenderBloom(context.CommandList, frameResources, reservoirResolvedRenderTarget, frameResources.ReservoirResolvedDescriptor);
PresentBackBuffer(context, frameResources);
}
@ -2625,6 +2658,52 @@ public sealed class D3D12Renderer : IAquariumRenderer
}
}
private void UpdateReservoirHistory(ID3D12GraphicsCommandList activeCommandList, FrameResources frameResources)
{
if (reservoirHistoryUpdatePipelineState is null)
{
return;
}
activeCommandList.BeginEvent("Reservoir History Update");
try
{
var historyReadIndex = temporalFrameIndex & 1;
var historyWriteIndex = 1 - historyReadIndex;
var historyReadBuffer = fieldReservoirHistoryBuffers[historyReadIndex];
var historyWriteBuffer = fieldReservoirHistoryBuffers[historyWriteIndex];
sceneRenderTarget.Transition(activeCommandList, ResourceStates.PixelShaderResource | ResourceStates.NonPixelShaderResource);
sceneMetadataRenderTarget.Transition(activeCommandList, ResourceStates.PixelShaderResource | ResourceStates.NonPixelShaderResource);
sceneControlRenderTarget.Transition(activeCommandList, ResourceStates.PixelShaderResource | ResourceStates.NonPixelShaderResource);
sceneReservoirGuideRenderTarget.Transition(activeCommandList, ResourceStates.PixelShaderResource | ResourceStates.NonPixelShaderResource);
fieldReservoirCandidateBuffer.Transition(activeCommandList, ResourceStates.PixelShaderResource | ResourceStates.NonPixelShaderResource);
historyReadBuffer.Transition(activeCommandList, ResourceStates.PixelShaderResource | ResourceStates.NonPixelShaderResource);
historyWriteBuffer.Transition(activeCommandList, ResourceStates.UnorderedAccess);
reservoirResolvedRenderTarget.Transition(activeCommandList, ResourceStates.UnorderedAccess);
activeCommandList.SetDescriptorHeaps(frameResources.TransientShaderDescriptors.Heap);
activeCommandList.SetPipelineState(reservoirHistoryUpdatePipelineState);
activeCommandList.SetComputeRootSignature(fullscreenRootSignature);
activeCommandList.SetComputeRootDescriptorTable(RootFrameConstants, frameResources.FrameConstantsDescriptor.Gpu);
activeCommandList.SetComputeRootDescriptorTable(RootSourceTexture, frameResources.SceneDescriptor.Gpu);
activeCommandList.SetComputeRootDescriptorTable(RootCurrentSceneMetadata, frameResources.SceneMetadataDescriptor.Gpu);
activeCommandList.SetComputeRootDescriptorTable(RootCurrentSceneControl, frameResources.SceneControlDescriptor.Gpu);
activeCommandList.SetComputeRootShaderResourceView(RootReservoirHistoryRead, historyReadBuffer.Resource.GPUVirtualAddress);
activeCommandList.SetComputeRootUnorderedAccessView(RootReservoirHistoryWrite, historyWriteBuffer.Resource.GPUVirtualAddress);
activeCommandList.SetComputeRootDescriptorTable(RootCurrentReservoirGuide, frameResources.SceneReservoirGuideDescriptor.Gpu);
activeCommandList.SetComputeRootShaderResourceView(RootFieldReservoirCandidates, fieldReservoirCandidateBuffer.Resource.GPUVirtualAddress);
activeCommandList.SetComputeRootDescriptorTable(RootReservoirResolvedOutput, frameResources.ReservoirResolvedUnorderedAccessDescriptor.Gpu);
activeCommandList.Dispatch((uint)((width + 7) / 8), (uint)((height + 7) / 8), 1);
activeCommandList.ResourceBarrier(ResourceBarrier.BarrierUnorderedAccessView(historyWriteBuffer.Resource));
activeCommandList.ResourceBarrier(ResourceBarrier.BarrierUnorderedAccessView(reservoirResolvedRenderTarget.Resource));
}
finally
{
activeCommandList.EndEvent();
}
}
private void RenderTubeFields(ID3D12GraphicsCommandList activeCommandList, FrameResources frameResources)
{
if (activeTubeFieldDrawIndexCount <= 0 || tubeFieldRenderPipelineState is null || tubeFieldDrawBatches.Count == 0)
@ -2967,7 +3046,11 @@ public sealed class D3D12Renderer : IAquariumRenderer
private static bool IsFinite(Vector3 value) =>
float.IsFinite(value.X) && float.IsFinite(value.Y) && float.IsFinite(value.Z);
private void RenderBloom(ID3D12GraphicsCommandList activeCommandList, FrameResources frameResources)
private void RenderBloom(
ID3D12GraphicsCommandList activeCommandList,
FrameResources frameResources,
D3D12RenderTarget sourceTarget,
D3D12DescriptorSlot initialSourceDescriptor)
{
activeCommandList.BeginEvent("Bloom Pyramid");
try
@ -2975,12 +3058,12 @@ public sealed class D3D12Renderer : IAquariumRenderer
activeCommandList.SetDescriptorHeaps(frameResources.TransientShaderDescriptors.Heap);
activeCommandList.SetGraphicsRootSignature(fullscreenRootSignature);
activeCommandList.SetGraphicsRootDescriptorTable(RootFrameConstants, frameResources.FrameConstantsDescriptor.Gpu);
sceneRenderTarget.Transition(activeCommandList, ResourceStates.PixelShaderResource);
sourceTarget.Transition(activeCommandList, ResourceStates.PixelShaderResource);
for (var level = 0; level < BloomLevelCount; level++)
{
var sourceDescriptor = level == 0
? frameResources.SceneDescriptor
? initialSourceDescriptor
: frameResources.BloomDescriptors[level - 1];
var pipelineState = level == 0
? bloomPrefilterPipelineState!
@ -3035,38 +3118,27 @@ public sealed class D3D12Renderer : IAquariumRenderer
private void PresentBackBuffer(D3D12PassContext context, FrameResources frameResources)
{
context.CommandList.BeginEvent("Reservoir Presentation Resolve");
context.CommandList.BeginEvent("Reservoir Presentation Resolve");
try
{
var historyReadIndex = temporalFrameIndex & 1;
var historyWriteIndex = 1 - historyReadIndex;
var historyReadBuffer = fieldReservoirHistoryBuffers[historyReadIndex];
var historyWriteBuffer = fieldReservoirHistoryBuffers[historyWriteIndex];
sceneRenderTarget.Transition(context.CommandList, ResourceStates.PixelShaderResource);
reservoirResolvedRenderTarget.Transition(context.CommandList, ResourceStates.PixelShaderResource);
sceneMetadataRenderTarget.Transition(context.CommandList, ResourceStates.PixelShaderResource);
sceneControlRenderTarget.Transition(context.CommandList, ResourceStates.PixelShaderResource);
sceneReservoirGuideRenderTarget.Transition(context.CommandList, ResourceStates.PixelShaderResource);
fieldReservoirCandidateBuffer.Transition(context.CommandList, ResourceStates.PixelShaderResource | ResourceStates.NonPixelShaderResource);
historyReadBuffer.Transition(context.CommandList, ResourceStates.PixelShaderResource | ResourceStates.NonPixelShaderResource);
historyWriteBuffer.Transition(context.CommandList, ResourceStates.UnorderedAccess);
context.BackBuffer.Transition(context.CommandList, ResourceStates.RenderTarget);
context.CommandList.SetDescriptorHeaps(frameResources.TransientShaderDescriptors.Heap);
context.CommandList.SetPipelineState(resolvePipelineState!);
context.CommandList.SetGraphicsRootSignature(fullscreenRootSignature);
context.CommandList.SetGraphicsRootDescriptorTable(RootFrameConstants, frameResources.FrameConstantsDescriptor.Gpu);
context.CommandList.SetGraphicsRootDescriptorTable(RootSourceTexture, frameResources.SceneDescriptor.Gpu);
context.CommandList.SetGraphicsRootDescriptorTable(RootSourceTexture, frameResources.ReservoirResolvedDescriptor.Gpu);
context.CommandList.SetGraphicsRootDescriptorTable(RootBloom, frameResources.BloomPresentationDescriptor.Gpu);
context.CommandList.SetGraphicsRootDescriptorTable(RootCurrentSceneMetadata, frameResources.SceneMetadataDescriptor.Gpu);
context.CommandList.SetGraphicsRootDescriptorTable(RootCurrentSceneControl, frameResources.SceneControlDescriptor.Gpu);
context.CommandList.SetGraphicsRootShaderResourceView(RootReservoirHistoryRead, historyReadBuffer.Resource.GPUVirtualAddress);
context.CommandList.SetGraphicsRootUnorderedAccessView(RootReservoirHistoryWrite, historyWriteBuffer.Resource.GPUVirtualAddress);
context.CommandList.SetGraphicsRootDescriptorTable(RootSdfObjects, frameResources.SdfObjectDescriptor.Gpu);
context.CommandList.SetGraphicsRootDescriptorTable(RootTemporalGaussians, frameResources.TemporalGaussianDescriptor.Gpu);
context.CommandList.SetGraphicsRootDescriptorTable(RootCurrentReservoirGuide, frameResources.SceneReservoirGuideDescriptor.Gpu);
context.CommandList.SetGraphicsRootDescriptorTable(RootBlueNoise, frameResources.BlueNoiseDescriptor.Gpu);
context.CommandList.SetGraphicsRootShaderResourceView(RootFieldReservoirCandidates, fieldReservoirCandidateBuffer.Resource.GPUVirtualAddress);
context.CommandList.RSSetViewports(viewport);
context.CommandList.RSSetScissorRects(scissorRect);
@ -3077,7 +3149,6 @@ public sealed class D3D12Renderer : IAquariumRenderer
null);
context.CommandList.IASetPrimitiveTopology(PrimitiveTopology.TriangleList);
context.CommandList.DrawInstanced(3, 1, 0, 0);
context.CommandList.ResourceBarrier(ResourceBarrier.BarrierUnorderedAccessView(historyWriteBuffer.Resource));
CopyProgramOutputBackBuffer(context.CommandList, context.BackBuffer);
}
finally
@ -4281,6 +4352,7 @@ public sealed class D3D12Renderer : IAquariumRenderer
bloomDownsamplePipelineState!,
bloomBlurHorizontalPipelineState!,
bloomBlurVerticalPipelineState!,
reservoirHistoryUpdatePipelineState!,
resolvePipelineState!)
: null;
}
@ -4316,6 +4388,7 @@ public sealed class D3D12Renderer : IAquariumRenderer
bloomDownsamplePipelineState = pipelines.BloomDownsample;
bloomBlurHorizontalPipelineState = pipelines.BloomBlurHorizontal;
bloomBlurVerticalPipelineState = pipelines.BloomBlurVertical;
reservoirHistoryUpdatePipelineState = pipelines.ReservoirHistoryUpdate;
resolvePipelineState = pipelines.Resolve;
}
@ -4346,6 +4419,7 @@ public sealed class D3D12Renderer : IAquariumRenderer
bloomDownsamplePipelineState = null;
bloomBlurHorizontalPipelineState = null;
bloomBlurVerticalPipelineState = null;
reservoirHistoryUpdatePipelineState = null;
resolvePipelineState = null;
}
@ -4497,28 +4571,35 @@ public sealed class D3D12Renderer : IAquariumRenderer
28,
0,
D3D12.DescriptorRangeOffsetAppend);
var reservoirResolvedOutputRange = new DescriptorRange(
DescriptorRangeType.UnorderedAccessView,
1,
23,
0,
D3D12.DescriptorRangeOffsetAppend);
var rootParameters = new[]
{
new RootParameter(new RootDescriptorTable([constantBufferRange]), ShaderVisibility.All),
new RootParameter(new RootDescriptorTable([sourceTextureRange]), ShaderVisibility.Pixel),
new RootParameter(new RootDescriptorTable([sourceTextureRange]), ShaderVisibility.All),
new RootParameter(new RootDescriptorTable([heightFieldBrushRange]), ShaderVisibility.All),
new RootParameter(new RootDescriptorTable([sdfLightRange]), ShaderVisibility.Pixel),
new RootParameter(new RootDescriptorTable([bloomRange]), ShaderVisibility.Pixel),
new RootParameter(new RootDescriptorTable([currentSceneMetadataRange]), ShaderVisibility.Pixel),
new RootParameter(new RootDescriptorTable([currentSceneControlRange]), ShaderVisibility.Pixel),
new RootParameter(RootParameterType.ShaderResourceView, new RootDescriptor(51, 0), ShaderVisibility.Pixel),
new RootParameter(RootParameterType.UnorderedAccessView, new RootDescriptor(22, 0), ShaderVisibility.Pixel),
new RootParameter(new RootDescriptorTable([currentSceneMetadataRange]), ShaderVisibility.All),
new RootParameter(new RootDescriptorTable([currentSceneControlRange]), ShaderVisibility.All),
new RootParameter(RootParameterType.ShaderResourceView, new RootDescriptor(51, 0), ShaderVisibility.All),
new RootParameter(RootParameterType.UnorderedAccessView, new RootDescriptor(22, 0), ShaderVisibility.All),
new RootParameter(new RootDescriptorTable([studioPmremRange]), ShaderVisibility.Pixel),
new RootParameter(new RootDescriptorTable([studioIrradianceRange]), ShaderVisibility.Pixel),
new RootParameter(new RootDescriptorTable([sdfObjectRange]), ShaderVisibility.All),
new RootParameter(new RootDescriptorTable([temporalGaussianRange]), ShaderVisibility.All),
new RootParameter(new RootDescriptorTable([currentReservoirGuideRange]), ShaderVisibility.Pixel),
new RootParameter(new RootDescriptorTable([currentReservoirGuideRange]), ShaderVisibility.All),
new RootParameter(RootParameterType.ShaderResourceView, new RootDescriptor(38, 0), ShaderVisibility.All),
new RootParameter(RootParameterType.ShaderResourceView, new RootDescriptor(39, 0), ShaderVisibility.All),
new RootParameter(RootParameterType.ShaderResourceView, new RootDescriptor(40, 0), ShaderVisibility.All),
new RootParameter(RootParameterType.ShaderResourceView, new RootDescriptor(41, 0), ShaderVisibility.All),
new RootParameter(new RootDescriptorTable([blueNoiseRange]), ShaderVisibility.Pixel),
new RootParameter(RootParameterType.ShaderResourceView, new RootDescriptor(45, 0), ShaderVisibility.Pixel),
new RootParameter(RootParameterType.ShaderResourceView, new RootDescriptor(45, 0), ShaderVisibility.All),
new RootParameter(new RootDescriptorTable([reservoirResolvedOutputRange]), ShaderVisibility.All),
};
var staticSamplers = new[]
{
@ -4534,7 +4615,7 @@ public sealed class D3D12Renderer : IAquariumRenderer
StaticBorderColor.TransparentBlack,
0.0f,
float.MaxValue,
ShaderVisibility.Pixel,
ShaderVisibility.All,
0),
};
var description = new RootSignatureDescription(
@ -5010,6 +5091,17 @@ public sealed class D3D12Renderer : IAquariumRenderer
return CreateFullscreenPipelineState(path, "FullscreenTriangleVS", "D3D12BloomBlurVerticalPS", SceneHdrFormat);
}
private ID3D12PipelineState CreateReservoirHistoryUpdatePipelineState(string path)
{
var computeShader = CompileShader(path, "D3D12ReservoirHistoryUpdateCS", "cs_5_0");
var description = new ComputePipelineStateDescription
{
RootSignature = fullscreenRootSignature,
ComputeShader = computeShader,
};
return device.CreateComputePipelineState(description);
}
private ID3D12PipelineState CreateResolvePipelineState(string path)
{
return CreateFullscreenPipelineState(
@ -5354,11 +5446,13 @@ public sealed class D3D12Renderer : IAquariumRenderer
ID3D12PipelineState BloomDownsample,
ID3D12PipelineState BloomBlurHorizontal,
ID3D12PipelineState BloomBlurVertical,
ID3D12PipelineState ReservoirHistoryUpdate,
ID3D12PipelineState Resolve) : IDisposable
{
public void Dispose()
{
Resolve.Dispose();
ReservoirHistoryUpdate.Dispose();
BloomBlurVertical.Dispose();
BloomBlurHorizontal.Dispose();
BloomDownsample.Dispose();
@ -5495,6 +5589,10 @@ public sealed class D3D12Renderer : IAquariumRenderer
public D3D12DescriptorSlot SceneCandidateReservoirGuideDescriptor { get; set; }
public D3D12DescriptorSlot ReservoirResolvedDescriptor { get; set; }
public D3D12DescriptorSlot ReservoirResolvedUnorderedAccessDescriptor { get; set; }
public D3D12DescriptorSlot FieldReservoirCandidateUnorderedAccessDescriptor { get; set; }
public D3D12DescriptorSlot FieldReservoirLockUnorderedAccessDescriptor { get; set; }

View file

@ -64,6 +64,7 @@ struct FieldReservoirCandidate
StructuredBuffer<FieldReservoirCandidate> fieldReservoirCandidates : register(t45);
StructuredBuffer<FieldReservoirCandidate> reservoirHistoryRead : register(t51);
RWStructuredBuffer<FieldReservoirCandidate> reservoirHistoryWrite : register(u22);
RWTexture2D<float4> reservoirResolvedTexture : register(u23);
static const uint FieldReservoirSlotsPerPixel = 4u;
@ -641,22 +642,23 @@ FieldReservoirResolveOut D3D12FieldReservoirResolvePS(VertexOut input)
return output;
}
// Reservoir presentation resolve owns temporal reconstruction. The history
// textures below are cache lines for validated reservoir evidence, not a
// separate final-pixel TAA authority.
ResolveOut D3D12ReservoirPresentationResolvePS(VertexOut input)
[numthreads(8, 8, 1)]
void D3D12ReservoirHistoryUpdateCS(uint3 dispatchThreadId : SV_DispatchThreadID)
{
float2 screenUv = float2(input.uv.x, 1.0 - input.uv.y);
float2 pixel = screenUv * resolution;
uint2 dimensions = (uint2)max(resolution, float2(1.0, 1.0));
uint2 currentPixel = min((uint2)max(pixel, float2(0.0, 0.0)), dimensions - 1u);
if (dispatchThreadId.x >= dimensions.x || dispatchThreadId.y >= dimensions.y)
{
return;
}
uint2 currentPixel = dispatchThreadId.xy;
float2 uv = (float2(currentPixel) + 0.5) / float2(dimensions);
float2 pixel = float2(currentPixel);
uint pixelIndex = currentPixel.y * dimensions.x + currentPixel.x;
uint baseIndex = pixelIndex * FieldReservoirSlotsPerPixel;
FieldReservoirCandidate sceneCandidate = sceneFieldReservoirCandidate(input.uv);
FieldReservoirCandidate sceneCandidate = sceneFieldReservoirCandidate(uv);
FieldReservoirCandidate bestResolved = emptyFieldReservoirCandidate();
float bestPriority = 1.0e20;
float combinedHistoryWeight = 0.0;
float combinedHistoryAge = 0.0;
bool hasSharedCandidate = false;
[unroll]
@ -679,7 +681,7 @@ ResolveOut D3D12ReservoirPresentationResolvePS(VertexOut input)
FieldReservoirCandidate resolvedCandidate = resolveReservoirHistoryCandidate(
currentCandidate,
pixel,
input.uv,
uv,
slotHistoryWeight,
slotHistoryAge);
reservoirHistoryWrite[baseIndex + slot] = resolvedCandidate;
@ -692,8 +694,6 @@ ResolveOut D3D12ReservoirPresentationResolvePS(VertexOut input)
{
bestResolved = resolvedCandidate;
bestPriority = priority;
combinedHistoryWeight = slotHistoryWeight;
combinedHistoryAge = slotHistoryAge;
}
}
@ -702,33 +702,39 @@ ResolveOut D3D12ReservoirPresentationResolvePS(VertexOut input)
bestResolved = sceneCandidate;
}
float3 rawCurrentColor = sceneCandidate.colorTravel.rgb;
float3 currentColor = bestResolved.colorTravel.rgb;
float currentCoverage = saturate(bestResolved.control.x);
float currentStepRatio = saturate(bestResolved.control.y);
float currentTemporalDetail = saturate(bestResolved.control.z);
float currentFieldId = bestResolved.metadata.x;
float currentReservoirConfidence = reservoirCandidateConfidence(bestResolved);
float reservoirSampleAge = max(bestResolved.reservoirGuide.y, 0.0);
float currentReservoirDomainValidity = reservoirCandidateDomainValidity(bestResolved);
float3 resolved = bestResolved.colorTravel.rgb;
float bloomStability = saturate((luminance(resolved) + 0.25) / max(luminance(currentColor) + 0.25, 0.0001));
float3 finalColor = presentColor(resolved, input.uv, bloomStability);
reservoirResolvedTexture[currentPixel] = bestResolved.colorTravel;
}
ResolveOut D3D12ReservoirPresentationResolvePS(VertexOut input)
{
float4 resolvedSample = sourceTexture.SampleLevel(sourceSampler, input.uv, 0.0);
float4 currentMetadata = loadCurrentMetadata(input.uv);
float4 currentControl = loadCurrentControl(input.uv);
float4 currentReservoirGuide = loadCurrentReservoirGuide(input.uv);
float currentCoverage = saturate(currentControl.x);
float currentStepRatio = saturate(currentControl.y);
float currentTemporalDetail = saturate(currentControl.z);
float currentFieldId = currentMetadata.x;
float currentReservoirConfidence = currentReservoirGuide.x > 0.0 ? saturate(currentReservoirGuide.x) : 1.0;
float reservoirSampleAge = max(currentReservoirGuide.y, 0.0);
float currentReservoirDomainValidity = currentReservoirGuide.z > 0.0 ? saturate(currentReservoirGuide.z) : 1.0;
float3 resolved = resolvedSample.rgb;
float3 finalColor = presentColor(resolved, input.uv, 1.0);
if (renderDebugMode > 0.5 && renderDebugMode < 1.5)
{
finalColor = aces(rawCurrentColor * max(exposure, 0.001));
finalColor = aces(resolved * max(exposure, 0.001));
}
else if (renderDebugMode >= 1.5 && renderDebugMode < 2.5)
{
finalColor = aces(bestResolved.colorTravel.rgb * max(exposure, 0.001));
finalColor = aces(resolved * max(exposure, 0.001));
}
else if (renderDebugMode >= 2.5 && renderDebugMode < 3.5)
{
finalColor = (combinedHistoryAge / MAX_HISTORY_AGE).xxx;
finalColor = saturate(reservoirSampleAge / MAX_HISTORY_AGE).xxx;
}
else if (renderDebugMode >= 3.5 && renderDebugMode < 4.5)
{
finalColor = combinedHistoryWeight.xxx;
finalColor = currentReservoirConfidence.xxx;
}
else if (renderDebugMode >= 4.5 && renderDebugMode < 5.5)
{
@ -745,7 +751,7 @@ ResolveOut D3D12ReservoirPresentationResolvePS(VertexOut input)
}
else if (renderDebugMode >= 7.5 && renderDebugMode < 8.5)
{
float luma = luminance(currentColor * max(exposure, 0.001));
float luma = luminance(resolved * max(exposure, 0.001));
finalColor = luma.xxx;
}
else if (renderDebugMode >= 8.5 && renderDebugMode < 9.5)

View file

@ -273,3 +273,4 @@
{"lesson":"TubeField blackbody ramp sampling was correct on X, but the old single-tube proof material branch treated high emission scale plus material.y near 1 as an exact red/orange proof tube and bypassed the ramp. Spectrum TubeFields now sample the ramp from saturated normalized amplitude on X and use amplitude squared for emission intensity; the exact proof branch is only reachable through an explicit negative tubeData.w sentinel. The high-end blue is expected to survive mostly as a cool fringe after ACES/SE bloom because the blackbody asset's right edge is pale blue and peak emission tonemaps toward white.","timestamp":"2026-05-28T15:06:53+01:00","files":["src/Aquarium.Engine/Render/Shaders/D3D12TubeField.hlsl","state/evidence.jsonl"],"source":"Mimir spectrum dashboard blackbody ramp QA","topic":"tubefield-blackbody-ramp-material"}
{"lesson":"TubeField no longer owns final scene contribution through a private blend/K-buffer path. D3D12 now separates scene-candidate MRTs from canonical scene MRTs, clears a shared two-slot-per-pixel field reservoir UAV, lets TubeField pixel evaluation inject opaque SDF candidates with color/travel/metadata/control/reservoir-guide, then runs a fullscreen field-reservoir resolve that writes the canonical scene lanes before temporal resolve, bloom, and presentation. Legacy/direct contributors currently enter that resolve as the scene-candidate input candidate; future cuts should move them to explicit claim injection too.","timestamp":"2026-05-28T15:41:26+01:00","files":["src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12TubeField.hlsl","src/Aquarium.Engine/Render/Shaders/D3D12Post.hlsl","state/map.yaml","state/evidence.jsonl"],"source":"field reservoir ownership cut","topic":"field-reservoir-canonical-scene-resolve"}
{"lesson":"The first TubeField ReSTIR cut replaces per-fragment reservoir scatter locks with pixel-owned compute reservoirs. TubeField expansion now emits reusable segment records with baked geometry/material endpoints; D3D12 bins active segments into screen tiles once per frame, streams a small fixed candidate set into a per-pixel initial reservoir, combines previous-frame reservoirs through reprojection, performs spatial neighbor reuse, and exports the selected sample into the shared field-reservoir candidate buffer for canonical scene resolve. Runtime shader compilation and Mimir synthetic spectrum execution passed; frame timing settles around the vsync band after ramp-up, but local window capture was unreliable and should not be treated as visual QA evidence.","timestamp":"2026-05-28T16:17:56+01:00","files":["src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12TubeField.hlsl","state/map.yaml","state/evidence.jsonl"],"source":"TubeField ReSTIR ownership rebuild","topic":"tubefield-restir-pixel-owned-reservoir"}
{"lesson":"Reservoir history update and reuse moved out of the presentation pixel shader into a shared D3D12 compute pass. The pass reads canonical scene lanes, shared field candidate rows, and ping-ponged structured history rows; writes the next four history rows per pixel; and emits a resolved HDR field texture consumed by bloom and final presentation. The presentation shader no longer writes reservoir history, so it cannot become a hidden TAA owner. Fensalir and Mimir builds passed, the Mimir spectrum upload smoke passed, and a synthetic spectrum headless capture rendered through the compute-owned reservoir path.","timestamp":"2026-05-28T17:05:00+01:00","files":["src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12Post.hlsl","docs/temporal-spatial-evidence-reservoir.md","docs/perfect-machine-architecture.md","state/map.yaml"],"source":"user selected full structured-buffer reservoir path","topic":"reservoir-history-compute-owner"}

View file

@ -64,10 +64,11 @@ invariants:
direct SDF proxies, meshes, volumes, fractal probes, and Mimir sensor
observations must publish bounded Form/Appearance/Transport field claims
before they can participate in reusable field evidence.
- Final composition reads canonical scene targets produced by reservoir
resolve. TubeField render lowerings inject opaque SDF candidates into the
shared field reservoir; they must not regain private final-visibility
authority through blend order, proxy depth, or a TubeField-local K-buffer.
- Final composition reads the resolved HDR field texture produced by the
shared reservoir history update pass. TubeField render lowerings inject
opaque SDF candidates into the shared field reservoir; they must not regain
private final-visibility authority through blend order, proxy depth, a
presentation-shader history writer, or a TubeField-local K-buffer.
- Field packet payload handles are not authority. Resource-backed claims and
packets must reference declared AquariumFieldResourceDeclaration rows with
kind, GPU residency, shader access, format, shape, validity, version, and
@ -203,11 +204,14 @@ next_actions:
distinguish source-lane claims while preserving the TubeField family range.
D3D12 now has the first shared field-reservoir resolve cut: legacy/direct
scene contributors render into scene-candidate MRTs, TubeField injects
two-slot-per-pixel opaque SDF candidates into a shared UAV, and a fullscreen
resolve writes canonical scene color/travel/metadata/control/reservoir-guide
before temporal resolve, bloom, and presentation. The remaining coherence
cut is to make every non-debug contributor inject explicit field candidates
instead of entering as the single legacy scene-candidate input.
four-slot-per-pixel opaque SDF candidates into a shared UAV, and a
fullscreen resolve writes canonical scene color/travel/metadata/control/
reservoir-guide lanes. A shared compute pass then updates ping-ponged
structured reservoir history, emits the resolved HDR field texture, and
bloom/presentation consume that texture without owning history. The
remaining coherence cut is to make every non-debug contributor inject
explicit field candidates instead of entering as the single legacy
scene-candidate input.
TubeField reservoir ownership has been rebuilt into the first ReSTIR-shaped
compute path: TubeField expansion emits reusable segment records with baked
material endpoints, segments are binned into screen tiles once per frame,