Make Adapter and Instance lifetimes more robust
Previously, we would get a use-after-free if you dropped the instance before an adapter created from it. This CL fixes up the lifetimes such that Device refs Adapter refs Instance. Instance uses a cycle-breaking refcount so that it releases internal refs to its adapters when the last external ref is dropped. Bug: none Change-Id: I5304ec86f425247d4c45ca342fda393cc19689e3 Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/99820 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:
parent
322bcf827a
commit
a09d05c10b
|
@ -190,6 +190,9 @@ DAWN_NATIVE_EXPORT size_t GetLazyClearCountForTesting(WGPUDevice device);
|
|||
// Backdoor to get the number of deprecation warnings for testing
|
||||
DAWN_NATIVE_EXPORT size_t GetDeprecationWarningCountForTesting(WGPUDevice device);
|
||||
|
||||
// Backdoor to get the number of adapters an instance knows about for testing
|
||||
DAWN_NATIVE_EXPORT size_t GetAdapterCountForTesting(WGPUInstance instance);
|
||||
|
||||
// Query if texture has been initialized
|
||||
DAWN_NATIVE_EXPORT bool IsTextureSubresourceInitialized(
|
||||
WGPUTexture texture,
|
||||
|
|
|
@ -31,6 +31,8 @@ AdapterBase::AdapterBase(InstanceBase* instance, wgpu::BackendType backend)
|
|||
mSupportedFeatures.EnableFeature(Feature::DawnInternalUsages);
|
||||
}
|
||||
|
||||
AdapterBase::~AdapterBase() = default;
|
||||
|
||||
MaybeError AdapterBase::Initialize() {
|
||||
DAWN_TRY_CONTEXT(InitializeImpl(), "initializing adapter (backend=%s)", mBackend);
|
||||
InitializeVendorArchitectureImpl();
|
||||
|
@ -157,7 +159,7 @@ wgpu::BackendType AdapterBase::GetBackendType() const {
|
|||
}
|
||||
|
||||
InstanceBase* AdapterBase::GetInstance() const {
|
||||
return mInstance;
|
||||
return mInstance.Get();
|
||||
}
|
||||
|
||||
FeaturesSet AdapterBase::GetSupportedFeatures() const {
|
||||
|
|
|
@ -33,7 +33,7 @@ class DeviceBase;
|
|||
class AdapterBase : public RefCounted {
|
||||
public:
|
||||
AdapterBase(InstanceBase* instance, wgpu::BackendType backend);
|
||||
~AdapterBase() override = default;
|
||||
~AdapterBase() override;
|
||||
|
||||
MaybeError Initialize();
|
||||
|
||||
|
@ -90,7 +90,7 @@ class AdapterBase : public RefCounted {
|
|||
ResultOrError<Ref<DeviceBase>> CreateDeviceInternal(const DeviceDescriptor* descriptor);
|
||||
|
||||
virtual MaybeError ResetInternalDeviceForTestingImpl();
|
||||
InstanceBase* mInstance = nullptr;
|
||||
Ref<InstanceBase> mInstance;
|
||||
wgpu::BackendType mBackend;
|
||||
CombinedLimits mLimits;
|
||||
bool mUseTieredLimits = false;
|
||||
|
|
|
@ -191,7 +191,7 @@ Instance::Instance(const WGPUInstanceDescriptor* desc)
|
|||
|
||||
Instance::~Instance() {
|
||||
if (mImpl != nullptr) {
|
||||
mImpl->Release();
|
||||
mImpl->APIRelease();
|
||||
mImpl = nullptr;
|
||||
}
|
||||
}
|
||||
|
@ -256,6 +256,10 @@ size_t GetDeprecationWarningCountForTesting(WGPUDevice device) {
|
|||
return FromAPI(device)->GetDeprecationWarningCountForTesting();
|
||||
}
|
||||
|
||||
size_t GetAdapterCountForTesting(WGPUInstance instance) {
|
||||
return FromAPI(instance)->GetAdapters().size();
|
||||
}
|
||||
|
||||
bool IsTextureSubresourceInitialized(WGPUTexture texture,
|
||||
uint32_t baseMipLevel,
|
||||
uint32_t levelCount,
|
||||
|
|
|
@ -171,8 +171,8 @@ ResultOrError<Ref<PipelineLayoutBase>> ValidateLayoutAndGetRenderPipelineDescrip
|
|||
// DeviceBase
|
||||
|
||||
DeviceBase::DeviceBase(AdapterBase* adapter, const DeviceDescriptor* descriptor)
|
||||
: mInstance(adapter->GetInstance()), mAdapter(adapter), mNextPipelineCompatibilityToken(1) {
|
||||
mInstance->IncrementDeviceCountForTesting();
|
||||
: mAdapter(adapter), mNextPipelineCompatibilityToken(1) {
|
||||
mAdapter->GetInstance()->IncrementDeviceCountForTesting();
|
||||
ASSERT(descriptor != nullptr);
|
||||
|
||||
AdapterProperties adapterProperties;
|
||||
|
@ -221,9 +221,9 @@ DeviceBase::~DeviceBase() {
|
|||
// We need to explicitly release the Queue before we complete the destructor so that the
|
||||
// Queue does not get destroyed after the Device.
|
||||
mQueue = nullptr;
|
||||
// mInstance is not set for mock test devices.
|
||||
if (mInstance != nullptr) {
|
||||
mInstance->DecrementDeviceCountForTesting();
|
||||
// mAdapter is not set for mock test devices.
|
||||
if (mAdapter != nullptr) {
|
||||
mAdapter->GetInstance()->DecrementDeviceCountForTesting();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -628,7 +628,7 @@ BlobCache* DeviceBase::GetBlobCache() {
|
|||
// generate cache keys. We can lift the dependency once we also cache frontend parsing,
|
||||
// transformations, and reflection.
|
||||
if (IsToggleEnabled(Toggle::EnableBlobCache)) {
|
||||
return mInstance->GetBlobCache();
|
||||
return mAdapter->GetInstance()->GetBlobCache();
|
||||
}
|
||||
#endif
|
||||
return nullptr;
|
||||
|
@ -696,7 +696,7 @@ std::mutex* DeviceBase::GetObjectListMutex(ObjectType type) {
|
|||
}
|
||||
|
||||
AdapterBase* DeviceBase::GetAdapter() const {
|
||||
return mAdapter;
|
||||
return mAdapter.Get();
|
||||
}
|
||||
|
||||
dawn::platform::Platform* DeviceBase::GetPlatform() const {
|
||||
|
@ -1286,7 +1286,7 @@ MaybeError DeviceBase::Tick() {
|
|||
|
||||
AdapterBase* DeviceBase::APIGetAdapter() {
|
||||
mAdapter->Reference();
|
||||
return mAdapter;
|
||||
return mAdapter.Get();
|
||||
}
|
||||
|
||||
QueueBase* DeviceBase::APIGetQueue() {
|
||||
|
|
|
@ -527,12 +527,7 @@ class DeviceBase : public RefCountedWithExternalCount {
|
|||
|
||||
std::unique_ptr<ErrorScopeStack> mErrorScopeStack;
|
||||
|
||||
// The Device keeps a ref to the Instance so that any live Device keeps the Instance alive.
|
||||
// The Instance shouldn't need to ref child objects so this shouldn't introduce ref cycles.
|
||||
// The Device keeps a simple pointer to the Adapter because the Adapter is owned by the
|
||||
// Instance.
|
||||
Ref<InstanceBase> mInstance;
|
||||
AdapterBase* mAdapter = nullptr;
|
||||
Ref<AdapterBase> mAdapter;
|
||||
|
||||
// The object caches aren't exposed in the header as they would require a lot of
|
||||
// additional includes.
|
||||
|
|
|
@ -125,6 +125,17 @@ InstanceBase::InstanceBase() = default;
|
|||
|
||||
InstanceBase::~InstanceBase() = default;
|
||||
|
||||
void InstanceBase::WillDropLastExternalRef() {
|
||||
// InstanceBase uses RefCountedWithExternalCount to break refcycles.
|
||||
//
|
||||
// InstanceBase holds Refs to AdapterBases it has discovered, which hold Refs back to the
|
||||
// InstanceBase.
|
||||
// In order to break this cycle and prevent leaks, when the application drops the last external
|
||||
// ref and WillDropLastExternalRef is called, the instance clears out any member refs to
|
||||
// adapters that hold back-refs to the instance - thus breaking any reference cycles.
|
||||
mAdapters.clear();
|
||||
}
|
||||
|
||||
// TODO(crbug.com/dawn/832): make the platform an initialization parameter of the instance.
|
||||
MaybeError InstanceBase::Initialize(const InstanceDescriptor* descriptor) {
|
||||
DAWN_TRY(ValidateSingleSType(descriptor->nextInChain, wgpu::SType::DawnInstanceDescriptor));
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
#include "dawn/native/BackendConnection.h"
|
||||
#include "dawn/native/BlobCache.h"
|
||||
#include "dawn/native/Features.h"
|
||||
#include "dawn/native/RefCountedWithExternalCount.h"
|
||||
#include "dawn/native/Toggles.h"
|
||||
#include "dawn/native/dawn_platform.h"
|
||||
|
||||
|
@ -45,7 +46,7 @@ InstanceBase* APICreateInstance(const InstanceDescriptor* descriptor);
|
|||
|
||||
// This is called InstanceBase for consistency across the frontend, even if the backends don't
|
||||
// specialize this class.
|
||||
class InstanceBase final : public RefCounted {
|
||||
class InstanceBase final : public RefCountedWithExternalCount {
|
||||
public:
|
||||
static Ref<InstanceBase> Create(const InstanceDescriptor* descriptor = nullptr);
|
||||
|
||||
|
@ -110,6 +111,8 @@ class InstanceBase final : public RefCounted {
|
|||
InstanceBase();
|
||||
~InstanceBase() override;
|
||||
|
||||
void WillDropLastExternalRef() override;
|
||||
|
||||
InstanceBase(const InstanceBase& other) = delete;
|
||||
InstanceBase& operator=(const InstanceBase& other) = delete;
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "dawn/dawn_proc.h"
|
||||
|
@ -21,9 +22,48 @@
|
|||
#include "dawn/utils/WGPUHelpers.h"
|
||||
|
||||
class DeviceInitializationTest : public testing::Test {
|
||||
protected:
|
||||
void SetUp() override { dawnProcSetProcs(&dawn::native::GetProcs()); }
|
||||
|
||||
void TearDown() override { dawnProcSetProcs(nullptr); }
|
||||
|
||||
// Test that the device can still be used by testing a buffer copy.
|
||||
void ExpectDeviceUsable(wgpu::Device device) {
|
||||
wgpu::Buffer src =
|
||||
utils::CreateBufferFromData<uint32_t>(device, wgpu::BufferUsage::CopySrc, {1, 2, 3, 4});
|
||||
|
||||
wgpu::Buffer dst = utils::CreateBufferFromData<uint32_t>(
|
||||
device, wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead, {0, 0, 0, 0});
|
||||
|
||||
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
|
||||
encoder.CopyBufferToBuffer(src, 0, dst, 0, 4 * sizeof(uint32_t));
|
||||
|
||||
wgpu::CommandBuffer commands = encoder.Finish();
|
||||
device.GetQueue().Submit(1, &commands);
|
||||
|
||||
bool done = false;
|
||||
dst.MapAsync(
|
||||
wgpu::MapMode::Read, 0, 4 * sizeof(uint32_t),
|
||||
[](WGPUBufferMapAsyncStatus status, void* userdata) {
|
||||
EXPECT_EQ(status, WGPUBufferMapAsyncStatus_Success);
|
||||
*static_cast<bool*>(userdata) = true;
|
||||
},
|
||||
&done);
|
||||
|
||||
// Note: we can't actually test this if Tick moves over to
|
||||
// wgpuInstanceProcessEvents. We can still test that object creation works
|
||||
// without crashing.
|
||||
while (!done) {
|
||||
device.Tick();
|
||||
utils::USleep(100);
|
||||
}
|
||||
|
||||
const uint32_t* mapping = static_cast<const uint32_t*>(dst.GetConstMappedRange());
|
||||
EXPECT_EQ(mapping[0], 1u);
|
||||
EXPECT_EQ(mapping[1], 2u);
|
||||
EXPECT_EQ(mapping[2], 3u);
|
||||
EXPECT_EQ(mapping[3], 4u);
|
||||
}
|
||||
};
|
||||
|
||||
// Test that device operations are still valid if the reference to the instance
|
||||
|
@ -66,40 +106,64 @@ TEST_F(DeviceInitializationTest, DeviceOutlivesInstance) {
|
|||
}
|
||||
}
|
||||
|
||||
// Now, test that the device can still be used by testing a buffer copy.
|
||||
wgpu::Buffer src =
|
||||
utils::CreateBufferFromData<uint32_t>(device, wgpu::BufferUsage::CopySrc, {1, 2, 3, 4});
|
||||
|
||||
wgpu::Buffer dst = utils::CreateBufferFromData<uint32_t>(
|
||||
device, wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead, {0, 0, 0, 0});
|
||||
|
||||
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
|
||||
encoder.CopyBufferToBuffer(src, 0, dst, 0, 4 * sizeof(uint32_t));
|
||||
|
||||
wgpu::CommandBuffer commands = encoder.Finish();
|
||||
device.GetQueue().Submit(1, &commands);
|
||||
|
||||
bool done = false;
|
||||
dst.MapAsync(
|
||||
wgpu::MapMode::Read, 0, 4 * sizeof(uint32_t),
|
||||
[](WGPUBufferMapAsyncStatus status, void* userdata) {
|
||||
EXPECT_EQ(status, WGPUBufferMapAsyncStatus_Success);
|
||||
*static_cast<bool*>(userdata) = true;
|
||||
},
|
||||
&done);
|
||||
|
||||
// Note: we can't actually test this if Tick moves over to
|
||||
// wgpuInstanceProcessEvents. We can still test that object creation works
|
||||
// without crashing.
|
||||
while (!done) {
|
||||
device.Tick();
|
||||
utils::USleep(100);
|
||||
}
|
||||
|
||||
const uint32_t* mapping = static_cast<const uint32_t*>(dst.GetConstMappedRange());
|
||||
EXPECT_EQ(mapping[0], 1u);
|
||||
EXPECT_EQ(mapping[1], 2u);
|
||||
EXPECT_EQ(mapping[2], 3u);
|
||||
EXPECT_EQ(mapping[3], 4u);
|
||||
if (device) {
|
||||
ExpectDeviceUsable(std::move(device));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test that it is still possible to create a device from an adapter after the reference to the
|
||||
// instance is dropped.
|
||||
TEST_F(DeviceInitializationTest, AdapterOutlivesInstance) {
|
||||
// Get properties of all available adapters and then free the instance.
|
||||
// We want to create a device on a fresh instance and adapter each time.
|
||||
std::vector<wgpu::AdapterProperties> availableAdapterProperties;
|
||||
{
|
||||
auto instance = std::make_unique<dawn::native::Instance>();
|
||||
instance->DiscoverDefaultAdapters();
|
||||
for (const dawn::native::Adapter& adapter : instance->GetAdapters()) {
|
||||
wgpu::AdapterProperties properties;
|
||||
adapter.GetProperties(&properties);
|
||||
|
||||
if (properties.backendType == wgpu::BackendType::Null) {
|
||||
continue;
|
||||
}
|
||||
availableAdapterProperties.push_back(properties);
|
||||
}
|
||||
}
|
||||
|
||||
for (const wgpu::AdapterProperties& desiredProperties : availableAdapterProperties) {
|
||||
wgpu::Adapter adapter;
|
||||
|
||||
auto instance = std::make_unique<dawn::native::Instance>();
|
||||
// Save a pointer to the instance.
|
||||
// It will only be valid as long as the instance is alive.
|
||||
WGPUInstance unsafeInstancePtr = instance->Get();
|
||||
|
||||
instance->DiscoverDefaultAdapters();
|
||||
for (dawn::native::Adapter& nativeAdapter : instance->GetAdapters()) {
|
||||
wgpu::AdapterProperties properties;
|
||||
nativeAdapter.GetProperties(&properties);
|
||||
|
||||
if (properties.deviceID == desiredProperties.deviceID &&
|
||||
properties.vendorID == desiredProperties.vendorID &&
|
||||
properties.adapterType == desiredProperties.adapterType &&
|
||||
properties.backendType == desiredProperties.backendType) {
|
||||
// Save the adapter, and reset the instance.
|
||||
// Check that the number of adapters before the reset is > 0, and after the reset
|
||||
// is 0. Unsafe, but we assume the pointer is still valid since the adapter is
|
||||
// holding onto the instance. The instance should have cleared all internal
|
||||
// references to adapters when the last external ref is dropped.
|
||||
adapter = wgpu::Adapter(nativeAdapter.Get());
|
||||
EXPECT_GT(dawn::native::GetAdapterCountForTesting(unsafeInstancePtr), 0u);
|
||||
instance.reset();
|
||||
EXPECT_EQ(dawn::native::GetAdapterCountForTesting(unsafeInstancePtr), 0u);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (adapter) {
|
||||
ExpectDeviceUsable(adapter.CreateDevice());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,17 +19,23 @@
|
|||
#include "dawn/common/RefCounted.h"
|
||||
#include "dawn/native/ToBackend.h"
|
||||
|
||||
// Make our own Base - Backend object pair, reusing the AdapterBase name
|
||||
// Make our own Base - Backend object pair, reusing the MyObjectBase name
|
||||
namespace dawn::native {
|
||||
class AdapterBase : public RefCounted {};
|
||||
|
||||
class MyAdapter : public AdapterBase {};
|
||||
class MyObjectBase : public RefCounted {};
|
||||
|
||||
class MyObject : public MyObjectBase {};
|
||||
|
||||
struct MyBackendTraits {
|
||||
using AdapterType = MyAdapter;
|
||||
using MyObjectType = MyObject;
|
||||
};
|
||||
|
||||
// Instanciate ToBackend for our "backend"
|
||||
template <typename BackendTraits>
|
||||
struct ToBackendTraits<MyObjectBase, BackendTraits> {
|
||||
using BackendType = typename BackendTraits::MyObjectType;
|
||||
};
|
||||
|
||||
// Instantiate ToBackend for our "backend"
|
||||
template <typename T>
|
||||
auto ToBackend(T&& common) -> decltype(ToBackendBase<MyBackendTraits>(common)) {
|
||||
return ToBackendBase<MyBackendTraits>(common);
|
||||
|
@ -38,49 +44,48 @@ auto ToBackend(T&& common) -> decltype(ToBackendBase<MyBackendTraits>(common)) {
|
|||
// Test that ToBackend correctly converts pointers to base classes.
|
||||
TEST(ToBackend, Pointers) {
|
||||
{
|
||||
MyAdapter* adapter = new MyAdapter;
|
||||
const AdapterBase* base = adapter;
|
||||
MyObject* myObject = new MyObject;
|
||||
const MyObjectBase* base = myObject;
|
||||
|
||||
auto* backendAdapter = ToBackend(base);
|
||||
static_assert(std::is_same<decltype(backendAdapter), const MyAdapter*>::value);
|
||||
ASSERT_EQ(adapter, backendAdapter);
|
||||
static_assert(std::is_same<decltype(backendAdapter), const MyObject*>::value);
|
||||
ASSERT_EQ(myObject, backendAdapter);
|
||||
|
||||
adapter->Release();
|
||||
myObject->Release();
|
||||
}
|
||||
{
|
||||
MyAdapter* adapter = new MyAdapter;
|
||||
AdapterBase* base = adapter;
|
||||
MyObject* myObject = new MyObject;
|
||||
MyObjectBase* base = myObject;
|
||||
|
||||
auto* backendAdapter = ToBackend(base);
|
||||
static_assert(std::is_same<decltype(backendAdapter), MyAdapter*>::value);
|
||||
ASSERT_EQ(adapter, backendAdapter);
|
||||
static_assert(std::is_same<decltype(backendAdapter), MyObject*>::value);
|
||||
ASSERT_EQ(myObject, backendAdapter);
|
||||
|
||||
adapter->Release();
|
||||
myObject->Release();
|
||||
}
|
||||
}
|
||||
|
||||
// Test that ToBackend correctly converts Refs to base classes.
|
||||
TEST(ToBackend, Ref) {
|
||||
{
|
||||
MyAdapter* adapter = new MyAdapter;
|
||||
const Ref<AdapterBase> base(adapter);
|
||||
MyObject* myObject = new MyObject;
|
||||
const Ref<MyObjectBase> base(myObject);
|
||||
|
||||
const auto& backendAdapter = ToBackend(base);
|
||||
static_assert(std::is_same<decltype(ToBackend(base)), const Ref<MyAdapter>&>::value);
|
||||
ASSERT_EQ(adapter, backendAdapter.Get());
|
||||
static_assert(std::is_same<decltype(ToBackend(base)), const Ref<MyObject>&>::value);
|
||||
ASSERT_EQ(myObject, backendAdapter.Get());
|
||||
|
||||
adapter->Release();
|
||||
myObject->Release();
|
||||
}
|
||||
{
|
||||
MyAdapter* adapter = new MyAdapter;
|
||||
Ref<AdapterBase> base(adapter);
|
||||
MyObject* myObject = new MyObject;
|
||||
Ref<MyObjectBase> base(myObject);
|
||||
|
||||
auto backendAdapter = ToBackend(base);
|
||||
static_assert(std::is_same<decltype(ToBackend(base)), Ref<MyAdapter>&>::value);
|
||||
ASSERT_EQ(adapter, backendAdapter.Get());
|
||||
static_assert(std::is_same<decltype(ToBackend(base)), Ref<MyObject>&>::value);
|
||||
ASSERT_EQ(myObject, backendAdapter.Get());
|
||||
|
||||
adapter->Release();
|
||||
myObject->Release();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dawn::native
|
||||
|
|
Loading…
Reference in New Issue