Remove device-child wire hack and enable DeviceLifetimeTests on the wire

Bug: dawn:384, dawn:1164
Change-Id: I88a503513295900975819b56f60738218a1c23ac
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/90920
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Loko Kung <lokokung@google.com>
Commit-Queue: Austin Eng <enga@chromium.org>
This commit is contained in:
Austin Eng 2022-05-24 13:23:33 +00:00 committed by Dawn LUCI CQ
parent 2066522311
commit 20a4bd8377
13 changed files with 48 additions and 211 deletions

View File

@ -77,11 +77,6 @@ namespace dawn::wire::server {
if (data == nullptr) {
return false;
}
if (data->deviceInfo != nullptr) {
if (!UntrackDeviceChild(data->deviceInfo, objectType, objectId)) {
return false;
}
}
if (data->state == AllocationState::Allocated) {
ASSERT(data->handle != nullptr);
{% if type.name.CamelCase() in server_reverse_lookup_objects %}
@ -89,17 +84,6 @@ namespace dawn::wire::server {
{% endif %}
{% if type.name.get() == "device" %}
//* TODO(crbug.com/dawn/384): This is a hack to make sure that all child objects
//* are destroyed before their device. We should have a solution in
//* Dawn native that makes all child objects internally null if their
//* Device is destroyed.
while (data->info->childObjectTypesAndIds.size() > 0) {
auto [childObjectType, childObjectId] = UnpackObjectTypeAndId(
*data->info->childObjectTypesAndIds.begin());
if (!DoDestroyObject(childObjectType, childObjectId)) {
return false;
}
}
if (data->handle != nullptr) {
//* Deregisters uncaptured error and device lost callbacks since
//* they should not be forwarded if the device no longer exists on the wire.

View File

@ -52,24 +52,6 @@ namespace dawn::wire::server {
return false;
}
{{name}}Data->generation = cmd.{{name}}.generation;
//* TODO(crbug.com/dawn/384): This is a hack to make sure that all child objects
//* are destroyed before their device. The dawn_native device needs to track all child objects so
//* it can destroy them if the device is destroyed first.
{% if command.derived_object %}
{% set type = command.derived_object %}
{% if type.name.get() == "device" %}
{{name}}Data->deviceInfo = DeviceObjects().Get(cmd.selfId)->info.get();
{% else %}
auto* selfData = {{type.name.CamelCase()}}Objects().Get(cmd.selfId);
{{name}}Data->deviceInfo = selfData->deviceInfo;
{% endif %}
if ({{name}}Data->deviceInfo != nullptr) {
if (!TrackDeviceChild({{name}}Data->deviceInfo, ObjectType::{{Type}}, cmd.{{name}}.id)) {
return false;
}
}
{% endif %}
{% endfor %}
//* Do command

View File

@ -307,7 +307,6 @@ dawn_test("dawn_unittests") {
"unittests/wire/WireBasicTests.cpp",
"unittests/wire/WireBufferMappingTests.cpp",
"unittests/wire/WireCreatePipelineAsyncTests.cpp",
"unittests/wire/WireDestroyObjectTests.cpp",
"unittests/wire/WireDisconnectTests.cpp",
"unittests/wire/WireErrorCallbackTests.cpp",
"unittests/wire/WireExtensionTests.cpp",

View File

@ -716,7 +716,6 @@ DawnTestBase::DawnTestBase(const AdapterTestParam& param)
mWireHelper(utils::CreateWireHelper(gTestEnv->UsesWire(), gTestEnv->GetWireTraceDir())) {}
DawnTestBase::~DawnTestBase() {
// We need to destroy child objects before the Device
mReadbackSlots.clear();
queue = wgpu::Queue();
device = wgpu::Device();

View File

@ -17,14 +17,7 @@
#include "dawn/tests/DawnTest.h"
#include "dawn/utils/WGPUHelpers.h"
class DeviceLifetimeTests : public DawnTest {
void SetUp() override {
DawnTest::SetUp();
// The wire currently has a different device / device-child lifetime mechanism
// which will be removed soon and these tests enabled.
DAWN_TEST_UNSUPPORTED_IF(UsesWire());
}
};
class DeviceLifetimeTests : public DawnTest {};
// Test that the device can be dropped before its queue.
TEST_P(DeviceLifetimeTests, DroppedBeforeQueue) {
@ -86,9 +79,16 @@ TEST_P(DeviceLifetimeTests, DroppedInsideQueueOnSubmittedWorkDone) {
// 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*) { EXPECT_EQ(type, WGPUErrorType_NoError); },
nullptr);
[](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;
}
@ -215,11 +215,12 @@ TEST_P(DeviceLifetimeTests, DroppedInsideBufferMapCallback) {
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, false};
Userdata data = Userdata{std::move(device), buffer, UsesWire(), false};
buffer.MapAsync(
wgpu::MapMode::Read, 0, wgpu::kWholeMapSize,
[](WGPUBufferMapAsyncStatus status, void* userdata) {
@ -229,7 +230,11 @@ TEST_P(DeviceLifetimeTests, DroppedInsideBufferMapCallback) {
data->done = true;
// Mapped data should be null since the buffer is implicitly destroyed.
EXPECT_EQ(data->buffer.GetConstMappedRange(), nullptr);
// 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);
@ -243,7 +248,11 @@ TEST_P(DeviceLifetimeTests, DroppedInsideBufferMapCallback) {
}
// Mapped data should be null since the buffer is implicitly destroyed.
EXPECT_EQ(buffer.GetConstMappedRange(), nullptr);
// 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.
@ -341,16 +350,20 @@ TEST_P(DeviceLifetimeTests, DroppedWhileCreatePipelineAsyncAlreadyCached) {
// 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* message,
[](WGPUCreatePipelineAsyncStatus status, WGPUComputePipeline cPipeline, const char*,
void* userdata) {
const bool wire = *static_cast<bool*>(userdata);
wgpu::ComputePipeline::Acquire(cPipeline);
// Success because it hits the frontend cache immediately.
EXPECT_EQ(status, WGPUCreatePipelineAsyncStatus_Success);
// 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);
},
nullptr);
&wire);
device = nullptr;
}

View File

@ -363,20 +363,10 @@ TEST_F(WireCreatePipelineAsyncTest, DeviceDeletedBeforeCallback) {
wgpuDeviceRelease(device);
// Expect release on all objects created by the client.
Sequence s1, s2;
EXPECT_CALL(api, QueueRelease(apiQueue)).Times(1).InSequence(s1);
EXPECT_CALL(api, ShaderModuleRelease(apiModule)).Times(1).InSequence(s2);
EXPECT_CALL(api, OnDeviceSetUncapturedErrorCallback(apiDevice, nullptr, nullptr))
.Times(1)
.InSequence(s1, s2);
EXPECT_CALL(api, OnDeviceSetLoggingCallback(apiDevice, nullptr, nullptr))
.Times(1)
.InSequence(s1, s2);
EXPECT_CALL(api, OnDeviceSetDeviceLostCallback(apiDevice, nullptr, nullptr))
.Times(1)
.InSequence(s1, s2);
EXPECT_CALL(api, DeviceRelease(apiDevice)).Times(1).InSequence(s1, s2);
EXPECT_CALL(api, OnDeviceSetUncapturedErrorCallback(apiDevice, nullptr, nullptr)).Times(1);
EXPECT_CALL(api, OnDeviceSetLoggingCallback(apiDevice, nullptr, nullptr)).Times(1);
EXPECT_CALL(api, OnDeviceSetDeviceLostCallback(apiDevice, nullptr, nullptr)).Times(1);
EXPECT_CALL(api, DeviceRelease(apiDevice)).Times(1);
FlushClient();
DefaultApiDeviceWasReleased();

View File

@ -1,62 +0,0 @@
// Copyright 2021 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 "dawn/tests/MockCallback.h"
#include "dawn/tests/unittests/wire/WireTest.h"
namespace dawn::wire {
using testing::Return;
using testing::Sequence;
class WireDestroyObjectTests : public WireTest {};
// Test that destroying the device also destroys child objects.
TEST_F(WireDestroyObjectTests, DestroyDeviceDestroysChildren) {
WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(device, nullptr);
WGPUCommandEncoder apiEncoder = api.GetNewCommandEncoder();
EXPECT_CALL(api, DeviceCreateCommandEncoder(apiDevice, nullptr)).WillOnce(Return(apiEncoder));
FlushClient();
// Release the device. It should cause the command encoder to be destroyed.
wgpuDeviceRelease(device);
Sequence s1, s2;
// The device and child objects should be released.
EXPECT_CALL(api, CommandEncoderRelease(apiEncoder)).InSequence(s1);
EXPECT_CALL(api, QueueRelease(apiQueue)).InSequence(s2);
EXPECT_CALL(api, OnDeviceSetUncapturedErrorCallback(apiDevice, nullptr, nullptr))
.Times(1)
.InSequence(s1, s2);
EXPECT_CALL(api, OnDeviceSetLoggingCallback(apiDevice, nullptr, nullptr))
.Times(1)
.InSequence(s1, s2);
EXPECT_CALL(api, OnDeviceSetDeviceLostCallback(apiDevice, nullptr, nullptr))
.Times(1)
.InSequence(s1, s2);
EXPECT_CALL(api, DeviceRelease(apiDevice)).InSequence(s1, s2);
FlushClient();
// Signal that we already released and cleared callbacks for |apiDevice|
DefaultApiDeviceWasReleased();
// Using the command encoder should be an error.
wgpuCommandEncoderFinish(encoder, nullptr);
FlushClient(false);
}
} // namespace dawn::wire

View File

@ -27,12 +27,6 @@
namespace dawn::wire::server {
struct DeviceInfo {
std::unordered_set<uint64_t> childObjectTypesAndIds;
Server* server;
ObjectHandle self;
};
// Whether this object has been allocated, or reserved for async object creation.
// Used by the KnownObjects queries
enum class AllocationState : uint32_t {
@ -48,9 +42,6 @@ struct ObjectDataBase {
uint32_t generation = 0;
AllocationState state;
// This points to an allocation that is owned by the device.
DeviceInfo* deviceInfo = nullptr;
};
// Stores what the backend knows about the type.
@ -70,28 +61,21 @@ struct ObjectData<WGPUBuffer> : public ObjectDataBase<WGPUBuffer> {
bool mappedAtCreation = false;
};
// Pack the ObjectType and ObjectId as a single value for storage in
// an std::unordered_set. This lets us avoid providing our own hash and
// equality comparison operators.
inline uint64_t PackObjectTypeAndId(ObjectType type, ObjectId id) {
static_assert(sizeof(ObjectType) * 8 <= 32);
static_assert(sizeof(ObjectId) * 8 <= 32);
return (static_cast<uint64_t>(type) << 32) + id;
}
inline std::pair<ObjectType, ObjectId> UnpackObjectTypeAndId(uint64_t payload) {
ObjectType type = static_cast<ObjectType>(payload >> 32);
ObjectId id = payload & 0xFFFFFFFF;
return std::make_pair(type, id);
}
struct DeviceInfo {
Server* server;
ObjectHandle self;
};
template <>
struct ObjectData<WGPUDevice> : public ObjectDataBase<WGPUDevice> {
// Store |info| as a separate allocation so that its address does not move.
// The pointer to |info| is stored in device child objects.
// The pointer to |info| is used as the userdata to device callback.
std::unique_ptr<DeviceInfo> info = std::make_unique<DeviceInfo>();
};
template <>
struct ObjectData<WGPUQueue> : public ObjectDataBase<WGPUQueue> {};
// Keeps track of the mapping between client IDs and backend objects.
template <typename T>
class KnownObjectsBase {

View File

@ -62,11 +62,6 @@ bool Server::InjectTexture(WGPUTexture texture,
data->handle = texture;
data->generation = generation;
data->state = AllocationState::Allocated;
data->deviceInfo = device->info.get();
if (!TrackDeviceChild(data->deviceInfo, ObjectType::Texture, id)) {
return false;
}
// The texture is externally owned so it shouldn't be destroyed when we receive a destroy
// message from the client. Add a reference to counterbalance the eventual release.
@ -94,11 +89,6 @@ bool Server::InjectSwapChain(WGPUSwapChain swapchain,
data->handle = swapchain;
data->generation = generation;
data->state = AllocationState::Allocated;
data->deviceInfo = device->info.get();
if (!TrackDeviceChild(data->deviceInfo, ObjectType::SwapChain, id)) {
return false;
}
// The texture is externally owned so it shouldn't be destroyed when we receive a destroy
// message from the client. Add a reference to counterbalance the eventual release.
@ -164,6 +154,9 @@ void Server::SetForwardingDeviceCallbacks(ObjectData<WGPUDevice>* deviceObject)
// free their userdata. Also unlike other callbacks, these are cleared and unset when
// the server is destroyed, so we don't need to check if the server is still alive
// inside them.
// Also, the device is special-cased in Server::DoDestroyObject to call
// ClearDeviceCallbacks. This ensures that callbacks will not fire after |deviceObject|
// is freed.
mProcs.deviceSetUncapturedErrorCallback(
deviceObject->handle,
[](WGPUErrorType type, const char* message, void* userdata) {
@ -197,24 +190,4 @@ void Server::ClearDeviceCallbacks(WGPUDevice device) {
mProcs.deviceSetDeviceLostCallback(device, nullptr, nullptr);
}
bool TrackDeviceChild(DeviceInfo* info, ObjectType type, ObjectId id) {
auto [_, inserted] = info->childObjectTypesAndIds.insert(PackObjectTypeAndId(type, id));
if (!inserted) {
// An object of this type and id already exists.
return false;
}
return true;
}
bool UntrackDeviceChild(DeviceInfo* info, ObjectType type, ObjectId id) {
auto& children = info->childObjectTypesAndIds;
auto it = children.find(PackObjectTypeAndId(type, id));
if (it == children.end()) {
// An object of this type and id was already deleted.
return false;
}
children.erase(it);
return true;
}
} // namespace dawn::wire::server

View File

@ -234,9 +234,6 @@ class Server : public ServerBase {
std::shared_ptr<bool> mIsAlive;
};
bool TrackDeviceChild(DeviceInfo* device, ObjectType type, ObjectId id);
bool UntrackDeviceChild(DeviceInfo* device, ObjectType type, ObjectId id);
std::unique_ptr<MemoryTransferService> CreateInlineMemoryTransferService();
} // namespace dawn::wire::server

View File

@ -116,12 +116,8 @@ bool Server::DoDeviceCreateBuffer(ObjectId deviceId,
}
resultData->generation = bufferResult.generation;
resultData->handle = mProcs.deviceCreateBuffer(device->handle, descriptor);
resultData->deviceInfo = device->info.get();
resultData->usage = descriptor->usage;
resultData->mappedAtCreation = descriptor->mappedAtCreation;
if (!TrackDeviceChild(resultData->deviceInfo, ObjectType::Buffer, bufferResult.id)) {
return false;
}
// isReadMode and isWriteMode could be true at the same time if usage contains
// WGPUMapMode_Read and buffer is mappedAtCreation

View File

@ -26,12 +26,6 @@ void HandleCreatePipelineAsyncCallback(KnownObjects<Pipeline>* knownObjects,
if (status == WGPUCreatePipelineAsyncStatus_Success) {
auto* pipelineObject = knownObjects->FillReservation(data->pipelineObjectID, pipeline);
ASSERT(pipelineObject != nullptr);
// This should be impossible to fail. It would require a command to be sent that
// creates a duplicate ObjectId, which would fail validation.
bool success =
TrackDeviceChild(pipelineObject->deviceInfo, objectType, data->pipelineObjectID);
ASSERT(success);
} else {
// Otherwise, free the ObjectId which will make it unusable.
knownObjects->Free(data->pipelineObjectID);
@ -111,7 +105,6 @@ bool Server::DoDeviceCreateComputePipelineAsync(ObjectId deviceId,
}
resultData->generation = pipelineObjectHandle.generation;
resultData->deviceInfo = device->info.get();
auto userdata = MakeUserdata<CreatePipelineAsyncUserData>();
userdata->device = ObjectHandle{deviceId, device->generation};
@ -156,7 +149,6 @@ bool Server::DoDeviceCreateRenderPipelineAsync(ObjectId deviceId,
}
resultData->generation = pipelineObjectHandle.generation;
resultData->deviceInfo = device->info.get();
auto userdata = MakeUserdata<CreatePipelineAsyncUserData>();
userdata->device = ObjectHandle{deviceId, device->generation};

View File

@ -59,12 +59,7 @@ bool Server::DoQueueWriteBuffer(ObjectId queueId,
}
if (size > std::numeric_limits<size_t>::max()) {
auto* device = DeviceObjects().Get(queue->deviceInfo->self.id);
if (device == nullptr) {
return false;
}
return DoDeviceInjectError(reinterpret_cast<WGPUDevice>(device), WGPUErrorType_OutOfMemory,
"Data size too large for write texture.");
return false;
}
mProcs.queueWriteBuffer(queue->handle, buffer->handle, bufferOffset, data,
@ -86,12 +81,7 @@ bool Server::DoQueueWriteTexture(ObjectId queueId,
}
if (dataSize > std::numeric_limits<size_t>::max()) {
auto* device = DeviceObjects().Get(queue->deviceInfo->self.id);
if (device == nullptr) {
return false;
}
return DoDeviceInjectError(reinterpret_cast<WGPUDevice>(device), WGPUErrorType_OutOfMemory,
"Data size too large for write texture.");
return false;
}
mProcs.queueWriteTexture(queue->handle, destination, data, static_cast<size_t>(dataSize),