blob: 21172f4d19e842a63b4311368883007eabc5442a [file] [log] [blame]
// 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