Upload D3D12 field resources
This commit is contained in:
parent
4b0bf997f4
commit
f32772c052
11 changed files with 456 additions and 3 deletions
|
|
@ -36,6 +36,10 @@ resource graph beyond smoke-pass scaffolding.
|
|||
up-facing gravity quad per body. The target is scalar `R16_Float`: height is
|
||||
one value, and the format still supports the additive render-target blending
|
||||
the brush pass needs. Mode `11` exposes it.
|
||||
- D3D12 uploads the live froxel primitive table and field instance table into
|
||||
default-heap structured buffers each frame, using the upload ring only as the
|
||||
copy source. Their SRVs are created in the transient descriptor heap used by
|
||||
the consuming pass, avoiding impossible static+transient heap co-binding.
|
||||
- Renderer calls receive current window dimensions, and the D3D12 backend can
|
||||
recreate swapchain buffers plus dependent smoke resources on resize.
|
||||
- Resize waits for the GPU, disposes swapchain-dependent targets, rebuilds
|
||||
|
|
@ -63,6 +67,9 @@ resource graph beyond smoke-pass scaffolding.
|
|||
- The offscreen smoke target copies to the swapchain. Keep this only if the real
|
||||
post-process graph needs it; otherwise present from the final render target
|
||||
path with no ornamental copy.
|
||||
- The D3D11 and D3D12 field table builders are temporarily duplicated. Extract
|
||||
them into one shared field ABI owner before the second backend starts to
|
||||
drift.
|
||||
|
||||
## Required Before Real Pass Migration
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,11 @@ has feature parity.
|
|||
draws one additive up-facing gravity quad per body into a 128x128
|
||||
scalar `R16_Float` Grid height target. Render debug mode `11` displays that
|
||||
target.
|
||||
- D3D12 now owns the first field-resource upload path: CPU-built froxel
|
||||
primitive ids and field instances upload through the per-frame upload ring,
|
||||
copy into default-heap structured buffers, and bind as transient SRVs for the
|
||||
diagnostic shader. This proves the backend can feed real Aquarium field data
|
||||
without reading directly from upload memory.
|
||||
- Resize waits for the GPU, releases swapchain-dependent resources, rebuilds
|
||||
static shader/RTV descriptor arenas, then recreates backbuffer views and
|
||||
dependent render targets. Descriptor exhaustion on resize is no longer a
|
||||
|
|
|
|||
|
|
@ -83,6 +83,13 @@ scalar `R16_Float` Grid height target. D3D12 render debug mode `11` displays
|
|||
that target. D3D11 remains the visual reference until the full pass chain
|
||||
reaches parity.
|
||||
|
||||
D3D12 also uploads the current froxel primitive id table and field instance
|
||||
table into default-heap structured buffers every frame. The diagnostic smoke
|
||||
shader reads both SRVs through the transient descriptor heap; this is only a
|
||||
resource-path proof, not the migrated scene or medium pass yet. The D3D11 and
|
||||
D3D12 table builders should be collapsed into one shared field ABI before more
|
||||
field consumers are ported.
|
||||
|
||||
The shader far distance is frame-derived, not a fixed constant. C# sends the
|
||||
distance from the camera to the Grid origin plus the Grid radius, which matches
|
||||
the circular Grid fade boundary and avoids wasting raymarch work past the
|
||||
|
|
|
|||
|
|
@ -18,6 +18,15 @@ public sealed class D3D12Renderer : IAquariumRenderer
|
|||
private const Format GridHeightFormat = Format.R16_Float;
|
||||
private const int PlanetCount = 5;
|
||||
private const int GridHeightBrushCount = PlanetCount + 1;
|
||||
private const int FroxelCountX = 8;
|
||||
private const int FroxelCountY = 8;
|
||||
private const int FroxelCountZ = 4;
|
||||
private const int FroxelSlotCount = 2;
|
||||
private const int FroxelBufferElementCount = FroxelCountX * FroxelCountY * FroxelCountZ * FroxelSlotCount;
|
||||
private const int FieldInstanceCount = PlanetCount + 5;
|
||||
private const float SunRadius = 1.12f;
|
||||
private const float FroxelMinZ = -2.0f;
|
||||
private const float FroxelMaxZ = 6.0f;
|
||||
private const string GridShaderRelativePath = "Render/Shaders/D3D12Grid.hlsl";
|
||||
private const string SmokeShaderRelativePath = "Render/Shaders/D3D12Smoke.hlsl";
|
||||
|
||||
|
|
@ -41,6 +50,10 @@ public sealed class D3D12Renderer : IAquariumRenderer
|
|||
private D3D12RenderTarget gridHeightRenderTarget;
|
||||
private D3D12RenderTarget smokeRenderTarget;
|
||||
private D3D12RenderTarget diagnosticUavTarget;
|
||||
private readonly D3D12StructuredBuffer froxelPrimitiveBuffer;
|
||||
private readonly D3D12StructuredBuffer fieldInstanceBuffer;
|
||||
private readonly Int4[] froxelPrimitiveIds = new Int4[FroxelBufferElementCount];
|
||||
private readonly FieldInstanceGpu[] fieldInstances = new FieldInstanceGpu[FieldInstanceCount];
|
||||
private Viewport viewport;
|
||||
private RawRect scissorRect;
|
||||
private int width;
|
||||
|
|
@ -102,6 +115,10 @@ public sealed class D3D12Renderer : IAquariumRenderer
|
|||
gridHeightRenderTarget = CreateGridHeightRenderTarget();
|
||||
smokeRenderTarget = CreateSmokeRenderTarget();
|
||||
diagnosticUavTarget = CreateDiagnosticUavTarget();
|
||||
froxelPrimitiveBuffer = new D3D12StructuredBuffer(device, FroxelBufferElementCount, Marshal.SizeOf<Int4>(), "Aquarium D3D12 Froxel Primitive Buffer");
|
||||
fieldInstanceBuffer = new D3D12StructuredBuffer(device, FieldInstanceCount, Marshal.SizeOf<FieldInstanceGpu>(), "Aquarium D3D12 Field Instance Buffer");
|
||||
resourceRegistry.Add("froxel-primitive-buffer", froxelPrimitiveBuffer);
|
||||
resourceRegistry.Add("field-instance-buffer", fieldInstanceBuffer);
|
||||
commandList = device.CreateCommandList<ID3D12GraphicsCommandList>(0, CommandListType.Direct, frames[frameIndex].CommandAllocator, null);
|
||||
commandList.Name = "Aquarium D3D12 Graphics Command List";
|
||||
commandList.Close();
|
||||
|
|
@ -178,6 +195,8 @@ public sealed class D3D12Renderer : IAquariumRenderer
|
|||
}
|
||||
|
||||
BuildGridHeightBrushes(frame);
|
||||
BuildFroxelPrimitiveTable(frame);
|
||||
BuildFieldInstanceTable(frame);
|
||||
var frameConstants = frameResources.UploadRing.WriteConstant(new FrameConstants(
|
||||
new Vector2(width, height),
|
||||
frame.TimeSeconds,
|
||||
|
|
@ -222,10 +241,15 @@ public sealed class D3D12Renderer : IAquariumRenderer
|
|||
frameResources.SmokeConstantsDescriptor.Cpu);
|
||||
frameResources.DiagnosticUavDescriptor = frameResources.TransientShaderDescriptors.Allocate();
|
||||
diagnosticUavTarget.CreateUnorderedAccessView(device, frameResources.DiagnosticUavDescriptor);
|
||||
frameResources.FroxelPrimitiveDescriptor = frameResources.TransientShaderDescriptors.Allocate();
|
||||
froxelPrimitiveBuffer.CreateShaderResourceView(device, frameResources.FroxelPrimitiveDescriptor);
|
||||
frameResources.FieldInstanceDescriptor = frameResources.TransientShaderDescriptors.Allocate();
|
||||
fieldInstanceBuffer.CreateShaderResourceView(device, frameResources.FieldInstanceDescriptor);
|
||||
frameResources.CommandAllocator.Reset();
|
||||
commandList.Reset(frameResources.CommandAllocator, null);
|
||||
|
||||
commandList.BeginEvent("Aquarium D3D12 Frame");
|
||||
UploadFieldResources(commandList, frameResources);
|
||||
RenderGridHeight(commandList, frameResources);
|
||||
ClearBackBuffer(new D3D12PassContext(commandList, frameResources.BackBuffer, frameResources.BackBufferRenderTargetView.Cpu), frame);
|
||||
commandList.EndEvent();
|
||||
|
|
@ -255,6 +279,8 @@ public sealed class D3D12Renderer : IAquariumRenderer
|
|||
gridHeightBrushPipelineState.Dispose();
|
||||
gridHeightBasePipelineState.Dispose();
|
||||
fullscreenRootSignature.Dispose();
|
||||
fieldInstanceBuffer.Dispose();
|
||||
froxelPrimitiveBuffer.Dispose();
|
||||
diagnosticUavTarget.Dispose();
|
||||
smokeRenderTarget.Dispose();
|
||||
gridHeightRenderTarget.Dispose();
|
||||
|
|
@ -394,6 +420,8 @@ public sealed class D3D12Renderer : IAquariumRenderer
|
|||
context.CommandList.SetGraphicsRootSignature(fullscreenRootSignature);
|
||||
context.CommandList.SetGraphicsRootDescriptorTable(0, frames[frameIndex].SmokeConstantsDescriptor.Gpu);
|
||||
context.CommandList.SetGraphicsRootDescriptorTable(2, frames[frameIndex].DiagnosticUavDescriptor.Gpu);
|
||||
context.CommandList.SetGraphicsRootDescriptorTable(4, frames[frameIndex].FroxelPrimitiveDescriptor.Gpu);
|
||||
context.CommandList.SetGraphicsRootDescriptorTable(5, frames[frameIndex].FieldInstanceDescriptor.Gpu);
|
||||
context.CommandList.RSSetViewports(viewport);
|
||||
context.CommandList.RSSetScissorRects(scissorRect);
|
||||
context.CommandList.OMSetRenderTargets(smokeRenderTarget.RenderTargetView.Cpu, null);
|
||||
|
|
@ -433,6 +461,20 @@ public sealed class D3D12Renderer : IAquariumRenderer
|
|||
}
|
||||
}
|
||||
|
||||
private void UploadFieldResources(ID3D12GraphicsCommandList activeCommandList, FrameResources frameResources)
|
||||
{
|
||||
activeCommandList.BeginEvent("Field Resource Upload");
|
||||
try
|
||||
{
|
||||
froxelPrimitiveBuffer.Upload(activeCommandList, frameResources.UploadRing, froxelPrimitiveIds);
|
||||
fieldInstanceBuffer.Upload(activeCommandList, frameResources.UploadRing, fieldInstances);
|
||||
}
|
||||
finally
|
||||
{
|
||||
activeCommandList.EndEvent();
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderGridHeight(ID3D12GraphicsCommandList activeCommandList, FrameResources frameResources)
|
||||
{
|
||||
var viewport = new Viewport(0.0f, 0.0f, GridHeightTextureSize, GridHeightTextureSize);
|
||||
|
|
@ -506,6 +548,168 @@ public sealed class D3D12Renderer : IAquariumRenderer
|
|||
gridHeightBrushConstants.Set(index, centerRadius, shape, wave);
|
||||
}
|
||||
|
||||
private void BuildFroxelPrimitiveTable(AquariumFrame frame)
|
||||
{
|
||||
for (var i = 0; i < froxelPrimitiveIds.Length; i++)
|
||||
{
|
||||
froxelPrimitiveIds[i] = new Int4(-1, -1, -1, -1);
|
||||
}
|
||||
|
||||
AddPrimitiveToFroxels(frame, 0, new Vector3(0.0f, 0.0f, 2.2f), SunRadius * 1.22f);
|
||||
|
||||
for (var i = 0; i < PlanetCount; i++)
|
||||
{
|
||||
var radius = PlanetRadius(i);
|
||||
AddPrimitiveToFroxels(frame, i + 1, PlanetCenter(i, frame.TimeSeconds, radius), radius * 1.16f);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddPrimitiveToFroxels(AquariumFrame frame, int primitiveId, Vector3 center, float boundRadius)
|
||||
{
|
||||
var min = center - new Vector3(boundRadius);
|
||||
var max = center + new Vector3(boundRadius);
|
||||
var minCell = FroxelCellForPosition(frame, min);
|
||||
var maxCell = FroxelCellForPosition(frame, max);
|
||||
|
||||
for (var z = minCell.Z; z <= maxCell.Z; z++)
|
||||
{
|
||||
for (var y = minCell.Y; y <= maxCell.Y; y++)
|
||||
{
|
||||
for (var x = minCell.X; x <= maxCell.X; x++)
|
||||
{
|
||||
AddPrimitiveToFroxel(FroxelIndex(x, y, z), primitiveId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddPrimitiveToFroxel(int froxelIndex, int primitiveId)
|
||||
{
|
||||
var baseElement = froxelIndex * FroxelSlotCount;
|
||||
for (var slotGroup = 0; slotGroup < FroxelSlotCount; slotGroup++)
|
||||
{
|
||||
var elementIndex = baseElement + slotGroup;
|
||||
var element = froxelPrimitiveIds[elementIndex];
|
||||
|
||||
if (element.X == primitiveId || element.Y == primitiveId || element.Z == primitiveId || element.W == primitiveId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.X == -1)
|
||||
{
|
||||
froxelPrimitiveIds[elementIndex] = element with { X = primitiveId };
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.Y == -1)
|
||||
{
|
||||
froxelPrimitiveIds[elementIndex] = element with { Y = primitiveId };
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.Z == -1)
|
||||
{
|
||||
froxelPrimitiveIds[elementIndex] = element with { Z = primitiveId };
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.W == -1)
|
||||
{
|
||||
froxelPrimitiveIds[elementIndex] = element with { W = primitiveId };
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static FroxelCell FroxelCellForPosition(AquariumFrame frame, Vector3 position)
|
||||
{
|
||||
var gridCenter = frame.Grid.Center;
|
||||
var gridRadius = MathF.Max(frame.Grid.Radius, 0.001f);
|
||||
var localX = ((position.X - gridCenter.X) / gridRadius) * 0.5f + 0.5f;
|
||||
var localY = ((position.Y - gridCenter.Y) / gridRadius) * 0.5f + 0.5f;
|
||||
var localZ = (position.Z - FroxelMinZ) / (FroxelMaxZ - FroxelMinZ);
|
||||
|
||||
return new FroxelCell(
|
||||
ClampCell(localX, FroxelCountX),
|
||||
ClampCell(localY, FroxelCountY),
|
||||
ClampCell(localZ, FroxelCountZ));
|
||||
}
|
||||
|
||||
private static int ClampCell(float normalized, int count)
|
||||
{
|
||||
return Math.Clamp((int)MathF.Floor(normalized * count), 0, count - 1);
|
||||
}
|
||||
|
||||
private static int FroxelIndex(int x, int y, int z)
|
||||
{
|
||||
return x + y * FroxelCountX + z * FroxelCountX * FroxelCountY;
|
||||
}
|
||||
|
||||
private void BuildFieldInstanceTable(AquariumFrame frame)
|
||||
{
|
||||
fieldInstances[0] = FieldInstanceGpu.Sphere(
|
||||
fieldId: 2.0f,
|
||||
flags: FieldFlags.Solid | FieldFlags.Emitter,
|
||||
center: new Vector3(0.0f, 0.0f, 2.2f),
|
||||
radius: SunRadius,
|
||||
materialId: 1.0f,
|
||||
mediumId: 0.0f,
|
||||
color: new Vector3(10.0f, 8.7f, 4.2f),
|
||||
medium: Vector4.Zero);
|
||||
|
||||
for (var i = 0; i < PlanetCount; i++)
|
||||
{
|
||||
var radius = PlanetRadius(i);
|
||||
fieldInstances[i + 1] = FieldInstanceGpu.Sphere(
|
||||
fieldId: 10.0f + i,
|
||||
flags: FieldFlags.Solid | FieldFlags.ShadowCaster | FieldFlags.Receiver,
|
||||
center: PlanetCenter(i, frame.TimeSeconds, radius),
|
||||
radius: radius,
|
||||
materialId: 10.0f + i,
|
||||
mediumId: 0.0f,
|
||||
color: Vector3.One,
|
||||
medium: Vector4.Zero);
|
||||
}
|
||||
|
||||
fieldInstances[6] = FieldInstanceGpu.Ellipsoid(
|
||||
fieldId: 32.0f,
|
||||
flags: FieldFlags.Cloud | FieldFlags.Receiver,
|
||||
center: new Vector3(frame.Grid.Center.X - 3.8f, frame.Grid.Center.Y + 1.8f, 1.15f),
|
||||
radius: new Vector3(3.4f, 1.25f, 0.92f),
|
||||
angle: frame.TimeSeconds * 0.055f,
|
||||
mediumId: 1.0f,
|
||||
color: new Vector3(0.50f, 0.72f, 0.86f),
|
||||
medium: new Vector4(0.220f, 0.160f, 0.0f, 1.25f));
|
||||
fieldInstances[7] = FieldInstanceGpu.Ellipsoid(
|
||||
fieldId: 33.0f,
|
||||
flags: FieldFlags.Cloud | FieldFlags.Receiver,
|
||||
center: new Vector3(frame.Grid.Center.X + 4.1f, frame.Grid.Center.Y - 1.4f, -0.45f),
|
||||
radius: new Vector3(4.5f, 1.55f, 0.78f),
|
||||
angle: -0.62f + frame.TimeSeconds * 0.033f,
|
||||
mediumId: 1.0f,
|
||||
color: new Vector3(0.34f, 0.58f, 0.72f),
|
||||
medium: new Vector4(0.200f, 0.140f, 0.0f, 1.20f));
|
||||
fieldInstances[8] = FieldInstanceGpu.Ellipsoid(
|
||||
fieldId: 34.0f,
|
||||
flags: FieldFlags.Cloud | FieldFlags.Receiver,
|
||||
center: new Vector3(frame.Grid.Center.X + 0.8f, frame.Grid.Center.Y + 3.7f, 2.7f),
|
||||
radius: new Vector3(2.2f, 1.1f, 0.62f),
|
||||
angle: 1.15f,
|
||||
mediumId: 1.0f,
|
||||
color: new Vector3(0.75f, 0.70f, 0.52f),
|
||||
medium: new Vector4(0.180f, 0.125f, 0.0f, 1.15f));
|
||||
fieldInstances[9] = FieldInstanceGpu.Ellipsoid(
|
||||
fieldId: 35.0f,
|
||||
flags: FieldFlags.Cloud | FieldFlags.Receiver,
|
||||
center: new Vector3(frame.Grid.Center.X - 0.4f, frame.Grid.Center.Y - 3.9f, -1.35f),
|
||||
radius: new Vector3(5.2f, 1.8f, 0.88f),
|
||||
angle: 0.46f,
|
||||
mediumId: 1.0f,
|
||||
color: new Vector3(0.30f, 0.50f, 0.68f),
|
||||
medium: new Vector4(0.190f, 0.130f, 0.0f, 1.20f));
|
||||
}
|
||||
|
||||
private void SignalFrame(FrameResources frameResources)
|
||||
{
|
||||
var signalValue = ++fenceValue;
|
||||
|
|
@ -599,12 +803,26 @@ public sealed class D3D12Renderer : IAquariumRenderer
|
|||
1,
|
||||
0,
|
||||
D3D12.DescriptorRangeOffsetAppend);
|
||||
var froxelPrimitiveRange = new DescriptorRange(
|
||||
DescriptorRangeType.ShaderResourceView,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
D3D12.DescriptorRangeOffsetAppend);
|
||||
var fieldInstanceRange = new DescriptorRange(
|
||||
DescriptorRangeType.ShaderResourceView,
|
||||
1,
|
||||
12,
|
||||
0,
|
||||
D3D12.DescriptorRangeOffsetAppend);
|
||||
var rootParameters = new[]
|
||||
{
|
||||
new RootParameter(new RootDescriptorTable([constantBufferRange]), ShaderVisibility.All),
|
||||
new RootParameter(new RootDescriptorTable([sourceTextureRange]), ShaderVisibility.Pixel),
|
||||
new RootParameter(new RootDescriptorTable([diagnosticUavRange]), ShaderVisibility.Pixel),
|
||||
new RootParameter(new RootDescriptorTable([gridBrushRange]), ShaderVisibility.All),
|
||||
new RootParameter(new RootDescriptorTable([froxelPrimitiveRange]), ShaderVisibility.Pixel),
|
||||
new RootParameter(new RootDescriptorTable([fieldInstanceRange]), ShaderVisibility.Pixel),
|
||||
};
|
||||
var staticSamplers = new[]
|
||||
{
|
||||
|
|
@ -826,6 +1044,70 @@ public sealed class D3D12Renderer : IAquariumRenderer
|
|||
}
|
||||
}
|
||||
|
||||
private readonly record struct FroxelCell(int X, int Y, int Z);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private readonly record struct Int4(int X, int Y, int Z, int W);
|
||||
|
||||
[Flags]
|
||||
private enum FieldFlags
|
||||
{
|
||||
Solid = 1,
|
||||
Cloud = 2,
|
||||
Hybrid = 4,
|
||||
Emitter = 8,
|
||||
ShadowCaster = 16,
|
||||
Receiver = 32,
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private readonly record struct FieldInstanceGpu(
|
||||
Vector4 CenterRadius,
|
||||
Vector4 RadiusAngle,
|
||||
Vector4 FieldFlags,
|
||||
Vector4 MaterialMedium,
|
||||
Vector4 ColorIntensity,
|
||||
Vector4 MediumTerms)
|
||||
{
|
||||
public static FieldInstanceGpu Sphere(
|
||||
float fieldId,
|
||||
FieldFlags flags,
|
||||
Vector3 center,
|
||||
float radius,
|
||||
float materialId,
|
||||
float mediumId,
|
||||
Vector3 color,
|
||||
Vector4 medium)
|
||||
{
|
||||
return new FieldInstanceGpu(
|
||||
new Vector4(center, radius),
|
||||
new Vector4(radius, radius, radius, 0.0f),
|
||||
new Vector4(fieldId, (float)flags, 1.0f, 0.0f),
|
||||
new Vector4(materialId, mediumId, 0.0f, 0.0f),
|
||||
new Vector4(color, 1.0f),
|
||||
medium);
|
||||
}
|
||||
|
||||
public static FieldInstanceGpu Ellipsoid(
|
||||
float fieldId,
|
||||
FieldFlags flags,
|
||||
Vector3 center,
|
||||
Vector3 radius,
|
||||
float angle,
|
||||
float mediumId,
|
||||
Vector3 color,
|
||||
Vector4 medium)
|
||||
{
|
||||
return new FieldInstanceGpu(
|
||||
new Vector4(center, MathF.Max(MathF.Max(radius.X, radius.Y), radius.Z)),
|
||||
new Vector4(radius, angle),
|
||||
new Vector4(fieldId, (float)flags, 2.0f, 0.0f),
|
||||
new Vector4(0.0f, mediumId, 0.0f, 0.0f),
|
||||
new Vector4(color, 1.0f),
|
||||
medium);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FrameResources(
|
||||
ID3D12CommandAllocator commandAllocator,
|
||||
D3D12UploadRing uploadRing,
|
||||
|
|
@ -845,6 +1127,10 @@ public sealed class D3D12Renderer : IAquariumRenderer
|
|||
|
||||
public D3D12DescriptorSlot DiagnosticUavDescriptor { get; set; }
|
||||
|
||||
public D3D12DescriptorSlot FroxelPrimitiveDescriptor { get; set; }
|
||||
|
||||
public D3D12DescriptorSlot FieldInstanceDescriptor { get; set; }
|
||||
|
||||
public D3D12TrackedResource BackBuffer { get; set; } = null!;
|
||||
|
||||
public D3D12DescriptorSlot BackBufferRenderTargetView { get; set; }
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ internal sealed class D3D12ResourceRegistry
|
|||
{
|
||||
private readonly Dictionary<string, D3D12TrackedResource> trackedResources = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, D3D12RenderTarget> renderTargets = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, D3D12StructuredBuffer> structuredBuffers = new(StringComparer.Ordinal);
|
||||
|
||||
public void Add(string name, D3D12TrackedResource resource)
|
||||
{
|
||||
|
|
@ -23,6 +24,14 @@ internal sealed class D3D12ResourceRegistry
|
|||
}
|
||||
}
|
||||
|
||||
public void Add(string name, D3D12StructuredBuffer structuredBuffer)
|
||||
{
|
||||
if (!structuredBuffers.TryAdd(name, structuredBuffer))
|
||||
{
|
||||
throw new InvalidOperationException($"D3D12 structured buffer already registered: {name}");
|
||||
}
|
||||
}
|
||||
|
||||
public D3D12TrackedResource GetResource(string name)
|
||||
{
|
||||
return trackedResources.TryGetValue(name, out var resource)
|
||||
|
|
@ -49,6 +58,6 @@ internal sealed class D3D12ResourceRegistry
|
|||
|
||||
public string Describe()
|
||||
{
|
||||
return $"resources={trackedResources.Count}, renderTargets={renderTargets.Count}";
|
||||
return $"resources={trackedResources.Count}, renderTargets={renderTargets.Count}, structuredBuffers={structuredBuffers.Count}";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
91
src/Aquarium.Engine/Render/D3D12StructuredBuffer.cs
Normal file
91
src/Aquarium.Engine/Render/D3D12StructuredBuffer.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using Vortice.Direct3D12;
|
||||
using Vortice.DXGI;
|
||||
|
||||
namespace Aquarium.Engine.Render;
|
||||
|
||||
internal sealed class D3D12StructuredBuffer : IDisposable
|
||||
{
|
||||
private readonly int elementCount;
|
||||
private readonly int strideBytes;
|
||||
|
||||
public D3D12StructuredBuffer(ID3D12Device device, int elementCount, int strideBytes, string name)
|
||||
{
|
||||
this.elementCount = elementCount;
|
||||
this.strideBytes = strideBytes;
|
||||
SizeBytes = elementCount * strideBytes;
|
||||
Resource = device.CreateCommittedResource(
|
||||
HeapType.Default,
|
||||
ResourceDescription.Buffer((ulong)SizeBytes),
|
||||
ResourceStates.Common,
|
||||
null);
|
||||
Resource.Name = name;
|
||||
}
|
||||
|
||||
public ID3D12Resource Resource { get; }
|
||||
|
||||
public ResourceStates State { get; private set; } = ResourceStates.Common;
|
||||
|
||||
public int SizeBytes { get; }
|
||||
|
||||
public string Describe()
|
||||
{
|
||||
return $"{elementCount} elements x {strideBytes} bytes";
|
||||
}
|
||||
|
||||
public void Upload<T>(ID3D12GraphicsCommandList commandList, D3D12UploadRing uploadRing, ReadOnlySpan<T> values)
|
||||
where T : unmanaged
|
||||
{
|
||||
if (values.Length != elementCount)
|
||||
{
|
||||
throw new ArgumentException($"Structured buffer expected {elementCount} elements but received {values.Length}.", nameof(values));
|
||||
}
|
||||
|
||||
var actualStride = Unsafe.SizeOf<T>();
|
||||
if (actualStride != strideBytes)
|
||||
{
|
||||
throw new ArgumentException($"Structured buffer stride mismatch. Expected {strideBytes} bytes but received {actualStride}.", nameof(values));
|
||||
}
|
||||
|
||||
var upload = uploadRing.WriteArray(values);
|
||||
Transition(commandList, ResourceStates.CopyDest);
|
||||
commandList.CopyBufferRegion(Resource, 0, uploadRing.Resource, (ulong)upload.OffsetBytes, (ulong)upload.DataBytes);
|
||||
Transition(commandList, ResourceStates.PixelShaderResource | ResourceStates.NonPixelShaderResource);
|
||||
}
|
||||
|
||||
public void CreateShaderResourceView(ID3D12Device device, D3D12DescriptorSlot descriptor)
|
||||
{
|
||||
device.CreateShaderResourceView(
|
||||
Resource,
|
||||
new ShaderResourceViewDescription
|
||||
{
|
||||
Format = Format.Unknown,
|
||||
ViewDimension = ShaderResourceViewDimension.Buffer,
|
||||
Shader4ComponentMapping = ShaderComponentMapping.Default,
|
||||
Buffer = new BufferShaderResourceView
|
||||
{
|
||||
FirstElement = 0,
|
||||
NumElements = (uint)elementCount,
|
||||
StructureByteStride = (uint)strideBytes,
|
||||
Flags = BufferShaderResourceViewFlags.None,
|
||||
},
|
||||
},
|
||||
descriptor.Cpu);
|
||||
}
|
||||
|
||||
private void Transition(ID3D12GraphicsCommandList commandList, ResourceStates nextState)
|
||||
{
|
||||
if (State == nextState)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
commandList.ResourceBarrier(ResourceBarrier.BarrierTransition(Resource, State, nextState));
|
||||
State = nextState;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Resource.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Vortice.Direct3D12;
|
||||
|
||||
namespace Aquarium.Engine.Render;
|
||||
|
|
@ -30,6 +31,8 @@ internal sealed unsafe class D3D12UploadRing : IDisposable
|
|||
|
||||
public ulong GpuVirtualAddress => resource.GPUVirtualAddress;
|
||||
|
||||
public ID3D12Resource Resource => resource;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
cursor = 0;
|
||||
|
|
@ -50,6 +53,27 @@ internal sealed unsafe class D3D12UploadRing : IDisposable
|
|||
return new D3D12UploadAllocation(GpuVirtualAddress + (ulong)offset, (uint)size);
|
||||
}
|
||||
|
||||
public D3D12UploadAllocation WriteArray<T>(ReadOnlySpan<T> values)
|
||||
where T : unmanaged
|
||||
{
|
||||
var byteCount = Unsafe.SizeOf<T>() * values.Length;
|
||||
var size = Align256(byteCount);
|
||||
if (cursor + size > CapacityBytes)
|
||||
{
|
||||
throw new InvalidOperationException($"D3D12 upload ring '{Name}' exhausted ({cursor}/{CapacityBytes} bytes used, requested {size} bytes).");
|
||||
}
|
||||
|
||||
var offset = cursor;
|
||||
if (byteCount > 0)
|
||||
{
|
||||
var source = MemoryMarshal.AsBytes(values);
|
||||
source.CopyTo(new Span<byte>(mapped + offset, byteCount));
|
||||
}
|
||||
|
||||
cursor += size;
|
||||
return new D3D12UploadAllocation(GpuVirtualAddress + (ulong)offset, (uint)size, offset, byteCount);
|
||||
}
|
||||
|
||||
public string Describe()
|
||||
{
|
||||
return $"{Name}: {cursor}/{CapacityBytes} bytes";
|
||||
|
|
@ -67,4 +91,4 @@ internal sealed unsafe class D3D12UploadRing : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
internal readonly record struct D3D12UploadAllocation(ulong GpuVirtualAddress, uint SizeInBytes);
|
||||
internal readonly record struct D3D12UploadAllocation(ulong GpuVirtualAddress, uint SizeInBytes, int OffsetBytes = 0, int DataBytes = 0);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,19 @@ Texture2D<float4> sourceTexture : register(t0);
|
|||
RWTexture2D<float4> diagnosticUav : register(u1);
|
||||
SamplerState sourceSampler : register(s0);
|
||||
|
||||
struct FieldInstance
|
||||
{
|
||||
float4 centerRadius;
|
||||
float4 radiusAngle;
|
||||
float4 fieldFlags;
|
||||
float4 materialMedium;
|
||||
float4 colorIntensity;
|
||||
float4 mediumTerms;
|
||||
};
|
||||
|
||||
StructuredBuffer<int4> froxelPrimitiveIds : register(t1);
|
||||
StructuredBuffer<FieldInstance> fieldInstances : register(t12);
|
||||
|
||||
VertexOut FullscreenTriangleVS(uint vertexId : SV_VertexID)
|
||||
{
|
||||
float2 uv = float2((vertexId << 1) & 2, vertexId & 2);
|
||||
|
|
@ -28,7 +41,9 @@ float4 D3D12SmokePS(VertexOut input) : SV_Target0
|
|||
float3 baseColor = lerp(float3(0.012, 0.022, 0.032), smokeTint.rgb, input.uv.y);
|
||||
float vignette = smoothstep(0.86, 0.18, length(input.uv - 0.5));
|
||||
float pulse = 0.92 + 0.08 * sin(smokeParams.x);
|
||||
diagnosticUav[uint2(input.position.xy)] = float4(input.uv, smokeParams.y / 16.0, 1.0);
|
||||
float fieldProbe = saturate(fieldInstances[0].centerRadius.w);
|
||||
float froxelProbe = max(froxelPrimitiveIds[0].x, 0) / 16.0;
|
||||
diagnosticUav[uint2(input.position.xy)] = float4(input.uv, saturate(fieldProbe + froxelProbe), 1.0);
|
||||
return float4(baseColor * (0.72 + 0.28 * vignette) * pulse, 1.0);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,3 +73,4 @@
|
|||
{"area":"renderer/d3d12","lesson":"The first real D3D12 pass migration exposed the Grid height target and frame-constant path, but its original fullscreen per-texel body loop was intentionally superseded by a brush-rendered pass. Keep temporal previous camera/Grid/time as renderer-level state; swapchain frame resources are allocator reuse buckets, not frame-history state.","files":["src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12Smoke.hlsl","docs/d3d12-migration.md","docs/d3d12-best-practices-audit.md","docs/hlsl-renderer.md","state/map.yaml","state/memory.json","state/scratch.md"],"verification":["dotnet build Aquarium.Engine.slnx",".\\scripts\\dev-reload.ps1 -Headless -RetainSlots 4 -HeadlessTimeoutSeconds 20 with AQUARIUM_RENDERER=d3d12 and AQUARIUM_RENDER_DEBUG_MODE=11 exited 0",".\\scripts\\dev-reload.ps1 -Headless -RetainSlots 4 -HeadlessTimeoutSeconds 20 with AQUARIUM_RENDERER=d3d12 and AQUARIUM_RENDER_DEBUG_MODE=0 exited 0"],"ts":"2026-05-09"}
|
||||
{"area":"renderer/d3d12","lesson":"The D3D12 Grid height pass must be a real gravity brush pass, not a fullscreen shader loop over every body per texel. It renders a base field, uploads a small body brush constant table, and draws one additive up-facing quad per body into a 128x128 scalar R16_Float target. R32G32B32A32_Float is the wrong target format for additive blending here, and extra channels are waste unless the pass contract actually stores extra data. The root signature must expose frame constants to the vertex shader because the brush vertex shader performs the top-down Grid-space orthographic transform.","files":["src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12Grid.hlsl","docs/d3d12-migration.md","docs/d3d12-best-practices-audit.md","docs/hlsl-renderer.md","state/memory.json","state/scratch.md"],"verification":["dotnet build src\\Aquarium.Engine\\Aquarium.Engine.csproj",".\\scripts\\dev-reload.ps1 -Headless -RetainSlots 4 -HeadlessTimeoutSeconds 20 with AQUARIUM_RENDERER=d3d12 and AQUARIUM_RENDER_DEBUG_MODE=11 exited 0","dotnet build Aquarium.Engine.slnx",".\\scripts\\dev-reload.ps1 -Headless -RetainSlots 4 -HeadlessTimeoutSeconds 20 with AQUARIUM_RENDERER=d3d12 and AQUARIUM_RENDER_DEBUG_MODE=0 exited 0"],"ts":"2026-05-09"}
|
||||
{"area":"renderer/d3d12","lesson":"A Grid height target is scalar. The D3D12 brush-rendered Grid height pass should use R16_Float and scalar pixel-shader outputs; four float channels were defensive waste after the R32G32B32A32_Float blend failure, not a real contract. Add channels only when the pass explicitly stores more than height.","files":["src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12Grid.hlsl","docs/d3d12-migration.md","docs/d3d12-best-practices-audit.md","docs/hlsl-renderer.md","state/memory.json","state/scratch.md"],"verification":["dotnet build src\\Aquarium.Engine\\Aquarium.Engine.csproj",".\\scripts\\dev-reload.ps1 -Headless -RetainSlots 4 -HeadlessTimeoutSeconds 20 with AQUARIUM_RENDERER=d3d12 and AQUARIUM_RENDER_DEBUG_MODE=11 exited 0","dotnet build Aquarium.Engine.slnx",".\\scripts\\dev-reload.ps1 -Headless -RetainSlots 4 -HeadlessTimeoutSeconds 20 with AQUARIUM_RENDERER=d3d12 and AQUARIUM_RENDER_DEBUG_MODE=0 exited 0"],"ts":"2026-05-09"}
|
||||
{"area":"renderer/d3d12","lesson":"D3D12 now has the first real Aquarium field data upload path: froxel primitive ids and field instances are built on CPU, uploaded through the per-frame upload ring, copied into default-heap structured buffers, and bound through transient SRV descriptors for a shader that actually reads them. Static and transient CBV/SRV/UAV heaps cannot be co-bound; pass-visible descriptor tables need to live in the active transient heap or be copied there. The table builders are temporarily duplicated from D3D11 and should be extracted into one shared field ABI owner before more consumers depend on them.","files":["src/Aquarium.Engine/Render/D3D12UploadRing.cs","src/Aquarium.Engine/Render/D3D12StructuredBuffer.cs","src/Aquarium.Engine/Render/D3D12ResourceRegistry.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12Smoke.hlsl","docs/d3d12-migration.md","docs/d3d12-best-practices-audit.md","docs/hlsl-renderer.md","state/memory.json","state/scratch.md"],"verification":["dotnet build src\\Aquarium.Engine\\Aquarium.Engine.csproj",".\\scripts\\dev-reload.ps1 -Headless -RetainSlots 4 -HeadlessTimeoutSeconds 20 with AQUARIUM_RENDERER=d3d12 and AQUARIUM_RENDER_DEBUG_MODE=0 exited 0",".\\scripts\\dev-reload.ps1 -Headless -RetainSlots 4 -HeadlessTimeoutSeconds 20 with AQUARIUM_RENDERER=d3d12 and AQUARIUM_RENDER_DEBUG_MODE=11 exited 0","dotnet build Aquarium.Engine.slnx"],"ts":"2026-05-09"}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"D3D12 is being added beside D3D11, not by mutating the reference renderer in place. Keep descriptor ownership explicit: static descriptors for long-lived SRV/UAV tables, per-frame transient descriptors reset only after fence wait, and whole-arena rebuilds on resize until real non-resize churn demands free lists. Resource capability flags are not descriptor ownership; do not burn static UAV descriptors just to allow a transient UAV view.",
|
||||
"D3D12 previous-frame camera/Grid/time state must be renderer-level temporal state, not per-swapchain-frame-resource state. The swapchain image index is an allocator/reuse index, not frame history.",
|
||||
"D3D12 Grid height must be brush-rendered, not a fullscreen per-texel loop over bodies. Use a top-down Grid-space orthographic brush pass: base field draw, then one additive up-facing quad per body into a scalar R16_Float height target. Height is one value; do not spend extra channels unless the pass contract actually gains more data. Do not use R32G32B32A32_Float for additive brush composition.",
|
||||
"D3D12 field data should live in default-heap structured buffers, updated from the per-frame upload ring by CopyBufferRegion, then bound through transient SRV descriptors owned by the consuming pass. Do not make shaders read dynamic field tables straight from upload memory. Collapse D3D11 and D3D12 field table builders into one shared ABI owner before more consumers depend on them.",
|
||||
"D3D12 pixel shader UAV registers share the output namespace. If a shader writes SV_Target0, bind the first manual UAV at u1, not u0.",
|
||||
"D3D12 shader routing must not inherit the D3D11 --shader-source override by accident. Backend-specific HLSL should compile from its copied output path unless an explicit backend-specific override exists.",
|
||||
"Use Vortice at the graphics API membrane. Keep CultMath as the CPU math grammar. Do not let third-party math opinions leak inward unless the conversion is explicit.",
|
||||
|
|
|
|||
|
|
@ -279,3 +279,10 @@ can start porting real passes while keeping D3D11 as the visual reference.
|
|||
previous camera/Grid/time state is renderer-level
|
||||
state, not swapchain-frame-resource state; swapchain image index is not frame
|
||||
history. Next D3D12 migration step is the froxel/field resource upload path.
|
||||
- D3D12 froxel/field upload pass: D3D12 now builds the froxel primitive id table
|
||||
and field instance table, uploads them through the per-frame upload ring,
|
||||
copies them into default-heap structured buffers, and binds transient SRVs to
|
||||
the diagnostic shader. This proves the real field data path without forcing
|
||||
shaders to read directly from upload heap memory. The table-building code is
|
||||
duplicated from D3D11 for this slice; extract it into one shared field ABI
|
||||
owner before porting the next field consumer.
|
||||
|
|
|
|||
Loading…
Reference in a new issue