diff --git "a/.qoder/repowiki/zh/content/API\345\217\202\350\200\203/\350\265\204\346\272\220\347\256\241\347\220\206API/\350\265\204\346\272\220\347\256\241\347\220\206API.md" "b/.qoder/repowiki/zh/content/API\345\217\202\350\200\203/\350\265\204\346\272\220\347\256\241\347\220\206API/\350\265\204\346\272\220\347\256\241\347\220\206API.md" index 399e37f..de553a8 100644 --- "a/.qoder/repowiki/zh/content/API\345\217\202\350\200\203/\350\265\204\346\272\220\347\256\241\347\220\206API/\350\265\204\346\272\220\347\256\241\347\220\206API.md" +++ "b/.qoder/repowiki/zh/content/API\345\217\202\350\200\203/\350\265\204\346\272\220\347\256\241\347\220\206API/\350\265\204\346\272\220\347\256\241\347\220\206API.md" @@ -272,7 +272,7 @@ class RHIWorkGraphPipeline { - 静态采样器 - RHIStaticSamplerDescriptor:按索引绑定的采样器集合,便于在管线布局中固定。 - 后端实现要点(DX12) - - 转换为原生采样器描述,分配 CPU/GPU 描述符对,复制到着色器可见区域。 + - 转换为原生采样器描述,按完整 descriptor intern 共享 CPU 槽;BindingTable sampler 段才占用 GPU heap。 ```mermaid classDiagram @@ -280,7 +280,6 @@ class RHISampler { } class Dx12Sampler { +NativeCpuDescriptorHandle -+NativeGpuDescriptorHandle } RHISampler <|-- Dx12Sampler ``` diff --git "a/.qoder/repowiki/zh/content/API\345\217\202\350\200\203/\350\265\204\346\272\220\347\256\241\347\220\206API/\351\207\207\346\240\267\345\231\250API.md" "b/.qoder/repowiki/zh/content/API\345\217\202\350\200\203/\350\265\204\346\272\220\347\256\241\347\220\206API/\351\207\207\346\240\267\345\231\250API.md" index 004fc6e..38a6aec 100644 --- "a/.qoder/repowiki/zh/content/API\345\217\202\350\200\203/\350\265\204\346\272\220\347\256\241\347\220\206API/\351\207\207\346\240\267\345\231\250API.md" +++ "b/.qoder/repowiki/zh/content/API\345\217\202\350\200\203/\350\265\204\346\272\220\347\256\241\347\220\206API/\351\207\207\346\240\267\345\231\250API.md" @@ -246,7 +246,6 @@ class RHISampler { } class Dx12Sampler { +NativeCpuDescriptorHandle -+NativeGpuDescriptorHandle +Release() } class MetalSampler { diff --git "a/.qoder/repowiki/zh/content/\350\265\204\346\272\220\347\256\241\347\220\206/\350\247\206\345\233\276\347\256\241\347\220\206.md" "b/.qoder/repowiki/zh/content/\350\265\204\346\272\220\347\256\241\347\220\206/\350\247\206\345\233\276\347\256\241\347\220\206.md" index 657d901..dec37e5 100644 --- "a/.qoder/repowiki/zh/content/\350\265\204\346\272\220\347\256\241\347\220\206/\350\247\206\345\233\276\347\256\241\347\220\206.md" +++ "b/.qoder/repowiki/zh/content/\350\265\204\346\272\220\347\256\241\347\220\206/\350\247\206\345\233\276\347\256\241\347\220\206.md" @@ -192,7 +192,6 @@ class Dx12TextureView { +Device +DescriptorClass +NativeCpuDescriptorHandle -+NativeGpuDescriptorHandle +Release() } class VulkanTextureView { @@ -233,7 +232,7 @@ RHITextureView <|-- MetalTextureView ## 依赖关系分析 - 抽象层依赖:无后端特定类型,仅定义描述符与基类 - DX12 实现依赖: - - 设备与描述符堆管理(AllocateCbvSrvUavDescriptorPair、CopyDescriptorToShaderVisible) + - 设备与描述符堆管理(view 只占 CPU staging;BindingTable 自有 CPU 镜像 + GPU 段,SetBindingTable flush) - 工具函数进行格式/维度转换与描述符填充 - Vulkan 实现依赖: - VkImageView 创建与销毁 diff --git "a/.qoder/repowiki/zh/content/\351\241\271\347\233\256\346\246\202\350\247\210/\346\240\270\345\277\203\347\211\271\346\200\247\350\257\264\346\230\216/\350\265\204\346\272\220\347\256\241\347\220\206/\350\247\206\345\233\276\347\256\241\347\220\206.md" "b/.qoder/repowiki/zh/content/\351\241\271\347\233\256\346\246\202\350\247\210/\346\240\270\345\277\203\347\211\271\346\200\247\350\257\264\346\230\216/\350\265\204\346\272\220\347\256\241\347\220\206/\350\247\206\345\233\276\347\256\241\347\220\206.md" index 9cdaf35..dc74680 100644 --- "a/.qoder/repowiki/zh/content/\351\241\271\347\233\256\346\246\202\350\247\210/\346\240\270\345\277\203\347\211\271\346\200\247\350\257\264\346\230\216/\350\265\204\346\272\220\347\256\241\347\220\206/\350\247\206\345\233\276\347\256\241\347\220\206.md" +++ "b/.qoder/repowiki/zh/content/\351\241\271\347\233\256\346\246\202\350\247\210/\346\240\270\345\277\203\347\211\271\346\200\247\350\257\264\346\230\216/\350\265\204\346\272\220\347\256\241\347\220\206/\350\247\206\345\233\276\347\256\241\347\220\206.md" @@ -140,9 +140,9 @@ RHI-->>App : 可用于命令编码阶段的绑定 - 作用:将纹理资源按 mip/数组/维度进行切片,并以特定访问语义(如只读采样、写入)暴露给着色器 - 关键配置:BaseMipLevel、MipCount、BaseArraySlice、ArrayCount、ViewType - 后端差异: - - D3D12:创建 SRV/UAV 描述符,并复制到可见堆;支持采样反馈特殊路径 + - D3D12:创建 SRV/UAV 到 CPU staging;BindingTable 在 SetBindingTable 时再 flush 到自有 GPU 段;支持采样反馈特殊路径 - Vulkan:创建 VkImageView,设置 subresourceRange;通过 GetDescriptorImageInfo 获取绑定信息 - - Metal:使用池化纹理视图索引,减少频繁创建开销;支持全视图快速路径 + - Metal:创建期锁定 ViewPool 容量,dispose 清 native 槽后再还软件 index;支持全视图快速路径 ```mermaid classDiagram @@ -150,12 +150,10 @@ class RHITextureView { +ViewType +Dimension +NativeCpuDescriptorHandle() -+NativeGpuDescriptorHandle() } class Dx12TextureView { +CreateSRV() +CreateUAV() -+CopyToShaderVisible() } class VulkanTextureView { +GetDescriptorImageInfo(layout) diff --git a/AGENTS.md b/AGENTS.md index b6c08ad..46ee730 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ ## Product boundary -Keep RHI and backend boundaries explicit. Preserve the maintained Vortice source and patches; do not replace them with an upstream package that lacks the custom behavior. +Keep RHI and backend boundaries explicit. Do not expose backend Heap/Pool/View containers on the public RHI surface. Preserve the maintained Vortice source and patches; do not replace them with an upstream package that lacks the custom behavior. ## Implementation and verification diff --git a/DESIGN.md b/DESIGN.md index 93b7331..2dd6b85 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -8,6 +8,8 @@ Output and intermediate paths are isolated by project, platform, RID, configurat Public native-backed operations enforce platform and ownership boundaries. No capability downgrade or compatibility implementation is permitted to conceal unsupported execution. Current extraction acceptance is tracked by InfinityBrowser TASK-20260907-INFINITYSTACK-EXTRACTION; this document is not a claim that migration gates have passed. +Binding and descriptor strategy stays behind the backend. The public surface is `RHIBindingTable` with `Count` and `SetBindElement(..., arrayIndex)` for finite bindless; RHI does not grow Heap/Pool/View containers. DX12 gives every table group its own CPU mirror plus GPU segment and publishes on `SetBindingTable`; views and interned sampler slots occupy CPU staging only. Vulkan keeps a set per table and pages pools on the device. Metal fills argument tables or reference buffers and locks a private ViewPool at device create. Backends do not resize shader-visible GPU heaps or native pools at runtime. + DX12 requires successful Agility device-factory initialization using the application D3D12 directory and UTF-8 paths. Source references and packages both deploy these assets; missing assets fail explicitly. The maintained binding and evidence are described in docs/SharpGPU/VorticeAgilityPathPatch.md. Backend implementation tests belong to the independent conformance harness. Infinity.Rendering.Tests has no product friend access. Test migration provenance is recorded in docs/provenance/backend-test-migration.json. Native configuration tests exercise actual Configure/Resolve behavior in isolated load contexts; product code does not contain a separate engine-path enumerator solely for tests. diff --git a/docs/SharpGPU/FeatureAudit-2026.md b/docs/SharpGPU/FeatureAudit-2026.md index d10d30b..4fb0bf1 100644 --- a/docs/SharpGPU/FeatureAudit-2026.md +++ b/docs/SharpGPU/FeatureAudit-2026.md @@ -100,7 +100,7 @@ ADR-0064 已完成第一个 clean break:公共 raster attachment 只保留 Inp | GPU file I/O | `StorageQueue` | DirectStorage native + qualified | unavailable/fail closed | MTLIO native/runtime-probed,当前无 Metal-specific qualified evidence | DX12 实装;Metal 部分;Vulkan 无同构是正确结果,命名仍应细分 | | Sparse 2D/3D | 有 texture API | reserved textures/tile mapping/residency;native Tier4 当前折叠为 RHI Tier3,Tier4-specific 语义/资格未独立公开 | sparse image 2D/3D feature与mapping | sparse/placement texture mapping | 部分:机制存在,2D/3D/MSAA/residency/Tier4 需要拆分 capability 与 GPU evidence | | Sparse buffer | capability skeleton | 未形成独立可证明 route | unavailable | unavailable | 不得由 sparse texture 推导 | -| Bindless/indexing | BindingTable + typed capabilities | descriptor table/indexing | descriptor indexing 部分 probe;无 descriptor buffer/heap lowering | MTL4 argument table strategy | 部分;strategy 与 limits 需要继续分层 | +| Bindless/indexing | BindingTable + `Count` + `SetBindElement(..., arrayIndex)`;不暴露 Heap/Pool/View 容器 | table 自有 CPU 镜像 + GPU 段;SetBindElement 只写 CPU;SetBindingTable flush `CopyDescriptors` | set + Device pool 页(SetsPerPage 分档 / 超集共页);`vkUpdateDescriptorSets` + BindSet | argument table / reference buffer + 创建期锁定的私有 ViewPool | 公共面已落地为有限 bindless BindingTable;后端 strategy 不进 RHI,也不热换 GPU heap / native pool | | Barriers/sync | state/barrier/fence/semaphore | Enhanced + classic barrier path | synchronization2 + core barrier path | Metal4 command/memory barrier path | 基础实装;timeline/shared event/external ownership 仍需公共查询 | | Pipeline cache | 有 factory | native | native | fail closed(当前 blob contract 不同构) | 部分;应引入 typed archive/binary strategy 而非伪 portable blob | | Video | 无 | 未暴露 D3D12 Video | 未暴露 KHR Video | 未接 VideoToolbox | 缺失但应归 media service,而非 graphics core | diff --git a/docs/SharpGPU/FeatureMatrix.md b/docs/SharpGPU/FeatureMatrix.md index b88f439..ef82726 100644 --- a/docs/SharpGPU/FeatureMatrix.md +++ b/docs/SharpGPU/FeatureMatrix.md @@ -54,7 +54,7 @@ A Successful native capability probe is never a substitute for a Passed Qualifie | OpacityMicromap | `RayTracing.OpacityMicromap` / `OpacityMicromapSerialization`, `RHIOpacityMicromap`, BLAS triangle attachment, instance `ForceOmm2State` / `DisableOmms` | Typed capability + AccelStruct object model | G4-T13 PASSED: DX12 Tier1_2 + factory/build; Vulkan only when `VK_EXT_opacity_micromap` is enabled, AS + sync2 (1.3 or `VK_KHR_synchronization2`) are enabled, and function pointers load; WindowsQualified tiny 2-state OMM + one-triangle BLAS; independent Vulkan OMM 1/1 | Metal and missing native path Unavailable; Create/Build `Require` throw; unknown format fail-closed; OMM input / triangle-array / index buffers must include `ERHIBufferUsage.AccelStruct` (maps to `MICROMAP_BUILD_INPUT_READ_ONLY`); `DisableOmms` requires BLAS `AllowDisableOmms` and sets native `ALLOW_DISABLE_OMMS`; serialization is honest Unavailable (no fake blob) | | Motion | `RayTracing.Motion`, motion triangles (`MotionVertexBuffer` / `MotionVertexOffset` / `MotionVertexStride`) and motion instances (`MotionType` / matrix / SRT), `ERHIAccelStructFlag.Motion` | Typed capability + AccelStruct object model | G4-T15 PASSED at SHA `11d5a8c1ba2bb9d131a96458d22e12ff167e3041`: Vulkan Available only when `VK_NV_ray_tracing_motion_blur` is listed, `rayTracingMotionBlur` is enabled, AS deps + function pointers load, and the native motion AS path is used; DX12 Unavailable (no standard D3D12 motion AS); Metal compile-level Available only when `supportsPrimitiveMotionBlur` plus MTL motion triangle / motion instance descriptors bind | Create/Build `Require` throw when Unavailable; `ERHIAccelStructFlag.Motion` must match motion data (XOR fail-closed); failed Update leaves descriptor + native motion mode unchanged; `MotionVertexStride` 0 means `VertexStride`, a different non-zero stride is fail-closed; unknown `MotionType` fail-closed; no silent static path; pipeline ABI **9** is an incompatible revision for the Motion public descriptor (AS is not a pipeline-cache key); SER / HitObject is G4-T14 `BLOCKED_SDK_BINDING` and is not this row | | MeshShading | `RHIDeviceCapabilities.Mesh.MeshShader` / `Mesh.TaskShader`, mesh raster pipeline and dispatch | Typed capability | native mesh pipeline plus pixel/readback evidence | mesh factories/encoding throw `NotSupportedException` when unavailable; no silent no-op dispatch | -| DescriptorIndexing | `RHIDeviceCapabilities.DescriptorIndexing`, `RHIBindingTableLayoutElement.Count`, `RHIBindingTable.SetBindElement` | Typed capability + API contract | required/optional arrays, namespace mapping, cross-device/disposed validation, pool rollback, dispatch/readback | unsupported native descriptor semantics fail at layout/table creation; no typed dummy descriptors | +| DescriptorIndexing | `RHIDeviceCapabilities.DescriptorIndexing`, `RHIBindingTableLayoutElement.Count`, `RHIBindingTable.SetBindElement(..., arrayIndex)` | Typed capability + API contract | required/optional arrays, `arrayIndex` updates, namespace mapping, cross-device/disposed validation, table-create pool rollback, dispatch/readback | unsupported native descriptor semantics fail at layout/table creation; no typed dummy descriptors | | StorageQueue | `RHIDeviceCapabilities.Storage.NativeGpuFileIo` / `GpuDecompression` / `RequestCancellation` / `IoPriority`, `RHIStorageQueue` | Independent typed capabilities + API contract | DX12 DirectStorage file → GPU-local buffer/texture → fence → readback under `SharpGpuDirectStorageQualified`; GDeflate / `CancelRequestsWithTag` / queue `Priority` only when the native queue expresses them | non-native backends and missing DirectStorage support throw `NotSupportedException`; Vulkan and unproven dimensions stay Unavailable; no FileStream/map/staging queue fallback | | PipelineCache | `RHIDeviceCapabilities.PipelineCache.NativeCache`, `RHIPipelineCache` | Typed capability + API contract | cold/warm/restart native hit, typed corrupt/incompatible import, full-key non-collision | unavailable native cache strategy throws `NotSupportedException`; caller owns opaque blobs | | WorkGraph | `RHIDeviceCapabilities.WorkGraph.Execution`, `RHIWorkGraphPipeline`, `RHIWorkGraphEncoder` | Typed capability | native create/dispatch/readback on each reported strategy | factory/encoding throws `NotSupportedException` when unavailable | diff --git a/docs/VERIFICATION.md b/docs/VERIFICATION.md index e5a9901..9c576bb 100644 --- a/docs/VERIFICATION.md +++ b/docs/VERIFICATION.md @@ -39,6 +39,9 @@ dotnet test tests/SharpGPU.Conformance.Tests/SharpGPU.Conformance.Tests.csproj ` --results-directory "$out/test-results/source-release" ``` +Descriptor-strategy unit tests that do not require a matching GPU host: +`Dx12DescriptorAllocatorTests`, `VulkanBindingTablePlanTests.PoolPolicy_ShouldTierSetsPerPageAndAllowSupersetShareWithinWasteLimit`, and `MetalBindingTablePlanTests.TextureViewPool_ShouldLockFirstSuccessfulCapacityOnTheLadder`. Metal native slot-clear after dispose remains `TODO(UNVERIFIED)` without a Metal host. + On the current Windows x64 host the Release source build completed with zero errors and the full conformance run passed **289/289**. It exercised the DirectX 12 and Vulkan compute/draw paths, native memory and synchronization, diff --git a/src/SharpGPU/Dx12/Dx12AccelStruct.cs b/src/SharpGPU/Dx12/Dx12AccelStruct.cs index af581d3..5583e9c 100644 --- a/src/SharpGPU/Dx12/Dx12AccelStruct.cs +++ b/src/SharpGPU/Dx12/Dx12AccelStruct.cs @@ -54,14 +54,13 @@ internal unsafe class Dx12TopLevelAccelStruct : RHITopLevelAccelStruct, IDx12Des { public Dx12Device Device => m_Dx12Device; public Dx12DescriptorClass DescriptorClass => Dx12DescriptorClass.AccelerationStructure; - public Vortice.Direct3D12.CpuDescriptorHandle NativeCpuDescriptorHandle => m_Descriptors.Staging.CpuHandle; - public Vortice.Direct3D12.GpuDescriptorHandle NativeGpuDescriptorHandle => m_Descriptors.ShaderVisible.GpuHandle; + public Vortice.Direct3D12.CpuDescriptorHandle NativeCpuDescriptorHandle => m_Staging.Descriptor.CpuHandle; public Vortice.Direct3D12.ID3D12Resource ResultBuffer => m_NativeResultBuffer; public Vortice.Direct3D12.BuildRaytracingAccelerationStructureDescription NativeAccelStructDescriptor => m_NativeAccelStructDescriptor; private Dx12Device m_Dx12Device; - private int m_DescriptionHeapIndex; - private Dx12DescriptorPair m_Descriptors; + private bool m_HasDescriptors; + private Dx12CpuDescriptorAllocation m_Staging; private Vortice.Direct3D12.ID3D12Resource m_NativeResultBuffer; private Vortice.Direct3D12.ID3D12Resource m_NativeScratchBuffer; private Vortice.Direct3D12.ID3D12Resource m_NativeInstancesBuffer; @@ -71,7 +70,7 @@ public Dx12TopLevelAccelStruct(Dx12Device device, in RHITopLevelAccelStructDescr { m_Dx12Device = device; m_Descriptor = descriptor; - m_DescriptionHeapIndex = -1; + m_HasDescriptors = false; RHIOpacityMicromapContract.ValidateTlasDescriptor(device, in descriptor); RHIAccelStructMotionContract.ValidateTlasDescriptor(device, in descriptor); Span asInstances = descriptor.Instances.Span; @@ -118,16 +117,15 @@ public Dx12TopLevelAccelStruct(Dx12Device device, in RHITopLevelAccelStructDescr m_NativeScratchBuffer = Dx12RaytracingHelper.CreateBuffer(m_Dx12Device.NativeDevice, (uint)nativeAccelStructPrebuildInfo.ScratchDataSizeInBytes, Vortice.Direct3D12.ResourceFlags.AllowUnorderedAccess, Vortice.Direct3D12.ResourceStates.Common | Vortice.Direct3D12.ResourceStates.UnorderedAccess, Dx12RaytracingHelper.kDefaultHeapProps); m_NativeResultBuffer = Dx12RaytracingHelper.CreateBuffer(m_Dx12Device.NativeDevice, (uint)nativeAccelStructPrebuildInfo.ResultDataMaxSizeInBytes, Vortice.Direct3D12.ResourceFlags.AllowUnorderedAccess, Vortice.Direct3D12.ResourceStates.Common | Vortice.Direct3D12.ResourceStates.RaytracingAccelerationStructure, Dx12RaytracingHelper.kDefaultHeapProps); - m_Descriptors = m_Dx12Device.AllocateCbvSrvUavDescriptorPair(); - m_DescriptionHeapIndex = m_Descriptors.ShaderVisible.Index; + m_Staging = m_Dx12Device.AllocateStagingCbvSrvUavDescriptor(1); + m_HasDescriptors = true; Vortice.Direct3D12.ShaderResourceViewDescription accelStructSrvDesc = new Vortice.Direct3D12.ShaderResourceViewDescription(); accelStructSrvDesc.Format = Vortice.DXGI.Format.Unknown; accelStructSrvDesc.ViewDimension = Vortice.Direct3D12.ShaderResourceViewDimension.RaytracingAccelerationStructure; accelStructSrvDesc.Shader4ComponentMapping = 5768; accelStructSrvDesc.RaytracingAccelerationStructure.Location = m_NativeResultBuffer.GPUVirtualAddress; - m_Dx12Device.NativeDevice.CreateShaderResourceView(null, accelStructSrvDesc, m_Descriptors.Staging.CpuHandle); - m_Dx12Device.CopyDescriptorToShaderVisible(m_Descriptors); + m_Dx12Device.NativeDevice.CreateShaderResourceView(null, accelStructSrvDesc, m_Staging.Descriptor.CpuHandle); m_NativeAccelStructDescriptor.Inputs = nativeAccelStructDescriptor; m_NativeAccelStructDescriptor.DestinationAccelerationStructureData = m_NativeResultBuffer.GPUVirtualAddress; @@ -191,10 +189,10 @@ public override void UpdateAccelerationStructure(in RHITopLevelAccelStructDescri protected override void Release() { - if (m_DescriptionHeapIndex >= 0) + if (m_HasDescriptors) { - m_Dx12Device.FreeDescriptorPair(m_Descriptors); - m_DescriptionHeapIndex = -1; + m_Dx12Device.FreeStagingCbvSrvUavDescriptor(m_Staging); + m_HasDescriptors = false; } m_NativeResultBuffer.Release(); m_NativeScratchBuffer.Release(); diff --git a/src/SharpGPU/Dx12/Dx12BindingTable.cs b/src/SharpGPU/Dx12/Dx12BindingTable.cs index b291d60..aa762ad 100644 --- a/src/SharpGPU/Dx12/Dx12BindingTable.cs +++ b/src/SharpGPU/Dx12/Dx12BindingTable.cs @@ -25,7 +25,6 @@ internal interface IDx12DescriptorView Dx12Device Device { get; } Dx12DescriptorClass DescriptorClass { get; } Vortice.Direct3D12.CpuDescriptorHandle NativeCpuDescriptorHandle { get; } - Vortice.Direct3D12.GpuDescriptorHandle NativeGpuDescriptorHandle { get; } } internal readonly struct Dx12BindingKey : IEquatable @@ -129,7 +128,6 @@ internal sealed class Dx12BindingTableGroupPlan public Vortice.Direct3D12.ShaderVisibility Visibility { get; } public int DescriptorCount { get; } public int[] BindingIndices { get; } - public bool RequiresOwnedRange => DescriptorCount > 1 || BindingIndices.Length > 1; public Dx12BindingTableGroupPlan( in Dx12DescriptorHeapClass heapClass, @@ -374,14 +372,14 @@ internal sealed class Dx12NullDescriptorCache : Disposal { private readonly object m_Gate = new object(); private readonly Dx12Device m_Device; - private readonly Dx12DescriptorPair?[] m_Descriptors = new Dx12DescriptorPair?[(int)ERHIBindType.Pending]; + private readonly Dx12CpuDescriptorAllocation?[] m_Descriptors = new Dx12CpuDescriptorAllocation?[(int)ERHIBindType.Pending]; public Dx12NullDescriptorCache(Dx12Device device) { m_Device = device; } - public Dx12DescriptorPair Get(in ERHIBindType bindType) + public Vortice.Direct3D12.CpuDescriptorHandle Get(in ERHIBindType bindType) { int descriptorIndex = (int)bindType; if (descriptorIndex < 0 || descriptorIndex >= m_Descriptors.Length) @@ -398,12 +396,12 @@ public Dx12DescriptorPair Get(in ERHIBindType bindType) { if (m_Descriptors[descriptorIndex].HasValue) { - return m_Descriptors[descriptorIndex]!.Value; + return m_Descriptors[descriptorIndex]!.Value.Descriptor.CpuHandle; } - Dx12DescriptorPair descriptor = Create(bindType); + Dx12CpuDescriptorAllocation descriptor = Create(bindType); m_Descriptors[descriptorIndex] = descriptor; - return descriptor; + return descriptor.Descriptor.CpuHandle; } } @@ -418,42 +416,38 @@ protected override void Release() continue; } - Dx12DescriptorPair descriptor = m_Descriptors[i]!.Value; - m_Device.FreeDescriptorPair(descriptor); + m_Device.FreeStagingCbvSrvUavDescriptor(m_Descriptors[i]!.Value); m_Descriptors[i] = null; } } } - private Dx12DescriptorPair Create(in ERHIBindType bindType) + private Dx12CpuDescriptorAllocation Create(in ERHIBindType bindType) { - Dx12DescriptorPair descriptors = m_Device.AllocateCbvSrvUavDescriptorPair(); + Dx12CpuDescriptorAllocation descriptors = m_Device.AllocateStagingCbvSrvUavDescriptor(1); try { if (bindType == ERHIBindType.UniformBuffer) { - m_Device.NativeDevice.CreateConstantBufferView(null, descriptors.Staging.CpuHandle); - m_Device.CopyDescriptorToShaderVisible(descriptors); + m_Device.NativeDevice.CreateConstantBufferView(null, descriptors.Descriptor.CpuHandle); return descriptors; } if (Dx12Utility.ConvertToDx12BindType(bindType) == Vortice.Direct3D12.DescriptorRangeType.ShaderResourceView) { Vortice.Direct3D12.ShaderResourceViewDescription srv = CreateNullSrvDescription(bindType); - m_Device.NativeDevice.CreateShaderResourceView(null, srv, descriptors.Staging.CpuHandle); - m_Device.CopyDescriptorToShaderVisible(descriptors); + m_Device.NativeDevice.CreateShaderResourceView(null, srv, descriptors.Descriptor.CpuHandle); return descriptors; } Vortice.Direct3D12.UnorderedAccessViewDescription uav = CreateNullUavDescription(bindType); - m_Device.NativeDevice.CreateUnorderedAccessView(null, null, uav, descriptors.Staging.CpuHandle); - m_Device.CopyDescriptorToShaderVisible(descriptors); + m_Device.NativeDevice.CreateUnorderedAccessView(null, null, uav, descriptors.Descriptor.CpuHandle); return descriptors; } catch { - m_Device.FreeDescriptorPair(descriptors); + m_Device.FreeStagingCbvSrvUavDescriptor(descriptors); throw; } } @@ -563,21 +557,18 @@ internal struct Dx12BindingTableGroupStorage public int HeapIndex; public int DescriptorCount; public bool IsSampler; - public bool OwnsRange; public Vortice.Direct3D12.GpuDescriptorHandle GpuHandle; + public Dx12CpuDescriptorAllocation Staging; + public Dx12DescriptorDirtyRanges Dirty; } internal readonly struct Dx12DescriptorSource { public Vortice.Direct3D12.CpuDescriptorHandle CpuHandle { get; } - public Vortice.Direct3D12.GpuDescriptorHandle GpuHandle { get; } - public Dx12DescriptorSource( - in Vortice.Direct3D12.CpuDescriptorHandle cpuHandle, - in Vortice.Direct3D12.GpuDescriptorHandle gpuHandle) + public Dx12DescriptorSource(in Vortice.Direct3D12.CpuDescriptorHandle cpuHandle) { CpuHandle = cpuHandle; - GpuHandle = gpuHandle; } } @@ -590,6 +581,7 @@ internal sealed class Dx12BindingTable : RHIBindingTable private readonly bool[] m_BoundStates; private readonly Dx12Device m_Device; private readonly Dx12BindingTableGroupStorage[] m_GroupStorages; + private readonly List<(int Start, int Count)> m_DirtyCopyScratch = new List<(int Start, int Count)>(4); private int m_MissingRequiredDescriptorCount; public Dx12BindingTable(Dx12Device device, in RHIBindingTableDescriptor descriptor) @@ -647,6 +639,66 @@ public Vortice.Direct3D12.GpuDescriptorHandle GetGroupGpuHandle(in int groupInde return m_GroupStorages[groupIndex].GpuHandle; } + public void PublishDirtyDescriptors() + { + ThrowIfDisposed(); + for (int groupIndex = 0; groupIndex < m_GroupStorages.Length; ++groupIndex) + { + ref Dx12BindingTableGroupStorage storage = ref m_GroupStorages[groupIndex]; + if (storage.Dirty == null || storage.Dirty.IsEmpty || storage.HeapIndex < 0) + { + continue; + } + + Dx12DescriptorHeap destinationHeap = storage.IsSampler + ? m_Device.DescriptorHeapSampler + : m_Device.DescriptorHeapCbvSrvUav; + Vortice.Direct3D12.DescriptorHeapType nativeType = destinationHeap.NativeType; + if (storage.Dirty.ShouldCopyAsSingleSpan(out int spanStart, out int spanCount)) + { + m_Device.NativeDevice.CopyDescriptorsSimple( + (uint)spanCount, + OffsetHandle(destinationHeap.NativeCpuStartHandle, storage.HeapIndex + spanStart, destinationHeap.DescriptorSize), + OffsetHandle(storage.Staging.Descriptor.CpuHandle, spanStart, storage.Staging.Heap.DescriptorSize), + nativeType); + } + else + { + storage.Dirty.CopyTo(m_DirtyCopyScratch); + int rangeCount = m_DirtyCopyScratch.Count; + Vortice.Direct3D12.CpuDescriptorHandle[] destStarts = new Vortice.Direct3D12.CpuDescriptorHandle[rangeCount]; + Vortice.Direct3D12.CpuDescriptorHandle[] srcStarts = new Vortice.Direct3D12.CpuDescriptorHandle[rangeCount]; + uint[] destSizes = new uint[rangeCount]; + uint[] srcSizes = new uint[rangeCount]; + for (int i = 0; i < rangeCount; ++i) + { + (int start, int count) = m_DirtyCopyScratch[i]; + destStarts[i] = OffsetHandle( + destinationHeap.NativeCpuStartHandle, + storage.HeapIndex + start, + destinationHeap.DescriptorSize); + srcStarts[i] = OffsetHandle( + storage.Staging.Descriptor.CpuHandle, + start, + storage.Staging.Heap.DescriptorSize); + destSizes[i] = (uint)count; + srcSizes[i] = (uint)count; + } + + m_Device.NativeDevice.CopyDescriptors( + (uint)rangeCount, + destStarts, + destSizes, + (uint)rangeCount, + srcStarts, + srcSizes, + nativeType); + } + + storage.Dirty.Clear(); + } + } + public void EnsureReadyForBinding() { ThrowIfDisposed(); @@ -716,30 +768,22 @@ private void InitializeGroups() storage.HeapIndex = -1; storage.DescriptorCount = groupPlan.DescriptorCount; storage.IsSampler = groupPlan.HeapClass == Dx12DescriptorHeapClass.Sampler; - storage.OwnsRange = groupPlan.RequiresOwnedRange; - - if (storage.OwnsRange) - { - Dx12DescriptorInfo allocation = storage.IsSampler - ? m_Device.AllocateSamplerDescriptor(storage.DescriptorCount) - : m_Device.AllocateCbvSrvUavDescriptor(storage.DescriptorCount); - storage.HeapIndex = allocation.Index; - storage.GpuHandle = allocation.GpuHandle; - InitializeOptionalRange(groupPlan, storage); - continue; - } - - int bindingIndex = groupPlan.BindingIndices[0]; - ref readonly Dx12BindInfo bindInfo = ref BindingTableLayout.BindInfos[bindingIndex]; - storage.GpuHandle = bindInfo.Requirement == ERHIBindingRequirement.Optional - ? m_Device.NullDescriptors.Get(bindInfo.Type).ShaderVisible.GpuHandle - : default; + storage.Dirty = new Dx12DescriptorDirtyRanges(); + storage.Staging = storage.IsSampler + ? m_Device.AllocateStagingSamplerDescriptor(storage.DescriptorCount) + : m_Device.AllocateStagingCbvSrvUavDescriptor(storage.DescriptorCount); + Dx12DescriptorInfo allocation = storage.IsSampler + ? m_Device.AllocateSamplerDescriptor(storage.DescriptorCount) + : m_Device.AllocateCbvSrvUavDescriptor(storage.DescriptorCount); + storage.HeapIndex = allocation.Index; + storage.GpuHandle = allocation.GpuHandle; + InitializeOptionalRange(groupPlan, ref storage); } } private void InitializeOptionalRange( Dx12BindingTableGroupPlan groupPlan, - in Dx12BindingTableGroupStorage storage) + ref Dx12BindingTableGroupStorage storage) { for (int i = 0; i < groupPlan.BindingIndices.Length; ++i) { @@ -749,10 +793,10 @@ private void InitializeOptionalRange( continue; } - Dx12DescriptorPair nullDescriptor = m_Device.NullDescriptors.Get(bindInfo.Type); + Vortice.Direct3D12.CpuDescriptorHandle nullHandle = m_Device.NullDescriptors.Get(bindInfo.Type); for (int arrayIndex = 0; arrayIndex < (int)bindInfo.Count; ++arrayIndex) { - CopyDescriptor(storage, bindInfo.DescriptorOffset + arrayIndex, nullDescriptor.Staging.CpuHandle); + CopyDescriptor(ref storage, bindInfo.DescriptorOffset + arrayIndex, nullHandle); } } } @@ -805,36 +849,19 @@ private void SetBinding( bool hasSource = TryGetDescriptorSource(element, bindInfo, out Dx12DescriptorSource source); ref Dx12BindingTableGroupStorage storage = ref m_GroupStorages[bindInfo.GroupIndex]; - Dx12DescriptorPair nullDescriptor = default; - if (!hasSource && bindInfo.Requirement == ERHIBindingRequirement.Optional) + if (hasSource) { - nullDescriptor = m_Device.NullDescriptors.Get(bindInfo.Type); + CopyDescriptor( + ref storage, + bindInfo.DescriptorOffset + arrayIndex, + source.CpuHandle); } - - if (storage.OwnsRange) - { - if (hasSource) - { - CopyDescriptor( - storage, - bindInfo.DescriptorOffset + arrayIndex, - source.CpuHandle); - } - else if (bindInfo.Requirement == ERHIBindingRequirement.Optional) - { - CopyDescriptor( - storage, - bindInfo.DescriptorOffset + arrayIndex, - nullDescriptor.Staging.CpuHandle); - } - } - else + else if (bindInfo.Requirement == ERHIBindingRequirement.Optional) { - storage.GpuHandle = hasSource - ? source.GpuHandle - : bindInfo.Requirement == ERHIBindingRequirement.Optional - ? nullDescriptor.ShaderVisible.GpuHandle - : default; + CopyDescriptor( + ref storage, + bindInfo.DescriptorOffset + arrayIndex, + m_Device.NullDescriptors.Get(bindInfo.Type)); } int stateIndex = bindInfo.StateOffset + arrayIndex; @@ -952,9 +979,7 @@ private Dx12DescriptorSource ValidateDescriptorSource( $"DX12 binding table space {BindingTableLayout.Index} binding ({bindInfo.Type}, slot {bindInfo.Slot}) requires descriptor class {expectedClass}, but received {descriptor.DescriptorClass}."); } - return new Dx12DescriptorSource( - descriptor.NativeCpuDescriptorHandle, - descriptor.NativeGpuDescriptorHandle); + return new Dx12DescriptorSource(descriptor.NativeCpuDescriptorHandle); } private ArgumentException CreateResourceTypeException(in Dx12BindInfo bindInfo, string expectedField) { @@ -964,17 +989,20 @@ private ArgumentException CreateResourceTypeException(in Dx12BindInfo bindInfo, } private void CopyDescriptor( - in Dx12BindingTableGroupStorage storage, + ref Dx12BindingTableGroupStorage storage, in int descriptorOffset, in Vortice.Direct3D12.CpuDescriptorHandle sourceHandle) { - Dx12DescriptorHeap destinationHeap = storage.IsSampler - ? m_Device.DescriptorHeapSampler - : m_Device.DescriptorHeapCbvSrvUav; - Vortice.Direct3D12.CpuDescriptorHandle destinationHandle = destinationHeap.NativeCpuStartHandle.Offset( - storage.HeapIndex + descriptorOffset, - destinationHeap.DescriptorSize); - m_Device.NativeDevice.CopyDescriptorsSimple(1, destinationHandle, sourceHandle, destinationHeap.NativeType); + Vortice.Direct3D12.CpuDescriptorHandle destinationHandle = OffsetHandle( + storage.Staging.Descriptor.CpuHandle, + descriptorOffset, + storage.Staging.Heap.DescriptorSize); + m_Device.NativeDevice.CopyDescriptorsSimple( + 1, + destinationHandle, + sourceHandle, + storage.Staging.Heap.NativeType); + storage.Dirty.Add(descriptorOffset); } private void ReleaseOwnedRanges() @@ -982,26 +1010,45 @@ private void ReleaseOwnedRanges() for (int i = 0; i < m_GroupStorages.Length; ++i) { ref Dx12BindingTableGroupStorage storage = ref m_GroupStorages[i]; - if (!storage.OwnsRange || storage.HeapIndex < 0) - { - continue; - } - - int heapIndex = storage.HeapIndex; int descriptorCount = storage.DescriptorCount; - storage.HeapIndex = -1; - storage.OwnsRange = false; - if (storage.IsSampler) + if (storage.HeapIndex >= 0) { - m_Device.FreeSamplerDescriptor(heapIndex, descriptorCount); + int heapIndex = storage.HeapIndex; + storage.HeapIndex = -1; + if (storage.IsSampler) + { + m_Device.FreeSamplerDescriptor(heapIndex, descriptorCount); + } + else + { + m_Device.FreeCbvSrvUavDescriptor(heapIndex, descriptorCount); + } } - else + + if (storage.Staging.Heap != null) { - m_Device.FreeCbvSrvUavDescriptor(heapIndex, descriptorCount); + Dx12CpuDescriptorAllocation staging = storage.Staging; + storage.Staging = default; + if (storage.IsSampler) + { + m_Device.FreeStagingSamplerDescriptor(staging, descriptorCount); + } + else + { + m_Device.FreeStagingCbvSrvUavDescriptor(staging, descriptorCount); + } } } } + private static Vortice.Direct3D12.CpuDescriptorHandle OffsetHandle( + in Vortice.Direct3D12.CpuDescriptorHandle start, + in int offset, + in uint descriptorSize) + { + return new Vortice.Direct3D12.CpuDescriptorHandle(start, offset, descriptorSize); + } + private static bool IsRequired(in ERHIBindingRequirement requirement) { diff --git a/src/SharpGPU/Dx12/Dx12BufferView.cs b/src/SharpGPU/Dx12/Dx12BufferView.cs index ca0f311..1b7a86b 100644 --- a/src/SharpGPU/Dx12/Dx12BufferView.cs +++ b/src/SharpGPU/Dx12/Dx12BufferView.cs @@ -15,25 +15,12 @@ internal unsafe class Dx12BufferView : RHIBufferView, IDx12DescriptorView _ => throw new InvalidOperationException($"DX12 buffer view has unsupported descriptor class {m_ViewType}."), }; - public Vortice.Direct3D12.CpuDescriptorHandle NativeCpuDescriptorHandle - { - get - { - return m_Descriptors.Staging.CpuHandle; - } - } - public Vortice.Direct3D12.GpuDescriptorHandle NativeGpuDescriptorHandle - { - get - { - return m_Descriptors.ShaderVisible.GpuHandle; - } - } + public Vortice.Direct3D12.CpuDescriptorHandle NativeCpuDescriptorHandle => m_Staging.Descriptor.CpuHandle; private bool m_HasDescriptors; private ERHIBufferViewType m_ViewType; private Dx12Buffer m_Dx12Buffer; - private Dx12DescriptorPair m_Descriptors; + private Dx12CpuDescriptorAllocation m_Staging; public Dx12BufferView(Dx12Buffer buffer, in RHIBufferViewDescriptor descriptor) { @@ -49,10 +36,9 @@ public Dx12BufferView(Dx12Buffer buffer, in RHIBufferViewDescriptor descriptor) desc.SizeInBytes = (uint)descriptor.Stride; desc.BufferLocation = m_Dx12Buffer.NativeResource.GPUVirtualAddress + (ulong)(descriptor.Stride * descriptor.Offset); - m_Descriptors = m_Dx12Buffer.Dx12Device.AllocateCbvSrvUavDescriptorPair(); + m_Staging = m_Dx12Buffer.Dx12Device.AllocateStagingCbvSrvUavDescriptor(1); m_HasDescriptors = true; - m_Dx12Buffer.Dx12Device.NativeDevice.CreateConstantBufferView(desc, m_Descriptors.Staging.CpuHandle); - m_Dx12Buffer.Dx12Device.CopyDescriptorToShaderVisible(m_Descriptors); + m_Dx12Buffer.Dx12Device.NativeDevice.CreateConstantBufferView(desc, m_Staging.Descriptor.CpuHandle); } } else if (descriptor.ViewType == ERHIBufferViewType.AccelStruct) @@ -65,10 +51,9 @@ public Dx12BufferView(Dx12Buffer buffer, in RHIBufferViewDescriptor descriptor) desc.ViewDimension = Vortice.Direct3D12.ShaderResourceViewDimension.RaytracingAccelerationStructure; desc.Shader4ComponentMapping = 5768; - m_Descriptors = m_Dx12Buffer.Dx12Device.AllocateCbvSrvUavDescriptorPair(); + m_Staging = m_Dx12Buffer.Dx12Device.AllocateStagingCbvSrvUavDescriptor(1); m_HasDescriptors = true; - m_Dx12Buffer.Dx12Device.NativeDevice.CreateShaderResourceView(m_Dx12Buffer.NativeResource, desc, m_Descriptors.Staging.CpuHandle); - m_Dx12Buffer.Dx12Device.CopyDescriptorToShaderVisible(m_Descriptors); + m_Dx12Buffer.Dx12Device.NativeDevice.CreateShaderResourceView(m_Dx12Buffer.NativeResource, desc, m_Staging.Descriptor.CpuHandle); } } else if (descriptor.ViewType == ERHIBufferViewType.ShaderResource) @@ -83,10 +68,9 @@ public Dx12BufferView(Dx12Buffer buffer, in RHIBufferViewDescriptor descriptor) desc.ViewDimension = Vortice.Direct3D12.ShaderResourceViewDimension.Buffer; desc.Shader4ComponentMapping = 5768; - m_Descriptors = m_Dx12Buffer.Dx12Device.AllocateCbvSrvUavDescriptorPair(); + m_Staging = m_Dx12Buffer.Dx12Device.AllocateStagingCbvSrvUavDescriptor(1); m_HasDescriptors = true; - m_Dx12Buffer.Dx12Device.NativeDevice.CreateShaderResourceView(m_Dx12Buffer.NativeResource, desc, m_Descriptors.Staging.CpuHandle); - m_Dx12Buffer.Dx12Device.CopyDescriptorToShaderVisible(m_Descriptors); + m_Dx12Buffer.Dx12Device.NativeDevice.CreateShaderResourceView(m_Dx12Buffer.NativeResource, desc, m_Staging.Descriptor.CpuHandle); } } else if (descriptor.ViewType == ERHIBufferViewType.UnorderedAccess) @@ -100,10 +84,9 @@ public Dx12BufferView(Dx12Buffer buffer, in RHIBufferViewDescriptor descriptor) desc.Buffer.StructureByteStride = (uint)descriptor.Stride; desc.ViewDimension = Vortice.Direct3D12.UnorderedAccessViewDimension.Buffer; - m_Descriptors = m_Dx12Buffer.Dx12Device.AllocateCbvSrvUavDescriptorPair(); + m_Staging = m_Dx12Buffer.Dx12Device.AllocateStagingCbvSrvUavDescriptor(1); m_HasDescriptors = true; - m_Dx12Buffer.Dx12Device.NativeDevice.CreateUnorderedAccessView(m_Dx12Buffer.NativeResource, null, desc, m_Descriptors.Staging.CpuHandle); - m_Dx12Buffer.Dx12Device.CopyDescriptorToShaderVisible(m_Descriptors); + m_Dx12Buffer.Dx12Device.NativeDevice.CreateUnorderedAccessView(m_Dx12Buffer.NativeResource, null, desc, m_Staging.Descriptor.CpuHandle); } } @@ -117,7 +100,7 @@ protected override void Release() { if (m_HasDescriptors) { - m_Dx12Buffer.Dx12Device.FreeDescriptorPair(m_Descriptors); + m_Dx12Buffer.Dx12Device.FreeStagingCbvSrvUavDescriptor(m_Staging); m_HasDescriptors = false; } } diff --git a/src/SharpGPU/Dx12/Dx12Device.cs b/src/SharpGPU/Dx12/Dx12Device.cs index 8f7cd75..ae87b29 100644 --- a/src/SharpGPU/Dx12/Dx12Device.cs +++ b/src/SharpGPU/Dx12/Dx12Device.cs @@ -124,6 +124,9 @@ internal Vortice.DirectML.IDMLCommandRecorder DirectMLCommandRecorder private Dx12CpuDescriptorPool? m_StagingPoolCbvSrvUav; private Dx12CpuDescriptorPool? m_StagingPoolSampler; + private readonly object m_SamplerInternGate = new object(); + private readonly Dictionary m_SamplerInterns = + new Dictionary(); private Vortice.Direct3D12.ID3D12CommandSignature? m_DrawIndirectSignature; private Vortice.Direct3D12.ID3D12CommandSignature? m_DrawIndexedIndirectSignature; private Vortice.Direct3D12.ID3D12CommandSignature? m_DispatchRayIndirectSignature; @@ -1278,7 +1281,16 @@ public override RHITexture CreateSparseTexture( public override RHISampler CreateSampler(in RHISamplerDescriptor descriptor) { - return new Dx12Sampler(this, descriptor); + Dx12CpuDescriptorAllocation allocation = RetainSamplerIntern(descriptor); + try + { + return new Dx12Sampler(this, descriptor, allocation); + } + catch + { + ReleaseSamplerIntern(descriptor); + throw; + } } public override RHITopLevelAccelStruct CreateTopAccelerationStructure(in RHITopLevelAccelStructDescriptor descriptor) @@ -1425,6 +1437,92 @@ public Dx12DescriptorInfo AllocateCbvSrvUavDescriptor(in int count) return AllocateDescriptor(DescriptorHeapCbvSrvUav, count, "shader-visible CBV/SRV/UAV"); } + public Dx12CpuDescriptorAllocation AllocateStagingCbvSrvUavDescriptor(in int count) + { + return StagingPoolCbvSrvUav.Allocate(count, "staging CBV/SRV/UAV"); + } + + public Dx12CpuDescriptorAllocation AllocateStagingSamplerDescriptor(in int count) + { + return StagingPoolSampler.Allocate(count, "staging sampler"); + } + + public void FreeStagingCbvSrvUavDescriptor(in Dx12CpuDescriptorAllocation allocation, in int count = 1) + { + StagingPoolCbvSrvUav.Free(allocation.Heap, allocation.Descriptor.Index, count); + } + + public void FreeStagingSamplerDescriptor(in Dx12CpuDescriptorAllocation allocation, in int count = 1) + { + StagingPoolSampler.Free(allocation.Heap, allocation.Descriptor.Index, count); + } + + internal Dx12CpuDescriptorAllocation RetainSamplerIntern(in RHISamplerDescriptor descriptor) + { + Dx12SamplerInternKey key = Dx12SamplerInternKey.From(descriptor); + lock (m_SamplerInternGate) + { + if (m_SamplerInterns.TryGetValue(key, out Dx12SamplerInternSlot? slot)) + { + slot.AddRef(); + return slot.Allocation; + } + + Dx12CpuDescriptorAllocation allocation = AllocateStagingSamplerDescriptor(1); + try + { + Vortice.Direct3D12.SamplerDescription desc = Dx12Sampler.CreateNativeDescription(descriptor); + NativeDevice.CreateSampler(ref desc, allocation.Descriptor.CpuHandle); + m_SamplerInterns.Add(key, new Dx12SamplerInternSlot(allocation)); + return allocation; + } + catch + { + FreeStagingSamplerDescriptor(allocation); + throw; + } + } + } + + internal void ReleaseSamplerIntern(in RHISamplerDescriptor descriptor) + { + Dx12SamplerInternKey key = Dx12SamplerInternKey.From(descriptor); + lock (m_SamplerInternGate) + { + if (!m_SamplerInterns.TryGetValue(key, out Dx12SamplerInternSlot? slot)) + { + throw new InvalidOperationException("DX12 sampler intern slot was released more times than it was retained."); + } + + if (!slot.ReleaseRef()) + { + return; + } + + m_SamplerInterns.Remove(key); + if (m_StagingPoolSampler != null) + { + FreeStagingSamplerDescriptor(slot.Allocation); + } + } + } + + private void DisposeSamplerInterns() + { + lock (m_SamplerInternGate) + { + if (m_StagingPoolSampler != null) + { + foreach (Dx12SamplerInternSlot slot in m_SamplerInterns.Values) + { + FreeStagingSamplerDescriptor(slot.Allocation); + } + } + + m_SamplerInterns.Clear(); + } + } + public Dx12DescriptorPair AllocateCbvSrvUavDescriptorPair() { return AllocateDescriptorPair( @@ -1434,15 +1532,6 @@ public Dx12DescriptorPair AllocateCbvSrvUavDescriptorPair() "CBV/SRV/UAV"); } - public Dx12DescriptorPair AllocateSamplerDescriptorPair() - { - return AllocateDescriptorPair( - DescriptorHeapSampler, - StagingPoolSampler, - Vortice.Direct3D12.DescriptorHeapType.Sampler, - "sampler"); - } - public void CopyDescriptorToShaderVisible(in Dx12DescriptorPair descriptors) { NativeDevice.CopyDescriptorsSimple( @@ -2668,6 +2757,7 @@ protected override void Release() ReleaseComObject(ref m_DirectMLDevice1); ReleaseComObject(ref m_DirectMLDevice); + DisposeSamplerInterns(); DisposeResource(ref m_NullDescriptors); DisposeResource(ref m_DescriptorHeapDSV); DisposeResource(ref m_DescriptorHeapHeapRTV); diff --git a/src/SharpGPU/Dx12/Dx12Pipeline.cs b/src/SharpGPU/Dx12/Dx12Pipeline.cs index a0fee86..e2de4f2 100644 --- a/src/SharpGPU/Dx12/Dx12Pipeline.cs +++ b/src/SharpGPU/Dx12/Dx12Pipeline.cs @@ -233,6 +233,7 @@ private static Dx12BindingTable ResolveReadyTable( } dx12BindingTable.EnsureReadyForBinding(); + dx12BindingTable.PublishDirtyDescriptors(); return dx12BindingTable; } } diff --git a/src/SharpGPU/Dx12/Dx12Sampler.cs b/src/SharpGPU/Dx12/Dx12Sampler.cs index 3b193cc..2788e8c 100644 --- a/src/SharpGPU/Dx12/Dx12Sampler.cs +++ b/src/SharpGPU/Dx12/Dx12Sampler.cs @@ -1,57 +1,151 @@ +using System; + namespace SharpGPU { #pragma warning disable CA1416 - internal unsafe class Dx12Sampler : RHISampler, IDx12DescriptorView + internal readonly struct Dx12SamplerInternKey : IEquatable { - public Dx12Device Device => m_Dx12Device; - public Dx12DescriptorClass DescriptorClass => Dx12DescriptorClass.Sampler; + public float LodMin { get; } + public float LodMax { get; } + public float MipLODBias { get; } + public uint Anisotropy { get; } + public ERHIFilterMode MinFilter { get; } + public ERHIFilterMode MagFilter { get; } + public ERHIFilterMode MipFilter { get; } + public ERHIAddressMode AddressModeU { get; } + public ERHIAddressMode AddressModeV { get; } + public ERHIAddressMode AddressModeW { get; } + public ERHIComparisonMode ComparisonMode { get; } - public Vortice.Direct3D12.CpuDescriptorHandle NativeCpuDescriptorHandle + private Dx12SamplerInternKey(in RHISamplerDescriptor descriptor) { - get - { - return m_Descriptors.Staging.CpuHandle; - } + LodMin = descriptor.LodMin; + LodMax = descriptor.LodMax; + MipLODBias = descriptor.MipLODBias; + Anisotropy = descriptor.Anisotropy; + MinFilter = descriptor.MinFilter; + MagFilter = descriptor.MagFilter; + MipFilter = descriptor.MipFilter; + AddressModeU = descriptor.AddressModeU; + AddressModeV = descriptor.AddressModeV; + AddressModeW = descriptor.AddressModeW; + ComparisonMode = descriptor.ComparisonMode; + } + + public static Dx12SamplerInternKey From(in RHISamplerDescriptor descriptor) + { + return new Dx12SamplerInternKey(descriptor); + } + + public bool Equals(Dx12SamplerInternKey other) + { + return LodMin.Equals(other.LodMin) + && LodMax.Equals(other.LodMax) + && MipLODBias.Equals(other.MipLODBias) + && Anisotropy == other.Anisotropy + && MinFilter == other.MinFilter + && MagFilter == other.MagFilter + && MipFilter == other.MipFilter + && AddressModeU == other.AddressModeU + && AddressModeV == other.AddressModeV + && AddressModeW == other.AddressModeW + && ComparisonMode == other.ComparisonMode; + } + + public override bool Equals(object? obj) + { + return obj is Dx12SamplerInternKey other && Equals(other); + } + + public override int GetHashCode() + { + HashCode hash = new HashCode(); + hash.Add(LodMin); + hash.Add(LodMax); + hash.Add(MipLODBias); + hash.Add(Anisotropy); + hash.Add(MinFilter); + hash.Add(MagFilter); + hash.Add(MipFilter); + hash.Add(AddressModeU); + hash.Add(AddressModeV); + hash.Add(AddressModeW); + hash.Add(ComparisonMode); + return hash.ToHashCode(); + } + } + + internal sealed class Dx12SamplerInternSlot + { + public Dx12CpuDescriptorAllocation Allocation { get; } + public int RefCount { get; private set; } + + public Dx12SamplerInternSlot(in Dx12CpuDescriptorAllocation allocation) + { + Allocation = allocation; + RefCount = 1; + } + + public void AddRef() + { + RefCount = checked(RefCount + 1); } - public Vortice.Direct3D12.GpuDescriptorHandle NativeGpuDescriptorHandle + + public bool ReleaseRef() { - get + if (RefCount <= 0) { - return m_Descriptors.ShaderVisible.GpuHandle; + throw new InvalidOperationException("DX12 sampler intern slot underflowed."); } + + RefCount--; + return RefCount == 0; } + } + + internal unsafe class Dx12Sampler : RHISampler, IDx12DescriptorView + { + public Dx12Device Device => m_Dx12Device; + public Dx12DescriptorClass DescriptorClass => Dx12DescriptorClass.Sampler; + public Vortice.Direct3D12.CpuDescriptorHandle NativeCpuDescriptorHandle => m_Allocation.Descriptor.CpuHandle; private bool m_HasDescriptors; + private readonly RHISamplerDescriptor m_Descriptor; private Dx12Device m_Dx12Device; - private Dx12DescriptorPair m_Descriptors; + private Dx12CpuDescriptorAllocation m_Allocation; - public Dx12Sampler(Dx12Device device, in RHISamplerDescriptor descriptor) + public Dx12Sampler( + Dx12Device device, + in RHISamplerDescriptor descriptor, + in Dx12CpuDescriptorAllocation allocation) { - m_HasDescriptors = false; m_Dx12Device = device; - - Vortice.Direct3D12.SamplerDescription desc = new Vortice.Direct3D12.SamplerDescription(); - desc.MinLOD = descriptor.LodMin; - desc.MaxLOD = descriptor.LodMax; - desc.MipLODBias = descriptor.MipLODBias; - desc.MaxAnisotropy = descriptor.Anisotropy; - desc.Filter = Dx12Utility.ConvertToDx12Filter(descriptor); - desc.AddressU = Dx12Utility.ConvertToDx12AddressMode(descriptor.AddressModeU); - desc.AddressV = Dx12Utility.ConvertToDx12AddressMode(descriptor.AddressModeV); - desc.AddressW = Dx12Utility.ConvertToDx12AddressMode(descriptor.AddressModeW); - desc.ComparisonFunction = Dx12Utility.ConvertToDx12ComparisonMode(descriptor.ComparisonMode); - - m_Descriptors = device.AllocateSamplerDescriptorPair(); + m_Descriptor = descriptor; + m_Allocation = allocation; m_HasDescriptors = true; - device.NativeDevice.CreateSampler(ref desc, m_Descriptors.Staging.CpuHandle); - device.CopyDescriptorToShaderVisible(m_Descriptors); + } + + internal static Vortice.Direct3D12.SamplerDescription CreateNativeDescription(in RHISamplerDescriptor descriptor) + { + return new Vortice.Direct3D12.SamplerDescription + { + MinLOD = descriptor.LodMin, + MaxLOD = descriptor.LodMax, + MipLODBias = descriptor.MipLODBias, + MaxAnisotropy = descriptor.Anisotropy, + Filter = Dx12Utility.ConvertToDx12Filter(descriptor), + AddressU = Dx12Utility.ConvertToDx12AddressMode(descriptor.AddressModeU), + AddressV = Dx12Utility.ConvertToDx12AddressMode(descriptor.AddressModeV), + AddressW = Dx12Utility.ConvertToDx12AddressMode(descriptor.AddressModeW), + ComparisonFunction = Dx12Utility.ConvertToDx12ComparisonMode(descriptor.ComparisonMode), + }; } protected override void Release() { if (m_HasDescriptors) { - m_Dx12Device.FreeDescriptorPair(m_Descriptors); + m_Dx12Device.ReleaseSamplerIntern(m_Descriptor); m_HasDescriptors = false; } } diff --git a/src/SharpGPU/Dx12/Dx12TextureView.cs b/src/SharpGPU/Dx12/Dx12TextureView.cs index 7677eaf..7a13ea3 100644 --- a/src/SharpGPU/Dx12/Dx12TextureView.cs +++ b/src/SharpGPU/Dx12/Dx12TextureView.cs @@ -15,26 +15,13 @@ internal unsafe class Dx12TextureView : RHITextureView, IDx12DescriptorView }; public ERHITextureDimension Dimension => m_Dimension; - public Vortice.Direct3D12.CpuDescriptorHandle NativeCpuDescriptorHandle - { - get - { - return m_Descriptors.Staging.CpuHandle; - } - } - public Vortice.Direct3D12.GpuDescriptorHandle NativeGpuDescriptorHandle - { - get - { - return m_Descriptors.ShaderVisible.GpuHandle; - } - } + public Vortice.Direct3D12.CpuDescriptorHandle NativeCpuDescriptorHandle => m_Staging.Descriptor.CpuHandle; private bool m_HasDescriptors; private ERHITextureViewType m_ViewType; private ERHITextureDimension m_Dimension; private Dx12Texture m_Dx12Texture; - private Dx12DescriptorPair m_Descriptors; + private Dx12CpuDescriptorAllocation m_Staging; public Dx12TextureView(Dx12Texture texture, in RHITextureViewDescriptor descriptor) { @@ -52,7 +39,7 @@ public Dx12TextureView(Dx12Texture texture, in RHITextureViewDescriptor descript nameof(descriptor)); } - if(Dx12Utility.IsShaderResourceTexture(texture.Descriptor.UsageFlag)) + if (Dx12Utility.IsShaderResourceTexture(texture.Descriptor.UsageFlag)) { Vortice.Direct3D12.ShaderResourceViewDescription desc = new Vortice.Direct3D12.ShaderResourceViewDescription(); desc.Format = Dx12Utility.ConvertToDx12ViewFormat(texture.Descriptor.Format); @@ -64,10 +51,9 @@ public Dx12TextureView(Dx12Texture texture, in RHITextureViewDescriptor descript Dx12Utility.FillTextureCubeArraySRV(ref desc.TextureCubeArray, descriptor, texture.Descriptor.Dimension); Dx12Utility.FillTexture3DSRV(ref desc.Texture3D, descriptor, texture.Descriptor.Dimension); - m_Descriptors = m_Dx12Texture.Dx12Device.AllocateCbvSrvUavDescriptorPair(); + m_Staging = m_Dx12Texture.Dx12Device.AllocateStagingCbvSrvUavDescriptor(1); m_HasDescriptors = true; - m_Dx12Texture.Dx12Device.NativeDevice.CreateShaderResourceView(m_Dx12Texture.NativeResource, desc, m_Descriptors.Staging.CpuHandle); - m_Dx12Texture.Dx12Device.CopyDescriptorToShaderVisible(m_Descriptors); + m_Dx12Texture.Dx12Device.NativeDevice.CreateShaderResourceView(m_Dx12Texture.NativeResource, desc, m_Staging.Descriptor.CpuHandle); } } else if (descriptor.ViewType == ERHITextureViewType.UnorderedAccess) @@ -82,15 +68,14 @@ public Dx12TextureView(Dx12Texture texture, in RHITextureViewDescriptor descript nameof(descriptor)); } - m_Descriptors = m_Dx12Texture.Dx12Device.AllocateCbvSrvUavDescriptorPair(); + m_Staging = m_Dx12Texture.Dx12Device.AllocateStagingCbvSrvUavDescriptor(1); m_HasDescriptors = true; m_Dx12Texture.Dx12Device.CreateSamplerFeedbackUnorderedAccessView( paired.NativeResource, m_Dx12Texture.NativeResource, - m_Descriptors.Staging.CpuHandle); - m_Dx12Texture.Dx12Device.CopyDescriptorToShaderVisible(m_Descriptors); + m_Staging.Descriptor.CpuHandle); } - else if(Dx12Utility.IsUnorderedAccessTexture(texture.Descriptor.UsageFlag)) + else if (Dx12Utility.IsUnorderedAccessTexture(texture.Descriptor.UsageFlag)) { Vortice.Direct3D12.UnorderedAccessViewDescription desc = new Vortice.Direct3D12.UnorderedAccessViewDescription(); desc.Format = Dx12Utility.ConvertToDx12ViewFormat(texture.Descriptor.Format); @@ -99,10 +84,9 @@ public Dx12TextureView(Dx12Texture texture, in RHITextureViewDescriptor descript Dx12Utility.FillTexture3DUAV(ref desc.Texture3D, descriptor, texture.Descriptor.Dimension); Dx12Utility.FillTexture2DArrayUAV(ref desc.Texture2DArray, descriptor, texture.Descriptor.Dimension); - m_Descriptors = m_Dx12Texture.Dx12Device.AllocateCbvSrvUavDescriptorPair(); + m_Staging = m_Dx12Texture.Dx12Device.AllocateStagingCbvSrvUavDescriptor(1); m_HasDescriptors = true; - m_Dx12Texture.Dx12Device.NativeDevice.CreateUnorderedAccessView(m_Dx12Texture.NativeResource, null, desc, m_Descriptors.Staging.CpuHandle); - m_Dx12Texture.Dx12Device.CopyDescriptorToShaderVisible(m_Descriptors); + m_Dx12Texture.Dx12Device.NativeDevice.CreateUnorderedAccessView(m_Dx12Texture.NativeResource, null, desc, m_Staging.Descriptor.CpuHandle); } } @@ -116,7 +100,7 @@ protected override void Release() { if (m_HasDescriptors) { - m_Dx12Texture.Dx12Device.FreeDescriptorPair(m_Descriptors); + m_Dx12Texture.Dx12Device.FreeStagingCbvSrvUavDescriptor(m_Staging); m_HasDescriptors = false; } } diff --git a/src/SharpGPU/Dx12/Dx12Utility.cs b/src/SharpGPU/Dx12/Dx12Utility.cs index 7b93e25..ce17742 100644 --- a/src/SharpGPU/Dx12/Dx12Utility.cs +++ b/src/SharpGPU/Dx12/Dx12Utility.cs @@ -40,9 +40,271 @@ public Dx12DescriptorPair( } } - internal unsafe class Dx12DescriptorHeap : Disposal + internal static class Dx12DescriptorSizeClass + { + public const int ClassCount = 5; + + public static int Of(in int count) + { + if (count <= 1) + { + return 0; + } + if (count <= 16) + { + return 1; + } + if (count <= 64) + { + return 2; + } + if (count <= 256) + { + return 3; + } + + return 4; + } + } + + internal sealed class Dx12DescriptorRangeAllocator { public int Capacity => m_Capacity; + public int AvailableDescriptorCount + { + get + { + int available = 0; + for (int i = 0; i < m_FreeBlocks.Count; ++i) + { + available = checked(available + m_FreeBlocks.Values[i]); + } + + return available; + } + } + + private readonly int m_Capacity; + private readonly SortedList m_FreeBlocks; + private readonly SortedSet[] m_ClassStarts; + + public Dx12DescriptorRangeAllocator(in int capacity) + { + if (capacity <= 0) + { + throw new ArgumentOutOfRangeException(nameof(capacity), capacity, "DX12 descriptor range capacity must be positive."); + } + + m_Capacity = capacity; + m_FreeBlocks = new SortedList(16); + m_ClassStarts = new SortedSet[Dx12DescriptorSizeClass.ClassCount]; + for (int i = 0; i < m_ClassStarts.Length; ++i) + { + m_ClassStarts[i] = new SortedSet(); + } + + AddBlock(0, m_Capacity); + } + + public int Allocate(in int count) + { + if (count <= 0 || m_FreeBlocks.Count == 0) + { + return -1; + } + + int startClass = Dx12DescriptorSizeClass.Of(count); + for (int cls = startClass; cls < Dx12DescriptorSizeClass.ClassCount; ++cls) + { + int chosenStart = -1; + foreach (int blockStart in m_ClassStarts[cls]) + { + if (m_FreeBlocks[blockStart] >= count) + { + chosenStart = blockStart; + break; + } + } + + if (chosenStart < 0) + { + continue; + } + + int blockSize = m_FreeBlocks[chosenStart]; + RemoveBlock(chosenStart); + if (blockSize > count) + { + AddBlock(chosenStart + count, blockSize - count); + } + + return chosenStart; + } + + return -1; + } + + public void Free(in int index, in int count) + { + if (count <= 0 || index < 0 || index > m_Capacity - count) + { + throw new ArgumentOutOfRangeException(nameof(index), index, $"DX12 descriptor free range [{index}, {index + count}) is outside heap capacity {m_Capacity}."); + } + + for (int i = 0; i < m_FreeBlocks.Count; ++i) + { + int freeStart = m_FreeBlocks.Keys[i]; + int freeEnd = checked(freeStart + m_FreeBlocks.Values[i]); + int releaseEnd = checked(index + count); + if (index < freeEnd && releaseEnd > freeStart) + { + throw new InvalidOperationException($"DX12 descriptor range [{index}, {releaseEnd}) overlaps the already free range [{freeStart}, {freeEnd})."); + } + } + + int newStart = index; + int newSize = count; + + if (m_FreeBlocks.TryGetValue(index + count, out int afterSize)) + { + RemoveBlock(index + count); + newSize += afterSize; + } + + int beforeStart = -1; + foreach (int blockStart in m_FreeBlocks.Keys) + { + if (blockStart + m_FreeBlocks[blockStart] == index) + { + beforeStart = blockStart; + break; + } + } + + if (beforeStart >= 0) + { + newSize += m_FreeBlocks[beforeStart]; + RemoveBlock(beforeStart); + newStart = beforeStart; + } + + AddBlock(newStart, newSize); + } + + public void Clear() + { + m_FreeBlocks.Clear(); + for (int i = 0; i < m_ClassStarts.Length; ++i) + { + m_ClassStarts[i].Clear(); + } + } + + private void AddBlock(in int start, in int size) + { + m_FreeBlocks.Add(start, size); + m_ClassStarts[Dx12DescriptorSizeClass.Of(size)].Add(start); + } + + private void RemoveBlock(in int start) + { + int size = m_FreeBlocks[start]; + m_FreeBlocks.Remove(start); + m_ClassStarts[Dx12DescriptorSizeClass.Of(size)].Remove(start); + } + } + + internal sealed class Dx12DescriptorDirtyRanges + { + public int RangeCount => m_Ranges.Count; + public bool IsEmpty => m_Ranges.Count == 0; + + private readonly List<(int Start, int Count)> m_Ranges = new List<(int Start, int Count)>(4); + + public void Add(in int start, in int count = 1) + { + if (count <= 0 || start < 0) + { + throw new ArgumentOutOfRangeException(nameof(start), start, "DX12 dirty descriptor range must be a non-empty non-negative interval."); + } + + int newStart = start; + int newEnd = checked(start + count); + int insertAt = m_Ranges.Count; + for (int i = 0; i < m_Ranges.Count; ++i) + { + int rangeStart = m_Ranges[i].Start; + int rangeEnd = checked(rangeStart + m_Ranges[i].Count); + if (newEnd < rangeStart) + { + insertAt = i; + break; + } + + if (newStart <= rangeEnd && newEnd >= rangeStart) + { + newStart = Math.Min(newStart, rangeStart); + newEnd = Math.Max(newEnd, rangeEnd); + m_Ranges.RemoveAt(i); + i--; + insertAt = i + 1; + continue; + } + + if (rangeEnd < newStart) + { + insertAt = i + 1; + } + } + + m_Ranges.Insert(insertAt, (newStart, newEnd - newStart)); + } + + public void Clear() + { + m_Ranges.Clear(); + } + + public void CopyTo(List<(int Start, int Count)> destination) + { + ArgumentNullException.ThrowIfNull(destination); + destination.Clear(); + for (int i = 0; i < m_Ranges.Count; ++i) + { + destination.Add(m_Ranges[i]); + } + } + + public bool ShouldCopyAsSingleSpan(out int spanStart, out int spanCount) + { + if (m_Ranges.Count == 0) + { + spanStart = 0; + spanCount = 0; + return false; + } + + spanStart = m_Ranges[0].Start; + int spanEnd = checked(m_Ranges[m_Ranges.Count - 1].Start + m_Ranges[m_Ranges.Count - 1].Count); + spanCount = spanEnd - spanStart; + if (m_Ranges.Count == 1) + { + return true; + } + + int live = 0; + for (int i = 0; i < m_Ranges.Count; ++i) + { + live = checked(live + m_Ranges[i].Count); + } + + return m_Ranges.Count > 16 || live * 2 >= spanCount; + } + } + + internal unsafe class Dx12DescriptorHeap : Disposal + { + public int Capacity => m_RangeAllocator.Capacity; public bool IsShaderVisible => m_IsShaderVisible; public uint DescriptorSize => m_DescriptorSize; public Vortice.Direct3D12.DescriptorHeapType NativeType => m_NativeType; @@ -55,22 +317,15 @@ public int AvailableDescriptorCount { lock (m_AllocationGate) { - int available = 0; - for (int i = 0; i < m_FreeBlocks.Count; ++i) - { - available = checked(available + m_FreeBlocks.Values[i]); - } - - return available; + return m_RangeAllocator.AvailableDescriptorCount; } } } - private int m_Capacity; private readonly object m_AllocationGate = new(); + private readonly Dx12DescriptorRangeAllocator m_RangeAllocator; private bool m_IsShaderVisible; private uint m_DescriptorSize; - private SortedList m_FreeBlocks; private Vortice.Direct3D12.DescriptorHeapType m_NativeType; private Vortice.Direct3D12.ID3D12DescriptorHeap m_NativeDescriptorHeap; @@ -81,9 +336,7 @@ public Dx12DescriptorHeap(Vortice.Direct3D12.ID3D12Device10 device, in Vortice.D throw new ArgumentOutOfRangeException(nameof(count), count, "DX12 descriptor heap capacity must be in the range [1, Int32.MaxValue]."); } - m_Capacity = checked((int)count); - m_FreeBlocks = new SortedList(16); - m_FreeBlocks.Add(0, m_Capacity); + m_RangeAllocator = new Dx12DescriptorRangeAllocator(checked((int)count)); m_NativeType = type; m_IsShaderVisible = (flag & Vortice.Direct3D12.DescriptorHeapFlags.ShaderVisible) != 0; @@ -104,9 +357,9 @@ public Dx12DescriptorHeap(Vortice.Direct3D12.ID3D12Device10 device, in Vortice.D public Dx12DescriptorInfo GetDescriptorInfo(in int index) { - if ((uint)index >= (uint)m_Capacity) + if ((uint)index >= (uint)Capacity) { - throw new ArgumentOutOfRangeException(nameof(index), index, $"DX12 descriptor index must be in [0, {m_Capacity})."); + throw new ArgumentOutOfRangeException(nameof(index), index, $"DX12 descriptor index must be in [0, {Capacity})."); } return new Dx12DescriptorInfo @@ -131,30 +384,7 @@ public int Allocate(in int count) { lock (m_AllocationGate) { - if (count <= 0 || m_FreeBlocks.Count == 0) - { - return -1; - } - - for (int i = 0; i < m_FreeBlocks.Count; ++i) - { - int blockStart = m_FreeBlocks.Keys[i]; - int blockSize = m_FreeBlocks.Values[i]; - - if (blockSize >= count) - { - m_FreeBlocks.RemoveAt(i); - - if (blockSize > count) - { - m_FreeBlocks.Add(blockStart + count, blockSize - count); - } - - return blockStart; - } - } - - return -1; + return m_RangeAllocator.Allocate(count); } } @@ -169,56 +399,7 @@ public void Free(in int index, in int count) { lock (m_AllocationGate) { - if (count <= 0 || index < 0 || index > m_Capacity - count) - { - throw new ArgumentOutOfRangeException(nameof(index), index, $"DX12 descriptor free range [{index}, {index + count}) is outside heap capacity {m_Capacity}."); - } - - for (int i = 0; i < m_FreeBlocks.Count; ++i) - { - int freeStart = m_FreeBlocks.Keys[i]; - int freeEnd = checked(freeStart + m_FreeBlocks.Values[i]); - int releaseEnd = checked(index + count); - if (index < freeEnd && releaseEnd > freeStart) - { - throw new InvalidOperationException($"DX12 descriptor range [{index}, {releaseEnd}) overlaps the already free range [{freeStart}, {freeEnd})."); - } - } - - int newStart = index; - int newSize = count; - - // Try to coalesce with the block immediately after - if (m_FreeBlocks.TryGetValue(index + count, out int afterSize)) - { - newSize += afterSize; - m_FreeBlocks.Remove(index + count); - } - - // Try to coalesce with the block immediately before - int beforeIndex = -1; - for (int i = 0; i < m_FreeBlocks.Count; ++i) - { - int blockStart = m_FreeBlocks.Keys[i]; - int blockSize = m_FreeBlocks.Values[i]; - - if (blockStart + blockSize == index) - { - beforeIndex = i; - break; - } - } - - if (beforeIndex >= 0) - { - int blockStart = m_FreeBlocks.Keys[beforeIndex]; - int blockSize = m_FreeBlocks.Values[beforeIndex]; - newStart = blockStart; - newSize += blockSize; - m_FreeBlocks.RemoveAt(beforeIndex); - } - - m_FreeBlocks.Add(newStart, newSize); + m_RangeAllocator.Free(index, count); } } @@ -226,7 +407,7 @@ protected override void Release() { lock (m_AllocationGate) { - m_FreeBlocks.Clear(); + m_RangeAllocator.Clear(); m_NativeDescriptorHeap.Release(); } } @@ -252,6 +433,24 @@ internal sealed class Dx12CpuDescriptorPool : Disposal private readonly int m_PageCapacity; private readonly List m_Pages = new List(); + public int AllocatedDescriptorCount + { + get + { + lock (m_Gate) + { + int allocated = 0; + for (int i = 0; i < m_Pages.Count; ++i) + { + Dx12DescriptorHeap page = m_Pages[i]; + allocated = checked(allocated + (page.Capacity - page.AvailableDescriptorCount)); + } + + return allocated; + } + } + } + public Dx12CpuDescriptorPool( Vortice.Direct3D12.ID3D12Device10 device, in Vortice.Direct3D12.DescriptorHeapType nativeType, diff --git a/src/SharpGPU/Metal/MetalBindingTable.cs b/src/SharpGPU/Metal/MetalBindingTable.cs index 9780e16..1b91c5c 100644 --- a/src/SharpGPU/Metal/MetalBindingTable.cs +++ b/src/SharpGPU/Metal/MetalBindingTable.cs @@ -13,8 +13,6 @@ internal readonly struct MetalBindInfo public readonly ERHIShaderStageMask Stages; public readonly ERHIBindingRequirement Requirement; - public bool HasDescriptorArray => Count > 1; - public MetalBindInfo( in uint slot, in uint index, diff --git a/src/SharpGPU/Metal/MetalDevice.cs b/src/SharpGPU/Metal/MetalDevice.cs index 53cd2ab..372039f 100644 --- a/src/SharpGPU/Metal/MetalDevice.cs +++ b/src/SharpGPU/Metal/MetalDevice.cs @@ -35,7 +35,7 @@ internal sealed class MetalDevice : RHIDevice private string? m_TimestampQueriesUnavailableReason; private string? m_MetalMLUnavailableReason; private MTLTextureViewPool m_TextureViewPool; - private readonly MetalTextureViewIndexAllocator m_TextureViewIndices = new(); + private MetalTextureViewIndexAllocator m_TextureViewIndices = null!; // Metal 4 ML runtime objects (pipeline / binding table / intermediates heap) are retained // for the device lifetime so a future Metal4-native artifact route can reuse the encoder path. private readonly List m_MetalMLPipelineStates = new List(); @@ -2027,6 +2027,19 @@ internal void ReleaseTextureViewIndex(in MetalTextureViewIndexLease lease) _ = m_TextureViewIndices.Release(lease); } + internal void ReleaseTextureView(in MetalTextureViewIndexLease lease) + { + ClearNativeTextureViewSlot(lease.Index); + _ = m_TextureViewIndices.Release(lease); + } + + private void ClearNativeTextureViewSlot(in uint index) + { + // SharpMetal exposes SetTextureView(texture, index). Passing a null texture pointer + // is the native nil-slot clear; this host cannot execute Metal to verify the slot. + _ = m_TextureViewPool.SetTextureView(IntPtr.Zero, (ulong)index); + } + internal void RemoveResidencyAllocation(in MTLAllocation allocation) { if (allocation.NativePtr == IntPtr.Zero || m_CommandQueueMap == null) @@ -2066,23 +2079,43 @@ private static string SanitizeFileName(string value) private void CreateTextureViewPool() { - MTLResourceViewPoolDescriptor poolDescriptor = MTLResourceViewPoolDescriptor.New(); - NSError error = default; + string? lastError = null; + uint lockedCapacity; try { - poolDescriptor.ResourceViewCount = m_TextureViewIndices.Capacity; - m_TextureViewPool = m_NativeDevice.NewTextureViewPool(poolDescriptor, ref error); + lockedCapacity = MetalTextureViewPoolCapacity.SelectLockedCapacity(capacity => + { + MTLResourceViewPoolDescriptor poolDescriptor = MTLResourceViewPoolDescriptor.New(); + NSError error = default; + try + { + poolDescriptor.ResourceViewCount = capacity; + m_TextureViewPool = m_NativeDevice.NewTextureViewPool(poolDescriptor, ref error); + } + finally + { + ObjectiveCRuntime.Release(poolDescriptor.NativePtr); + } + + if (m_TextureViewPool.NativePtr != IntPtr.Zero) + { + return true; + } + + lastError = error.NativePtr != IntPtr.Zero + ? error.LocalizedDescription.ToString() + : "unknown error"; + return false; + }); } - finally + catch (InvalidOperationException exception) { - ObjectiveCRuntime.Release(poolDescriptor.NativePtr); + throw new InvalidOperationException( + $"Failed to create MTLTextureViewPool: {lastError ?? "unknown error"}", + exception); } - if (m_TextureViewPool.NativePtr == IntPtr.Zero) - { - string errorText = error.NativePtr != IntPtr.Zero ? error.LocalizedDescription.ToString() : "unknown error"; - throw new InvalidOperationException($"Failed to create MTLTextureViewPool: {errorText}"); - } + m_TextureViewIndices = new MetalTextureViewIndexAllocator(lockedCapacity); } protected override void Release() diff --git a/src/SharpGPU/Metal/MetalTextureView.cs b/src/SharpGPU/Metal/MetalTextureView.cs index f96689d..967d889 100644 --- a/src/SharpGPU/Metal/MetalTextureView.cs +++ b/src/SharpGPU/Metal/MetalTextureView.cs @@ -18,6 +18,28 @@ internal MetalTextureViewIndexLease(uint index, ulong generation) } } + internal static class MetalTextureViewPoolCapacity + { + internal static readonly uint[] Ladder = { 262144, 65536, 16384, 4096 }; + + internal static uint SelectLockedCapacity(Func tryCreate) + { + ArgumentNullException.ThrowIfNull(tryCreate); + for (int i = 0; i < Ladder.Length; ++i) + { + uint capacity = Ladder[i]; + if (tryCreate(capacity)) + { + return capacity; + } + } + + throw new InvalidOperationException( + "Failed to create MTLTextureViewPool at any locked capacity " + + $"[{string.Join(", ", Ladder)}]."); + } + } + internal sealed class MetalTextureViewIndexAllocator { internal const uint DefaultCapacity = 4096; @@ -160,7 +182,7 @@ public MetalTextureView(MetalTexture texture, in RHITextureViewDescriptor descri protected override void Release() { - m_Texture.MetalDevice.ReleaseTextureViewIndex(m_PoolLease); + m_Texture.MetalDevice.ReleaseTextureView(m_PoolLease); m_ResourceID = default; } } diff --git a/src/SharpGPU/Vulkan/VulkanBindingTable.cs b/src/SharpGPU/Vulkan/VulkanBindingTable.cs index f7c78f7..1c6effd 100644 --- a/src/SharpGPU/Vulkan/VulkanBindingTable.cs +++ b/src/SharpGPU/Vulkan/VulkanBindingTable.cs @@ -820,6 +820,15 @@ public VulkanBindInfo( public uint StorageBuffers { get; } public uint AccelerationStructures { get; } public uint InputAttachments { get; } + public uint TotalDescriptorCount => + checked( + Samplers + + SampledImages + + StorageImages + + UniformBuffers + + StorageBuffers + + AccelerationStructures + + InputAttachments); public VulkanDescriptorPoolRequirements( uint samplers, @@ -1132,25 +1141,86 @@ public VulkanDescriptorSetLease( } } + internal static class VulkanDescriptorPoolPolicy + { + public static int SetsPerPage(in uint descriptorCount) + { + if (descriptorCount <= 8) + { + return 128; + } + + if (descriptorCount <= 32) + { + return 32; + } + + if (descriptorCount <= 128) + { + return 4; + } + + return 1; + } + + public static bool CanSharePage( + in VulkanDescriptorPoolRequirements page, + in VulkanDescriptorPoolRequirements request) + { + if (page.Equals(request)) + { + return true; + } + + if (!IsSuperset(page, request)) + { + return false; + } + + uint requestTotal = request.TotalDescriptorCount; + uint pageTotal = page.TotalDescriptorCount; + if (requestTotal == 0) + { + return pageTotal == 0; + } + + return pageTotal <= checked(requestTotal * 2u); + } + + private static bool IsSuperset( + in VulkanDescriptorPoolRequirements page, + in VulkanDescriptorPoolRequirements request) + { + return page.Samplers >= request.Samplers + && page.SampledImages >= request.SampledImages + && page.StorageImages >= request.StorageImages + && page.UniformBuffers >= request.UniformBuffers + && page.StorageBuffers >= request.StorageBuffers + && page.AccelerationStructures >= request.AccelerationStructures + && page.InputAttachments >= request.InputAttachments; + } + } + internal sealed class VulkanDescriptorPoolPage { public VkDescriptorPool Pool { get; } public VulkanDescriptorPoolRequirements Requirements { get; } + public int MaxSets { get; } public int AllocatedSetCount { get; set; } public VulkanDescriptorPoolPage( in VkDescriptorPool pool, - in VulkanDescriptorPoolRequirements requirements) + in VulkanDescriptorPoolRequirements requirements, + in int maxSets) { Pool = pool; Requirements = requirements; + MaxSets = maxSets; } } internal unsafe sealed class VulkanDescriptorPoolAllocator : IDisposable { - private const int SetsPerPage = 32; - private readonly object m_Gate = new object(); private readonly VulkanDevice m_Device; private readonly List m_Pages = @@ -1203,29 +1273,14 @@ internal VulkanDescriptorSetLease Allocate( lock (m_Gate) { ThrowIfDisposed(); - for (int pageIndex = 0; pageIndex < m_Pages.Count; ++pageIndex) + if (TryAllocateFromPages(requirements, nativeLayout, exactOnly: true, out VulkanDescriptorSetLease exactLease)) { - VulkanDescriptorPoolPage page = m_Pages[pageIndex]; - if (!page.Requirements.Equals(requirements) - || page.AllocatedSetCount >= SetsPerPage) - { - continue; - } + return exactLease; + } - VkResult result = TryAllocate( - page.Pool, - nativeLayout, - out VkDescriptorSet set); - if (result == VkResult.Success) - { - page.AllocatedSetCount++; - return new VulkanDescriptorSetLease(page.Pool, set); - } - if (result != VkResult.ErrorOutOfPoolMemory - && result != VkResult.ErrorFragmentedPool) - { - VulkanUtility.CheckErrors(result); - } + if (TryAllocateFromPages(requirements, nativeLayout, exactOnly: false, out VulkanDescriptorSetLease sharedLease)) + { + return sharedLease; } VulkanDescriptorPoolPage newPage = CreatePage(requirements); @@ -1310,10 +1365,50 @@ public void Dispose() } } + private bool TryAllocateFromPages( + in VulkanDescriptorPoolRequirements requirements, + in VkDescriptorSetLayout nativeLayout, + in bool exactOnly, + out VulkanDescriptorSetLease lease) + { + for (int pageIndex = 0; pageIndex < m_Pages.Count; ++pageIndex) + { + VulkanDescriptorPoolPage page = m_Pages[pageIndex]; + bool compatible = exactOnly + ? page.Requirements.Equals(requirements) + : VulkanDescriptorPoolPolicy.CanSharePage(page.Requirements, requirements) + && !page.Requirements.Equals(requirements); + if (!compatible || page.AllocatedSetCount >= page.MaxSets) + { + continue; + } + + VkResult result = TryAllocate( + page.Pool, + nativeLayout, + out VkDescriptorSet set); + if (result == VkResult.Success) + { + page.AllocatedSetCount++; + lease = new VulkanDescriptorSetLease(page.Pool, set); + return true; + } + if (result != VkResult.ErrorOutOfPoolMemory + && result != VkResult.ErrorFragmentedPool) + { + VulkanUtility.CheckErrors(result); + } + } + + lease = default; + return false; + } + private VulkanDescriptorPoolPage CreatePage( in VulkanDescriptorPoolRequirements requirements) { - VkDescriptorPoolSize[] sizes = BuildPoolSizes(requirements); + int maxSets = VulkanDescriptorPoolPolicy.SetsPerPage(requirements.TotalDescriptorCount); + VkDescriptorPoolSize[] sizes = BuildPoolSizes(requirements, maxSets); VkDescriptorPool pool = default; fixed (VkDescriptorPoolSize* sizesPointer = sizes) { @@ -1322,7 +1417,7 @@ private VulkanDescriptorPoolPage CreatePage( { sType = VkStructureType.DescriptorPoolCreateInfo, flags = VkDescriptorPoolCreateFlags.FreeDescriptorSet, - maxSets = SetsPerPage, + maxSets = (uint)maxSets, poolSizeCount = checked((uint)sizes.Length), pPoolSizes = sizes.Length == 0 ? null : sizesPointer, }; @@ -1334,46 +1429,54 @@ private VulkanDescriptorPoolPage CreatePage( &pool)); } - return new VulkanDescriptorPoolPage(pool, requirements); + return new VulkanDescriptorPoolPage(pool, requirements, maxSets); } private static VkDescriptorPoolSize[] BuildPoolSizes( - in VulkanDescriptorPoolRequirements requirements) + in VulkanDescriptorPoolRequirements requirements, + in int maxSets) { List sizes = new List(7); - AddSize(sizes, VkDescriptorType.Sampler, requirements.Samplers); + AddSize(sizes, VkDescriptorType.Sampler, requirements.Samplers, maxSets); AddSize( sizes, VkDescriptorType.SampledImage, - requirements.SampledImages); + requirements.SampledImages, + maxSets); AddSize( sizes, VkDescriptorType.StorageImage, - requirements.StorageImages); + requirements.StorageImages, + maxSets); AddSize( sizes, VkDescriptorType.UniformBuffer, - requirements.UniformBuffers); + requirements.UniformBuffers, + maxSets); AddSize( sizes, VkDescriptorType.StorageBuffer, - requirements.StorageBuffers); + requirements.StorageBuffers, + maxSets); AddSize( sizes, VkDescriptorType.AccelerationStructureKHR, - requirements.AccelerationStructures); + requirements.AccelerationStructures, + maxSets); AddSize( sizes, VkDescriptorType.InputAttachment, - requirements.InputAttachments); + requirements.InputAttachments, + maxSets); return sizes.ToArray(); } private static void AddSize( List sizes, in VkDescriptorType type, - in uint descriptorsPerSet) + in uint descriptorsPerSet, + in int maxSets) { if (descriptorsPerSet == 0) { @@ -1384,7 +1487,7 @@ private static void AddSize( { type = type, descriptorCount = checked( - descriptorsPerSet * (uint)SetsPerPage), + descriptorsPerSet * (uint)maxSets), }); } diff --git a/tests/SharpGPU.Conformance.Tests/Dx12BindingTableBindingTests.cs b/tests/SharpGPU.Conformance.Tests/Dx12BindingTableBindingTests.cs index 73ffb70..33429a4 100644 --- a/tests/SharpGPU.Conformance.Tests/Dx12BindingTableBindingTests.cs +++ b/tests/SharpGPU.Conformance.Tests/Dx12BindingTableBindingTests.cs @@ -315,6 +315,8 @@ public void BindingTable_DescriptorExhaustion_RollsBackPreviouslyAllocatedGroups _ = device.NullDescriptors.Get(ERHIBindType.Buffer); int cbvSrvUavAvailable = device.DescriptorHeapCbvSrvUav.AvailableDescriptorCount; int samplerAvailable = device.DescriptorHeapSampler.AvailableDescriptorCount; + int stagingCbvAllocated = device.StagingPoolCbvSrvUav.AllocatedDescriptorCount; + int stagingSamplerAllocated = device.StagingPoolSampler.AllocatedDescriptorCount; using RHIBindingTableLayout layout = context.Device.CreateBindingTableLayout(new RHIBindingTableLayoutDescriptor { @@ -333,6 +335,8 @@ public void BindingTable_DescriptorExhaustion_RollsBackPreviouslyAllocatedGroups })); Assert.Equal(cbvSrvUavAvailable, device.DescriptorHeapCbvSrvUav.AvailableDescriptorCount); Assert.Equal(samplerAvailable, device.DescriptorHeapSampler.AvailableDescriptorCount); + Assert.Equal(stagingCbvAllocated, device.StagingPoolCbvSrvUav.AllocatedDescriptorCount); + Assert.Equal(stagingSamplerAllocated, device.StagingPoolSampler.AllocatedDescriptorCount); using Dx12CpuDescriptorPool stagingPool = new Dx12CpuDescriptorPool( device.NativeDevice, diff --git a/tests/SharpGPU.Conformance.Tests/Dx12DescriptorAllocatorTests.cs b/tests/SharpGPU.Conformance.Tests/Dx12DescriptorAllocatorTests.cs new file mode 100644 index 0000000..1d9681c --- /dev/null +++ b/tests/SharpGPU.Conformance.Tests/Dx12DescriptorAllocatorTests.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using Xunit; + +namespace SharpGPU.Conformance.Tests; + +public sealed class Dx12DescriptorAllocatorTests +{ + [Fact] + public void SizeClass_MapsCountsToStableTiers() + { + Assert.Equal(0, Dx12DescriptorSizeClass.Of(1)); + Assert.Equal(1, Dx12DescriptorSizeClass.Of(2)); + Assert.Equal(1, Dx12DescriptorSizeClass.Of(16)); + Assert.Equal(2, Dx12DescriptorSizeClass.Of(17)); + Assert.Equal(2, Dx12DescriptorSizeClass.Of(64)); + Assert.Equal(3, Dx12DescriptorSizeClass.Of(65)); + Assert.Equal(3, Dx12DescriptorSizeClass.Of(256)); + Assert.Equal(4, Dx12DescriptorSizeClass.Of(257)); + Assert.Equal(4, Dx12DescriptorSizeClass.Of(1024)); + } + + [Fact] + public void RangeAllocator_PrefersMatchingClassOverCarvingALargeBlock() + { + Dx12DescriptorRangeAllocator allocator = new Dx12DescriptorRangeAllocator(1024); + int large = allocator.Allocate(300); + int smallA = allocator.Allocate(1); + int smallB = allocator.Allocate(1); + Assert.Equal(0, large); + Assert.Equal(300, smallA); + Assert.Equal(301, smallB); + + allocator.Free(smallA, 1); + + int reusedSmall = allocator.Allocate(1); + Assert.Equal(smallA, reusedSmall); + Assert.Equal(722, allocator.AvailableDescriptorCount); + } + + [Fact] + public void RangeAllocator_CoalescesAcrossClassesAndRejectsDoubleFree() + { + Dx12DescriptorRangeAllocator allocator = new Dx12DescriptorRangeAllocator(32); + int first = allocator.Allocate(8); + int second = allocator.Allocate(8); + Assert.Equal(0, first); + Assert.Equal(8, second); + allocator.Free(first, 8); + allocator.Free(second, 8); + Assert.Equal(0, allocator.Allocate(16)); + Assert.Equal(16, allocator.AvailableDescriptorCount); + allocator.Free(0, 16); + Assert.Throws(() => allocator.Free(0, 8)); + } + + [Fact] + public void DirtyRanges_CoalesceAndChooseSingleSpanWhenCoverageIsHigh() + { + Dx12DescriptorDirtyRanges dirty = new Dx12DescriptorDirtyRanges(); + dirty.Add(0, 1); + dirty.Add(2, 1); + dirty.Add(1, 1); + Assert.Equal(1, dirty.RangeCount); + Assert.True(dirty.ShouldCopyAsSingleSpan(out int start, out int count)); + Assert.Equal(0, start); + Assert.Equal(3, count); + + dirty.Clear(); + dirty.Add(0, 1); + dirty.Add(100, 1); + Assert.Equal(2, dirty.RangeCount); + Assert.False(dirty.ShouldCopyAsSingleSpan(out start, out count)); + Assert.Equal(0, start); + Assert.Equal(101, count); + + List<(int Start, int Count)> copied = new List<(int Start, int Count)>(); + dirty.CopyTo(copied); + Assert.Equal(new[] { (0, 1), (100, 1) }, copied); + } + + [Fact] + public void SamplerInternKey_MatchesCompleteDescriptorAndSlotRefCountReachesZero() + { + RHISamplerDescriptor descriptor = new RHISamplerDescriptor + { + LodMin = 0, + LodMax = 8, + MipLODBias = 0.25f, + Anisotropy = 4, + MinFilter = ERHIFilterMode.Linear, + MagFilter = ERHIFilterMode.Linear, + MipFilter = ERHIFilterMode.Point, + AddressModeU = ERHIAddressMode.ClampToEdge, + AddressModeV = ERHIAddressMode.Repeat, + AddressModeW = ERHIAddressMode.MirrorRepeat, + ComparisonMode = ERHIComparisonMode.Less, + }; + Dx12SamplerInternKey left = Dx12SamplerInternKey.From(descriptor); + Dx12SamplerInternKey right = Dx12SamplerInternKey.From(descriptor); + Assert.Equal(left, right); + Assert.Equal(left.GetHashCode(), right.GetHashCode()); + + descriptor.Anisotropy = 8; + Assert.NotEqual(left, Dx12SamplerInternKey.From(descriptor)); + + Dx12SamplerInternSlot slot = new Dx12SamplerInternSlot(default); + Assert.Equal(1, slot.RefCount); + slot.AddRef(); + Assert.Equal(2, slot.RefCount); + Assert.False(slot.ReleaseRef()); + Assert.True(slot.ReleaseRef()); + Assert.Equal(0, slot.RefCount); + Assert.Throws(() => slot.ReleaseRef()); + } +} diff --git a/tests/SharpGPU.Conformance.Tests/MetalBindingTablePlanTests.cs b/tests/SharpGPU.Conformance.Tests/MetalBindingTablePlanTests.cs index 9031610..3b13802 100644 --- a/tests/SharpGPU.Conformance.Tests/MetalBindingTablePlanTests.cs +++ b/tests/SharpGPU.Conformance.Tests/MetalBindingTablePlanTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Reflection; using System.Runtime.CompilerServices; using SharpGPU; @@ -252,6 +253,31 @@ public void BindingTable_ShouldRejectOperationsAfterItsLayoutIsDisposed() table.SetBindElement(default, ERHIBindType.Buffer, 0)); } + [Fact] + public void TextureViewPool_ShouldLockFirstSuccessfulCapacityOnTheLadder() + { + Assert.Equal(new uint[] { 262144, 65536, 16384, 4096 }, MetalTextureViewPoolCapacity.Ladder); + + List attempted = new(); + uint locked = MetalTextureViewPoolCapacity.SelectLockedCapacity(capacity => + { + attempted.Add(capacity); + return capacity == 16384; + }); + + Assert.Equal(16384u, locked); + Assert.Equal(new uint[] { 262144, 65536, 16384 }, attempted); + Assert.Equal( + 16384u, + new MetalTextureViewIndexAllocator(locked).Capacity); + InvalidOperationException exhausted = Assert.Throws( + () => MetalTextureViewPoolCapacity.SelectLockedCapacity(_ => false)); + Assert.Contains("262144", exhausted.Message, StringComparison.Ordinal); + Assert.Contains("4096", exhausted.Message, StringComparison.Ordinal); + // TODO(UNVERIFIED): Metal host create/dispose must clear the native ViewPool slot + // and reuse the software index. This environment has no Metal device. + } + [Fact] public void TextureViewIndexAllocator_ShouldExhaustReuseAndRejectStaleOrDoubleRelease() { diff --git a/tests/SharpGPU.Conformance.Tests/VulkanArgumentTablePlanTests.cs b/tests/SharpGPU.Conformance.Tests/VulkanArgumentTablePlanTests.cs index 6883dd7..527d4a3 100644 --- a/tests/SharpGPU.Conformance.Tests/VulkanArgumentTablePlanTests.cs +++ b/tests/SharpGPU.Conformance.Tests/VulkanArgumentTablePlanTests.cs @@ -30,6 +30,54 @@ public void Plan_ShouldMapLogicalNamespacesToDensePhysicalBindings() Assert.Equal(1u, plan.PoolRequirements.Samplers); } + [Fact] + public void PoolPolicy_ShouldTierSetsPerPageAndAllowSupersetShareWithinWasteLimit() + { + Assert.Equal(128, VulkanDescriptorPoolPolicy.SetsPerPage(1)); + Assert.Equal(128, VulkanDescriptorPoolPolicy.SetsPerPage(8)); + Assert.Equal(32, VulkanDescriptorPoolPolicy.SetsPerPage(9)); + Assert.Equal(32, VulkanDescriptorPoolPolicy.SetsPerPage(32)); + Assert.Equal(4, VulkanDescriptorPoolPolicy.SetsPerPage(33)); + Assert.Equal(4, VulkanDescriptorPoolPolicy.SetsPerPage(128)); + Assert.Equal(1, VulkanDescriptorPoolPolicy.SetsPerPage(129)); + + VulkanDescriptorPoolRequirements exact = new( + samplers: 2, + sampledImages: 1, + storageImages: 0, + uniformBuffers: 0, + storageBuffers: 1, + accelerationStructures: 0); + Assert.True(VulkanDescriptorPoolPolicy.CanSharePage(exact, exact)); + + VulkanDescriptorPoolRequirements shareable = new( + samplers: 2, + sampledImages: 2, + storageImages: 0, + uniformBuffers: 0, + storageBuffers: 1, + accelerationStructures: 0); + Assert.True(VulkanDescriptorPoolPolicy.CanSharePage(shareable, exact)); + + VulkanDescriptorPoolRequirements tooWasteful = new( + samplers: 8, + sampledImages: 8, + storageImages: 0, + uniformBuffers: 0, + storageBuffers: 1, + accelerationStructures: 0); + Assert.False(VulkanDescriptorPoolPolicy.CanSharePage(tooWasteful, exact)); + + VulkanDescriptorPoolRequirements missingStorage = new( + samplers: 2, + sampledImages: 2, + storageImages: 0, + uniformBuffers: 0, + storageBuffers: 0, + accelerationStructures: 0); + Assert.False(VulkanDescriptorPoolPolicy.CanSharePage(missingStorage, exact)); + } + [Fact] public void Plan_ShouldRejectUnknownInvalidAndAmbiguousBindings() {