From 34466cac1d720fb00253fc47e85f4d79a16288a6 Mon Sep 17 00:00:00 2001 From: Alex Sepkowski Date: Tue, 26 May 2026 17:52:07 -0700 Subject: [PATCH 01/11] [DX] Support multi-mip Texture2D SRVs (#1039) Lifts the early-return guard on MipLevels > 1 for SRV textures and threads the mip count through to the D3D12_RESOURCE_DESC and the SRV's MipLevels field. Adds a per-mip upload path using GetCopyableFootprints so each subresource is copied with the correct D3D12_TEXTURE_DATA_PITCH_ALIGNMENT row pitch. The single-mip path is unchanged. RWTexture2D mips are still rejected with not_supported (no per-mip UAV support yet). Adds a new Texture2D.Load.MipMaps.test.yaml covering the 4x4 + 3-mip layout documented in docs/MipMappedTextures.md. Re-enables Texture2D.mips.OperatorIndex / Texture2D.OperatorIndex / Texture2D.GetDimensions on dxc-d3d12 and dxc-warp-d3d12 by qualifying the XFAIL to 'Clang && DirectX' for the unrelated Clang-DX path tracked in llvm/llvm-project#101558. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/API/DX/Device.cpp | 93 ++++++++++++++----- .../Texture2D.GetDimensions.test.yaml | 2 +- .../Textures/Texture2D.Load.MipMaps.test.yaml | 80 ++++++++++++++++ .../Texture2D.OperatorIndex.test.yaml | 2 +- .../Texture2D.mips.OperatorIndex.test.yaml | 4 +- 5 files changed, 155 insertions(+), 26 deletions(-) create mode 100644 test/Feature/Textures/Texture2D.Load.MipMaps.test.yaml diff --git a/lib/API/DX/Device.cpp b/lib/API/DX/Device.cpp index 1a5d09124..8ccdc8c35 100644 --- a/lib/API/DX/Device.cpp +++ b/lib/API/DX/Device.cpp @@ -227,16 +227,18 @@ getResourceDescription(const Resource &R) { const D3D12_RESOURCE_DIMENSION Dimension = getDXDimension(R.Kind); const offloadtest::CPUBuffer &B = *R.BufferPtr; - if (B.OutputProps.MipLevels != 1) + if (B.OutputProps.MipLevels > 1 && getDescriptorKind(R.Kind) != DescriptorKind::SRV) return llvm::createStringError(std::errc::not_supported, - "Multiple mip levels are not yet supported " - "for DirectX textures."); + "Multiple mip levels are only supported " + "for read-only SRV textures."); const DXGI_FORMAT Format = R.isTexture() ? getDXFormat(B.Format, B.Channels) : DXGI_FORMAT_UNKNOWN; const uint32_t Width = R.isTexture() ? B.OutputProps.Width : getUAVBufferSize(R); const uint32_t Height = R.isTexture() ? B.OutputProps.Height : 1; + const uint16_t MipLevels = + R.isTexture() ? static_cast(B.OutputProps.MipLevels) : 1; D3D12_TEXTURE_LAYOUT Layout; if (R.isTexture()) @@ -251,8 +253,9 @@ getResourceDescription(const Resource &R) { const D3D12_RESOURCE_FLAGS Flags = R.isReadWrite() ? D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS : D3D12_RESOURCE_FLAG_NONE; - const D3D12_RESOURCE_DESC ResDesc = {Dimension, 0, Width, Height, 1, 1, - Format, {1, 0}, Layout, Flags}; + const D3D12_RESOURCE_DESC ResDesc = {Dimension, 0, Width, Height, 1, + MipLevels, Format, {1, 0}, + Layout, Flags}; return ResDesc; } @@ -280,7 +283,8 @@ static D3D12_SHADER_RESOURCE_VIEW_DESC getSRVDescription(const Resource &R) { break; case ResourceKind::Texture2D: Desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; - Desc.Texture2D = D3D12_TEX2D_SRV{0, 1, 0, 0}; + Desc.Texture2D = D3D12_TEX2D_SRV{ + 0, static_cast(R.BufferPtr->OutputProps.MipLevels), 0, 0.0f}; break; case ResourceKind::RWStructuredBuffer: case ResourceKind::RWBuffer: @@ -1517,21 +1521,31 @@ class DXDevice : public offloadtest::Device { return std::make_unique(Desc); } - void addResourceUploadCommands(Resource &R, InvocationState &IS, - ComPtr Destination, - ComPtr Source) { + void addResourceUploadCommands( + Resource &R, InvocationState &IS, ComPtr Destination, + ComPtr Source, + llvm::ArrayRef MipFootprints = {}) { addUploadBeginBarrier(IS, Destination); if (R.isTexture()) { const offloadtest::CPUBuffer &B = *R.BufferPtr; - const D3D12_PLACED_SUBRESOURCE_FOOTPRINT Footprint{ - 0, CD3DX12_SUBRESOURCE_FOOTPRINT( - getDXFormat(B.Format, B.Channels), B.OutputProps.Width, - B.OutputProps.Height, 1, - B.OutputProps.Width * B.getElementSize())}; - const CD3DX12_TEXTURE_COPY_LOCATION DstLoc(Destination.Get(), 0); - const CD3DX12_TEXTURE_COPY_LOCATION SrcLoc(Source.Get(), Footprint); - - IS.CB->CmdList->CopyTextureRegion(&DstLoc, 0, 0, 0, &SrcLoc, nullptr); + if (!MipFootprints.empty()) { + for (uint32_t Mip = 0; Mip < MipFootprints.size(); ++Mip) { + const CD3DX12_TEXTURE_COPY_LOCATION DstLoc(Destination.Get(), Mip); + const CD3DX12_TEXTURE_COPY_LOCATION SrcLoc(Source.Get(), + MipFootprints[Mip]); + IS.CB->CmdList->CopyTextureRegion(&DstLoc, 0, 0, 0, &SrcLoc, nullptr); + } + } else { + const D3D12_PLACED_SUBRESOURCE_FOOTPRINT Footprint{ + 0, CD3DX12_SUBRESOURCE_FOOTPRINT( + getDXFormat(B.Format, B.Channels), B.OutputProps.Width, + B.OutputProps.Height, 1, + B.OutputProps.Width * B.getElementSize())}; + const CD3DX12_TEXTURE_COPY_LOCATION DstLoc(Destination.Get(), 0); + const CD3DX12_TEXTURE_COPY_LOCATION SrcLoc(Source.Get(), Footprint); + + IS.CB->CmdList->CopyTextureRegion(&DstLoc, 0, 0, 0, &SrcLoc, nullptr); + } } else IS.CB->CmdList->CopyBufferRegion(Destination.Get(), 0, Source.Get(), 0, R.size()); @@ -1613,8 +1627,24 @@ class DXDevice : public offloadtest::Device { const D3D12_RESOURCE_DESC ResDesc = *ResDescOrErr; const D3D12_HEAP_PROPERTIES UploadHeapProp = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD); - const D3D12_RESOURCE_DESC UploadResDesc = - CD3DX12_RESOURCE_DESC::Buffer(R.size()); + + const bool IsMipMappedTexture = + R.isTexture() && R.BufferPtr->OutputProps.MipLevels > 1; + llvm::SmallVector MipFootprints; + llvm::SmallVector MipNumRows; + llvm::SmallVector MipRowSizes; + UINT64 MipTotalBytes = 0; + if (IsMipMappedTexture) { + const uint32_t MipLevels = R.BufferPtr->OutputProps.MipLevels; + MipFootprints.resize(MipLevels); + MipNumRows.resize(MipLevels); + MipRowSizes.resize(MipLevels); + Device->GetCopyableFootprints(&ResDesc, 0, MipLevels, 0, + MipFootprints.data(), MipNumRows.data(), + MipRowSizes.data(), &MipTotalBytes); + } + const D3D12_RESOURCE_DESC UploadResDesc = CD3DX12_RESOURCE_DESC::Buffer( + IsMipMappedTexture ? MipTotalBytes : R.size()); uint32_t RegOffset = 0; @@ -1666,14 +1696,33 @@ class DXDevice : public offloadtest::Device { // Upload data initialization void *ResDataPtr = nullptr; if (SUCCEEDED(UploadBuffer->Map(0, NULL, &ResDataPtr))) { - memcpy(ResDataPtr, ResData.get(), R.size()); + if (IsMipMappedTexture) { + // Source CPU data is tightly packed for all mips (per docs). + // Destination upload buffer has D3D12-aligned per-mip layout from + // GetCopyableFootprints; copy each mip row-by-row applying the + // possibly-padded row pitch. + const uint8_t *Src = reinterpret_cast(ResData.get()); + uint8_t *Dst = static_cast(ResDataPtr); + for (uint32_t Mip = 0; Mip < MipFootprints.size(); ++Mip) { + const auto &FP = MipFootprints[Mip]; + const size_t TightRowBytes = static_cast(MipRowSizes[Mip]); + const size_t PaddedRowPitch = + static_cast(FP.Footprint.RowPitch); + for (UINT Row = 0; Row < MipNumRows[Mip]; ++Row) + memcpy(Dst + FP.Offset + Row * PaddedRowPitch, + Src + Row * TightRowBytes, TightRowBytes); + Src += TightRowBytes * MipNumRows[Mip]; + } + } else { + memcpy(ResDataPtr, ResData.get(), R.size()); + } UploadBuffer->Unmap(0, nullptr); } else { return llvm::createStringError(std::errc::io_error, "Failed to map SRV upload buffer."); } - addResourceUploadCommands(R, IS, Buffer, UploadBuffer); + addResourceUploadCommands(R, IS, Buffer, UploadBuffer, MipFootprints); Bundle.emplace_back(UploadBuffer, Buffer, nullptr, Heap); RegOffset++; diff --git a/test/Feature/Textures/Texture2D.GetDimensions.test.yaml b/test/Feature/Textures/Texture2D.GetDimensions.test.yaml index 484fcc93e..d74667a1e 100644 --- a/test/Feature/Textures/Texture2D.GetDimensions.test.yaml +++ b/test/Feature/Textures/Texture2D.GetDimensions.test.yaml @@ -115,7 +115,7 @@ Results: #--- end # Unimplemented: Clang + DX: https://github.com/llvm/llvm-project/issues/101558 -# XFAIL: DirectX || Metal +# XFAIL: (Clang && DirectX) || Metal # Bug https://github.com/llvm/llvm-project/issues/197837 # XFAIL: Clang && Vulkan diff --git a/test/Feature/Textures/Texture2D.Load.MipMaps.test.yaml b/test/Feature/Textures/Texture2D.Load.MipMaps.test.yaml new file mode 100644 index 000000000..750168d8f --- /dev/null +++ b/test/Feature/Textures/Texture2D.Load.MipMaps.test.yaml @@ -0,0 +1,80 @@ +#--- source.hlsl +[[vk::binding(0, 0)]] Texture2D Tex : register(t0); +[[vk::binding(1, 0)]] RWBuffer Out : register(u0); + +[numthreads(1, 1, 1)] +void main() { + // Texture2D::Load(int3(x, y, mip)) + // Mip 0 (4x4) is Red + Out[0] = Tex.Load(int3(0, 0, 0)); + Out[1] = Tex.Load(int3(3, 3, 0)); + // Mip 1 (2x2) is Green + Out[2] = Tex.Load(int3(0, 0, 1)); + Out[3] = Tex.Load(int3(1, 1, 1)); + // Mip 2 (1x1) is Blue + Out[4] = Tex.Load(int3(0, 0, 2)); +} + +//--- pipeline.yaml +--- +Shaders: + - Stage: Compute + Entry: main + +Buffers: + - Name: Tex + Format: Float32 + Channels: 4 + OutputProps: { Width: 4, Height: 4, Depth: 1, MipLevels: 3 } + Data: [ + # --- Mip 0 (4x4 = 16 RGBA texels) - Red --- + 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, + 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, + 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, + 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, + # --- Mip 1 (2x2 = 4 RGBA texels) - Green --- + 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, + 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, + # --- Mip 2 (1x1 = 1 RGBA texel) - Blue --- + 0.0, 0.0, 1.0, 1.0 + ] + + - Name: Out + Format: Float32 + Channels: 4 + FillSize: 80 # 5 * sizeof(float4) + + - Name: Expected + Format: Float32 + Channels: 4 + Data: [ 1.0, 0.0, 0.0, 1.0, # [0] Red (mip 0 corner) + 1.0, 0.0, 0.0, 1.0, # [1] Red (mip 0 corner) + 0.0, 1.0, 0.0, 1.0, # [2] Green (mip 1 corner) + 0.0, 1.0, 0.0, 1.0, # [3] Green (mip 1 corner) + 0.0, 0.0, 1.0, 1.0 ] # [4] Blue (mip 2) + +DescriptorSets: + - Resources: + - Name: Tex + Kind: Texture2D + DirectXBinding: { Register: 0, Space: 0 } + VulkanBinding: { Binding: 0 } + - Name: Out + Kind: RWBuffer + DirectXBinding: { Register: 0, Space: 0 } + VulkanBinding: { Binding: 1 } + +Results: + - Result: LoadMipsTest + Rule: BufferExact + Actual: Out + Expected: Expected +... +#--- end + +# XFAIL: Clang && DirectX +# XFAIL: Metal + +# RUN: split-file %s %t +# RUN: %dxc_target -T cs_6_0 -Fo %t.o %t/source.hlsl +# RUN: %offloader %t/pipeline.yaml %t.o diff --git a/test/Feature/Textures/Texture2D.OperatorIndex.test.yaml b/test/Feature/Textures/Texture2D.OperatorIndex.test.yaml index e57df3748..00185fd28 100644 --- a/test/Feature/Textures/Texture2D.OperatorIndex.test.yaml +++ b/test/Feature/Textures/Texture2D.OperatorIndex.test.yaml @@ -61,7 +61,7 @@ Results: #--- end # Unimplemented: Clang + DX: https://github.com/llvm/llvm-project/issues/101558 -# XFAIL: DirectX +# XFAIL: Clang && DirectX # RUN: split-file %s %t # RUN: %dxc_target -T cs_6_0 -Fo %t.o %t/source.hlsl diff --git a/test/Feature/Textures/Texture2D.mips.OperatorIndex.test.yaml b/test/Feature/Textures/Texture2D.mips.OperatorIndex.test.yaml index 49f8ee4ba..6fd165d14 100644 --- a/test/Feature/Textures/Texture2D.mips.OperatorIndex.test.yaml +++ b/test/Feature/Textures/Texture2D.mips.OperatorIndex.test.yaml @@ -61,8 +61,8 @@ Results: #--- end # Unimplemented: https://github.com/llvm/offload-test-suite/issues/1039 -# XFAIL: DirectX - +# DXC+DX (d3d12/warp) now supported; Clang+DX still has a separate issue. +# XFAIL: Clang && DirectX # XFAIL: Metal # RUN: split-file %s %t From 5eb4bd0165cf2596069559e31bb3a20440e1e920 Mon Sep 17 00:00:00 2001 From: Alex Sepkowski Date: Tue, 26 May 2026 18:16:25 -0700 Subject: [PATCH 02/11] [DX] Validate MipLevels >= 1 in getResourceDescription Addresses code review feedback on the multi-mip support change: the relaxed guard (changed from `!= 1` to `> 1 && !SRV`) silently accepted MipLevels values of 0 or negative integers. Since OutputProps.MipLevels is signed int, 0 would cast to a UINT(0) in the SRV desc (which D3D12 interprets as the magic "use all mip levels" value) and negative values would wrap to a huge uint16_t in the RESOURCE_DESC. Add an explicit lower-bound check so invalid input is rejected with a clear error instead of silently producing surprising behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/API/DX/Device.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/API/DX/Device.cpp b/lib/API/DX/Device.cpp index 8ccdc8c35..422aabe3a 100644 --- a/lib/API/DX/Device.cpp +++ b/lib/API/DX/Device.cpp @@ -227,6 +227,9 @@ getResourceDescription(const Resource &R) { const D3D12_RESOURCE_DIMENSION Dimension = getDXDimension(R.Kind); const offloadtest::CPUBuffer &B = *R.BufferPtr; + if (B.OutputProps.MipLevels < 1) + return llvm::createStringError(std::errc::invalid_argument, + "MipLevels must be >= 1."); if (B.OutputProps.MipLevels > 1 && getDescriptorKind(R.Kind) != DescriptorKind::SRV) return llvm::createStringError(std::errc::not_supported, "Multiple mip levels are only supported " From 33313c82a2b6bd189651cf40feb49a46b14ccfb8 Mon Sep 17 00:00:00 2001 From: Alex Sepkowski Date: Tue, 26 May 2026 18:27:20 -0700 Subject: [PATCH 03/11] [NFC] clang-format Apply clang-format 19.1.6 to changed regions per pr-code-format CI. --- lib/API/DX/Device.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/API/DX/Device.cpp b/lib/API/DX/Device.cpp index 422aabe3a..74fe55ad0 100644 --- a/lib/API/DX/Device.cpp +++ b/lib/API/DX/Device.cpp @@ -230,7 +230,8 @@ getResourceDescription(const Resource &R) { if (B.OutputProps.MipLevels < 1) return llvm::createStringError(std::errc::invalid_argument, "MipLevels must be >= 1."); - if (B.OutputProps.MipLevels > 1 && getDescriptorKind(R.Kind) != DescriptorKind::SRV) + if (B.OutputProps.MipLevels > 1 && + getDescriptorKind(R.Kind) != DescriptorKind::SRV) return llvm::createStringError(std::errc::not_supported, "Multiple mip levels are only supported " "for read-only SRV textures."); @@ -256,9 +257,8 @@ getResourceDescription(const Resource &R) { const D3D12_RESOURCE_FLAGS Flags = R.isReadWrite() ? D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS : D3D12_RESOURCE_FLAG_NONE; - const D3D12_RESOURCE_DESC ResDesc = {Dimension, 0, Width, Height, 1, - MipLevels, Format, {1, 0}, - Layout, Flags}; + const D3D12_RESOURCE_DESC ResDesc = { + Dimension, 0, Width, Height, 1, MipLevels, Format, {1, 0}, Layout, Flags}; return ResDesc; } From 4774832e92d6989ef5f5bc344e05c2bc731b5d33 Mon Sep 17 00:00:00 2001 From: Alex Sepkowski Date: Tue, 26 May 2026 19:50:44 -0700 Subject: [PATCH 04/11] [DX] Add Texture2DArray Load support (#1077) Adds a new `Texture2DArray` `ResourceKind` and wires it through the DirectX backend so shaders can `Load` from layered SRVs: * `OutputProperties` gains an `ArraySize` field (default 1) and the YAML size validation now accounts for it. * `getResourceDescription` sets `DepthOrArraySize` from `ArraySize` for `Texture2DArray` resources and rejects mismatched uses of `ArraySize > 1` on non-array kinds. * `getSRVDescription` emits a `TEXTURE2DARRAY` view covering all slices. * The upload path enumerates slices via `CopyTextureRegion` with a per-slice `D3D12_PLACED_SUBRESOURCE_FOOTPRINT`; the host-side data layout is tightly packed slice-major (slice0 rows | slice1 rows | ...). Vulkan and Metal switches gain `Texture2DArray` cases for build completeness; their device implementations remain TODO and the new test is gated `UNSUPPORTED: Vulkan || Metal`. Adds `Feature/Textures/Texture2DArray.Load.test.yaml` exercising a 2x2x3 array with `Load(int4)` and offset variants. Verified passing on d3d12 (NVIDIA RTX 5060 Ti) and warp-d3d12. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- include/API/Enums.h | 1 + include/Support/Pipeline.h | 7 ++ lib/API/DX/Device.cpp | 48 +++++++-- lib/API/MTL/MTLDevice.cpp | 2 + lib/API/VK/Device.cpp | 5 + lib/Support/Pipeline.cpp | 2 + .../Textures/Texture2DArray.Load.test.yaml | 101 ++++++++++++++++++ 7 files changed, 155 insertions(+), 11 deletions(-) create mode 100644 test/Feature/Textures/Texture2DArray.Load.test.yaml diff --git a/include/API/Enums.h b/include/API/Enums.h index 5fbcb9ad6..a73a3283c 100644 --- a/include/API/Enums.h +++ b/include/API/Enums.h @@ -16,6 +16,7 @@ enum class ResourceKind { StructuredBuffer, ByteAddressBuffer, Texture2D, + Texture2DArray, RWBuffer, RWStructuredBuffer, RWByteAddressBuffer, diff --git a/include/Support/Pipeline.h b/include/Support/Pipeline.h index c13f54d2b..f926e06c4 100644 --- a/include/Support/Pipeline.h +++ b/include/Support/Pipeline.h @@ -77,6 +77,7 @@ static inline DescriptorKind getDescriptorKind(ResourceKind RK) { case ResourceKind::StructuredBuffer: case ResourceKind::ByteAddressBuffer: case ResourceKind::Texture2D: + case ResourceKind::Texture2DArray: return DescriptorKind::SRV; case ResourceKind::RWStructuredBuffer: @@ -140,6 +141,7 @@ struct OutputProperties { int Width; int Depth; int MipLevels = 1; + int ArraySize = 1; }; static inline uint32_t getFormatSize(DataFormat Format) { @@ -234,6 +236,7 @@ struct Resource { case ResourceKind::Buffer: case ResourceKind::RWBuffer: case ResourceKind::Texture2D: + case ResourceKind::Texture2DArray: case ResourceKind::RWTexture2D: case ResourceKind::Sampler: case ResourceKind::SampledTexture2D: @@ -260,6 +263,7 @@ struct Resource { case ResourceKind::RWByteAddressBuffer: case ResourceKind::ConstantBuffer: case ResourceKind::Texture2D: + case ResourceKind::Texture2DArray: case ResourceKind::RWTexture2D: case ResourceKind::SampledTexture2D: return false; @@ -279,6 +283,7 @@ struct Resource { case ResourceKind::Sampler: return false; case ResourceKind::Texture2D: + case ResourceKind::Texture2DArray: case ResourceKind::RWTexture2D: case ResourceKind::SampledTexture2D: return true; @@ -337,6 +342,7 @@ struct Resource { case ResourceKind::StructuredBuffer: case ResourceKind::ByteAddressBuffer: case ResourceKind::Texture2D: + case ResourceKind::Texture2DArray: case ResourceKind::ConstantBuffer: case ResourceKind::Sampler: case ResourceKind::SampledTexture2D: @@ -714,6 +720,7 @@ template <> struct ScalarEnumerationTraits { ENUM_CASE(StructuredBuffer); ENUM_CASE(ByteAddressBuffer); ENUM_CASE(Texture2D); + ENUM_CASE(Texture2DArray); ENUM_CASE(RWBuffer); ENUM_CASE(RWStructuredBuffer); ENUM_CASE(RWByteAddressBuffer); diff --git a/lib/API/DX/Device.cpp b/lib/API/DX/Device.cpp index 1a5d09124..7529e3345 100644 --- a/lib/API/DX/Device.cpp +++ b/lib/API/DX/Device.cpp @@ -212,6 +212,7 @@ static D3D12_RESOURCE_DIMENSION getDXDimension(ResourceKind RK) { case ResourceKind::ConstantBuffer: return D3D12_RESOURCE_DIMENSION_BUFFER; case ResourceKind::Texture2D: + case ResourceKind::Texture2DArray: case ResourceKind::RWTexture2D: return D3D12_RESOURCE_DIMENSION_TEXTURE2D; case ResourceKind::Sampler: @@ -232,11 +233,24 @@ getResourceDescription(const Resource &R) { "Multiple mip levels are not yet supported " "for DirectX textures."); + if (B.OutputProps.ArraySize < 1) + return llvm::createStringError(std::errc::invalid_argument, + "OutputProps.ArraySize must be >= 1."); + + if (B.OutputProps.ArraySize > 1 && R.Kind != ResourceKind::Texture2DArray) + return llvm::createStringError( + std::errc::not_supported, + "OutputProps.ArraySize > 1 is only supported for Texture2DArray."); + const DXGI_FORMAT Format = R.isTexture() ? getDXFormat(B.Format, B.Channels) : DXGI_FORMAT_UNKNOWN; const uint32_t Width = R.isTexture() ? B.OutputProps.Width : getUAVBufferSize(R); const uint32_t Height = R.isTexture() ? B.OutputProps.Height : 1; + const uint16_t DepthOrArraySize = + R.Kind == ResourceKind::Texture2DArray + ? static_cast(B.OutputProps.ArraySize) + : 1; D3D12_TEXTURE_LAYOUT Layout; if (R.isTexture()) @@ -251,8 +265,9 @@ getResourceDescription(const Resource &R) { const D3D12_RESOURCE_FLAGS Flags = R.isReadWrite() ? D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS : D3D12_RESOURCE_FLAG_NONE; - const D3D12_RESOURCE_DESC ResDesc = {Dimension, 0, Width, Height, 1, 1, - Format, {1, 0}, Layout, Flags}; + const D3D12_RESOURCE_DESC ResDesc = {Dimension, 0, Width, Height, + DepthOrArraySize, 1, Format, {1, 0}, + Layout, Flags}; return ResDesc; } @@ -282,6 +297,11 @@ static D3D12_SHADER_RESOURCE_VIEW_DESC getSRVDescription(const Resource &R) { Desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; Desc.Texture2D = D3D12_TEX2D_SRV{0, 1, 0, 0}; break; + case ResourceKind::Texture2DArray: + Desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2DARRAY; + Desc.Texture2DArray = D3D12_TEX2D_ARRAY_SRV{ + 0, 1, 0, static_cast(R.BufferPtr->OutputProps.ArraySize), 0, 0}; + break; case ResourceKind::RWStructuredBuffer: case ResourceKind::RWBuffer: case ResourceKind::RWByteAddressBuffer: @@ -325,6 +345,7 @@ static D3D12_UNORDERED_ACCESS_VIEW_DESC getUAVDescription(const Resource &R) { case ResourceKind::Buffer: case ResourceKind::ByteAddressBuffer: case ResourceKind::Texture2D: + case ResourceKind::Texture2DArray: case ResourceKind::ConstantBuffer: case ResourceKind::Sampler: llvm_unreachable("Not a UAV type!"); @@ -1523,15 +1544,20 @@ class DXDevice : public offloadtest::Device { addUploadBeginBarrier(IS, Destination); if (R.isTexture()) { const offloadtest::CPUBuffer &B = *R.BufferPtr; - const D3D12_PLACED_SUBRESOURCE_FOOTPRINT Footprint{ - 0, CD3DX12_SUBRESOURCE_FOOTPRINT( - getDXFormat(B.Format, B.Channels), B.OutputProps.Width, - B.OutputProps.Height, 1, - B.OutputProps.Width * B.getElementSize())}; - const CD3DX12_TEXTURE_COPY_LOCATION DstLoc(Destination.Get(), 0); - const CD3DX12_TEXTURE_COPY_LOCATION SrcLoc(Source.Get(), Footprint); - - IS.CB->CmdList->CopyTextureRegion(&DstLoc, 0, 0, 0, &SrcLoc, nullptr); + const DXGI_FORMAT DXFormat = getDXFormat(B.Format, B.Channels); + const uint32_t RowPitch = B.OutputProps.Width * B.getElementSize(); + const uint32_t SliceBytes = RowPitch * B.OutputProps.Height; + const uint32_t NumSlices = + R.Kind == ResourceKind::Texture2DArray ? B.OutputProps.ArraySize : 1; + for (uint32_t Slice = 0; Slice < NumSlices; ++Slice) { + const D3D12_PLACED_SUBRESOURCE_FOOTPRINT Footprint{ + Slice * SliceBytes, + CD3DX12_SUBRESOURCE_FOOTPRINT(DXFormat, B.OutputProps.Width, + B.OutputProps.Height, 1, RowPitch)}; + const CD3DX12_TEXTURE_COPY_LOCATION DstLoc(Destination.Get(), Slice); + const CD3DX12_TEXTURE_COPY_LOCATION SrcLoc(Source.Get(), Footprint); + IS.CB->CmdList->CopyTextureRegion(&DstLoc, 0, 0, 0, &SrcLoc, nullptr); + } } else IS.CB->CmdList->CopyBufferRegion(Destination.Get(), 0, Source.Get(), 0, R.size()); diff --git a/lib/API/MTL/MTLDevice.cpp b/lib/API/MTL/MTLDevice.cpp index f591fe089..afd645c74 100644 --- a/lib/API/MTL/MTLDevice.cpp +++ b/lib/API/MTL/MTLDevice.cpp @@ -1064,6 +1064,8 @@ class MTLDevice : public offloadtest::Device { Desc = MTL::TextureDescriptor::texture2DDescriptor(Format, Width, Height, false); break; + case ResourceKind::Texture2DArray: + llvm_unreachable("Texture2DArray is not yet supported in Metal."); case ResourceKind::Sampler: llvm_unreachable("Not implemented yet."); case ResourceKind::SampledTexture2D: diff --git a/lib/API/VK/Device.cpp b/lib/API/VK/Device.cpp index b26e66393..c5a8d0e91 100644 --- a/lib/API/VK/Device.cpp +++ b/lib/API/VK/Device.cpp @@ -77,6 +77,7 @@ static VkDescriptorType getDescriptorType(const ResourceKind RK) { return VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER; case ResourceKind::Texture2D: + case ResourceKind::Texture2DArray: return VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; case ResourceKind::RWTexture2D: @@ -161,6 +162,7 @@ static VkBufferUsageFlagBits getFlagBits(const ResourceKind RK) { case ResourceKind::ConstantBuffer: return VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; case ResourceKind::Texture2D: + case ResourceKind::Texture2DArray: case ResourceKind::RWTexture2D: case ResourceKind::Sampler: case ResourceKind::SampledTexture2D: @@ -175,6 +177,8 @@ static VkImageViewType getImageViewType(const ResourceKind RK) { case ResourceKind::RWTexture2D: case ResourceKind::SampledTexture2D: return VK_IMAGE_VIEW_TYPE_2D; + case ResourceKind::Texture2DArray: + return VK_IMAGE_VIEW_TYPE_2D_ARRAY; case ResourceKind::Buffer: case ResourceKind::RWBuffer: case ResourceKind::ByteAddressBuffer: @@ -191,6 +195,7 @@ static VkImageViewType getImageViewType(const ResourceKind RK) { static VkImageType getVKImageType(const ResourceKind RK) { switch (RK) { case ResourceKind::Texture2D: + case ResourceKind::Texture2DArray: case ResourceKind::RWTexture2D: case ResourceKind::SampledTexture2D: return VK_IMAGE_TYPE_2D; diff --git a/lib/Support/Pipeline.cpp b/lib/Support/Pipeline.cpp index 80ff1b03f..b480ac6d9 100644 --- a/lib/Support/Pipeline.cpp +++ b/lib/Support/Pipeline.cpp @@ -381,6 +381,7 @@ void MappingTraits::mapping(IO &I, H = std::max(1u, H / 2); D = std::max(1u, D / 2); } + ExpectedSize *= static_cast(std::max(1, B.OutputProps.ArraySize)); if (B.Size != ExpectedSize) I.setError(Twine("Buffer '") + B.Name + "' size (" + Twine(B.Size) + @@ -497,6 +498,7 @@ void MappingTraits::mapping( I.mapRequired("Width", P.Width); I.mapRequired("Depth", P.Depth); I.mapOptional("MipLevels", P.MipLevels, 1); + I.mapOptional("ArraySize", P.ArraySize, 1); } void MappingTraits::mapping( diff --git a/test/Feature/Textures/Texture2DArray.Load.test.yaml b/test/Feature/Textures/Texture2DArray.Load.test.yaml new file mode 100644 index 000000000..06fb90864 --- /dev/null +++ b/test/Feature/Textures/Texture2DArray.Load.test.yaml @@ -0,0 +1,101 @@ +#--- source.hlsl +[[vk::binding(0, 0)]] Texture2DArray Tex : register(t0); +[[vk::binding(1, 0)]] RWBuffer Out : register(u0); + +[numthreads(1, 1, 1)] +void main() { + // Explicit Load(int4) + // Location: (x, y, slice, mip) + // Slice 0: Red, Green, Blue, White + Out[0] = Tex.Load(int4(0, 0, 0, 0)); + Out[1] = Tex.Load(int4(1, 0, 0, 0)); + Out[2] = Tex.Load(int4(0, 1, 0, 0)); + Out[3] = Tex.Load(int4(1, 1, 0, 0)); + + // Slice 1: solid Red + Out[4] = Tex.Load(int4(0, 0, 1, 0)); + Out[5] = Tex.Load(int4(1, 1, 1, 0)); + + // Slice 2: solid Green + Out[6] = Tex.Load(int4(0, 0, 2, 0)); + Out[7] = Tex.Load(int4(1, 1, 2, 0)); + + // Load(int4, int2) - With Offset, slice 0 + // (0,0) + (1,0) = (1,0) -> Green + Out[8] = Tex.Load(int4(0, 0, 0, 0), int2(1, 0)); + // (1,1) + (-1,-1) = (0,0) -> Red + Out[9] = Tex.Load(int4(1, 1, 0, 0), int2(-1, -1)); +} + +//--- pipeline.yaml +--- +Shaders: + - Stage: Compute + Entry: main + +Buffers: + - Name: Tex + Format: Float32 + Channels: 4 + OutputProps: { Width: 2, Height: 2, Depth: 1, ArraySize: 3 } + Data: [ # Slice 0: Red, Green, Blue, White + 1.0, 0.0, 0.0, 1.0, + 0.0, 1.0, 0.0, 1.0, + 0.0, 0.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, + # Slice 1: solid Red + 1.0, 0.0, 0.0, 1.0, + 1.0, 0.0, 0.0, 1.0, + 1.0, 0.0, 0.0, 1.0, + 1.0, 0.0, 0.0, 1.0, + # Slice 2: solid Green + 0.0, 1.0, 0.0, 1.0, + 0.0, 1.0, 0.0, 1.0, + 0.0, 1.0, 0.0, 1.0, + 0.0, 1.0, 0.0, 1.0 ] + + - Name: Out + Format: Float32 + Channels: 4 + FillSize: 160 # 10 * sizeof(float4) + + - Name: Expected + Format: Float32 + Channels: 4 + Data: [ 1.0, 0.0, 0.0, 1.0, # Slice 0 (0,0) Red + 0.0, 1.0, 0.0, 1.0, # Slice 0 (1,0) Green + 0.0, 0.0, 1.0, 1.0, # Slice 0 (0,1) Blue + 1.0, 1.0, 1.0, 1.0, # Slice 0 (1,1) White + 1.0, 0.0, 0.0, 1.0, # Slice 1 (0,0) Red + 1.0, 0.0, 0.0, 1.0, # Slice 1 (1,1) Red + 0.0, 1.0, 0.0, 1.0, # Slice 2 (0,0) Green + 0.0, 1.0, 0.0, 1.0, # Slice 2 (1,1) Green + 0.0, 1.0, 0.0, 1.0, # Offset (1,0) -> Slice 0 Green + 1.0, 0.0, 0.0, 1.0 ] # Offset (-1,-1) -> Slice 0 Red + +DescriptorSets: + - Resources: + - Name: Tex + Kind: Texture2DArray + DirectXBinding: { Register: 0, Space: 0 } + VulkanBinding: { Binding: 0 } + - Name: Out + Kind: RWBuffer + DirectXBinding: { Register: 0, Space: 0 } + VulkanBinding: { Binding: 1 } + +Results: + - Result: LoadTest + Rule: BufferExact + Actual: Out + Expected: Expected +... +#--- end + + +# Texture2DArray support is currently DirectX-only. +# UNSUPPORTED: Vulkan || Metal + +# RUN: split-file %s %t +# RUN: %dxc_target -T cs_6_0 -Fo %t.o %t/source.hlsl +# RUN: %offloader %t/pipeline.yaml %t.o From a28733a645d63f3a72d191429989a6eeb99860d7 Mon Sep 17 00:00:00 2001 From: Alex Sepkowski Date: Wed, 27 May 2026 13:29:16 -0700 Subject: [PATCH 05/11] [NFC] Add const to ReadbackDX references for clang-tidy Fixes misc-const-correctness warnings-as-errors from clang-tidy in CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/API/DX/Device.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/API/DX/Device.cpp b/lib/API/DX/Device.cpp index 0acab738a..df1c9bfc7 100644 --- a/lib/API/DX/Device.cpp +++ b/lib/API/DX/Device.cpp @@ -2425,7 +2425,7 @@ class DXDevice : public offloadtest::Device { for (const ResourceSet &RS : R.second) { if (RS.Readback == nullptr) continue; - DXBuffer &ReadbackDX = llvm::cast(*RS.Readback); + const DXBuffer &ReadbackDX = llvm::cast(*RS.Readback); addReadbackBeginBarrier(IS, RS.Buffer); const CD3DX12_TEXTURE_COPY_LOCATION DstLoc(ReadbackDX.Buffer.Get(), Footprint); @@ -2438,7 +2438,7 @@ class DXDevice : public offloadtest::Device { for (const ResourceSet &RS : R.second) { if (RS.Readback == nullptr) continue; - DXBuffer &ReadbackDX = llvm::cast(*RS.Readback); + const DXBuffer &ReadbackDX = llvm::cast(*RS.Readback); addReadbackBeginBarrier(IS, RS.Buffer); IS.CB->CmdList->CopyResource(ReadbackDX.Buffer.Get(), RS.Buffer.Get()); addReadbackEndBarrier(IS, RS.Buffer); From 2a8987d0671edde62da2c2d632ce11fdeb05fe47 Mon Sep 17 00:00:00 2001 From: Alex Sepkowski Date: Wed, 27 May 2026 13:32:58 -0700 Subject: [PATCH 06/11] [DX] Add RWTexture2DArray UAV support Extends Texture2DArray support to RWTexture2DArray: - Pipeline.h: add ResourceKind::RWTexture2DArray; update isTexture, isUAV, getDXKind, getDXDimension switches and YAML enum trait. - Enums.h: API enum value. - DX/Device.cpp: createUAV uses D3D12_UAV_DIMENSION_TEXTURE2DARRAY with FirstArraySlice=0 and ArraySize from OutputProperties. Multi-slice readback via per-slice CD3DX12_TEXTURE_COPY_LOCATION with placed footprint offset (note: requires Width*ElementSize to be 256-aligned; documented in issue plan). - VK/Device.cpp, MTL/MTLDevice.cpp: llvm_unreachable placeholders. Test: RWTexture2DArray.Store.test.yaml verifies a 16x4x2 compute write/readback round-trip. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- include/API/Enums.h | 1 + include/Support/Pipeline.h | 6 ++ lib/API/DX/Device.cpp | 45 ++++++--- lib/API/MTL/MTLDevice.cpp | 2 + lib/API/VK/Device.cpp | 5 + .../Textures/RWTexture2DArray.Store.test.yaml | 93 +++++++++++++++++++ 6 files changed, 141 insertions(+), 11 deletions(-) create mode 100644 test/Feature/Textures/RWTexture2DArray.Store.test.yaml diff --git a/include/API/Enums.h b/include/API/Enums.h index a73a3283c..ee20f1b7f 100644 --- a/include/API/Enums.h +++ b/include/API/Enums.h @@ -21,6 +21,7 @@ enum class ResourceKind { RWStructuredBuffer, RWByteAddressBuffer, RWTexture2D, + RWTexture2DArray, ConstantBuffer, Sampler, SampledTexture2D, diff --git a/include/Support/Pipeline.h b/include/Support/Pipeline.h index f926e06c4..2c6a930f5 100644 --- a/include/Support/Pipeline.h +++ b/include/Support/Pipeline.h @@ -84,6 +84,7 @@ static inline DescriptorKind getDescriptorKind(ResourceKind RK) { case ResourceKind::RWBuffer: case ResourceKind::RWByteAddressBuffer: case ResourceKind::RWTexture2D: + case ResourceKind::RWTexture2DArray: return DescriptorKind::UAV; case ResourceKind::ConstantBuffer: @@ -238,6 +239,7 @@ struct Resource { case ResourceKind::Texture2D: case ResourceKind::Texture2DArray: case ResourceKind::RWTexture2D: + case ResourceKind::RWTexture2DArray: case ResourceKind::Sampler: case ResourceKind::SampledTexture2D: return false; @@ -265,6 +267,7 @@ struct Resource { case ResourceKind::Texture2D: case ResourceKind::Texture2DArray: case ResourceKind::RWTexture2D: + case ResourceKind::RWTexture2DArray: case ResourceKind::SampledTexture2D: return false; } @@ -285,6 +288,7 @@ struct Resource { case ResourceKind::Texture2D: case ResourceKind::Texture2DArray: case ResourceKind::RWTexture2D: + case ResourceKind::RWTexture2DArray: case ResourceKind::SampledTexture2D: return true; } @@ -351,6 +355,7 @@ struct Resource { case ResourceKind::RWStructuredBuffer: case ResourceKind::RWByteAddressBuffer: case ResourceKind::RWTexture2D: + case ResourceKind::RWTexture2DArray: return true; } llvm_unreachable("All cases handled"); @@ -725,6 +730,7 @@ template <> struct ScalarEnumerationTraits { ENUM_CASE(RWStructuredBuffer); ENUM_CASE(RWByteAddressBuffer); ENUM_CASE(RWTexture2D); + ENUM_CASE(RWTexture2DArray); ENUM_CASE(ConstantBuffer); ENUM_CASE(Sampler); ENUM_CASE(SampledTexture2D); diff --git a/lib/API/DX/Device.cpp b/lib/API/DX/Device.cpp index 951843938..665b85090 100644 --- a/lib/API/DX/Device.cpp +++ b/lib/API/DX/Device.cpp @@ -214,6 +214,7 @@ static D3D12_RESOURCE_DIMENSION getDXDimension(ResourceKind RK) { case ResourceKind::Texture2D: case ResourceKind::Texture2DArray: case ResourceKind::RWTexture2D: + case ResourceKind::RWTexture2DArray: return D3D12_RESOURCE_DIMENSION_TEXTURE2D; case ResourceKind::Sampler: return D3D12_RESOURCE_DIMENSION_UNKNOWN; @@ -237,10 +238,12 @@ getResourceDescription(const Resource &R) { return llvm::createStringError(std::errc::invalid_argument, "OutputProps.ArraySize must be >= 1."); - if (B.OutputProps.ArraySize > 1 && R.Kind != ResourceKind::Texture2DArray) + if (B.OutputProps.ArraySize > 1 && R.Kind != ResourceKind::Texture2DArray && + R.Kind != ResourceKind::RWTexture2DArray) return llvm::createStringError( std::errc::not_supported, - "OutputProps.ArraySize > 1 is only supported for Texture2DArray."); + "OutputProps.ArraySize > 1 is only supported for Texture2DArray and " + "RWTexture2DArray."); const DXGI_FORMAT Format = R.isTexture() ? getDXFormat(B.Format, B.Channels) : DXGI_FORMAT_UNKNOWN; @@ -248,7 +251,8 @@ getResourceDescription(const Resource &R) { R.isTexture() ? B.OutputProps.Width : getUAVBufferSize(R); const uint32_t Height = R.isTexture() ? B.OutputProps.Height : 1; const uint16_t DepthOrArraySize = - R.Kind == ResourceKind::Texture2DArray + (R.Kind == ResourceKind::Texture2DArray || + R.Kind == ResourceKind::RWTexture2DArray) ? static_cast(B.OutputProps.ArraySize) : 1; D3D12_TEXTURE_LAYOUT Layout; @@ -306,6 +310,7 @@ static D3D12_SHADER_RESOURCE_VIEW_DESC getSRVDescription(const Resource &R) { case ResourceKind::RWBuffer: case ResourceKind::RWByteAddressBuffer: case ResourceKind::RWTexture2D: + case ResourceKind::RWTexture2DArray: case ResourceKind::ConstantBuffer: case ResourceKind::Sampler: llvm_unreachable("Not an SRV type!"); @@ -341,6 +346,11 @@ static D3D12_UNORDERED_ACCESS_VIEW_DESC getUAVDescription(const Resource &R) { Desc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D; Desc.Texture2D = D3D12_TEX2D_UAV{0, 0}; break; + case ResourceKind::RWTexture2DArray: + Desc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2DARRAY; + Desc.Texture2DArray = D3D12_TEX2D_ARRAY_UAV{ + 0, 0, static_cast(R.BufferPtr->OutputProps.ArraySize), 0}; + break; case ResourceKind::StructuredBuffer: case ResourceKind::Buffer: case ResourceKind::ByteAddressBuffer: @@ -1565,7 +1575,10 @@ class DXDevice : public offloadtest::Device { const uint32_t RowPitch = B.OutputProps.Width * B.getElementSize(); const uint32_t SliceBytes = RowPitch * B.OutputProps.Height; const uint32_t NumSlices = - R.Kind == ResourceKind::Texture2DArray ? B.OutputProps.ArraySize : 1; + (R.Kind == ResourceKind::Texture2DArray || + R.Kind == ResourceKind::RWTexture2DArray) + ? B.OutputProps.ArraySize + : 1; for (uint32_t Slice = 0; Slice < NumSlices; ++Slice) { const D3D12_PLACED_SUBRESOURCE_FOOTPRINT Footprint{ Slice * SliceBytes, @@ -2168,20 +2181,30 @@ class DXDevice : public offloadtest::Device { auto CopyBackResource = [&IS, this](ResourcePair &R) { if (R.first->isTexture()) { const offloadtest::CPUBuffer &B = *R.first->BufferPtr; - const D3D12_PLACED_SUBRESOURCE_FOOTPRINT Footprint{ + const uint32_t RowPitch = B.OutputProps.Width * B.getElementSize(); + const uint32_t SliceBytes = RowPitch * B.OutputProps.Height; + const uint32_t NumSlices = + R.first->Kind == ResourceKind::RWTexture2DArray + ? B.OutputProps.ArraySize + : 1; + const D3D12_PLACED_SUBRESOURCE_FOOTPRINT BaseFootprint{ 0, CD3DX12_SUBRESOURCE_FOOTPRINT( getDXFormat(B.Format, B.Channels), B.OutputProps.Width, - B.OutputProps.Height, 1, - B.OutputProps.Width * B.getElementSize())}; + B.OutputProps.Height, 1, RowPitch)}; for (const ResourceSet &RS : R.second) { if (RS.Readback == nullptr) continue; const DXBuffer &ReadbackDX = llvm::cast(*RS.Readback); addReadbackBeginBarrier(IS, RS.Buffer); - const CD3DX12_TEXTURE_COPY_LOCATION DstLoc(ReadbackDX.Buffer.Get(), - Footprint); - const CD3DX12_TEXTURE_COPY_LOCATION SrcLoc(RS.Buffer.Get(), 0); - IS.CB->CmdList->CopyTextureRegion(&DstLoc, 0, 0, 0, &SrcLoc, nullptr); + for (uint32_t Slice = 0; Slice < NumSlices; ++Slice) { + D3D12_PLACED_SUBRESOURCE_FOOTPRINT Footprint = BaseFootprint; + Footprint.Offset = Slice * SliceBytes; + const CD3DX12_TEXTURE_COPY_LOCATION DstLoc(ReadbackDX.Buffer.Get(), + Footprint); + const CD3DX12_TEXTURE_COPY_LOCATION SrcLoc(RS.Buffer.Get(), Slice); + IS.CB->CmdList->CopyTextureRegion(&DstLoc, 0, 0, 0, &SrcLoc, + nullptr); + } addReadbackEndBarrier(IS, RS.Buffer); } return; diff --git a/lib/API/MTL/MTLDevice.cpp b/lib/API/MTL/MTLDevice.cpp index ae9f427ca..8ca48bbd2 100644 --- a/lib/API/MTL/MTLDevice.cpp +++ b/lib/API/MTL/MTLDevice.cpp @@ -1066,6 +1066,8 @@ class MTLDevice : public offloadtest::Device { break; case ResourceKind::Texture2DArray: llvm_unreachable("Texture2DArray is not yet supported in Metal."); + case ResourceKind::RWTexture2DArray: + llvm_unreachable("RWTexture2DArray is not yet supported in Metal."); case ResourceKind::Sampler: llvm_unreachable("Not implemented yet."); case ResourceKind::SampledTexture2D: diff --git a/lib/API/VK/Device.cpp b/lib/API/VK/Device.cpp index 84ffc6211..a73b5ff1c 100644 --- a/lib/API/VK/Device.cpp +++ b/lib/API/VK/Device.cpp @@ -81,6 +81,7 @@ static VkDescriptorType getDescriptorType(const ResourceKind RK) { return VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; case ResourceKind::RWTexture2D: + case ResourceKind::RWTexture2DArray: return VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; case ResourceKind::ByteAddressBuffer: @@ -164,6 +165,7 @@ static VkBufferUsageFlagBits getFlagBits(const ResourceKind RK) { case ResourceKind::Texture2D: case ResourceKind::Texture2DArray: case ResourceKind::RWTexture2D: + case ResourceKind::RWTexture2DArray: case ResourceKind::Sampler: case ResourceKind::SampledTexture2D: llvm_unreachable("Textures and samplers don't have buffer usage bits!"); @@ -179,6 +181,8 @@ static VkImageViewType getImageViewType(const ResourceKind RK) { return VK_IMAGE_VIEW_TYPE_2D; case ResourceKind::Texture2DArray: return VK_IMAGE_VIEW_TYPE_2D_ARRAY; + case ResourceKind::RWTexture2DArray: + llvm_unreachable("RWTexture2DArray is not yet supported in Vulkan."); case ResourceKind::Buffer: case ResourceKind::RWBuffer: case ResourceKind::ByteAddressBuffer: @@ -197,6 +201,7 @@ static VkImageType getVKImageType(const ResourceKind RK) { case ResourceKind::Texture2D: case ResourceKind::Texture2DArray: case ResourceKind::RWTexture2D: + case ResourceKind::RWTexture2DArray: case ResourceKind::SampledTexture2D: return VK_IMAGE_TYPE_2D; default: diff --git a/test/Feature/Textures/RWTexture2DArray.Store.test.yaml b/test/Feature/Textures/RWTexture2DArray.Store.test.yaml new file mode 100644 index 000000000..5973bdeca --- /dev/null +++ b/test/Feature/Textures/RWTexture2DArray.Store.test.yaml @@ -0,0 +1,93 @@ +#--- source.hlsl +[[vk::binding(0, 0)]] RWTexture2DArray Out : register(u0); + +[numthreads(16, 4, 1)] +void main(uint3 DTid : SV_DispatchThreadID) { + // Encode (x, slice) into the texel so readback can verify per-slice writes. + // Slice 0: x ramp in the R channel; slice 1: x ramp in the G channel. + if (DTid.z == 0) + Out[DTid] = float4(DTid.x / 15.0f, 0.0f, 0.0f, 1.0f); + else + Out[DTid] = float4(0.0f, DTid.x / 15.0f, 0.0f, 1.0f); +} + +//--- pipeline.yaml +--- +Shaders: + - Stage: Compute + Entry: main + +DispatchParameters: + DispatchGroupCount: [ 1, 1, 2 ] + +Buffers: + - Name: Out + Format: Float32 + Channels: 4 + # Width * sizeof(float4) = 16 * 16 = 256 (matches D3D12_TEXTURE_DATA_PITCH_ALIGNMENT) + OutputProps: { Width: 16, Height: 4, Depth: 1, ArraySize: 2 } + FillSize: 2048 # 16 * 4 * 4 channels * sizeof(float) * 2 slices + + - Name: Expected + Format: Float32 + Channels: 4 + Data: [ + # Slice 0 (red ramp, 4 identical rows of 16 texels) + 0.0,0,0,1, 0.0666666667,0,0,1, 0.1333333333,0,0,1, 0.2,0,0,1, + 0.2666666667,0,0,1, 0.3333333333,0,0,1, 0.4,0,0,1, 0.4666666667,0,0,1, + 0.5333333333,0,0,1, 0.6,0,0,1, 0.6666666667,0,0,1, 0.7333333333,0,0,1, + 0.8,0,0,1, 0.8666666667,0,0,1, 0.9333333333,0,0,1, 1.0,0,0,1, + 0.0,0,0,1, 0.0666666667,0,0,1, 0.1333333333,0,0,1, 0.2,0,0,1, + 0.2666666667,0,0,1, 0.3333333333,0,0,1, 0.4,0,0,1, 0.4666666667,0,0,1, + 0.5333333333,0,0,1, 0.6,0,0,1, 0.6666666667,0,0,1, 0.7333333333,0,0,1, + 0.8,0,0,1, 0.8666666667,0,0,1, 0.9333333333,0,0,1, 1.0,0,0,1, + 0.0,0,0,1, 0.0666666667,0,0,1, 0.1333333333,0,0,1, 0.2,0,0,1, + 0.2666666667,0,0,1, 0.3333333333,0,0,1, 0.4,0,0,1, 0.4666666667,0,0,1, + 0.5333333333,0,0,1, 0.6,0,0,1, 0.6666666667,0,0,1, 0.7333333333,0,0,1, + 0.8,0,0,1, 0.8666666667,0,0,1, 0.9333333333,0,0,1, 1.0,0,0,1, + 0.0,0,0,1, 0.0666666667,0,0,1, 0.1333333333,0,0,1, 0.2,0,0,1, + 0.2666666667,0,0,1, 0.3333333333,0,0,1, 0.4,0,0,1, 0.4666666667,0,0,1, + 0.5333333333,0,0,1, 0.6,0,0,1, 0.6666666667,0,0,1, 0.7333333333,0,0,1, + 0.8,0,0,1, 0.8666666667,0,0,1, 0.9333333333,0,0,1, 1.0,0,0,1, + # Slice 1 (green ramp, 4 identical rows of 16 texels) + 0,0.0,0,1, 0,0.0666666667,0,1, 0,0.1333333333,0,1, 0,0.2,0,1, + 0,0.2666666667,0,1, 0,0.3333333333,0,1, 0,0.4,0,1, 0,0.4666666667,0,1, + 0,0.5333333333,0,1, 0,0.6,0,1, 0,0.6666666667,0,1, 0,0.7333333333,0,1, + 0,0.8,0,1, 0,0.8666666667,0,1, 0,0.9333333333,0,1, 0,1.0,0,1, + 0,0.0,0,1, 0,0.0666666667,0,1, 0,0.1333333333,0,1, 0,0.2,0,1, + 0,0.2666666667,0,1, 0,0.3333333333,0,1, 0,0.4,0,1, 0,0.4666666667,0,1, + 0,0.5333333333,0,1, 0,0.6,0,1, 0,0.6666666667,0,1, 0,0.7333333333,0,1, + 0,0.8,0,1, 0,0.8666666667,0,1, 0,0.9333333333,0,1, 0,1.0,0,1, + 0,0.0,0,1, 0,0.0666666667,0,1, 0,0.1333333333,0,1, 0,0.2,0,1, + 0,0.2666666667,0,1, 0,0.3333333333,0,1, 0,0.4,0,1, 0,0.4666666667,0,1, + 0,0.5333333333,0,1, 0,0.6,0,1, 0,0.6666666667,0,1, 0,0.7333333333,0,1, + 0,0.8,0,1, 0,0.8666666667,0,1, 0,0.9333333333,0,1, 0,1.0,0,1, + 0,0.0,0,1, 0,0.0666666667,0,1, 0,0.1333333333,0,1, 0,0.2,0,1, + 0,0.2666666667,0,1, 0,0.3333333333,0,1, 0,0.4,0,1, 0,0.4666666667,0,1, + 0,0.5333333333,0,1, 0,0.6,0,1, 0,0.6666666667,0,1, 0,0.7333333333,0,1, + 0,0.8,0,1, 0,0.8666666667,0,1, 0,0.9333333333,0,1, 0,1.0,0,1 + ] + +DescriptorSets: + - Resources: + - Name: Out + Kind: RWTexture2DArray + DirectXBinding: { Register: 0, Space: 0 } + VulkanBinding: { Binding: 0 } + +Results: + - Result: StoreTest + Rule: BufferFloatULP + ULPT: 4 + Actual: Out + Expected: Expected +... +#--- end + + +# RWTexture2DArray support is currently DirectX-only. +# UNSUPPORTED: Vulkan || Metal + +# RUN: split-file %s %t +# RUN: %dxc_target -T cs_6_0 -Fo %t.o %t/source.hlsl +# RUN: %offloader %t/pipeline.yaml %t.o From 37d3c27aacbb8fd246dd985decef44937b70e59f Mon Sep 17 00:00:00 2001 From: Alex Sepkowski Date: Wed, 27 May 2026 13:40:36 -0700 Subject: [PATCH 07/11] [NFC] Add const to ReadbackDX references for clang-tidy Fixes misc-const-correctness warnings-as-errors from clang-tidy in CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/API/DX/Device.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/API/DX/Device.cpp b/lib/API/DX/Device.cpp index 665b85090..9b31019c6 100644 --- a/lib/API/DX/Device.cpp +++ b/lib/API/DX/Device.cpp @@ -2422,7 +2422,7 @@ class DXDevice : public offloadtest::Device { for (const ResourceSet &RS : R.second) { if (RS.Readback == nullptr) continue; - DXBuffer &ReadbackDX = llvm::cast(*RS.Readback); + const DXBuffer &ReadbackDX = llvm::cast(*RS.Readback); addReadbackBeginBarrier(IS, RS.Buffer); const CD3DX12_TEXTURE_COPY_LOCATION DstLoc(ReadbackDX.Buffer.Get(), Footprint); @@ -2435,7 +2435,7 @@ class DXDevice : public offloadtest::Device { for (const ResourceSet &RS : R.second) { if (RS.Readback == nullptr) continue; - DXBuffer &ReadbackDX = llvm::cast(*RS.Readback); + const DXBuffer &ReadbackDX = llvm::cast(*RS.Readback); addReadbackBeginBarrier(IS, RS.Buffer); IS.CB->CmdList->CopyResource(ReadbackDX.Buffer.Get(), RS.Buffer.Get()); addReadbackEndBarrier(IS, RS.Buffer); From d1dfb114c95ef10f87476377081e3632b4113dce Mon Sep 17 00:00:00 2001 From: Alex Sepkowski Date: Wed, 27 May 2026 13:49:40 -0700 Subject: [PATCH 08/11] [NFC] clang-format Texture2DArray NumSlices ternary Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/API/DX/Device.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/API/DX/Device.cpp b/lib/API/DX/Device.cpp index 9b31019c6..174a6d183 100644 --- a/lib/API/DX/Device.cpp +++ b/lib/API/DX/Device.cpp @@ -1574,11 +1574,10 @@ class DXDevice : public offloadtest::Device { const DXGI_FORMAT DXFormat = getDXFormat(B.Format, B.Channels); const uint32_t RowPitch = B.OutputProps.Width * B.getElementSize(); const uint32_t SliceBytes = RowPitch * B.OutputProps.Height; - const uint32_t NumSlices = - (R.Kind == ResourceKind::Texture2DArray || - R.Kind == ResourceKind::RWTexture2DArray) - ? B.OutputProps.ArraySize - : 1; + const uint32_t NumSlices = (R.Kind == ResourceKind::Texture2DArray || + R.Kind == ResourceKind::RWTexture2DArray) + ? B.OutputProps.ArraySize + : 1; for (uint32_t Slice = 0; Slice < NumSlices; ++Slice) { const D3D12_PLACED_SUBRESOURCE_FOOTPRINT Footprint{ Slice * SliceBytes, From fe4b17b4bf753544a03471bc5d22a3ca1935e589 Mon Sep 17 00:00:00 2001 From: Alex Sepkowski Date: Fri, 29 May 2026 18:05:21 -0700 Subject: [PATCH 09/11] XFAIL Clang on Texture2DArray tests Clang HLSL does not yet implement the Texture2DArray / RWTexture2DArray templates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/Feature/Textures/RWTexture2DArray.Store.test.yaml | 3 +++ test/Feature/Textures/Texture2DArray.Load.test.yaml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/test/Feature/Textures/RWTexture2DArray.Store.test.yaml b/test/Feature/Textures/RWTexture2DArray.Store.test.yaml index 5973bdeca..5cb9b0ae1 100644 --- a/test/Feature/Textures/RWTexture2DArray.Store.test.yaml +++ b/test/Feature/Textures/RWTexture2DArray.Store.test.yaml @@ -88,6 +88,9 @@ Results: # RWTexture2DArray support is currently DirectX-only. # UNSUPPORTED: Vulkan || Metal +# Clang HLSL has no RWTexture2DArray template yet. +# XFAIL: Clang + # RUN: split-file %s %t # RUN: %dxc_target -T cs_6_0 -Fo %t.o %t/source.hlsl # RUN: %offloader %t/pipeline.yaml %t.o diff --git a/test/Feature/Textures/Texture2DArray.Load.test.yaml b/test/Feature/Textures/Texture2DArray.Load.test.yaml index 06fb90864..33fc72ede 100644 --- a/test/Feature/Textures/Texture2DArray.Load.test.yaml +++ b/test/Feature/Textures/Texture2DArray.Load.test.yaml @@ -96,6 +96,9 @@ Results: # Texture2DArray support is currently DirectX-only. # UNSUPPORTED: Vulkan || Metal +# Clang HLSL has no Texture2DArray template yet. +# XFAIL: Clang + # RUN: split-file %s %t # RUN: %dxc_target -T cs_6_0 -Fo %t.o %t/source.hlsl # RUN: %offloader %t/pipeline.yaml %t.o From 780adf15e1c56e6e947838c576c8d97ea0feab90 Mon Sep 17 00:00:00 2001 From: Alex Sepkowski Date: Fri, 29 May 2026 18:19:51 -0700 Subject: [PATCH 10/11] Plumb RWTexture2DArray through VK image view type Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/API/VK/Device.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/API/VK/Device.cpp b/lib/API/VK/Device.cpp index c4be8934e..0a4358827 100644 --- a/lib/API/VK/Device.cpp +++ b/lib/API/VK/Device.cpp @@ -184,9 +184,8 @@ static VkImageViewType getImageViewType(const ResourceKind RK) { case ResourceKind::SampledTexture2D: return VK_IMAGE_VIEW_TYPE_2D; case ResourceKind::Texture2DArray: - return VK_IMAGE_VIEW_TYPE_2D_ARRAY; case ResourceKind::RWTexture2DArray: - llvm_unreachable("RWTexture2DArray is not yet supported in Vulkan."); + return VK_IMAGE_VIEW_TYPE_2D_ARRAY; case ResourceKind::Buffer: case ResourceKind::RWBuffer: case ResourceKind::ByteAddressBuffer: From 9cf5e1ed05bbf50d9e3673c4a9bdcc7f920f5bcd Mon Sep 17 00:00:00 2001 From: alsepkow Date: Mon, 1 Jun 2026 17:49:07 -0700 Subject: [PATCH 11/11] [DX] Unify multi-subresource texture upload + add Texture2DArray multi-mip support Combines the previously-separate per-mip and per-slice texture-upload paths into a single shared path driven by ID3D12Device::GetCopyableFootprints so that Texture2DArray resources can now carry per-slice mip chains. Changes ------- * Texture2DArray SRV/UAV view descriptions now use `MipLevels` from the CPU buffer (rather than hardcoding 1) so multi-mip array textures expose the full mip chain to the shader. * `addResourceUploadCommands` now accepts an optional `ArrayRef` and, when supplied, iterates all subresources in D3D12 ordering (`Sub = Slice * MipLevels + Mip`) instead of only the base subresource. The single-subresource fast path is preserved for unaffected callers. * `createSRV` / `createUAV` route through `GetCopyableFootprints` whenever `MipLevels > 1 || ArraySize > 1` to obtain D3D12-aligned per-subresource row pitches for the upload buffer. This subsumes the prior bespoke per-mip and per-slice handling. * Memcpy loop copies each subresource's tightly packed CPU rows into the appropriately padded D3D12 footprint, advancing the source pointer by the tight row size for each subresource. CPU data layout is the natural D3D12 subresource order: slice-major, mip-major within slice. This consolidates the per-slice 256-byte-alignment helper requested as a follow-up on #1077 -- there is now exactly one path for all multi-subresource textures. Test ---- New `Texture2DArray.Load.MipMaps.test.yaml` exercises a 2-slice, 2-mip Texture2DArray and reads back one texel from each (slice, mip) pair via `Texture2DArray::Load(int4(x, y, slice, mip))`. Existing tests verified locally on d3d12: PASS Texture2DArray.Load PASS Texture2DArray.Load.MipMaps <-- new PASS RWTexture2DArray.Store PASS Texture2D.Load.MipMaps PASS Texture2D.mips.OperatorIndex VK/MTL scope: the new test is gated `UNSUPPORTED: Vulkan || Metal` to match the existing Texture2DArray tests. Multi-slice + multi-mip support on the Vulkan and Metal backends remains a follow-up (tracked under #1077 backend-parity ACs). Refs #1077 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/API/DX/Device.cpp | 164 ++++++++++++------ .../Texture2DArray.Load.MipMaps.test.yaml | 102 +++++++++++ 2 files changed, 210 insertions(+), 56 deletions(-) create mode 100644 test/Feature/Textures/Texture2DArray.Load.MipMaps.test.yaml diff --git a/lib/API/DX/Device.cpp b/lib/API/DX/Device.cpp index d6e6a43f2..0bfd8e0c8 100644 --- a/lib/API/DX/Device.cpp +++ b/lib/API/DX/Device.cpp @@ -289,10 +289,9 @@ getResourceDescription(const Resource &R) { const D3D12_RESOURCE_FLAGS Flags = R.isReadWrite() ? D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS : D3D12_RESOURCE_FLAG_NONE; - const D3D12_RESOURCE_DESC ResDesc = {Dimension, 0, Width, - Height, DepthOrArraySize, - MipLevels, Format, {1, 0}, - Layout, Flags}; + const D3D12_RESOURCE_DESC ResDesc = { + Dimension, 0, Width, Height, DepthOrArraySize, + MipLevels, Format, {1, 0}, Layout, Flags}; return ResDesc; } @@ -326,7 +325,9 @@ static D3D12_SHADER_RESOURCE_VIEW_DESC getSRVDescription(const Resource &R) { case ResourceKind::Texture2DArray: Desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2DARRAY; Desc.Texture2DArray = D3D12_TEX2D_ARRAY_SRV{ - 0, 1, 0, static_cast(R.BufferPtr->OutputProps.ArraySize), 0, 0}; + 0, static_cast(R.BufferPtr->OutputProps.MipLevels), + 0, static_cast(R.BufferPtr->OutputProps.ArraySize), + 0, 0}; break; case ResourceKind::RWStructuredBuffer: case ResourceKind::RWBuffer: @@ -1601,34 +1602,30 @@ class DXDevice : public offloadtest::Device { void addResourceUploadCommands( Resource &R, InvocationState &IS, ComPtr Destination, ComPtr Source, - llvm::ArrayRef MipFootprints = {}) { + llvm::ArrayRef SubresFootprints = + {}) { addUploadBeginBarrier(IS, Destination); if (R.isTexture()) { const offloadtest::CPUBuffer &B = *R.BufferPtr; - if (!MipFootprints.empty()) { - for (uint32_t Mip = 0; Mip < MipFootprints.size(); ++Mip) { - const CD3DX12_TEXTURE_COPY_LOCATION DstLoc(Destination.Get(), Mip); + if (!SubresFootprints.empty()) { + // Multi-subresource path (mips and/or array slices). Subresource index + // ordering matches D3D12: Sub = Slice * MipLevels + Mip. + for (uint32_t Sub = 0; Sub < SubresFootprints.size(); ++Sub) { + const CD3DX12_TEXTURE_COPY_LOCATION DstLoc(Destination.Get(), Sub); const CD3DX12_TEXTURE_COPY_LOCATION SrcLoc(Source.Get(), - MipFootprints[Mip]); + SubresFootprints[Sub]); IS.CB->CmdList->CopyTextureRegion(&DstLoc, 0, 0, 0, &SrcLoc, nullptr); } } else { - const DXGI_FORMAT DXFormat = getDXFormat(B.Format, B.Channels); - const uint32_t RowPitch = B.OutputProps.Width * B.getElementSize(); - const uint32_t SliceBytes = RowPitch * B.OutputProps.Height; - const uint32_t NumSlices = (R.Kind == ResourceKind::Texture2DArray || - R.Kind == ResourceKind::RWTexture2DArray) - ? B.OutputProps.ArraySize - : 1; - for (uint32_t Slice = 0; Slice < NumSlices; ++Slice) { - const D3D12_PLACED_SUBRESOURCE_FOOTPRINT Footprint{ - Slice * SliceBytes, - CD3DX12_SUBRESOURCE_FOOTPRINT(DXFormat, B.OutputProps.Width, - B.OutputProps.Height, 1, RowPitch)}; - const CD3DX12_TEXTURE_COPY_LOCATION DstLoc(Destination.Get(), Slice); - const CD3DX12_TEXTURE_COPY_LOCATION SrcLoc(Source.Get(), Footprint); - IS.CB->CmdList->CopyTextureRegion(&DstLoc, 0, 0, 0, &SrcLoc, nullptr); - } + // Single-subresource fast path (no mips, no array slices). + const D3D12_PLACED_SUBRESOURCE_FOOTPRINT Footprint{ + 0, CD3DX12_SUBRESOURCE_FOOTPRINT( + getDXFormat(B.Format, B.Channels), B.OutputProps.Width, + B.OutputProps.Height, 1, + B.OutputProps.Width * B.getElementSize())}; + const CD3DX12_TEXTURE_COPY_LOCATION DstLoc(Destination.Get(), 0); + const CD3DX12_TEXTURE_COPY_LOCATION SrcLoc(Source.Get(), Footprint); + IS.CB->CmdList->CopyTextureRegion(&DstLoc, 0, 0, 0, &SrcLoc, nullptr); } } else IS.CB->CmdList->CopyBufferRegion(Destination.Get(), 0, Source.Get(), 0, @@ -1712,23 +1709,35 @@ class DXDevice : public offloadtest::Device { const D3D12_HEAP_PROPERTIES UploadHeapProp = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD); - const bool IsMipMappedTexture = - R.isTexture() && R.BufferPtr->OutputProps.MipLevels > 1; - llvm::SmallVector MipFootprints; - llvm::SmallVector MipNumRows; - llvm::SmallVector MipRowSizes; - UINT64 MipTotalBytes = 0; - if (IsMipMappedTexture) { - const uint32_t MipLevels = R.BufferPtr->OutputProps.MipLevels; - MipFootprints.resize(MipLevels); - MipNumRows.resize(MipLevels); - MipRowSizes.resize(MipLevels); - Device->GetCopyableFootprints(&ResDesc, 0, MipLevels, 0, - MipFootprints.data(), MipNumRows.data(), - MipRowSizes.data(), &MipTotalBytes); + const bool IsTexture = R.isTexture(); + const uint32_t MipLevels = + IsTexture ? R.BufferPtr->OutputProps.MipLevels : 1; + const uint32_t ArraySize = + IsTexture && (R.Kind == ResourceKind::Texture2DArray || + R.Kind == ResourceKind::RWTexture2DArray) + ? R.BufferPtr->OutputProps.ArraySize + : 1; + // Use the GetCopyableFootprints layout whenever a texture has multiple + // subresources (mip > 1, array slices > 1, or both). This keeps row-pitch + // alignment (D3D12_TEXTURE_DATA_PITCH_ALIGNMENT) handled by the runtime + // for every (mip, slice) instead of the tight Width*EltSize assumption. + const bool NeedsSubresourceLayout = + IsTexture && (MipLevels > 1 || ArraySize > 1); + const uint32_t NumSubresources = MipLevels * ArraySize; + llvm::SmallVector SubresFootprints; + llvm::SmallVector SubresNumRows; + llvm::SmallVector SubresRowSizes; + UINT64 SubresTotalBytes = 0; + if (NeedsSubresourceLayout) { + SubresFootprints.resize(NumSubresources); + SubresNumRows.resize(NumSubresources); + SubresRowSizes.resize(NumSubresources); + Device->GetCopyableFootprints( + &ResDesc, 0, NumSubresources, 0, SubresFootprints.data(), + SubresNumRows.data(), SubresRowSizes.data(), &SubresTotalBytes); } const D3D12_RESOURCE_DESC UploadResDesc = CD3DX12_RESOURCE_DESC::Buffer( - IsMipMappedTexture ? MipTotalBytes : R.size()); + NeedsSubresourceLayout ? SubresTotalBytes : R.size()); uint32_t RegOffset = 0; @@ -1780,22 +1789,24 @@ class DXDevice : public offloadtest::Device { // Upload data initialization void *ResDataPtr = nullptr; if (SUCCEEDED(UploadBuffer->Map(0, NULL, &ResDataPtr))) { - if (IsMipMappedTexture) { - // Source CPU data is tightly packed for all mips (per docs). - // Destination upload buffer has D3D12-aligned per-mip layout from - // GetCopyableFootprints; copy each mip row-by-row applying the - // possibly-padded row pitch. + if (NeedsSubresourceLayout) { + // Source CPU data is tightly packed in subresource order + // (slice-major, mip-major within slice: Sub = Slice * MipLevels + Mip + // matching D3D12). Destination upload buffer has D3D12-aligned + // per-subresource layout from GetCopyableFootprints; copy each + // subresource row-by-row applying the possibly-padded row pitch. const uint8_t *Src = reinterpret_cast(ResData.get()); uint8_t *Dst = static_cast(ResDataPtr); - for (uint32_t Mip = 0; Mip < MipFootprints.size(); ++Mip) { - const auto &FP = MipFootprints[Mip]; - const size_t TightRowBytes = static_cast(MipRowSizes[Mip]); + for (uint32_t Sub = 0; Sub < NumSubresources; ++Sub) { + const auto &FP = SubresFootprints[Sub]; + const size_t TightRowBytes = + static_cast(SubresRowSizes[Sub]); const size_t PaddedRowPitch = static_cast(FP.Footprint.RowPitch); - for (UINT Row = 0; Row < MipNumRows[Mip]; ++Row) + for (UINT Row = 0; Row < SubresNumRows[Sub]; ++Row) memcpy(Dst + FP.Offset + Row * PaddedRowPitch, Src + Row * TightRowBytes, TightRowBytes); - Src += TightRowBytes * MipNumRows[Mip]; + Src += TightRowBytes * SubresNumRows[Sub]; } } else { memcpy(ResDataPtr, ResData.get(), R.size()); @@ -1806,7 +1817,7 @@ class DXDevice : public offloadtest::Device { "Failed to map SRV upload buffer."); } - addResourceUploadCommands(R, IS, Buffer, UploadBuffer, MipFootprints); + addResourceUploadCommands(R, IS, Buffer, UploadBuffer, SubresFootprints); Bundle.emplace_back(UploadBuffer, Buffer, nullptr, Heap); RegOffset++; @@ -1847,8 +1858,33 @@ class DXDevice : public offloadtest::Device { const D3D12_HEAP_PROPERTIES UploadHeapProp = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD); - const D3D12_RESOURCE_DESC UploadResDesc = - CD3DX12_RESOURCE_DESC::Buffer(BufferSize); + const bool IsTexture = R.isTexture(); + const uint32_t MipLevels = + IsTexture ? R.BufferPtr->OutputProps.MipLevels : 1; + const uint32_t ArraySize = + IsTexture && (R.Kind == ResourceKind::Texture2DArray || + R.Kind == ResourceKind::RWTexture2DArray) + ? R.BufferPtr->OutputProps.ArraySize + : 1; + // UAVs only ever address a single mip per descriptor; + // getResourceDescription already rejects mip>1 for non-SRV kinds. + // Multi-subresource layout is only needed here for array slices. + const bool NeedsSubresourceLayout = IsTexture && ArraySize > 1; + const uint32_t NumSubresources = MipLevels * ArraySize; + llvm::SmallVector SubresFootprints; + llvm::SmallVector SubresNumRows; + llvm::SmallVector SubresRowSizes; + UINT64 SubresTotalBytes = 0; + if (NeedsSubresourceLayout) { + SubresFootprints.resize(NumSubresources); + SubresNumRows.resize(NumSubresources); + SubresRowSizes.resize(NumSubresources); + Device->GetCopyableFootprints( + &ResDesc, 0, NumSubresources, 0, SubresFootprints.data(), + SubresNumRows.data(), SubresRowSizes.data(), &SubresTotalBytes); + } + const D3D12_RESOURCE_DESC UploadResDesc = CD3DX12_RESOURCE_DESC::Buffer( + NeedsSubresourceLayout ? SubresTotalBytes : BufferSize); uint32_t RegOffset = 0; @@ -1906,14 +1942,30 @@ class DXDevice : public offloadtest::Device { // Upload data initialization void *ResDataPtr = nullptr; if (SUCCEEDED(UploadBuffer->Map(0, NULL, &ResDataPtr))) { - memcpy(ResDataPtr, ResData.get(), R.size()); + if (NeedsSubresourceLayout) { + const uint8_t *Src = reinterpret_cast(ResData.get()); + uint8_t *Dst = static_cast(ResDataPtr); + for (uint32_t Sub = 0; Sub < NumSubresources; ++Sub) { + const auto &FP = SubresFootprints[Sub]; + const size_t TightRowBytes = + static_cast(SubresRowSizes[Sub]); + const size_t PaddedRowPitch = + static_cast(FP.Footprint.RowPitch); + for (UINT Row = 0; Row < SubresNumRows[Sub]; ++Row) + memcpy(Dst + FP.Offset + Row * PaddedRowPitch, + Src + Row * TightRowBytes, TightRowBytes); + Src += TightRowBytes * SubresNumRows[Sub]; + } + } else { + memcpy(ResDataPtr, ResData.get(), R.size()); + } UploadBuffer->Unmap(0, nullptr); } else { return llvm::createStringError(std::errc::io_error, "Failed to map UAV upload buffer."); } - addResourceUploadCommands(R, IS, Buffer, UploadBuffer); + addResourceUploadCommands(R, IS, Buffer, UploadBuffer, SubresFootprints); Bundle.emplace_back(UploadBuffer, Buffer, std::move(*ReadbackOrErr), Heap); diff --git a/test/Feature/Textures/Texture2DArray.Load.MipMaps.test.yaml b/test/Feature/Textures/Texture2DArray.Load.MipMaps.test.yaml new file mode 100644 index 000000000..2855810a8 --- /dev/null +++ b/test/Feature/Textures/Texture2DArray.Load.MipMaps.test.yaml @@ -0,0 +1,102 @@ +#--- source.hlsl +[[vk::binding(0, 0)]] Texture2DArray Tex : register(t0); +[[vk::binding(1, 0)]] RWBuffer Out : register(u0); + +[numthreads(1, 1, 1)] +void main() { + // Texture2DArray::Load(int4(x, y, slice, mip)) + // Per-slice mip chain (slice-major then mip-major within slice). + + // Slice 0, mip 0 (2x2): R, G, B, W + Out[0] = Tex.Load(int4(0, 0, 0, 0)); + Out[1] = Tex.Load(int4(1, 0, 0, 0)); + Out[2] = Tex.Load(int4(0, 1, 0, 0)); + Out[3] = Tex.Load(int4(1, 1, 0, 0)); + + // Slice 0, mip 1 (1x1): cyan + Out[4] = Tex.Load(int4(0, 0, 0, 1)); + + // Slice 1, mip 0 (2x2): solid red + Out[5] = Tex.Load(int4(0, 0, 1, 0)); + Out[6] = Tex.Load(int4(1, 1, 1, 0)); + + // Slice 1, mip 1 (1x1): solid green + Out[7] = Tex.Load(int4(0, 0, 1, 1)); +} + +//--- pipeline.yaml +--- +Shaders: + - Stage: Compute + Entry: main + +Buffers: + - Name: Tex + Format: Float32 + Channels: 4 + OutputProps: { Width: 2, Height: 2, Depth: 1, ArraySize: 2, MipLevels: 2 } + Data: [ + # --- Slice 0, mip 0 (2x2 = 4 RGBA texels): R, G, B, W --- + 1.0, 0.0, 0.0, 1.0, + 0.0, 1.0, 0.0, 1.0, + 0.0, 0.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, + # --- Slice 0, mip 1 (1x1 = 1 RGBA texel): cyan --- + 0.0, 1.0, 1.0, 1.0, + # --- Slice 1, mip 0 (2x2 = 4 RGBA texels): solid red --- + 1.0, 0.0, 0.0, 1.0, + 1.0, 0.0, 0.0, 1.0, + 1.0, 0.0, 0.0, 1.0, + 1.0, 0.0, 0.0, 1.0, + # --- Slice 1, mip 1 (1x1 = 1 RGBA texel): solid green --- + 0.0, 1.0, 0.0, 1.0 + ] + + - Name: Out + Format: Float32 + Channels: 4 + FillSize: 128 # 8 * sizeof(float4) + + - Name: Expected + Format: Float32 + Channels: 4 + Data: [ + 1.0, 0.0, 0.0, 1.0, # [0] R (slice 0, mip 0, (0,0)) + 0.0, 1.0, 0.0, 1.0, # [1] G (slice 0, mip 0, (1,0)) + 0.0, 0.0, 1.0, 1.0, # [2] B (slice 0, mip 0, (0,1)) + 1.0, 1.0, 1.0, 1.0, # [3] W (slice 0, mip 0, (1,1)) + 0.0, 1.0, 1.0, 1.0, # [4] cyan (slice 0, mip 1) + 1.0, 0.0, 0.0, 1.0, # [5] red (slice 1, mip 0) + 1.0, 0.0, 0.0, 1.0, # [6] red (slice 1, mip 0) + 0.0, 1.0, 0.0, 1.0 # [7] green (slice 1, mip 1) + ] + +DescriptorSets: + - Resources: + - Name: Tex + Kind: Texture2DArray + DirectXBinding: { Register: 0, Space: 0 } + VulkanBinding: { Binding: 0 } + - Name: Out + Kind: RWBuffer + DirectXBinding: { Register: 0, Space: 0 } + VulkanBinding: { Binding: 1 } + +Results: + - Result: LoadMipsTest + Rule: BufferExact + Actual: Out + Expected: Expected +... +#--- end + + +# Texture2DArray support is currently DirectX-only. +# UNSUPPORTED: Vulkan || Metal + +# Clang HLSL has no Texture2DArray template yet. +# XFAIL: Clang + +# RUN: split-file %s %t +# RUN: %dxc_target -T cs_6_0 -Fo %t.o %t/source.hlsl +# RUN: %offloader %t/pipeline.yaml %t.o