// 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 #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)->device = nullptr; static_cast(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(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)->device = nullptr; static_cast(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(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(userdata) = true; }, &done); while (!done) { WaitABit(); } } // Test that the device can be dropped before a buffer created from it, then mapping the buffer // twice (one inside callback) will both fail. TEST_P(DeviceLifetimeTests, Dropped_ThenMapBuffer_ThenMapBufferInCallback) { wgpu::BufferDescriptor desc = {}; desc.size = 4; desc.usage = wgpu::BufferUsage::MapRead | wgpu::BufferUsage::CopyDst; wgpu::Buffer buffer = device.CreateBuffer(&desc); device = nullptr; struct UserData { wgpu::Buffer buffer; bool done = false; }; UserData userData; userData.buffer = buffer; // First mapping. buffer.MapAsync( wgpu::MapMode::Read, 0, wgpu::kWholeMapSize, [](WGPUBufferMapAsyncStatus status, void* userdataPtr) { EXPECT_EQ(status, WGPUBufferMapAsyncStatus_DeviceLost); auto userdata = static_cast(userdataPtr); // Second mapping. userdata->buffer.MapAsync( wgpu::MapMode::Read, 0, wgpu::kWholeMapSize, [](WGPUBufferMapAsyncStatus status, void* userdataPtr) { EXPECT_EQ(status, WGPUBufferMapAsyncStatus_DeviceLost); *static_cast(userdataPtr) = true; }, &userdata->done); }, &userData); while (!userData.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); 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)->device = nullptr; static_cast(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(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)->device = nullptr; static_cast(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)->device = nullptr; static_cast(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(userdataPtr); userdata->device2 = nullptr; userdata->done = true; }, &userdata); while (!userdata.done) { WaitABit(); } } DAWN_INSTANTIATE_TEST(DeviceLifetimeTests, D3D11Backend(), D3D12Backend(), MetalBackend(), NullBackend(), OpenGLBackend(), OpenGLESBackend(), VulkanBackend());