Add/update destruction handling for command encoding objects

- Renames ProgrammablePassEncoder to just ProgrammableEncoder since it is also used in RenderBundleEncoder which is not a "pass"
- Adds testing infrastructure to further test device errors
- Ensures AttachmentStates are de-reffed when encoder objects are destroyed for proper cleanup
- Makes sure that both encoded and partial encoded commands are freed at destruction

Bug: dawn:628
Change-Id: Id62ab02d54461c4da266963035e8666799f61e9a
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/68461
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Reviewed-by: Austin Eng <enga@chromium.org>
Commit-Queue: Loko Kung <lokokung@google.com>
This commit is contained in:
Loko Kung
2021-11-16 22:46:34 +00:00
committed by Dawn LUCI CQ
parent 8f4eacd082
commit 970739e4e3
24 changed files with 298 additions and 81 deletions

View File

@@ -12,10 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#include <gmock/gmock.h>
#include "dawn_native/CommandEncoder.h"
#include "tests/unittests/validation/ValidationTest.h"
#include "utils/WGPUHelpers.h"
using ::testing::HasSubstr;
class CommandBufferValidationTest : public ValidationTest {};
// Test for an empty command buffer
@@ -39,7 +45,9 @@ TEST_F(CommandBufferValidationTest, EndedMidRenderPass) {
{
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&dummyRenderPass);
ASSERT_DEVICE_ERROR(encoder.Finish());
ASSERT_DEVICE_ERROR(
encoder.Finish(),
HasSubstr("Command buffer recording ended before [RenderPassEncoder] was ended."));
}
// Error case, command buffer ended mid-pass. Trying to use encoders after Finish
@@ -47,8 +55,12 @@ TEST_F(CommandBufferValidationTest, EndedMidRenderPass) {
{
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&dummyRenderPass);
ASSERT_DEVICE_ERROR(encoder.Finish());
ASSERT_DEVICE_ERROR(pass.EndPass());
ASSERT_DEVICE_ERROR(
encoder.Finish(),
HasSubstr("Command buffer recording ended before [RenderPassEncoder] was ended."));
ASSERT_DEVICE_ERROR(
pass.EndPass(),
HasSubstr("Recording in an error or already ended [RenderPassEncoder]."));
}
}
@@ -66,7 +78,9 @@ TEST_F(CommandBufferValidationTest, EndedMidComputePass) {
{
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::ComputePassEncoder pass = encoder.BeginComputePass();
ASSERT_DEVICE_ERROR(encoder.Finish());
ASSERT_DEVICE_ERROR(
encoder.Finish(),
HasSubstr("Command buffer recording ended before [ComputePassEncoder] was ended."));
}
// Error case, command buffer ended mid-pass. Trying to use encoders after Finish
@@ -74,8 +88,12 @@ TEST_F(CommandBufferValidationTest, EndedMidComputePass) {
{
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::ComputePassEncoder pass = encoder.BeginComputePass();
ASSERT_DEVICE_ERROR(encoder.Finish());
ASSERT_DEVICE_ERROR(pass.EndPass());
ASSERT_DEVICE_ERROR(
encoder.Finish(),
HasSubstr("Command buffer recording ended before [ComputePassEncoder] was ended."));
ASSERT_DEVICE_ERROR(
pass.EndPass(),
HasSubstr("Recording in an error or already ended [ComputePassEncoder]."));
}
}
@@ -97,7 +115,9 @@ TEST_F(CommandBufferValidationTest, RenderPassEndedTwice) {
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&dummyRenderPass);
pass.EndPass();
pass.EndPass();
ASSERT_DEVICE_ERROR(encoder.Finish());
ASSERT_DEVICE_ERROR(
encoder.Finish(),
HasSubstr("Recording in an error or already ended [RenderPassEncoder]."));
}
}
@@ -117,7 +137,9 @@ TEST_F(CommandBufferValidationTest, ComputePassEndedTwice) {
wgpu::ComputePassEncoder pass = encoder.BeginComputePass();
pass.EndPass();
pass.EndPass();
ASSERT_DEVICE_ERROR(encoder.Finish());
ASSERT_DEVICE_ERROR(
encoder.Finish(),
HasSubstr("Recording in an error or already ended [ComputePassEncoder]."));
}
}
@@ -223,14 +245,18 @@ TEST_F(CommandBufferValidationTest, PassDereferenced) {
{
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
encoder.BeginRenderPass(&dummyRenderPass);
ASSERT_DEVICE_ERROR(encoder.Finish());
ASSERT_DEVICE_ERROR(
encoder.Finish(),
HasSubstr("Command buffer recording ended before [RenderPassEncoder] was ended."));
}
// Error case, no reference is kept to a compute pass.
{
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
encoder.BeginComputePass();
ASSERT_DEVICE_ERROR(encoder.Finish());
ASSERT_DEVICE_ERROR(
encoder.Finish(),
HasSubstr("Command buffer recording ended before [ComputePassEncoder] was ended."));
}
// Error case, beginning a new pass after failing to end a de-referenced pass.
@@ -239,21 +265,25 @@ TEST_F(CommandBufferValidationTest, PassDereferenced) {
encoder.BeginRenderPass(&dummyRenderPass);
wgpu::ComputePassEncoder pass = encoder.BeginComputePass();
pass.EndPass();
ASSERT_DEVICE_ERROR(encoder.Finish());
ASSERT_DEVICE_ERROR(
encoder.Finish(),
HasSubstr("Command buffer recording ended before [RenderPassEncoder] was ended."));
}
// Error case, deleting the pass after finishing the commend encoder shouldn't generate an
// Error case, deleting the pass after finishing the command encoder shouldn't generate an
// uncaptured error.
{
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::ComputePassEncoder pass = encoder.BeginComputePass();
ASSERT_DEVICE_ERROR(encoder.Finish());
ASSERT_DEVICE_ERROR(
encoder.Finish(),
HasSubstr("Command buffer recording ended before [ComputePassEncoder] was ended."));
pass = nullptr;
}
// Valid case, command encoder is never finished so the de-referenced pass shouldn't generate an
// uncaptured error.
// Valid case, command encoder is never finished so the de-referenced pass shouldn't
// generate an uncaptured error.
{
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
encoder.BeginComputePass();
@@ -264,5 +294,80 @@ TEST_F(CommandBufferValidationTest, PassDereferenced) {
TEST_F(CommandBufferValidationTest, InjectValidationError) {
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
encoder.InjectValidationError("my error");
ASSERT_DEVICE_ERROR(encoder.Finish());
ASSERT_DEVICE_ERROR(encoder.Finish(), HasSubstr("my error"));
}
TEST_F(CommandBufferValidationTest, DestroyEncoder) {
// Skip these tests if we are using wire because the destroy functionality is not exposed
// and needs to use a cast to call manually. We cannot test this in the wire case since the
// only way to trigger the destroy call is by losing all references which means we cannot
// call finish.
DAWN_SKIP_TEST_IF(UsesWire());
DummyRenderPass dummyRenderPass(device);
// Control case, command buffer ended after the pass is ended.
{
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&dummyRenderPass);
pass.EndPass();
encoder.Finish();
}
// Destroyed encoder with encoded commands should emit error on finish.
{
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&dummyRenderPass);
pass.EndPass();
reinterpret_cast<dawn_native::CommandEncoder*>(encoder.Get())->Destroy();
ASSERT_DEVICE_ERROR(encoder.Finish(), HasSubstr("Destroyed encoder cannot be finished."));
}
// Destroyed encoder with encoded commands shouldn't emit an error if never finished.
{
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&dummyRenderPass);
pass.EndPass();
reinterpret_cast<dawn_native::CommandEncoder*>(encoder.Get())->Destroy();
}
// Destroyed encoder should allow encoding, and emit error on finish.
{
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
reinterpret_cast<dawn_native::CommandEncoder*>(encoder.Get())->Destroy();
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&dummyRenderPass);
pass.EndPass();
ASSERT_DEVICE_ERROR(encoder.Finish(), HasSubstr("Destroyed encoder cannot be finished."));
}
// Destroyed encoder should allow encoding and shouldn't emit an error if never finished.
{
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
reinterpret_cast<dawn_native::CommandEncoder*>(encoder.Get())->Destroy();
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&dummyRenderPass);
pass.EndPass();
}
// Destroying a finished encoder should not emit any errors.
{
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&dummyRenderPass);
pass.EndPass();
encoder.Finish();
reinterpret_cast<dawn_native::CommandEncoder*>(encoder.Get())->Destroy();
}
// Destroying an encoder twice should not emit any errors.
{
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
reinterpret_cast<dawn_native::CommandEncoder*>(encoder.Get())->Destroy();
reinterpret_cast<dawn_native::CommandEncoder*>(encoder.Get())->Destroy();
}
// Destroying an encoder twice and then calling finish should fail.
{
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
reinterpret_cast<dawn_native::CommandEncoder*>(encoder.Get())->Destroy();
reinterpret_cast<dawn_native::CommandEncoder*>(encoder.Get())->Destroy();
ASSERT_DEVICE_ERROR(encoder.Finish(), HasSubstr("Destroyed encoder cannot be finished."));
}
}

View File

@@ -129,12 +129,19 @@ void ValidationTest::TearDown() {
}
}
void ValidationTest::StartExpectDeviceError() {
void ValidationTest::StartExpectDeviceError(testing::Matcher<std::string> errorMatcher) {
mExpectError = true;
mError = false;
mErrorMatcher = errorMatcher;
}
void ValidationTest::StartExpectDeviceError() {
StartExpectDeviceError(testing::_);
}
bool ValidationTest::EndExpectDeviceError() {
mExpectError = false;
mErrorMatcher = testing::_;
return mError;
}
std::string ValidationTest::GetLastDeviceErrorMessage() const {
@@ -210,6 +217,9 @@ void ValidationTest::OnDeviceError(WGPUErrorType type, const char* message, void
ASSERT_TRUE(self->mExpectError) << "Got unexpected device error: " << message;
ASSERT_FALSE(self->mError) << "Got two errors in expect block";
if (self->mExpectError) {
ASSERT_THAT(message, self->mErrorMatcher);
}
self->mError = true;
}

View File

@@ -19,10 +19,30 @@
#include "dawn/webgpu_cpp.h"
#include "dawn_native/DawnNative.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#define ASSERT_DEVICE_ERROR(statement) \
FlushWire(); \
// Argument helpers to allow macro overriding.
#define UNIMPLEMENTED_MACRO(...) UNREACHABLE()
#define GET_3RD_ARG_HELPER_(_1, _2, NAME, ...) NAME
#define GET_3RD_ARG_(args) GET_3RD_ARG_HELPER_ args
// Overloaded to allow further validation of the error messages given an error is expected.
// Especially useful to verify that the expected errors are occuring, not just any error.
//
// Example usages:
// 1 Argument Case:
// ASSERT_DEVICE_ERROR(FunctionThatExpectsError());
//
// 2 Argument Case:
// ASSERT_DEVICE_ERROR(FunctionThatHasLongError(), HasSubstr("partial match"))
// ASSERT_DEVICE_ERROR(FunctionThatHasShortError(), Eq("exact match"));
#define ASSERT_DEVICE_ERROR(...) \
GET_3RD_ARG_((__VA_ARGS__, ASSERT_DEVICE_ERROR_IMPL_2_, ASSERT_DEVICE_ERROR_IMPL_1_, \
UNIMPLEMENTED_MACRO)) \
(__VA_ARGS__)
#define ASSERT_DEVICE_ERROR_IMPL_1_(statement) \
StartExpectDeviceError(); \
statement; \
FlushWire(); \
@@ -32,6 +52,16 @@
do { \
} while (0)
#define ASSERT_DEVICE_ERROR_IMPL_2_(statement, matcher) \
StartExpectDeviceError(matcher); \
statement; \
FlushWire(); \
if (!EndExpectDeviceError()) { \
FAIL() << "Expected device error in:\n " << #statement; \
} \
do { \
} while (0)
// Skip a test when the given condition is satisfied.
#define DAWN_SKIP_TEST_IF(condition) \
do { \
@@ -69,6 +99,7 @@ class ValidationTest : public testing::Test {
void SetUp() override;
void TearDown() override;
void StartExpectDeviceError(testing::Matcher<std::string> errorMatcher);
void StartExpectDeviceError();
bool EndExpectDeviceError();
std::string GetLastDeviceErrorMessage() const;
@@ -118,6 +149,7 @@ class ValidationTest : public testing::Test {
std::string mDeviceErrorMessage;
bool mExpectError = false;
bool mError = false;
testing::Matcher<std::string> mErrorMatcher;
};
#endif // TESTS_UNITTESTS_VALIDATIONTEST_H_