| // Copyright 2024 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 <optional> |
| #include <ostream> |
| #include <string> |
| #include <vector> |
| |
| #include "dawn/tests/DawnTest.h" |
| |
| #include "dawn/utils/WGPUHelpers.h" |
| |
| namespace dawn { |
| namespace { |
| |
| enum class LoopKind { |
| kLoop, // loop { if breakCond {break;}} |
| kLoopContinuing, // loop { continuing { break if breakCond; } }} |
| kFor, // for ( ; !breakCond ; ) { } |
| kWhile, // while ( !breakCond ) { } |
| }; |
| std::ostream& operator<<(std::ostream& os, const LoopKind lk) { |
| switch (lk) { |
| case LoopKind::kLoop: |
| os << "loop"; |
| break; |
| case LoopKind::kLoopContinuing: |
| os << "loopContinuing"; |
| break; |
| case LoopKind::kFor: |
| os << "for"; |
| break; |
| case LoopKind::kWhile: |
| os << "while"; |
| break; |
| } |
| return os; |
| } |
| |
| // Conditions that are always false, and cause the loop to iterate forever. |
| enum class BreakCond { |
| kFalse, |
| kClampedIndexEqualsArrayLen, |
| kClampedIndexExceedsArrayLen, |
| }; |
| std::ostream& operator<<(std::ostream& os, const BreakCond bc) { |
| switch (bc) { |
| case BreakCond::kFalse: |
| os << "false"; |
| break; |
| case BreakCond::kClampedIndexEqualsArrayLen: |
| os << "clampIndexEqualsArrayLen"; |
| break; |
| case BreakCond::kClampedIndexExceedsArrayLen: |
| os << "clampIndexExceedsArrayLen"; |
| break; |
| } |
| return os; |
| } |
| |
| // What kind of expression provides the index? |
| enum class IndexKind { |
| kConst, |
| kFromBuffer, // Use the first entry in the input buffer. |
| }; |
| std::ostream& operator<<(std::ostream& os, const IndexKind ik) { |
| switch (ik) { |
| case IndexKind::kConst: |
| os << "const"; |
| break; |
| case IndexKind::kFromBuffer: |
| os << "fromBuffer"; |
| break; |
| } |
| return os; |
| } |
| |
| DAWN_TEST_PARAM_STRUCT(InfiniteLoopTestParams, LoopKind, BreakCond, IndexKind); |
| |
| // Tests somewhat safe results from dynamically infinite loops. |
| // It's hard to write a conformnace test because the specified result is a dynamic |
| // error. But we can check for platform-specific results. |
| |
| struct InfiniteLoopTests : public DawnTestWithParams<InfiniteLoopTestParams> { |
| using DawnTestWithParams<InfiniteLoopTestParams>::GetParam; |
| const uint32_t kArraySize = 50; |
| const uint32_t kOOBIndex = 100; |
| |
| // Runs a test on an input array, yielding a buffer of outputs. |
| // Checks the given expectation on the output buffer. Takes ownership |
| // of the expectation object. |
| void RunTest(const char* file, |
| size_t line, |
| const char* shader, |
| uint32_t OOBIndex, |
| size_t numOutputs, |
| detail::Expectation* expectation); |
| |
| #define RUN_TEST(shader, inputs, numOutputs, expectation) \ |
| this->RunTest(__FILE__, __LINE__, shader, inputs, numOutputs, expectation) |
| |
| // Returns expression for the break condition. |
| // It should always evaluate to false. |
| std::string IndexStr() { |
| switch (GetParam().mIndexKind) { |
| case IndexKind::kConst: |
| return std::to_string(kOOBIndex); |
| case IndexKind::kFromBuffer: |
| return "inputs[0]"; |
| } |
| DAWN_UNREACHABLE(); |
| } |
| // Returns expression for the break condition. |
| // It should always evaluate to false. |
| std::string BreakCondStr() { |
| switch (GetParam().mBreakCond) { |
| case BreakCond::kFalse: |
| return "false"; |
| case BreakCond::kClampedIndexEqualsArrayLen: |
| return "(min(" + IndexStr() + |
| ", arrayLength(&outputs)-1) == arrayLength(&outputs))"; |
| case BreakCond::kClampedIndexExceedsArrayLen: |
| return "(min(" + IndexStr() + ", arrayLength(&outputs)-1) > arrayLength(&outputs))"; |
| } |
| DAWN_UNREACHABLE(); |
| } |
| // Returns code for the the infinite loop. |
| std::string LoopStr() { |
| switch (GetParam().mLoopKind) { |
| case LoopKind::kLoop: |
| return "loop { if " + BreakCondStr() + "{break;} }\n"; |
| case LoopKind::kLoopContinuing: |
| return "loop { continuing { break if " + BreakCondStr() + "; } }\n"; |
| case LoopKind::kFor: |
| return "for (; !" + BreakCondStr() + "; ) { }\n"; |
| case LoopKind::kWhile: |
| return "while (!" + BreakCondStr() + ") { }\n"; |
| } |
| DAWN_UNREACHABLE(); |
| } |
| |
| // Returns the shader string for the current parameterization. |
| std::string Shader(uint32_t sentinelValue) { |
| return R"( |
| @group(0) @binding(0) var<storage, read> inputs : array<u32>; |
| @group(0) @binding(1) var<storage, read_write> outputs : array<u32>; |
| |
| @compute @workgroup_size(1) |
| fn main() { |
| _ = &outputs[0]; |
| _ = &inputs[0]; |
| )" + LoopStr() + |
| R"( |
| outputs[)" + IndexStr() + |
| "] = " + std::to_string(sentinelValue) + R"(; |
| } |
| )"; |
| } |
| }; |
| |
| void InfiniteLoopTests::RunTest(const char* file, |
| size_t line, |
| const char* shader, |
| uint32_t OOBIndex, |
| size_t numOutputs, |
| detail::Expectation* expectation) { |
| // Set up shader and pipeline |
| auto module = utils::CreateShaderModule(device, shader); |
| |
| wgpu::ComputePipelineDescriptor csDesc; |
| csDesc.compute.module = module; |
| |
| wgpu::ComputePipeline pipeline = device.CreateComputePipeline(&csDesc); |
| |
| // Set up src storage buffer |
| wgpu::Buffer src = utils::CreateBufferFromData( |
| device, &OOBIndex, sizeof(OOBIndex), |
| wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopySrc | wgpu::BufferUsage::CopyDst); |
| |
| // Set up dst storage buffer |
| std::vector<uint32_t> dstInitValues(numOutputs, 0); |
| |
| const auto outSize = numOutputs * sizeof(uint32_t); |
| wgpu::Buffer dst = utils::CreateBufferFromData( |
| device, dstInitValues.data(), outSize, |
| wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopySrc | wgpu::BufferUsage::CopyDst); |
| |
| // Set up bind group and issue dispatch |
| wgpu::BindGroup bindGroup = utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0), |
| { |
| {0, src}, |
| {1, dst}, |
| }); |
| |
| wgpu::CommandBuffer commands; |
| { |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| wgpu::ComputePassEncoder pass = encoder.BeginComputePass(); |
| pass.SetPipeline(pipeline); |
| pass.SetBindGroup(0, bindGroup); |
| pass.DispatchWorkgroups(1); |
| pass.End(); |
| |
| commands = encoder.Finish(); |
| } |
| queue.Submit(1, &commands); |
| |
| AddBufferExpectation(file, line, dst, 0, outSize, expectation); |
| } |
| |
| // A gtest assertion checking that the given array is filled with zeros, except for one entry which |
| // contains the given value. |
| template <typename T> |
| class ExpectOneNonZero : public detail::Expectation { |
| public: |
| explicit ExpectOneNonZero(T value) : mValue(value) { DAWN_ASSERT(mValue != T(0)); } |
| |
| testing::AssertionResult Check(const void* data, size_t size) override { |
| DAWN_ASSERT(size % sizeof(T) == 0 && size > 0); |
| const T* actual = static_cast<const T*>(data); |
| |
| std::optional<size_t> whereFound; |
| for (size_t i = 0; i < size / sizeof(T); i++) { |
| if (actual[i] == mValue) { |
| if (whereFound.has_value()) { |
| return testing::AssertionFailure() |
| << "Found value " << mValue << " at data[" << whereFound.value() |
| << "] and data[" << i << "]\n"; |
| } |
| whereFound = i; |
| } else if (actual[i] != 0) { |
| return testing::AssertionFailure() |
| << "Found unexpected value data[" << i << "] = " << actual[i] |
| << " instead of " << mValue << "\n"; |
| } |
| } |
| if (!whereFound.has_value()) { |
| return testing::AssertionFailure() << "Sentinel value " << mValue << " was not found\n"; |
| } |
| return testing::AssertionSuccess(); |
| } |
| |
| private: |
| const T mValue; |
| }; |
| |
| TEST_P(InfiniteLoopTests, LoopDeletedThenBoundedWrite) { |
| DAWN_SKIP_TEST_IF_BASE(!IsMetal(), "infinite-loops", "only test on Metal"); |
| DAWN_SKIP_TEST_IF_BASE( |
| IsMetal(), "infinite-loops", |
| "Metal loops run forever: TODO(crbug.com/371840056) rewrite as death tests with watchdog?"); |
| |
| const uint32_t sentinelValue = 777; |
| std::string shader = Shader(sentinelValue); |
| RUN_TEST(shader.c_str(), kOOBIndex, kArraySize, new ExpectOneNonZero<uint32_t>(sentinelValue)); |
| } |
| |
| DAWN_INSTANTIATE_TEST_P(InfiniteLoopTests, |
| {MetalBackend()}, |
| { |
| LoopKind::kLoop, |
| LoopKind::kLoopContinuing, |
| LoopKind::kFor, |
| LoopKind::kWhile, |
| }, |
| { |
| BreakCond::kFalse, |
| BreakCond::kClampedIndexEqualsArrayLen, |
| BreakCond::kClampedIndexExceedsArrayLen, |
| }, |
| {IndexKind::kConst, IndexKind::kFromBuffer}); |
| |
| } // anonymous namespace |
| } // namespace dawn |