| // Copyright 2026 The Dawn & Tint Authors |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are met: |
| // |
| // 1. Redistributions of source code must retain the above copyright notice, this |
| // list of conditions and the following disclaimer. |
| // |
| // 2. Redistributions in binary form must reproduce the above copyright notice, |
| // this list of conditions and the following disclaimer in the documentation |
| // and/or other materials provided with the distribution. |
| // |
| // 3. Neither the name of the copyright holder nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| #include <string> |
| #include <vector> |
| |
| #include "dawn/tests/DawnTest.h" |
| #include "dawn/utils/ComboRenderPipelineDescriptor.h" |
| #include "dawn/utils/WGPUHelpers.h" |
| |
| namespace dawn { |
| namespace { |
| |
| class MetalBufferRobustnessTest : public DawnTest { |
| protected: |
| void GetRequiredLimits(const dawn::utils::ComboLimits& supported, |
| dawn::utils::ComboLimits& required) override { |
| required.maxBufferSize = supported.maxBufferSize; |
| required.maxStorageBufferBindingSize = supported.maxStorageBufferBindingSize; |
| } |
| |
| std::vector<wgpu::FeatureName> GetRequiredFeatures() override { |
| return {wgpu::FeatureName::IndirectFirstInstance}; |
| } |
| |
| wgpu::Buffer CreateBuffer(uint64_t size, wgpu::BufferUsage usage) { |
| wgpu::BufferDescriptor descriptor; |
| descriptor.size = size; |
| descriptor.usage = usage; |
| return device.CreateBuffer(&descriptor); |
| } |
| |
| enum class BindType { Vertex, Storage }; |
| |
| void TestBuffer(BindType bindType, |
| uint64_t bufferSize, |
| uint64_t bindingOffset, |
| uint32_t firstVertex, |
| std::array<uint32_t, 4> expected) { |
| DAWN_TEST_UNSUPPORTED_IF(deviceLimits.maxBufferSize < bufferSize); |
| |
| constexpr uint32_t kNumChecks = expected.size(); |
| |
| // Create a vertex buffer containing known data. We expect the out-of-bounds access to be |
| // clamped so just populate the very end of the buffer with known data. |
| wgpu::Buffer testBuffer = |
| CreateBuffer(bufferSize, wgpu::BufferUsage::Vertex | wgpu::BufferUsage::Storage | |
| wgpu::BufferUsage::CopyDst); |
| constexpr uint32_t kKnownData = 0xAAAAAAAA; |
| queue.WriteBuffer(testBuffer, bufferSize - sizeof(kKnownData), &kKnownData, |
| sizeof(kKnownData)); |
| |
| // Draw one point to each output pixel, containing the value we got from the vertex buffer. |
| wgpu::ShaderModule shader = utils::CreateShaderModule(device, absl::StrFormat(R"( |
| // Common code |
| |
| const kNumChecks: u32 = %u; |
| |
| struct VOut { @builtin(position) pos: vec4f, @location(0) @interpolate(flat) val: u32 } |
| |
| fn vsCommon(instanceIndex: u32, val: u32) -> VOut { |
| var o: VOut; |
| o.pos = vec4f((f32(instanceIndex) + 0.5) / f32(kNumChecks) * 2 - 1, 0, 0, 1); |
| o.val = val; |
| return o; |
| } |
| |
| @fragment fn fs(i: VOut) -> @location(0) u32 { |
| return i.val; |
| } |
| |
| // Vertex buffer test |
| |
| struct VIn { @location(0) val: u32 } |
| |
| @vertex fn vsVertexBufferTest(v: VIn, |
| @builtin(instance_index) instanceIndex: u32) -> VOut { |
| return vsCommon(instanceIndex, v.val); |
| } |
| |
| // Storage buffer test |
| |
| @group(0) @binding(0) var<storage, read> buf: array<u32>; |
| |
| @vertex fn vsStorageBufferTest(@builtin(vertex_index) vertexIndex: u32, |
| @builtin(instance_index) instanceIndex: u32) -> VOut { |
| return vsCommon(instanceIndex, buf[vertexIndex]); |
| } |
| )", |
| kNumChecks)); |
| |
| utils::ComboRenderPipelineDescriptor pipelineDesc; |
| pipelineDesc.vertex.module = shader; |
| if (bindType == BindType::Vertex) { |
| pipelineDesc.vertex.entryPoint = "vsVertexBufferTest"; |
| pipelineDesc.vertex.bufferCount = 1; |
| pipelineDesc.cAttributes[0].format = wgpu::VertexFormat::Uint32; |
| pipelineDesc.cAttributes[0].shaderLocation = 0; |
| pipelineDesc.cBuffers[0].arrayStride = sizeof(uint32_t); |
| pipelineDesc.cBuffers[0].attributes = pipelineDesc.cAttributes.data(); |
| pipelineDesc.cBuffers[0].attributeCount = 1; |
| } else { |
| pipelineDesc.vertex.entryPoint = "vsStorageBufferTest"; |
| } |
| pipelineDesc.cFragment.module = shader; |
| pipelineDesc.cTargets[0].format = wgpu::TextureFormat::R32Uint; |
| pipelineDesc.primitive.topology = wgpu::PrimitiveTopology::PointList; |
| wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDesc); |
| |
| wgpu::TextureDescriptor textureDesc; |
| textureDesc.size = {kNumChecks, 1, 1}; |
| textureDesc.format = wgpu::TextureFormat::R32Uint; |
| textureDesc.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc; |
| wgpu::Texture texture = device.CreateTexture(&textureDesc); |
| |
| // Generate indirect draw data. |
| struct Draw { |
| uint32_t vertexCount, instanceCount, firstVertex, firstInstance; |
| }; |
| std::array<Draw, kNumChecks> indirectData; |
| for (uint32_t i = 0; i < kNumChecks; ++i) { |
| // One check at each vertex offset. Uses the instance_index to pass the output position. |
| indirectData[i] = {1, 1, firstVertex + i, i}; |
| } |
| wgpu::Buffer indirectBuffer = utils::CreateBufferFromData( |
| device, indirectData.data(), indirectData.size() * sizeof(Draw), |
| wgpu::BufferUsage::Indirect); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| { |
| utils::ComboRenderPassDescriptor renderPass({texture.CreateView()}); |
| renderPass.cColorAttachments[0].loadOp = wgpu::LoadOp::Clear; |
| // Initial value indicating there's a bug in the test and points aren't drawn correctly |
| renderPass.cColorAttachments[0].clearValue = {7, 7, 7, 7}; |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass); |
| { |
| pass.SetPipeline(pipeline); |
| if (bindType == BindType::Vertex) { |
| pass.SetVertexBuffer(0, testBuffer, bindingOffset); |
| } else { |
| wgpu::BindGroup bg = |
| utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0), |
| { |
| {0, testBuffer, bindingOffset}, |
| }); |
| pass.SetBindGroup(0, bg); |
| } |
| // Indirect draw avoids the CPU-side validation of the vertex buffer binding size. |
| for (uint32_t i = 0; i < kNumChecks; ++i) { |
| pass.DrawIndirect(indirectBuffer, i * sizeof(Draw)); |
| } |
| } |
| pass.End(); |
| } |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| queue.Submit(1, &commands); |
| |
| // Check if any of the vertices failed. |
| EXPECT_TEXTURE_EQ(expected.data(), texture, {0, 0}, textureDesc.size); |
| |
| testBuffer.Destroy(); |
| } |
| }; |
| |
| // Regression test for crbug.com/488400770. |
| // Test that vertex buffer robustness works even with a vertex buffer around 4GB: buffer sizes are |
| // passed to MSL as u32, so they risk overflowing to 0. If that happens this test should both fail |
| // and trigger a Metal Shader Validation layer error. (As of this writing, that itself won't fail |
| // the test, but it will cause the OOB access to return 0.) |
| TEST_P(MetalBufferRobustnessTest, VertexBuffer_Under4GB) { |
| // Implementation adds an extra 4B at the end, in case the buffer is bound with offset=size, so |
| // there will be space at the end to clamp into. This should result in a 4GiB MTLBuffer, but the |
| // bound size should still be 4GiB-4 which fits in u32. If the buffer size is not passed to MSL |
| // correctly, it can overflow to 0, and clamp the the access to the u32[] vertex buffer to |
| // 0 - 1 = UINT32_MAX, which allows access to 12GiB of space past the end of the buffer. |
| uint32_t bufferSizeInts = 0x4000'0000 - 1; |
| uint64_t bufferSize = static_cast<uint64_t>(bufferSizeInts) * sizeof(uint32_t); |
| TestBuffer(BindType::Vertex, bufferSize, 0, bufferSizeInts - 1, |
| {0xAAAAAAAA, 0xAAAAAAAA, 0xAAAAAAAA, 0xAAAAAAAA}); |
| } |
| |
| // Regression test for crbug.com/488400770. |
| // If the actual size is 4GiB, then even passing the correct size to MSL for clamping would |
| // result in the bug. (As of this writing, this test will skip itself.) |
| TEST_P(MetalBufferRobustnessTest, VertexBuffer_4GB) { |
| uint32_t kBufferSizeInts = 0x4000'0000; |
| uint64_t kBufferSize = static_cast<uint64_t>(kBufferSizeInts) * sizeof(uint32_t); |
| TestBuffer(BindType::Vertex, kBufferSize, 0, kBufferSizeInts - 1, |
| {0xAAAAAAAA, 0xAAAAAAAA, 0xAAAAAAAA, 0xAAAAAAAA}); |
| } |
| |
| // If we bind the buffer with offset=size so that there's no space at the end (the binding size is |
| // 0), we expect to read the padding (which should be 0). Note unfortunately, in this case, we can't |
| // tell if the Metal shader validation layer caught an OOB, except by looking at stderr manually. |
| TEST_P(MetalBufferRobustnessTest, VertexBuffer_ZeroSizeRemaining) { |
| uint32_t kBufferSizeInts = 0x4000'0000 - 1; |
| uint64_t kBufferSize = static_cast<uint64_t>(kBufferSizeInts) * sizeof(uint32_t); |
| TestBuffer(BindType::Vertex, kBufferSize, kBufferSize, 0, {0, 0, 0, 0}); |
| } |
| |
| // Regression test for crbug.com/488400770. |
| // The same bug also applies to regular storage buffers, not just the ones we generate for |
| // vertex-pulling. (As of this writing, this test will skip itself.) |
| TEST_P(MetalBufferRobustnessTest, StorageBuffer) { |
| constexpr uint32_t kBufferSizeInts = 0x4000'0000; |
| constexpr uint64_t kBufferSize = kBufferSizeInts * sizeof(uint32_t); |
| DAWN_TEST_UNSUPPORTED_IF(deviceLimits.maxStorageBufferBindingSize < kBufferSize); |
| TestBuffer(BindType::Storage, kBufferSize, 0, kBufferSizeInts - 1, |
| {0xAAAAAAAA, 0xAAAAAAAA, 0xAAAAAAAA, 0xAAAAAAAA}); |
| } |
| |
| DAWN_INSTANTIATE_TEST(MetalBufferRobustnessTest, MetalBackend()); |
| |
| } // anonymous namespace |
| } // namespace dawn |