mirror of
https://github.com/encounter/dawn-cmake.git
synced 2025-08-24 20:52:19 +00:00
Since https://dawn-review.googlesource.com/c/dawn/+/120940, callbacks will be deferred to be executed in next device.APITick() instead of immediately. However, if the device is already destroyed (last ref dropped), user/wire_server has no chance to call device.APITick() anymore, leading to the callbacks waiting in queue forever. This is also possibly the cause of memory leaks in cluserfuzz tests. This CL attempt to fix it by implementing Instance::ProcessEvents(): In this method, every created device will invoke APITick() even if it is already lost/externally released. bug: chromium:1422507 bug: dawn:752 Change-Id: Iec69ad3b547a7e88c6e1a2225b13ad060a501a4f Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/123420 Commit-Queue: Quyen Le <lehoangquyen@chromium.org> Kokoro: Kokoro <noreply+kokoro@google.com> Reviewed-by: Austin Eng <enga@chromium.org>
518 lines
18 KiB
C++
518 lines
18 KiB
C++
// Copyright 2022 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 <utility>
|
|
|
|
#include "dawn/tests/DawnTest.h"
|
|
#include "dawn/utils/WGPUHelpers.h"
|
|
|
|
class DeviceLifetimeTests : public DawnTest {};
|
|
|
|
// Test that the device can be dropped before its queue.
|
|
TEST_P(DeviceLifetimeTests, DroppedBeforeQueue) {
|
|
wgpu::Queue queue = device.GetQueue();
|
|
|
|
device = nullptr;
|
|
}
|
|
|
|
// Test that the device can be dropped while an onSubmittedWorkDone callback is in flight.
|
|
TEST_P(DeviceLifetimeTests, DroppedWhileQueueOnSubmittedWorkDone) {
|
|
// Submit some work.
|
|
wgpu::CommandEncoder encoder = device.CreateCommandEncoder(nullptr);
|
|
wgpu::CommandBuffer commandBuffer = encoder.Finish();
|
|
queue.Submit(1, &commandBuffer);
|
|
|
|
// Ask for an onSubmittedWorkDone callback and drop the device.
|
|
queue.OnSubmittedWorkDone(
|
|
0,
|
|
[](WGPUQueueWorkDoneStatus status, void*) {
|
|
// There is a bug in DeviceBase::Destroy(). If all submitted work is done when
|
|
// OnSubmittedWorkDone() is being called, the callback will be resolved with
|
|
// DeviceLost, otherwise the callback will be resolved with Success.
|
|
// TODO(dawn:1640): fix DeviceBase::Destroy() to always reslove the callback
|
|
// with success.
|
|
EXPECT_TRUE(status == WGPUQueueWorkDoneStatus_Success ||
|
|
status == WGPUQueueWorkDoneStatus_DeviceLost);
|
|
},
|
|
nullptr);
|
|
|
|
device = nullptr;
|
|
}
|
|
|
|
// Test that the device can be dropped inside an onSubmittedWorkDone callback.
|
|
TEST_P(DeviceLifetimeTests, DroppedInsideQueueOnSubmittedWorkDone) {
|
|
// Submit some work.
|
|
wgpu::CommandEncoder encoder = device.CreateCommandEncoder(nullptr);
|
|
wgpu::CommandBuffer commandBuffer = encoder.Finish();
|
|
queue.Submit(1, &commandBuffer);
|
|
|
|
struct Userdata {
|
|
wgpu::Device device;
|
|
bool done;
|
|
};
|
|
// Ask for an onSubmittedWorkDone callback and drop the device inside the callback.
|
|
Userdata data = Userdata{std::move(device), false};
|
|
queue.OnSubmittedWorkDone(
|
|
0,
|
|
[](WGPUQueueWorkDoneStatus status, void* userdata) {
|
|
EXPECT_EQ(status, WGPUQueueWorkDoneStatus_Success);
|
|
static_cast<Userdata*>(userdata)->device = nullptr;
|
|
static_cast<Userdata*>(userdata)->done = true;
|
|
},
|
|
&data);
|
|
|
|
while (!data.done) {
|
|
// WaitABit no longer can call tick since we've moved the device from the fixture into the
|
|
// userdata.
|
|
if (data.device) {
|
|
data.device.Tick();
|
|
}
|
|
WaitABit();
|
|
}
|
|
}
|
|
|
|
// Test that the device can be dropped while a popErrorScope callback is in flight.
|
|
TEST_P(DeviceLifetimeTests, DroppedWhilePopErrorScope) {
|
|
device.PushErrorScope(wgpu::ErrorFilter::Validation);
|
|
bool wire = UsesWire();
|
|
device.PopErrorScope(
|
|
[](WGPUErrorType type, const char*, void* userdata) {
|
|
const bool wire = *static_cast<bool*>(userdata);
|
|
// On the wire, all callbacks get rejected immediately with once the device is deleted.
|
|
// In native, popErrorScope is called synchronously.
|
|
// TODO(crbug.com/dawn/1122): These callbacks should be made consistent.
|
|
EXPECT_EQ(type, wire ? WGPUErrorType_Unknown : WGPUErrorType_NoError);
|
|
},
|
|
&wire);
|
|
device = nullptr;
|
|
}
|
|
|
|
// Test that the device can be dropped inside an onSubmittedWorkDone callback.
|
|
TEST_P(DeviceLifetimeTests, DroppedInsidePopErrorScope) {
|
|
struct Userdata {
|
|
wgpu::Device device;
|
|
bool done;
|
|
};
|
|
device.PushErrorScope(wgpu::ErrorFilter::Validation);
|
|
|
|
// Ask for a popErrorScope callback and drop the device inside the callback.
|
|
Userdata data = Userdata{std::move(device), false};
|
|
data.device.PopErrorScope(
|
|
[](WGPUErrorType type, const char*, void* userdata) {
|
|
EXPECT_EQ(type, WGPUErrorType_NoError);
|
|
static_cast<Userdata*>(userdata)->device = nullptr;
|
|
static_cast<Userdata*>(userdata)->done = true;
|
|
},
|
|
&data);
|
|
|
|
while (!data.done) {
|
|
// WaitABit no longer can call tick since we've moved the device from the fixture into the
|
|
// userdata.
|
|
if (data.device) {
|
|
data.device.Tick();
|
|
}
|
|
WaitABit();
|
|
}
|
|
}
|
|
|
|
// Test that the device can be dropped before a buffer created from it.
|
|
TEST_P(DeviceLifetimeTests, DroppedBeforeBuffer) {
|
|
wgpu::BufferDescriptor desc = {};
|
|
desc.size = 4;
|
|
desc.usage = wgpu::BufferUsage::CopySrc | wgpu::BufferUsage::CopyDst;
|
|
wgpu::Buffer buffer = device.CreateBuffer(&desc);
|
|
|
|
device = nullptr;
|
|
}
|
|
|
|
// Test that the device can be dropped while a buffer created from it is being mapped.
|
|
TEST_P(DeviceLifetimeTests, DroppedWhileMappingBuffer) {
|
|
wgpu::BufferDescriptor desc = {};
|
|
desc.size = 4;
|
|
desc.usage = wgpu::BufferUsage::MapRead | wgpu::BufferUsage::CopyDst;
|
|
wgpu::Buffer buffer = device.CreateBuffer(&desc);
|
|
|
|
buffer.MapAsync(
|
|
wgpu::MapMode::Read, 0, wgpu::kWholeMapSize,
|
|
[](WGPUBufferMapAsyncStatus status, void*) {
|
|
EXPECT_EQ(status, WGPUBufferMapAsyncStatus_DestroyedBeforeCallback);
|
|
},
|
|
nullptr);
|
|
|
|
device = nullptr;
|
|
}
|
|
|
|
// Test that the device can be dropped before a mapped buffer created from it.
|
|
TEST_P(DeviceLifetimeTests, DroppedBeforeMappedBuffer) {
|
|
wgpu::BufferDescriptor desc = {};
|
|
desc.size = 4;
|
|
desc.usage = wgpu::BufferUsage::MapRead | wgpu::BufferUsage::CopyDst;
|
|
wgpu::Buffer buffer = device.CreateBuffer(&desc);
|
|
|
|
bool done = false;
|
|
buffer.MapAsync(
|
|
wgpu::MapMode::Read, 0, wgpu::kWholeMapSize,
|
|
[](WGPUBufferMapAsyncStatus status, void* userdata) {
|
|
EXPECT_EQ(status, WGPUBufferMapAsyncStatus_Success);
|
|
*static_cast<bool*>(userdata) = true;
|
|
},
|
|
&done);
|
|
|
|
while (!done) {
|
|
WaitABit();
|
|
}
|
|
|
|
device = nullptr;
|
|
}
|
|
|
|
// Test that the device can be dropped before a mapped at creation buffer created from it.
|
|
TEST_P(DeviceLifetimeTests, DroppedBeforeMappedAtCreationBuffer) {
|
|
wgpu::BufferDescriptor desc = {};
|
|
desc.size = 4;
|
|
desc.usage = wgpu::BufferUsage::MapRead | wgpu::BufferUsage::CopyDst;
|
|
desc.mappedAtCreation = true;
|
|
wgpu::Buffer buffer = device.CreateBuffer(&desc);
|
|
|
|
device = nullptr;
|
|
}
|
|
|
|
// Test that the device can be dropped before a buffer created from it, then mapping the buffer
|
|
// fails.
|
|
TEST_P(DeviceLifetimeTests, DroppedThenMapBuffer) {
|
|
wgpu::BufferDescriptor desc = {};
|
|
desc.size = 4;
|
|
desc.usage = wgpu::BufferUsage::MapRead | wgpu::BufferUsage::CopyDst;
|
|
wgpu::Buffer buffer = device.CreateBuffer(&desc);
|
|
|
|
device = nullptr;
|
|
|
|
bool done = false;
|
|
buffer.MapAsync(
|
|
wgpu::MapMode::Read, 0, wgpu::kWholeMapSize,
|
|
[](WGPUBufferMapAsyncStatus status, void* userdata) {
|
|
EXPECT_EQ(status, WGPUBufferMapAsyncStatus_DeviceLost);
|
|
*static_cast<bool*>(userdata) = true;
|
|
},
|
|
&done);
|
|
|
|
while (!done) {
|
|
WaitABit();
|
|
}
|
|
}
|
|
|
|
// Test that the device can be dropped inside a buffer map callback.
|
|
TEST_P(DeviceLifetimeTests, DroppedInsideBufferMapCallback) {
|
|
wgpu::BufferDescriptor desc = {};
|
|
desc.size = 4;
|
|
desc.usage = wgpu::BufferUsage::MapRead | wgpu::BufferUsage::CopyDst;
|
|
wgpu::Buffer buffer = device.CreateBuffer(&desc);
|
|
|
|
struct Userdata {
|
|
wgpu::Device device;
|
|
wgpu::Buffer buffer;
|
|
bool wire;
|
|
bool done;
|
|
};
|
|
|
|
// Ask for a mapAsync callback and drop the device inside the callback.
|
|
Userdata data = Userdata{std::move(device), buffer, UsesWire(), false};
|
|
buffer.MapAsync(
|
|
wgpu::MapMode::Read, 0, wgpu::kWholeMapSize,
|
|
[](WGPUBufferMapAsyncStatus status, void* userdata) {
|
|
EXPECT_EQ(status, WGPUBufferMapAsyncStatus_Success);
|
|
auto* data = static_cast<Userdata*>(userdata);
|
|
data->device = nullptr;
|
|
data->done = true;
|
|
|
|
// Mapped data should be null since the buffer is implicitly destroyed.
|
|
// TODO(crbug.com/dawn/1424): On the wire client, we don't track device child objects so
|
|
// the mapped data is still available when the device is destroyed.
|
|
if (!data->wire) {
|
|
EXPECT_EQ(data->buffer.GetConstMappedRange(), nullptr);
|
|
}
|
|
},
|
|
&data);
|
|
|
|
while (!data.done) {
|
|
// WaitABit no longer can call tick since we've moved the device from the fixture into the
|
|
// userdata.
|
|
if (data.device) {
|
|
data.device.Tick();
|
|
}
|
|
WaitABit();
|
|
}
|
|
|
|
// Mapped data should be null since the buffer is implicitly destroyed.
|
|
// TODO(crbug.com/dawn/1424): On the wire client, we don't track device child objects so the
|
|
// mapped data is still available when the device is destroyed.
|
|
if (!UsesWire()) {
|
|
EXPECT_EQ(buffer.GetConstMappedRange(), nullptr);
|
|
}
|
|
}
|
|
|
|
// Test that the device can be dropped while a write buffer operation is enqueued.
|
|
TEST_P(DeviceLifetimeTests, DroppedWhileWriteBuffer) {
|
|
wgpu::BufferDescriptor desc = {};
|
|
desc.size = 4;
|
|
desc.usage = wgpu::BufferUsage::CopySrc | wgpu::BufferUsage::CopyDst;
|
|
wgpu::Buffer buffer = device.CreateBuffer(&desc);
|
|
|
|
uint32_t value = 7;
|
|
queue.WriteBuffer(buffer, 0, &value, sizeof(value));
|
|
device = nullptr;
|
|
}
|
|
|
|
// Test that the device can be dropped while a write buffer operation is enqueued and then
|
|
// a queue submit occurs. This is slightly different from the former test since it ensures
|
|
// that pending work is flushed.
|
|
TEST_P(DeviceLifetimeTests, DroppedWhileWriteBufferAndSubmit) {
|
|
wgpu::BufferDescriptor desc = {};
|
|
desc.size = 4;
|
|
desc.usage = wgpu::BufferUsage::CopySrc | wgpu::BufferUsage::CopyDst;
|
|
wgpu::Buffer buffer = device.CreateBuffer(&desc);
|
|
|
|
uint32_t value = 7;
|
|
queue.WriteBuffer(buffer, 0, &value, sizeof(value));
|
|
queue.Submit(0, nullptr);
|
|
device = nullptr;
|
|
}
|
|
|
|
// Test that the device can be dropped while createPipelineAsync is in flight
|
|
TEST_P(DeviceLifetimeTests, DroppedWhileCreatePipelineAsync) {
|
|
wgpu::ComputePipelineDescriptor desc;
|
|
desc.compute.module = utils::CreateShaderModule(device, R"(
|
|
@compute @workgroup_size(1) fn main() {
|
|
})");
|
|
desc.compute.entryPoint = "main";
|
|
|
|
device.CreateComputePipelineAsync(
|
|
&desc,
|
|
[](WGPUCreatePipelineAsyncStatus status, WGPUComputePipeline cPipeline, const char* message,
|
|
void* userdata) {
|
|
wgpu::ComputePipeline::Acquire(cPipeline);
|
|
EXPECT_EQ(status, WGPUCreatePipelineAsyncStatus_DeviceDestroyed);
|
|
},
|
|
nullptr);
|
|
|
|
device = nullptr;
|
|
}
|
|
|
|
// Test that the device can be dropped inside a createPipelineAsync callback
|
|
TEST_P(DeviceLifetimeTests, DroppedInsideCreatePipelineAsync) {
|
|
wgpu::ComputePipelineDescriptor desc;
|
|
desc.compute.module = utils::CreateShaderModule(device, R"(
|
|
@compute @workgroup_size(1) fn main() {
|
|
})");
|
|
desc.compute.entryPoint = "main";
|
|
|
|
struct Userdata {
|
|
wgpu::Device device;
|
|
bool done;
|
|
};
|
|
// Call CreateComputePipelineAsync and drop the device inside the callback.
|
|
Userdata data = Userdata{std::move(device), false};
|
|
data.device.CreateComputePipelineAsync(
|
|
&desc,
|
|
[](WGPUCreatePipelineAsyncStatus status, WGPUComputePipeline cPipeline, const char* message,
|
|
void* userdata) {
|
|
wgpu::ComputePipeline::Acquire(cPipeline);
|
|
EXPECT_EQ(status, WGPUCreatePipelineAsyncStatus_Success);
|
|
|
|
static_cast<Userdata*>(userdata)->device = nullptr;
|
|
static_cast<Userdata*>(userdata)->done = true;
|
|
},
|
|
&data);
|
|
|
|
while (!data.done) {
|
|
// WaitABit no longer can call tick since we've moved the device from the fixture into the
|
|
// userdata.
|
|
if (data.device) {
|
|
data.device.Tick();
|
|
}
|
|
WaitABit();
|
|
}
|
|
}
|
|
|
|
// Test that the device can be dropped while createPipelineAsync which will hit the frontend cache
|
|
// is in flight
|
|
TEST_P(DeviceLifetimeTests, DroppedWhileCreatePipelineAsyncAlreadyCached) {
|
|
wgpu::ComputePipelineDescriptor desc;
|
|
desc.compute.module = utils::CreateShaderModule(device, R"(
|
|
@compute @workgroup_size(1) fn main() {
|
|
})");
|
|
desc.compute.entryPoint = "main";
|
|
|
|
// Create a pipeline ahead of time so it's in the cache.
|
|
wgpu::ComputePipeline p = device.CreateComputePipeline(&desc);
|
|
|
|
bool wire = UsesWire();
|
|
device.CreateComputePipelineAsync(
|
|
&desc,
|
|
[](WGPUCreatePipelineAsyncStatus status, WGPUComputePipeline cPipeline, const char*,
|
|
void* userdata) {
|
|
const bool wire = *static_cast<bool*>(userdata);
|
|
wgpu::ComputePipeline::Acquire(cPipeline);
|
|
// On the wire, all callbacks get rejected immediately with once the device is deleted.
|
|
// In native, expect success since the compilation hits the frontend cache immediately.
|
|
// TODO(crbug.com/dawn/1122): These callbacks should be made consistent.
|
|
EXPECT_EQ(status, wire ? WGPUCreatePipelineAsyncStatus_DeviceDestroyed
|
|
: WGPUCreatePipelineAsyncStatus_Success);
|
|
},
|
|
&wire);
|
|
device = nullptr;
|
|
}
|
|
|
|
// Test that the device can be dropped inside a createPipelineAsync callback which will hit the
|
|
// frontend cache
|
|
TEST_P(DeviceLifetimeTests, DroppedInsideCreatePipelineAsyncAlreadyCached) {
|
|
wgpu::ComputePipelineDescriptor desc;
|
|
desc.compute.module = utils::CreateShaderModule(device, R"(
|
|
@compute @workgroup_size(1) fn main() {
|
|
})");
|
|
desc.compute.entryPoint = "main";
|
|
|
|
// Create a pipeline ahead of time so it's in the cache.
|
|
wgpu::ComputePipeline p = device.CreateComputePipeline(&desc);
|
|
|
|
struct Userdata {
|
|
wgpu::Device device;
|
|
bool done;
|
|
};
|
|
// Call CreateComputePipelineAsync and drop the device inside the callback.
|
|
Userdata data = Userdata{std::move(device), false};
|
|
data.device.CreateComputePipelineAsync(
|
|
&desc,
|
|
[](WGPUCreatePipelineAsyncStatus status, WGPUComputePipeline cPipeline, const char* message,
|
|
void* userdata) {
|
|
wgpu::ComputePipeline::Acquire(cPipeline);
|
|
// Success because it hits the frontend cache immediately.
|
|
EXPECT_EQ(status, WGPUCreatePipelineAsyncStatus_Success);
|
|
|
|
static_cast<Userdata*>(userdata)->device = nullptr;
|
|
static_cast<Userdata*>(userdata)->done = true;
|
|
},
|
|
&data);
|
|
|
|
while (!data.done) {
|
|
// WaitABit no longer can call tick since we've moved the device from the fixture into the
|
|
// userdata.
|
|
if (data.device) {
|
|
data.device.Tick();
|
|
}
|
|
WaitABit();
|
|
}
|
|
}
|
|
|
|
// Test that the device can be dropped while createPipelineAsync which will race with a compilation
|
|
// to add the same pipeline to the frontend cache
|
|
TEST_P(DeviceLifetimeTests, DroppedWhileCreatePipelineAsyncRaceCache) {
|
|
wgpu::ComputePipelineDescriptor desc;
|
|
desc.compute.module = utils::CreateShaderModule(device, R"(
|
|
@compute @workgroup_size(1) fn main() {
|
|
})");
|
|
desc.compute.entryPoint = "main";
|
|
|
|
device.CreateComputePipelineAsync(
|
|
&desc,
|
|
[](WGPUCreatePipelineAsyncStatus status, WGPUComputePipeline cPipeline, const char* message,
|
|
void* userdata) {
|
|
wgpu::ComputePipeline::Acquire(cPipeline);
|
|
EXPECT_EQ(status, WGPUCreatePipelineAsyncStatus_DeviceDestroyed);
|
|
},
|
|
nullptr);
|
|
|
|
// Create the same pipeline synchronously which will get added to the cache.
|
|
wgpu::ComputePipeline p = device.CreateComputePipeline(&desc);
|
|
|
|
device = nullptr;
|
|
}
|
|
|
|
// Test that the device can be dropped inside a createPipelineAsync callback which which will race
|
|
// with a compilation to add the same pipeline to the frontend cache
|
|
TEST_P(DeviceLifetimeTests, DroppedInsideCreatePipelineAsyncRaceCache) {
|
|
wgpu::ComputePipelineDescriptor desc;
|
|
desc.compute.module = utils::CreateShaderModule(device, R"(
|
|
@compute @workgroup_size(1) fn main() {
|
|
})");
|
|
desc.compute.entryPoint = "main";
|
|
|
|
struct Userdata {
|
|
wgpu::Device device;
|
|
bool done;
|
|
};
|
|
// Call CreateComputePipelineAsync and drop the device inside the callback.
|
|
Userdata data = Userdata{std::move(device), false};
|
|
data.device.CreateComputePipelineAsync(
|
|
&desc,
|
|
[](WGPUCreatePipelineAsyncStatus status, WGPUComputePipeline cPipeline, const char* message,
|
|
void* userdata) {
|
|
wgpu::ComputePipeline::Acquire(cPipeline);
|
|
EXPECT_EQ(status, WGPUCreatePipelineAsyncStatus_Success);
|
|
|
|
static_cast<Userdata*>(userdata)->device = nullptr;
|
|
static_cast<Userdata*>(userdata)->done = true;
|
|
},
|
|
&data);
|
|
|
|
// Create the same pipeline synchronously which will get added to the cache.
|
|
wgpu::ComputePipeline p = data.device.CreateComputePipeline(&desc);
|
|
|
|
while (!data.done) {
|
|
// WaitABit no longer can call tick since we've moved the device from the fixture into the
|
|
// userdata.
|
|
if (data.device) {
|
|
data.device.Tick();
|
|
}
|
|
WaitABit();
|
|
}
|
|
}
|
|
|
|
// Tests that dropping 2nd device inside 1st device's callback triggered by instance.ProcessEvents
|
|
// won't crash.
|
|
TEST_P(DeviceLifetimeTests, DropDevice2InProcessEvents) {
|
|
wgpu::Device device2 = CreateDevice();
|
|
|
|
struct UserData {
|
|
wgpu::Device device2;
|
|
bool done = false;
|
|
} userdata;
|
|
|
|
userdata.device2 = std::move(device2);
|
|
|
|
device.PushErrorScope(wgpu::ErrorFilter::Validation);
|
|
|
|
// The following callback will drop the 2nd device. It won't be triggered until
|
|
// instance.ProcessEvents() is called.
|
|
device.PopErrorScope(
|
|
[](WGPUErrorType type, const char*, void* userdataPtr) {
|
|
auto userdata = static_cast<UserData*>(userdataPtr);
|
|
|
|
userdata->device2 = nullptr;
|
|
userdata->done = true;
|
|
},
|
|
&userdata);
|
|
|
|
while (!userdata.done) {
|
|
WaitABit();
|
|
}
|
|
}
|
|
|
|
DAWN_INSTANTIATE_TEST(DeviceLifetimeTests,
|
|
D3D12Backend(),
|
|
MetalBackend(),
|
|
NullBackend(),
|
|
OpenGLBackend(),
|
|
OpenGLESBackend(),
|
|
VulkanBackend());
|