From 024b43900607c755050dcb3ef082eee8a3f559f2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 26 May 2026 21:08:34 +0100 Subject: [PATCH] Resolve field volume texture resources --- .../Render/AquariumFieldEvidence.cs | 26 +++++ .../Render/AquariumFieldScriptCompiler.cs | 27 +++++ .../Render/D3D12FieldResourceRegistry.cs | 104 +++++++++++++++++- .../Render/D3D12FieldTexture2D.cs | 15 +-- .../Render/D3D12FieldTexture3D.cs | 101 +++++++++++++++++ .../Render/D3D12FieldTextureFormat.cs | 20 ++++ src/Aquarium.Engine/Render/D3D12Renderer.cs | 1 + state/evidence.jsonl | 1 + state/map.yaml | 9 +- .../FieldScriptCompilerTests.cs | 21 ++++ 10 files changed, 307 insertions(+), 18 deletions(-) create mode 100644 src/Aquarium.Engine/Render/D3D12FieldTexture3D.cs create mode 100644 src/Aquarium.Engine/Render/D3D12FieldTextureFormat.cs diff --git a/src/Aquarium.Engine.Contracts/Render/AquariumFieldEvidence.cs b/src/Aquarium.Engine.Contracts/Render/AquariumFieldEvidence.cs index 21cc893..0363e83 100644 --- a/src/Aquarium.Engine.Contracts/Render/AquariumFieldEvidence.cs +++ b/src/Aquarium.Engine.Contracts/Render/AquariumFieldEvidence.cs @@ -388,6 +388,32 @@ public readonly record struct AquariumFieldResourceDeclaration( NativeHandleKind: string.IsNullOrWhiteSpace(sourceUri) ? "fensalir-surface-page" : "local-asset", SourceUri: sourceUri); + public static AquariumFieldResourceDeclaration VolumeTexture( + string resourceKey, + int width, + int height, + int depth, + string format = "R16Float", + ulong version = 0, + long validFromNs = 0, + long validUntilNs = 0) => + new( + ResourceKey: resourceKey, + Kind: AquariumFieldResourceKind.VolumeTexture, + Residency: AquariumFieldResourceResidency.GpuResident, + Access: AquariumFieldShaderAccess.ShaderResource, + Format: format, + Width: Math.Max(1, width), + Height: Math.Max(1, height), + DepthOrCount: Math.Max(1, depth), + StrideBytes: FormatStrideBytes(format), + ValidFromNs: validFromNs, + ValidUntilNs: validUntilNs, + Version: version, + NativeHandle: IntPtr.Zero, + NativeHandleKind: "fensalir-volume-texture", + SourceUri: ""); + private static int FormatStrideBytes(string format) => format switch { diff --git a/src/Aquarium.Engine.Contracts/Render/AquariumFieldScriptCompiler.cs b/src/Aquarium.Engine.Contracts/Render/AquariumFieldScriptCompiler.cs index 0112241..986a6eb 100644 --- a/src/Aquarium.Engine.Contracts/Render/AquariumFieldScriptCompiler.cs +++ b/src/Aquarium.Engine.Contracts/Render/AquariumFieldScriptCompiler.cs @@ -42,6 +42,9 @@ public static class AquariumFieldScriptCompiler case "surfacepage": BindSurfacePage(args, resourceAliases, resources, lineIndex); break; + case "volumetexture": + BindVolumeTexture(args, resourceAliases, resources, lineIndex); + break; case "domain": var domain = ParseFieldDomain(args, lineIndex); if (domainKeys.Add(domain.DomainKey)) @@ -128,6 +131,30 @@ public static class AquariumFieldScriptCompiler } } + private static void BindVolumeTexture( + IReadOnlyDictionary args, + IDictionary resourceAliases, + ICollection resources, + int lineIndex) + { + var id = Required(args, "id", lineIndex); + var key = StringValue(args, "key", $"aquarium:resource:volume-texture:{id}"); + var resource = AquariumFieldResourceDeclaration.VolumeTexture( + key, + Int(args, "width", 1, lineIndex), + Int(args, "height", 1, lineIndex), + Int(args, "depth", 1, lineIndex), + StringValue(args, "format", "R16Float"), + UInt64(args, "version", 0, lineIndex)); + + resourceAliases[id] = resource; + resourceAliases[key] = resource; + if (resources.All(existing => !string.Equals(existing.ResourceKey, resource.ResourceKey, StringComparison.Ordinal))) + { + resources.Add(resource); + } + } + public static AquariumBufferFieldFrame Compile( string source, IReadOnlyDictionary textureBindings, diff --git a/src/Aquarium.Engine/Render/D3D12FieldResourceRegistry.cs b/src/Aquarium.Engine/Render/D3D12FieldResourceRegistry.cs index dcc9809..629379b 100644 --- a/src/Aquarium.Engine/Render/D3D12FieldResourceRegistry.cs +++ b/src/Aquarium.Engine/Render/D3D12FieldResourceRegistry.cs @@ -9,10 +9,11 @@ internal readonly record struct D3D12FieldResourceStats( int StructuredBuffers, int Texture2D, int SurfacePages, + int VolumeTextures, int Unsupported, int StaleRemoved) { - public static D3D12FieldResourceStats Empty { get; } = new(0, 0, 0, 0, 0, 0, 0); + public static D3D12FieldResourceStats Empty { get; } = new(0, 0, 0, 0, 0, 0, 0, 0); } internal sealed class D3D12FieldResourceRegistry : IDisposable @@ -21,6 +22,7 @@ internal sealed class D3D12FieldResourceRegistry : IDisposable private readonly Dictionary structuredBuffers = new(StringComparer.Ordinal); private readonly Dictionary textures = new(StringComparer.Ordinal); + private readonly Dictionary volumeTextures = new(StringComparer.Ordinal); private readonly HashSet liveKeys = new(StringComparer.Ordinal); public D3D12FieldResourceStats Resolve( @@ -38,6 +40,7 @@ internal sealed class D3D12FieldResourceRegistry : IDisposable var structuredBufferCount = 0; var textureCount = 0; var surfacePageCount = 0; + var volumeTextureCount = 0; var unsupported = 0; foreach (var declaration in declarations) { @@ -93,17 +96,34 @@ internal sealed class D3D12FieldResourceRegistry : IDisposable continue; } + if (declaration.Kind == AquariumFieldResourceKind.VolumeTexture) + { + volumeTextureCount++; + if (ResolveVolumeTexture(device, declaration)) + { + resolved++; + } + else + { + unsupported++; + } + + continue; + } + unsupported++; } var staleRemoved = RemoveStaleStructuredBuffers(resourceRegistry, liveKeys); staleRemoved += RemoveStaleTextures(liveKeys); + staleRemoved += RemoveStaleVolumeTextures(liveKeys); return new D3D12FieldResourceStats( Declared: declarations.Count, Resolved: resolved, StructuredBuffers: structuredBufferCount, Texture2D: textureCount, SurfacePages: surfacePageCount, + VolumeTextures: volumeTextureCount, Unsupported: unsupported, StaleRemoved: staleRemoved); } @@ -120,8 +140,14 @@ internal sealed class D3D12FieldResourceRegistry : IDisposable slot.Texture.Dispose(); } + foreach (var slot in volumeTextures.Values) + { + slot.Texture.Dispose(); + } + structuredBuffers.Clear(); textures.Clear(); + volumeTextures.Clear(); liveKeys.Clear(); } @@ -162,6 +188,18 @@ internal sealed class D3D12FieldResourceRegistry : IDisposable return false; } + public bool TryGetVolumeTexture(string resourceKey, out D3D12FieldTexture3D volumeTexture) + { + if (volumeTextures.TryGetValue(resourceKey, out var slot)) + { + volumeTexture = slot.Texture; + return true; + } + + volumeTexture = null!; + return false; + } + public bool TryResolveTexture2D( ID3D12Device device, ID3D12GraphicsCommandList commandList, @@ -386,6 +424,45 @@ internal sealed class D3D12FieldResourceRegistry : IDisposable return true; } + private bool ResolveVolumeTexture( + ID3D12Device device, + AquariumFieldResourceDeclaration declaration) + { + if (declaration.Residency != AquariumFieldResourceResidency.GpuResident || + declaration.Access != AquariumFieldShaderAccess.ShaderResource) + { + return false; + } + + if (volumeTextures.TryGetValue(declaration.ResourceKey, out var existing) && + existing.Version == declaration.Version && + existing.Width == declaration.Width && + existing.Height == declaration.Height && + existing.Depth == declaration.DepthOrCount) + { + return true; + } + + if (existing is not null) + { + existing.Texture.Dispose(); + volumeTextures.Remove(declaration.ResourceKey); + } + + if (!D3D12FieldTexture3D.TryCreateEmpty(device, declaration, out var volumeTexture)) + { + return false; + } + + volumeTextures[declaration.ResourceKey] = new VolumeTextureSlot( + volumeTexture, + declaration.Width, + declaration.Height, + declaration.DepthOrCount, + declaration.Version); + return true; + } + private int RemoveStaleStructuredBuffers(D3D12ResourceRegistry resourceRegistry, HashSet currentKeys) { var removed = 0; @@ -423,6 +500,24 @@ internal sealed class D3D12FieldResourceRegistry : IDisposable return removed; } + private int RemoveStaleVolumeTextures(HashSet currentKeys) + { + var removed = 0; + foreach (var key in volumeTextures.Keys.ToArray()) + { + if (currentKeys.Contains(key)) + { + continue; + } + + volumeTextures[key].Texture.Dispose(); + volumeTextures.Remove(key); + removed++; + } + + return removed; + } + private static string RegistryKey(string resourceKey) => RegistryPrefix + resourceKey; private static D3D12StructuredBuffer? OpenSharedStructuredBuffer( @@ -468,4 +563,11 @@ internal sealed class D3D12FieldResourceRegistry : IDisposable AquariumFieldResourceKind Kind, string SourceUri, ulong Version); + + private sealed record VolumeTextureSlot( + D3D12FieldTexture3D Texture, + int Width, + int Height, + int Depth, + ulong Version); } diff --git a/src/Aquarium.Engine/Render/D3D12FieldTexture2D.cs b/src/Aquarium.Engine/Render/D3D12FieldTexture2D.cs index 486616d..a6cbbaa 100644 --- a/src/Aquarium.Engine/Render/D3D12FieldTexture2D.cs +++ b/src/Aquarium.Engine/Render/D3D12FieldTexture2D.cs @@ -88,7 +88,7 @@ internal sealed unsafe class D3D12FieldTexture2D : IDisposable out D3D12FieldTexture2D texture) { texture = null!; - if (!TryFormat(declaration.Format, out var format)) + if (!D3D12FieldTextureFormat.TryFormat(declaration.Format, out var format)) { return false; } @@ -258,17 +258,4 @@ internal sealed unsafe class D3D12FieldTexture2D : IDisposable return (value + alignment - 1) & ~(alignment - 1); } - private static bool TryFormat(string formatName, out Format format) - { - format = formatName switch - { - "R8Unorm" or "R8_UNorm" or "R8_UNORM" => Format.R8_UNorm, - "R16Float" or "R16_Float" or "R16_FLOAT" => Format.R16_Float, - "R32Float" or "R32_Float" or "R32_FLOAT" or "Float32" => Format.R32_Float, - "Rgba8Unorm" or "R8G8B8A8_UNorm" or "R8G8B8A8_UNORM" => Format.R8G8B8A8_UNorm, - "Rgba16Float" or "R16G16B16A16_Float" or "R16G16B16A16_FLOAT" => Format.R16G16B16A16_Float, - _ => Format.Unknown, - }; - return format != Format.Unknown; - } } diff --git a/src/Aquarium.Engine/Render/D3D12FieldTexture3D.cs b/src/Aquarium.Engine/Render/D3D12FieldTexture3D.cs new file mode 100644 index 0000000..0242fd2 --- /dev/null +++ b/src/Aquarium.Engine/Render/D3D12FieldTexture3D.cs @@ -0,0 +1,101 @@ +using Vortice.Direct3D12; +using Vortice.DXGI; + +namespace Aquarium.Engine.Render; + +internal sealed class D3D12FieldTexture3D : IDisposable +{ + private D3D12FieldTexture3D( + ID3D12Resource resource, + int width, + int height, + int depth, + Format format, + string name) + { + Resource = resource; + Width = width; + Height = height; + Depth = depth; + Format = format; + Resource.Name = name; + } + + public ID3D12Resource Resource { get; } + + public int Width { get; } + + public int Height { get; } + + public int Depth { get; } + + public Format Format { get; } + + public ResourceStates State { get; private set; } = ResourceStates.PixelShaderResource; + + public static bool TryCreateEmpty( + ID3D12Device device, + AquariumFieldResourceDeclaration declaration, + out D3D12FieldTexture3D texture) + { + texture = null!; + if (!D3D12FieldTextureFormat.TryFormat(declaration.Format, out var format)) + { + return false; + } + + var width = Math.Max(1, declaration.Width); + var height = Math.Max(1, declaration.Height); + var depth = Math.Max(1, declaration.DepthOrCount); + var resource = device.CreateCommittedResource( + HeapType.Default, + ResourceDescription.Texture3D( + format, + (uint)width, + (uint)height, + (ushort)Math.Min(depth, ushort.MaxValue), + 1, + ResourceFlags.None), + ResourceStates.PixelShaderResource, + null); + + texture = new D3D12FieldTexture3D( + resource, + width, + height, + depth, + format, + $"Aquarium D3D12 Field VolumeTexture {declaration.ResourceKey}"); + return true; + } + + public void CreateShaderResourceView(ID3D12Device device, D3D12DescriptorSlot descriptor) + { + device.CreateShaderResourceView( + Resource, + new ShaderResourceViewDescription + { + Format = Format, + ViewDimension = ShaderResourceViewDimension.Texture3D, + Shader4ComponentMapping = ShaderComponentMapping.Default, + Texture3D = new Texture3DShaderResourceView { MipLevels = 1 }, + }, + descriptor.Cpu); + } + + public void Transition(ID3D12GraphicsCommandList commandList, ResourceStates nextState) + { + if (State == nextState) + { + return; + } + + commandList.ResourceBarrier(ResourceBarrier.BarrierTransition(Resource, State, nextState)); + State = nextState; + } + + public void Dispose() + { + Resource.Dispose(); + } +} diff --git a/src/Aquarium.Engine/Render/D3D12FieldTextureFormat.cs b/src/Aquarium.Engine/Render/D3D12FieldTextureFormat.cs new file mode 100644 index 0000000..511bb49 --- /dev/null +++ b/src/Aquarium.Engine/Render/D3D12FieldTextureFormat.cs @@ -0,0 +1,20 @@ +using Vortice.DXGI; + +namespace Aquarium.Engine.Render; + +internal static class D3D12FieldTextureFormat +{ + public static bool TryFormat(string formatName, out Format format) + { + format = formatName switch + { + "R8Unorm" or "R8_UNorm" or "R8_UNORM" => Format.R8_UNorm, + "R16Float" or "R16_Float" or "R16_FLOAT" => Format.R16_Float, + "R32Float" or "R32_Float" or "R32_FLOAT" or "Float32" => Format.R32_Float, + "Rgba8Unorm" or "R8G8B8A8_UNorm" or "R8G8B8A8_UNORM" => Format.R8G8B8A8_UNorm, + "Rgba16Float" or "R16G16B16A16_Float" or "R16G16B16A16_FLOAT" => Format.R16G16B16A16_Float, + _ => Format.Unknown, + }; + return format != Format.Unknown; + } +} diff --git a/src/Aquarium.Engine/Render/D3D12Renderer.cs b/src/Aquarium.Engine/Render/D3D12Renderer.cs index 3db5236..b219cf6 100644 --- a/src/Aquarium.Engine/Render/D3D12Renderer.cs +++ b/src/Aquarium.Engine/Render/D3D12Renderer.cs @@ -3031,6 +3031,7 @@ public sealed class D3D12Renderer : IAquariumRenderer $"structured buffers {activeFieldResourceStats.StructuredBuffers:N0}; " + $"texture2d {activeFieldResourceStats.Texture2D:N0}; " + $"surface pages {activeFieldResourceStats.SurfacePages:N0}; " + + $"volume textures {activeFieldResourceStats.VolumeTextures:N0}; " + $"unsupported resources {activeFieldResourceStats.Unsupported:N0}; " + $"claims {activeFieldEvidenceFrame.Claims.Count:N0}; " + $"candidates {activeFieldEvidenceFrame.Candidates.Count:N0}; " + diff --git a/state/evidence.jsonl b/state/evidence.jsonl index c6d63bc..d1beb6d 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -240,3 +240,4 @@ {"lesson":"Texture2D local assets are now first-class field resources. AquariumFieldResourceDeclaration carries SourceUri and has a LocalTexture2D factory; the field DSL accepts `texture2d id=... key=... path=...` and TubeField `ramp=...` resolves aliases into RampResourceKey. D3D12FieldResourceRegistry lazily uploads local Rgba8 assets into GPU-resident textures, TubeField rendering binds the ramp SRV plus linear clamp sampler, and unresolved ramps use a Fensalir-owned 0-1 fallback texture. This replaces raw ramp paths as live authority while keeping them as compatibility metadata. Fensalir build/tests/headless reload and Mimir build/field smokes passed.","timestamp":"2026-05-26T20:40:00+01:00","files":["src/Aquarium.Engine.Contracts/Render/AquariumFieldEvidence.cs","src/Aquarium.Engine.Contracts/Render/AquariumFieldScriptCompiler.cs","src/Aquarium.Engine/Render/D3D12FieldTexture2D.cs","src/Aquarium.Engine/Render/D3D12FieldResourceRegistry.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12TubeField.hlsl","tests/Aquarium.Engine.Tests/FieldScriptCompilerTests.cs","state/map.yaml"],"source":"user requested Texture2D local asset resource declaration and automatic DSL binding","topic":"field-texture2d-resource-binding"} {"lesson":"TubeField draw dimensions are now emitted as GPU-resident indirect draw packets. The expansion compute shader writes one D3D12 DrawIndexedArguments packet per resolved TubeField batch into a UAV argument buffer; render binds the matching source/ramp material state and calls ExecuteIndirect instead of CPU-manufacturing DrawIndexedInstanced counts. CPU still schedules one indirect call per material/resource batch because each batch has distinct root bindings, but draw count/start index authority now lives in the GPU expansion output. Fensalir build/tests/headless reload and Mimir build/field smokes passed.","timestamp":"2026-05-26T20:59:00+01:00","files":["src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12TubeField.hlsl","state/map.yaml"],"source":"continued migration plan after Texture2D resolver cut","topic":"d3d12-tubefield-indirect-draw-packets"} {"lesson":"SurfacePage is now a resolvable field resource kind rather than just a planner enum. AquariumFieldResourceDeclaration has a SurfacePage factory, the field evidence DSL accepts `surfacepage id=... key=... width=... height=... format=...`, and D3D12FieldResourceRegistry allocates empty GPU-resident shader-readable page textures or lazily loads local source-backed pages through the same texture upload path. This does not finish terrain/heightfield lowering; it creates the resource authority that future Height/Sdf2D/Material packets can bind. Fensalir build/tests/headless reload and Mimir build/field smokes passed.","timestamp":"2026-05-26T21:05:00+01:00","files":["src/Aquarium.Engine.Contracts/Render/AquariumFieldEvidence.cs","src/Aquarium.Engine.Contracts/Render/AquariumFieldScriptCompiler.cs","src/Aquarium.Engine/Render/D3D12FieldTexture2D.cs","src/Aquarium.Engine/Render/D3D12FieldResourceRegistry.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","tests/Aquarium.Engine.Tests/FieldScriptCompilerTests.cs","state/map.yaml"],"source":"continued field resource resolver migration","topic":"field-surface-page-resource-resolver"} +{"lesson":"VolumeTexture is now a resolvable field resource kind. AquariumFieldResourceDeclaration has a VolumeTexture factory, the field evidence DSL accepts `volumetexture id=... key=... width=... height=... depth=... format=...`, and D3D12FieldResourceRegistry allocates empty GPU-resident shader-readable Texture3D resources with format/dimension/version tracking. This creates resource authority for future density/extinction/SDF3D lowerings without pretending those render paths exist yet. Fensalir build/tests/headless reload and Mimir build/field smokes passed.","timestamp":"2026-05-26T21:11:00+01:00","files":["src/Aquarium.Engine.Contracts/Render/AquariumFieldEvidence.cs","src/Aquarium.Engine.Contracts/Render/AquariumFieldScriptCompiler.cs","src/Aquarium.Engine/Render/D3D12FieldTexture3D.cs","src/Aquarium.Engine/Render/D3D12FieldTextureFormat.cs","src/Aquarium.Engine/Render/D3D12FieldResourceRegistry.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","tests/Aquarium.Engine.Tests/FieldScriptCompilerTests.cs","state/map.yaml"],"source":"continued field resource resolver migration","topic":"field-volume-texture-resource-resolver"} diff --git a/state/map.yaml b/state/map.yaml index 7dac306..993558f 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -100,9 +100,12 @@ next_actions: reporting. SurfacePage resources now have a first resolver: contracts and DSL can declare GPU-resident 2D pages with width, height, format, version, and optional local source, and the D3D12 registry allocates/imports them as - shader-readable page textures. Remaining cuts are mesh and volume resolvers, - plus render lowerings that consume surface pages as height/SDF/material - domains. + shader-readable page textures. VolumeTexture resources now have the same + first resolver shape: contracts and DSL can declare GPU-resident 3D + textures with dimensions, depth, format, and version, and the D3D12 registry + allocates them as shader-readable volume textures. Remaining resolver cut + is Mesh, plus render lowerings that consume surface pages and volumes as + height/SDF/material/density domains. - Move any remaining client-only Epiphany state found here back to `E:\Projects\Aquarium-Engine`. - Keep Mimir app/adapters out of this repo; Fensalir may expose generic sensor-fusion contracts only. - Package contracts/host deliberately before replacing sibling source references in client repos. diff --git a/tests/Aquarium.Engine.Tests/FieldScriptCompilerTests.cs b/tests/Aquarium.Engine.Tests/FieldScriptCompilerTests.cs index 13cbd09..e6a44d4 100644 --- a/tests/Aquarium.Engine.Tests/FieldScriptCompilerTests.cs +++ b/tests/Aquarium.Engine.Tests/FieldScriptCompilerTests.cs @@ -147,4 +147,25 @@ public sealed class FieldScriptCompilerTests Assert.Equal("R16Float", resource.Format); Assert.Equal("fensalir-surface-page", resource.NativeHandleKind); } + + [Fact] + public void CompileEvidenceDeclaresVolumeTexturesAsGpuResources() + { + var frame = AquariumFieldScriptCompiler.CompileEvidence( + """ + volumetexture id=density key=aquarium:resource:volume:density width=32 height=32 depth=16 format=R16Float version=5 + """, + new Dictionary(StringComparer.Ordinal)); + + var resource = Assert.Single(frame.Resources); + Assert.Equal("aquarium:resource:volume:density", resource.ResourceKey); + Assert.Equal(AquariumFieldResourceKind.VolumeTexture, resource.Kind); + Assert.Equal(AquariumFieldResourceResidency.GpuResident, resource.Residency); + Assert.Equal(AquariumFieldShaderAccess.ShaderResource, resource.Access); + Assert.Equal(32, resource.Width); + Assert.Equal(32, resource.Height); + Assert.Equal(16, resource.DepthOrCount); + Assert.Equal("R16Float", resource.Format); + Assert.Equal("fensalir-volume-texture", resource.NativeHandleKind); + } }