// Copyright 2017 The Dawn Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include #include "common/Assert.h" #include "common/Constants.h" #include "common/Math.h" #include "dawn/webgpu_cpp_print.h" #include "dawn_native/Format.h" #include "dawn_native/d3d12/TextureCopySplitter.h" #include "dawn_native/d3d12/d3d12_platform.h" #include "utils/TestUtils.h" using namespace dawn_native::d3d12; namespace { struct TextureSpec { uint32_t x; uint32_t y; uint32_t z; uint32_t width; uint32_t height; uint32_t depthOrArrayLayers; uint32_t texelBlockSizeInBytes; uint32_t blockWidth = 1; uint32_t blockHeight = 1; }; struct BufferSpec { uint64_t offset; uint32_t bytesPerRow; uint32_t rowsPerImage; }; // Check that each copy region fits inside the buffer footprint void ValidateFootprints(const TextureSpec& textureSpec, const BufferSpec& bufferSpec, const TextureCopySubresource& copySplit, wgpu::TextureDimension dimension) { for (uint32_t i = 0; i < copySplit.count; ++i) { const auto& copy = copySplit.copies[i]; ASSERT_LE(copy.bufferOffset.x + copy.copySize.width, copy.bufferSize.width); ASSERT_LE(copy.bufferOffset.y + copy.copySize.height, copy.bufferSize.height); ASSERT_LE(copy.bufferOffset.z + copy.copySize.depthOrArrayLayers, copy.bufferSize.depthOrArrayLayers); // If there are multiple layers, 2D texture splitter actually splits each layer // independently. See the details in Compute2DTextureCopySplits(). As a result, // if we simply expand a copy region generated by 2D texture splitter to all // layers, the copy region might be OOB. But that is not the approach that the current // 2D texture splitter is doing, although Compute2DTextureCopySubresource forwards // "copySize.depthOrArrayLayers" to the copy region it generated. So skip the test // below for 2D textures with multiple layers. if (textureSpec.depthOrArrayLayers <= 1 || dimension == wgpu::TextureDimension::e3D) { uint32_t widthInBlocks = textureSpec.width / textureSpec.blockWidth; uint32_t heightInBlocks = textureSpec.height / textureSpec.blockHeight; uint64_t minimumRequiredBufferSize = bufferSpec.offset + utils::RequiredBytesInCopy(bufferSpec.bytesPerRow, bufferSpec.rowsPerImage, widthInBlocks, heightInBlocks, textureSpec.depthOrArrayLayers, textureSpec.texelBlockSizeInBytes); // The last pixel (buffer footprint) of each copy region depends on its bufferOffset // and copySize. It is not the last pixel where the bufferSize ends. ASSERT_EQ(copy.bufferOffset.x % textureSpec.blockWidth, 0u); ASSERT_EQ(copy.copySize.width % textureSpec.blockWidth, 0u); uint32_t footprintWidth = copy.bufferOffset.x + copy.copySize.width; ASSERT_EQ(footprintWidth % textureSpec.blockWidth, 0u); uint32_t footprintWidthInBlocks = footprintWidth / textureSpec.blockWidth; ASSERT_EQ(copy.bufferOffset.y % textureSpec.blockHeight, 0u); ASSERT_EQ(copy.copySize.height % textureSpec.blockHeight, 0u); uint32_t footprintHeight = copy.bufferOffset.y + copy.copySize.height; ASSERT_EQ(footprintHeight % textureSpec.blockHeight, 0u); uint32_t footprintHeightInBlocks = footprintHeight / textureSpec.blockHeight; uint64_t bufferSizeForFootprint = copy.alignedOffset + utils::RequiredBytesInCopy(bufferSpec.bytesPerRow, copy.bufferSize.height, footprintWidthInBlocks, footprintHeightInBlocks, copy.bufferSize.depthOrArrayLayers, textureSpec.texelBlockSizeInBytes); // The buffer footprint of each copy region should not exceed the minimum required // buffer size. Otherwise, pixels accessed by copy may be OOB. ASSERT_LE(bufferSizeForFootprint, minimumRequiredBufferSize); } } } // Check that the offset is aligned void ValidateOffset(const TextureCopySubresource& copySplit) { for (uint32_t i = 0; i < copySplit.count; ++i) { ASSERT_TRUE( Align(copySplit.copies[i].alignedOffset, D3D12_TEXTURE_DATA_PLACEMENT_ALIGNMENT) == copySplit.copies[i].alignedOffset); } } bool InclusiveRangesOverlap(uint32_t minA, uint32_t maxA, uint32_t minB, uint32_t maxB) { return (minA <= minB && minB <= maxA) || (minB <= minA && minA <= maxB); } // Check that no pair of copy regions intersect each other void ValidateDisjoint(const TextureCopySubresource& copySplit) { for (uint32_t i = 0; i < copySplit.count; ++i) { const auto& a = copySplit.copies[i]; for (uint32_t j = i + 1; j < copySplit.count; ++j) { const auto& b = copySplit.copies[j]; // If textureOffset.x is 0, and copySize.width is 2, we are copying pixel 0 and // 1. We never touch pixel 2 on x-axis. So the copied range on x-axis should be // [textureOffset.x, textureOffset.x + copySize.width - 1] and both ends are // included. bool overlapX = InclusiveRangesOverlap( a.textureOffset.x, a.textureOffset.x + a.copySize.width - 1, b.textureOffset.x, b.textureOffset.x + b.copySize.width - 1); bool overlapY = InclusiveRangesOverlap( a.textureOffset.y, a.textureOffset.y + a.copySize.height - 1, b.textureOffset.y, b.textureOffset.y + b.copySize.height - 1); bool overlapZ = InclusiveRangesOverlap( a.textureOffset.z, a.textureOffset.z + a.copySize.depthOrArrayLayers - 1, b.textureOffset.z, b.textureOffset.z + b.copySize.depthOrArrayLayers - 1); ASSERT_TRUE(!overlapX || !overlapY || !overlapZ); } } } // Check that the union of the copy regions exactly covers the texture region void ValidateTextureBounds(const TextureSpec& textureSpec, const TextureCopySubresource& copySplit) { ASSERT_TRUE(copySplit.count > 0); uint32_t minX = copySplit.copies[0].textureOffset.x; uint32_t minY = copySplit.copies[0].textureOffset.y; uint32_t minZ = copySplit.copies[0].textureOffset.z; uint32_t maxX = copySplit.copies[0].textureOffset.x + copySplit.copies[0].copySize.width; uint32_t maxY = copySplit.copies[0].textureOffset.y + copySplit.copies[0].copySize.height; uint32_t maxZ = copySplit.copies[0].textureOffset.z + copySplit.copies[0].copySize.depthOrArrayLayers; for (uint32_t i = 1; i < copySplit.count; ++i) { const auto& copy = copySplit.copies[i]; minX = std::min(minX, copy.textureOffset.x); minY = std::min(minY, copy.textureOffset.y); minZ = std::min(minZ, copy.textureOffset.z); maxX = std::max(maxX, copy.textureOffset.x + copy.copySize.width); maxY = std::max(maxY, copy.textureOffset.y + copy.copySize.height); maxZ = std::max(maxZ, copy.textureOffset.z + copy.copySize.depthOrArrayLayers); } ASSERT_EQ(minX, textureSpec.x); ASSERT_EQ(minY, textureSpec.y); ASSERT_EQ(minZ, textureSpec.z); ASSERT_EQ(maxX, textureSpec.x + textureSpec.width); ASSERT_EQ(maxY, textureSpec.y + textureSpec.height); ASSERT_EQ(maxZ, textureSpec.z + textureSpec.depthOrArrayLayers); } // Validate that the number of pixels copied is exactly equal to the number of pixels in the // texture region void ValidatePixelCount(const TextureSpec& textureSpec, const TextureCopySubresource& copySplit) { uint32_t count = 0; for (uint32_t i = 0; i < copySplit.count; ++i) { const auto& copy = copySplit.copies[i]; uint32_t copiedPixels = copy.copySize.width * copy.copySize.height * copy.copySize.depthOrArrayLayers; ASSERT_GT(copiedPixels, 0u); count += copiedPixels; } ASSERT_EQ(count, textureSpec.width * textureSpec.height * textureSpec.depthOrArrayLayers); } // Check that every buffer offset is at the correct pixel location void ValidateBufferOffset(const TextureSpec& textureSpec, const BufferSpec& bufferSpec, const TextureCopySubresource& copySplit, wgpu::TextureDimension dimension) { ASSERT_TRUE(copySplit.count > 0); uint32_t texelsPerBlock = textureSpec.blockWidth * textureSpec.blockHeight; for (uint32_t i = 0; i < copySplit.count; ++i) { const auto& copy = copySplit.copies[i]; uint32_t bytesPerRowInTexels = bufferSpec.bytesPerRow / textureSpec.texelBlockSizeInBytes * texelsPerBlock; uint32_t slicePitchInTexels = bytesPerRowInTexels * (bufferSpec.rowsPerImage / textureSpec.blockHeight); uint32_t absoluteTexelOffset = copy.alignedOffset / textureSpec.texelBlockSizeInBytes * texelsPerBlock + copy.bufferOffset.x / textureSpec.blockWidth * texelsPerBlock + copy.bufferOffset.y / textureSpec.blockHeight * bytesPerRowInTexels; // There is one empty row at most in a 2D copy region. However, it is not true for // a 3D texture copy region when we are copying the last row of each slice. We may // need to offset a lot rows and copy.bufferOffset.y may be big. if (dimension == wgpu::TextureDimension::e2D) { ASSERT_LE(copy.bufferOffset.y, textureSpec.blockHeight); } ASSERT_EQ(copy.bufferOffset.z, 0u); ASSERT_GE(absoluteTexelOffset, bufferSpec.offset / textureSpec.texelBlockSizeInBytes * texelsPerBlock); uint32_t relativeTexelOffset = absoluteTexelOffset - bufferSpec.offset / textureSpec.texelBlockSizeInBytes * texelsPerBlock; uint32_t z = relativeTexelOffset / slicePitchInTexels; uint32_t y = (relativeTexelOffset % slicePitchInTexels) / bytesPerRowInTexels; uint32_t x = relativeTexelOffset % bytesPerRowInTexels; ASSERT_EQ(copy.textureOffset.x - textureSpec.x, x); ASSERT_EQ(copy.textureOffset.y - textureSpec.y, y); ASSERT_EQ(copy.textureOffset.z - textureSpec.z, z); } } void ValidateCopySplit(const TextureSpec& textureSpec, const BufferSpec& bufferSpec, const TextureCopySubresource& copySplit, wgpu::TextureDimension dimension) { ValidateFootprints(textureSpec, bufferSpec, copySplit, dimension); ValidateOffset(copySplit); ValidateDisjoint(copySplit); ValidateTextureBounds(textureSpec, copySplit); ValidatePixelCount(textureSpec, copySplit); ValidateBufferOffset(textureSpec, bufferSpec, copySplit, dimension); } std::ostream& operator<<(std::ostream& os, const TextureSpec& textureSpec) { os << "TextureSpec(" << "[(" << textureSpec.x << ", " << textureSpec.y << ", " << textureSpec.z << "), (" << textureSpec.width << ", " << textureSpec.height << ", " << textureSpec.depthOrArrayLayers << ")], " << textureSpec.texelBlockSizeInBytes << ")"; return os; } std::ostream& operator<<(std::ostream& os, const BufferSpec& bufferSpec) { os << "BufferSpec(" << bufferSpec.offset << ", " << bufferSpec.bytesPerRow << ", " << bufferSpec.rowsPerImage << ")"; return os; } std::ostream& operator<<(std::ostream& os, const TextureCopySubresource& copySplit) { os << "CopySplit" << std::endl; for (uint32_t i = 0; i < copySplit.count; ++i) { const auto& copy = copySplit.copies[i]; os << " " << i << ": Texture at (" << copy.textureOffset.x << ", " << copy.textureOffset.y << ", " << copy.textureOffset.z << "), size (" << copy.copySize.width << ", " << copy.copySize.height << ", " << copy.copySize.depthOrArrayLayers << ")" << std::endl; os << " " << i << ": Buffer at (" << copy.bufferOffset.x << ", " << copy.bufferOffset.y << ", " << copy.bufferOffset.z << "), footprint (" << copy.bufferSize.width << ", " << copy.bufferSize.height << ", " << copy.bufferSize.depthOrArrayLayers << ")" << std::endl; } return os; } // Define base texture sizes and offsets to test with: some aligned, some unaligned constexpr TextureSpec kBaseTextureSpecs[] = { {0, 0, 0, 1, 1, 1, 4}, {0, 0, 0, 64, 1, 1, 4}, {0, 0, 0, 128, 1, 1, 4}, {0, 0, 0, 192, 1, 1, 4}, {31, 16, 0, 1, 1, 1, 4}, {64, 16, 0, 1, 1, 1, 4}, {64, 16, 8, 1, 1, 1, 4}, {0, 0, 0, 64, 2, 1, 4}, {0, 0, 0, 64, 1, 2, 4}, {0, 0, 0, 64, 2, 2, 4}, {0, 0, 0, 128, 2, 1, 4}, {0, 0, 0, 128, 1, 2, 4}, {0, 0, 0, 128, 2, 2, 4}, {0, 0, 0, 192, 2, 1, 4}, {0, 0, 0, 192, 1, 2, 4}, {0, 0, 0, 192, 2, 2, 4}, {0, 0, 0, 1024, 1024, 1, 4}, {256, 512, 0, 1024, 1024, 1, 4}, {64, 48, 0, 1024, 1024, 1, 4}, {64, 48, 16, 1024, 1024, 1024, 4}, {0, 0, 0, 257, 31, 1, 4}, {0, 0, 0, 17, 93, 1, 4}, {59, 13, 0, 257, 31, 1, 4}, {17, 73, 0, 17, 93, 1, 4}, {17, 73, 59, 17, 93, 99, 4}, {0, 0, 0, 4, 4, 1, 8, 4, 4}, {64, 16, 0, 4, 4, 1, 8, 4, 4}, {64, 16, 8, 4, 4, 1, 8, 4, 4}, {0, 0, 0, 4, 4, 1, 16, 4, 4}, {64, 16, 0, 4, 4, 1, 16, 4, 4}, {64, 16, 8, 4, 4, 1, 16, 4, 4}, {0, 0, 0, 1024, 1024, 1, 8, 4, 4}, {256, 512, 0, 1024, 1024, 1, 8, 4, 4}, {64, 48, 0, 1024, 1024, 1, 8, 4, 4}, {64, 48, 16, 1024, 1024, 1, 8, 4, 4}, {0, 0, 0, 1024, 1024, 1, 16, 4, 4}, {256, 512, 0, 1024, 1024, 1, 16, 4, 4}, {64, 48, 0, 1024, 1024, 1, 4, 16, 4}, {64, 48, 16, 1024, 1024, 1, 16, 4, 4}, }; // Define base buffer sizes to work with: some offsets aligned, some unaligned. bytesPerRow is // the minimum required std::array BaseBufferSpecs(const TextureSpec& textureSpec) { uint32_t bytesPerRow = Align(textureSpec.texelBlockSizeInBytes * textureSpec.width, kTextureBytesPerRowAlignment); auto alignNonPow2 = [](uint32_t value, uint32_t size) -> uint32_t { return value == 0 ? 0 : ((value - 1) / size + 1) * size; }; return { BufferSpec{alignNonPow2(0, textureSpec.texelBlockSizeInBytes), bytesPerRow, textureSpec.height}, BufferSpec{alignNonPow2(256, textureSpec.texelBlockSizeInBytes), bytesPerRow, textureSpec.height}, BufferSpec{alignNonPow2(512, textureSpec.texelBlockSizeInBytes), bytesPerRow, textureSpec.height}, BufferSpec{alignNonPow2(1024, textureSpec.texelBlockSizeInBytes), bytesPerRow, textureSpec.height}, BufferSpec{alignNonPow2(1024, textureSpec.texelBlockSizeInBytes), bytesPerRow, textureSpec.height * 2}, BufferSpec{alignNonPow2(32, textureSpec.texelBlockSizeInBytes), bytesPerRow, textureSpec.height}, BufferSpec{alignNonPow2(64, textureSpec.texelBlockSizeInBytes), bytesPerRow, textureSpec.height}, BufferSpec{alignNonPow2(64, textureSpec.texelBlockSizeInBytes), bytesPerRow, textureSpec.height * 2}, BufferSpec{alignNonPow2(31, textureSpec.texelBlockSizeInBytes), bytesPerRow, textureSpec.height}, BufferSpec{alignNonPow2(257, textureSpec.texelBlockSizeInBytes), bytesPerRow, textureSpec.height}, BufferSpec{alignNonPow2(384, textureSpec.texelBlockSizeInBytes), bytesPerRow, textureSpec.height}, BufferSpec{alignNonPow2(511, textureSpec.texelBlockSizeInBytes), bytesPerRow, textureSpec.height}, BufferSpec{alignNonPow2(513, textureSpec.texelBlockSizeInBytes), bytesPerRow, textureSpec.height}, BufferSpec{alignNonPow2(1023, textureSpec.texelBlockSizeInBytes), bytesPerRow, textureSpec.height}, BufferSpec{alignNonPow2(1023, textureSpec.texelBlockSizeInBytes), bytesPerRow, textureSpec.height * 2}, }; } // Define a list of values to set properties in the spec structs constexpr uint32_t kCheckValues[] = {1, 2, 3, 4, 5, 6, 7, 8, // small values 16, 32, 64, 128, 256, 512, 1024, 2048, // powers of 2 15, 31, 63, 127, 257, 511, 1023, 2047, // misalignments 17, 33, 65, 129, 257, 513, 1025, 2049}; } // namespace class CopySplitTest : public testing::TestWithParam { protected: void DoTest(const TextureSpec& textureSpec, const BufferSpec& bufferSpec) { ASSERT(textureSpec.width % textureSpec.blockWidth == 0 && textureSpec.height % textureSpec.blockHeight == 0); wgpu::TextureDimension dimension = GetParam(); TextureCopySubresource copySplit; switch (dimension) { case wgpu::TextureDimension::e2D: { copySplit = Compute2DTextureCopySubresource( {textureSpec.x, textureSpec.y, textureSpec.z}, {textureSpec.width, textureSpec.height, textureSpec.depthOrArrayLayers}, {textureSpec.texelBlockSizeInBytes, textureSpec.blockWidth, textureSpec.blockHeight}, bufferSpec.offset, bufferSpec.bytesPerRow); break; } case wgpu::TextureDimension::e3D: { copySplit = Compute3DTextureCopySplits( {textureSpec.x, textureSpec.y, textureSpec.z}, {textureSpec.width, textureSpec.height, textureSpec.depthOrArrayLayers}, {textureSpec.texelBlockSizeInBytes, textureSpec.blockWidth, textureSpec.blockHeight}, bufferSpec.offset, bufferSpec.bytesPerRow, bufferSpec.rowsPerImage); break; } default: UNREACHABLE(); break; } ValidateCopySplit(textureSpec, bufferSpec, copySplit, dimension); if (HasFatalFailure()) { std::ostringstream message; message << "Failed generating splits: " << textureSpec << ", " << bufferSpec << std::endl << dimension << " " << copySplit << std::endl; FAIL() << message.str(); } } }; TEST_P(CopySplitTest, General) { for (TextureSpec textureSpec : kBaseTextureSpecs) { for (BufferSpec bufferSpec : BaseBufferSpecs(textureSpec)) { DoTest(textureSpec, bufferSpec); } } } TEST_P(CopySplitTest, TextureWidth) { for (TextureSpec textureSpec : kBaseTextureSpecs) { for (uint32_t val : kCheckValues) { if (val % textureSpec.blockWidth != 0) { continue; } textureSpec.width = val; for (BufferSpec bufferSpec : BaseBufferSpecs(textureSpec)) { DoTest(textureSpec, bufferSpec); } } } } TEST_P(CopySplitTest, TextureHeight) { for (TextureSpec textureSpec : kBaseTextureSpecs) { for (uint32_t val : kCheckValues) { if (val % textureSpec.blockHeight != 0) { continue; } textureSpec.height = val; for (BufferSpec bufferSpec : BaseBufferSpecs(textureSpec)) { DoTest(textureSpec, bufferSpec); } } } } TEST_P(CopySplitTest, TextureX) { for (TextureSpec textureSpec : kBaseTextureSpecs) { for (uint32_t val : kCheckValues) { textureSpec.x = val; for (BufferSpec bufferSpec : BaseBufferSpecs(textureSpec)) { DoTest(textureSpec, bufferSpec); } } } } TEST_P(CopySplitTest, TextureY) { for (TextureSpec textureSpec : kBaseTextureSpecs) { for (uint32_t val : kCheckValues) { textureSpec.y = val; for (BufferSpec bufferSpec : BaseBufferSpecs(textureSpec)) { DoTest(textureSpec, bufferSpec); } } } } TEST_P(CopySplitTest, TexelSize) { for (TextureSpec textureSpec : kBaseTextureSpecs) { for (uint32_t texelSize : {4, 8, 16, 32, 64}) { textureSpec.texelBlockSizeInBytes = texelSize; for (BufferSpec bufferSpec : BaseBufferSpecs(textureSpec)) { DoTest(textureSpec, bufferSpec); } } } } TEST_P(CopySplitTest, BufferOffset) { for (TextureSpec textureSpec : kBaseTextureSpecs) { for (BufferSpec bufferSpec : BaseBufferSpecs(textureSpec)) { for (uint32_t val : kCheckValues) { bufferSpec.offset = textureSpec.texelBlockSizeInBytes * val; DoTest(textureSpec, bufferSpec); } } } } TEST_P(CopySplitTest, RowPitch) { for (TextureSpec textureSpec : kBaseTextureSpecs) { for (BufferSpec bufferSpec : BaseBufferSpecs(textureSpec)) { uint32_t baseRowPitch = bufferSpec.bytesPerRow; for (uint32_t i = 0; i < 5; ++i) { bufferSpec.bytesPerRow = baseRowPitch + i * 256; DoTest(textureSpec, bufferSpec); } } } } TEST_P(CopySplitTest, ImageHeight) { for (TextureSpec textureSpec : kBaseTextureSpecs) { for (BufferSpec bufferSpec : BaseBufferSpecs(textureSpec)) { uint32_t baseImageHeight = bufferSpec.rowsPerImage; for (uint32_t i = 0; i < 5; ++i) { bufferSpec.rowsPerImage = baseImageHeight + i * 256; DoTest(textureSpec, bufferSpec); } } } } INSTANTIATE_TEST_SUITE_P(, CopySplitTest, testing::Values(wgpu::TextureDimension::e2D, wgpu::TextureDimension::e3D));