Add more multithread tests.

Bug: dawn:1662
Change-Id: I2b2c66c6f9a7b512ae9f8010a082e7306feaa6f3
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/122060
Commit-Queue: Quyen Le <lehoangquyen@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Austin Eng <enga@chromium.org>
This commit is contained in:
Le Hoang Quyen 2023-04-24 20:12:00 +00:00 committed by Dawn LUCI CQ
parent 653e99478e
commit 23f4396177
8 changed files with 1141 additions and 61 deletions

View File

@ -15,6 +15,7 @@
#include "dawn/tests/DawnTest.h" #include "dawn/tests/DawnTest.h"
#include <algorithm> #include <algorithm>
#include <atomic>
#include <fstream> #include <fstream>
#include <iomanip> #include <iomanip>
#include <regex> #include <regex>
@ -1146,6 +1147,9 @@ std::ostringstream& DawnTestBase::AddBufferExpectation(const char* file,
deferred.size = size; deferred.size = size;
deferred.expectation.reset(expectation); deferred.expectation.reset(expectation);
// This expectation might be called from multiple threads
dawn::Mutex::AutoLock lg(&mMutex);
mDeferredExpectations.push_back(std::move(deferred)); mDeferredExpectations.push_back(std::move(deferred));
mDeferredExpectations.back().message = std::make_unique<std::ostringstream>(); mDeferredExpectations.back().message = std::make_unique<std::ostringstream>();
return *(mDeferredExpectations.back().message.get()); return *(mDeferredExpectations.back().message.get());
@ -1200,6 +1204,9 @@ std::ostringstream& DawnTestBase::AddTextureExpectationImpl(const char* file,
deferred.bytesPerRow = bytesPerRow; deferred.bytesPerRow = bytesPerRow;
deferred.expectation.reset(expectation); deferred.expectation.reset(expectation);
// This expectation might be called from multiple threads
dawn::Mutex::AutoLock lg(&mMutex);
mDeferredExpectations.push_back(std::move(deferred)); mDeferredExpectations.push_back(std::move(deferred));
mDeferredExpectations.back().message = std::make_unique<std::ostringstream>(); mDeferredExpectations.back().message = std::make_unique<std::ostringstream>();
return *(mDeferredExpectations.back().message.get()); return *(mDeferredExpectations.back().message.get());
@ -1504,11 +1511,16 @@ void DawnTestBase::FlushWire() {
} }
void DawnTestBase::WaitForAllOperations() { void DawnTestBase::WaitForAllOperations() {
bool done = false; // Callback might be invoked on another thread that calls the same WaitABit() method, not
// necessarily the current thread. So we need to use atomic here.
std::atomic<bool> done(false);
device.GetQueue().OnSubmittedWorkDone( device.GetQueue().OnSubmittedWorkDone(
0u, [](WGPUQueueWorkDoneStatus, void* userdata) { *static_cast<bool*>(userdata) = true; }, 0u,
[](WGPUQueueWorkDoneStatus, void* userdata) {
*static_cast<std::atomic<bool>*>(userdata) = true;
},
&done); &done);
while (!done) { while (!done.load()) {
WaitABit(); WaitABit();
} }
} }
@ -1526,6 +1538,9 @@ DawnTestBase::ReadbackReservation DawnTestBase::ReserveReadback(wgpu::Device tar
utils::CreateBufferFromData(targetDevice, initialBufferData.data(), readbackSize, utils::CreateBufferFromData(targetDevice, initialBufferData.data(), readbackSize,
wgpu::BufferUsage::MapRead | wgpu::BufferUsage::CopyDst); wgpu::BufferUsage::MapRead | wgpu::BufferUsage::CopyDst);
// This readback might be called from multiple threads
dawn::Mutex::AutoLock lg(&mMutex);
ReadbackReservation reservation; ReadbackReservation reservation;
reservation.device = targetDevice; reservation.device = targetDevice;
reservation.buffer = slot.buffer; reservation.buffer = slot.buffer;
@ -1551,7 +1566,7 @@ void DawnTestBase::MapSlotsSynchronously() {
} }
// Busy wait until all map operations are done. // Busy wait until all map operations are done.
while (mNumPendingMapOperations != 0) { while (mNumPendingMapOperations.load(std::memory_order_acquire) != 0) {
WaitABit(); WaitABit();
} }
} }
@ -1562,7 +1577,8 @@ void DawnTestBase::SlotMapCallback(WGPUBufferMapAsyncStatus status, void* userda
status == WGPUBufferMapAsyncStatus_DeviceLost); status == WGPUBufferMapAsyncStatus_DeviceLost);
std::unique_ptr<MapReadUserdata> userdata(static_cast<MapReadUserdata*>(userdata_)); std::unique_ptr<MapReadUserdata> userdata(static_cast<MapReadUserdata*>(userdata_));
DawnTestBase* test = userdata->test; DawnTestBase* test = userdata->test;
test->mNumPendingMapOperations--;
dawn::Mutex::AutoLock lg(&test->mMutex);
ReadbackSlot* slot = &test->mReadbackSlots[userdata->slot]; ReadbackSlot* slot = &test->mReadbackSlots[userdata->slot];
if (status == WGPUBufferMapAsyncStatus_Success) { if (status == WGPUBufferMapAsyncStatus_Success) {
@ -1571,6 +1587,8 @@ void DawnTestBase::SlotMapCallback(WGPUBufferMapAsyncStatus status, void* userda
} else { } else {
slot->mappedData = nullptr; slot->mappedData = nullptr;
} }
test->mNumPendingMapOperations.fetch_sub(1, std::memory_order_release);
} }
void DawnTestBase::ResolveExpectations() { void DawnTestBase::ResolveExpectations() {
@ -1629,6 +1647,8 @@ void DawnTestBase::ResolveDeferredExpectationsNow() {
FlushWire(); FlushWire();
MapSlotsSynchronously(); MapSlotsSynchronously();
dawn::Mutex::AutoLock lg(&mMutex);
ResolveExpectations(); ResolveExpectations();
mDeferredExpectations.clear(); mDeferredExpectations.clear();

View File

@ -15,6 +15,7 @@
#ifndef SRC_DAWN_TESTS_DAWNTEST_H_ #ifndef SRC_DAWN_TESTS_DAWNTEST_H_
#define SRC_DAWN_TESTS_DAWNTEST_H_ #define SRC_DAWN_TESTS_DAWNTEST_H_
#include <atomic>
#include <memory> #include <memory>
#include <queue> #include <queue>
#include <string> #include <string>
@ -23,6 +24,7 @@
#include <vector> #include <vector>
#include "dawn/common/Log.h" #include "dawn/common/Log.h"
#include "dawn/common/Mutex.h"
#include "dawn/common/Platform.h" #include "dawn/common/Platform.h"
#include "dawn/common/Preprocessor.h" #include "dawn/common/Preprocessor.h"
#include "dawn/dawn_proc_table.h" #include "dawn/dawn_proc_table.h"
@ -623,7 +625,7 @@ class DawnTestBase {
// Maps all the buffers and fill ReadbackSlot::mappedData // Maps all the buffers and fill ReadbackSlot::mappedData
void MapSlotsSynchronously(); void MapSlotsSynchronously();
static void SlotMapCallback(WGPUBufferMapAsyncStatus status, void* userdata); static void SlotMapCallback(WGPUBufferMapAsyncStatus status, void* userdata);
size_t mNumPendingMapOperations = 0; std::atomic<size_t> mNumPendingMapOperations = 0;
// Reserve space where the data for an expectation can be copied // Reserve space where the data for an expectation can be copied
struct ReadbackReservation { struct ReadbackReservation {
@ -656,6 +658,8 @@ class DawnTestBase {
WGPUDevice mLastCreatedBackendDevice; WGPUDevice mLastCreatedBackendDevice;
std::unique_ptr<dawn::platform::Platform> mTestPlatform; std::unique_ptr<dawn::platform::Platform> mTestPlatform;
dawn::Mutex mMutex;
}; };
#define DAWN_SKIP_TEST_IF_BASE(condition, type, reason) \ #define DAWN_SKIP_TEST_IF_BASE(condition, type, reason) \

View File

@ -19,6 +19,7 @@
#include <wrl/client.h> #include <wrl/client.h>
#include <memory> #include <memory>
#include <mutex>
#include <thread> #include <thread>
#include <utility> #include <utility>
#include <vector> #include <vector>
@ -395,14 +396,11 @@ class D3D12SharedHandleUsageTests : public D3D12ResourceTestBase {
queue.Submit(1, &commands); queue.Submit(1, &commands);
} }
void WrapAndClearD3D11Texture( void CreateSharedD3D11Texture(const D3D11_TEXTURE2D_DESC& d3dDescriptor,
const wgpu::TextureDescriptor& dawnDescriptor, ID3D11Texture2D** d3d11TextureOut,
const D3D11_TEXTURE2D_DESC& d3dDescriptor, ID3D11Fence** d3d11FenceOut,
const wgpu::Color& clearColor, HANDLE* sharedHandleOut,
wgpu::Texture* dawnTextureOut, HANDLE* fenceSharedHandleOut) const {
ID3D11Texture2D** d3d11TextureOut,
std::unique_ptr<dawn::native::d3d12::ExternalImageDXGI>* externalImageOut,
bool isInitialized = true) const {
ComPtr<ID3D11Texture2D> d3d11Texture; ComPtr<ID3D11Texture2D> d3d11Texture;
HRESULT hr = mD3d11Device->CreateTexture2D(&d3dDescriptor, nullptr, &d3d11Texture); HRESULT hr = mD3d11Device->CreateTexture2D(&d3dDescriptor, nullptr, &d3d11Texture);
ASSERT_EQ(hr, S_OK); ASSERT_EQ(hr, S_OK);
@ -417,20 +415,10 @@ class D3D12SharedHandleUsageTests : public D3D12ResourceTestBase {
&sharedHandle); &sharedHandle);
ASSERT_EQ(hr, S_OK); ASSERT_EQ(hr, S_OK);
ComPtr<IDXGIKeyedMutex> dxgiKeyedMutex;
HANDLE fenceSharedHandle = nullptr; HANDLE fenceSharedHandle = nullptr;
ComPtr<ID3D11Fence> d3d11Fence; ComPtr<ID3D11Fence> d3d11Fence;
ComPtr<ID3D11DeviceContext4> d3d11DeviceContext4; if (GetParam().mSyncMode == SyncMode::kFence) {
if (GetParam().mSyncMode == SyncMode::kKeyedMutex) {
hr = d3d11Texture.As(&dxgiKeyedMutex);
ASSERT_EQ(hr, S_OK);
hr = dxgiKeyedMutex->AcquireSync(kDXGIKeyedMutexAcquireReleaseKey, INFINITE);
ASSERT_EQ(hr, S_OK);
} else {
ComPtr<ID3D11Device5> d3d11Device5; ComPtr<ID3D11Device5> d3d11Device5;
hr = mD3d11Device.As(&d3d11Device5); hr = mD3d11Device.As(&d3d11Device5);
ASSERT_EQ(hr, S_OK); ASSERT_EQ(hr, S_OK);
@ -442,6 +430,33 @@ class D3D12SharedHandleUsageTests : public D3D12ResourceTestBase {
ASSERT_EQ(hr, S_OK); ASSERT_EQ(hr, S_OK);
} }
*d3d11TextureOut = d3d11Texture.Detach();
*d3d11FenceOut = d3d11Fence.Detach();
*sharedHandleOut = sharedHandle;
*fenceSharedHandleOut = fenceSharedHandle;
}
void ClearD3D11Texture(const wgpu::Color& clearColor,
ID3D11Texture2D* d3d11TexturePtr,
ID3D11Fence* d3d11Fence,
uint64_t fenceSignalValue) const {
ComPtr<ID3D11Texture2D> d3d11Texture = d3d11TexturePtr;
ComPtr<IDXGIResource1> dxgiResource;
HRESULT hr = d3d11Texture.As(&dxgiResource);
ASSERT_EQ(hr, S_OK);
ComPtr<IDXGIKeyedMutex> dxgiKeyedMutex;
ComPtr<ID3D11DeviceContext4> d3d11DeviceContext4;
if (GetParam().mSyncMode == SyncMode::kKeyedMutex) {
hr = d3d11Texture.As(&dxgiKeyedMutex);
ASSERT_EQ(hr, S_OK);
hr = dxgiKeyedMutex->AcquireSync(kDXGIKeyedMutexAcquireReleaseKey, INFINITE);
ASSERT_EQ(hr, S_OK);
}
ComPtr<ID3D11RenderTargetView> d3d11RTV; ComPtr<ID3D11RenderTargetView> d3d11RTV;
hr = mD3d11Device->CreateRenderTargetView(d3d11Texture.Get(), nullptr, &d3d11RTV); hr = mD3d11Device->CreateRenderTargetView(d3d11Texture.Get(), nullptr, &d3d11RTV);
ASSERT_EQ(hr, S_OK); ASSERT_EQ(hr, S_OK);
@ -451,7 +466,6 @@ class D3D12SharedHandleUsageTests : public D3D12ResourceTestBase {
static_cast<float>(clearColor.b), static_cast<float>(clearColor.a)}; static_cast<float>(clearColor.b), static_cast<float>(clearColor.a)};
mD3d11DeviceContext->ClearRenderTargetView(d3d11RTV.Get(), colorRGBA); mD3d11DeviceContext->ClearRenderTargetView(d3d11RTV.Get(), colorRGBA);
constexpr uint64_t kFenceSignalValue = 1;
if (dxgiKeyedMutex) { if (dxgiKeyedMutex) {
hr = dxgiKeyedMutex->ReleaseSync(kDXGIKeyedMutexAcquireReleaseKey); hr = dxgiKeyedMutex->ReleaseSync(kDXGIKeyedMutexAcquireReleaseKey);
ASSERT_EQ(hr, S_OK); ASSERT_EQ(hr, S_OK);
@ -460,9 +474,18 @@ class D3D12SharedHandleUsageTests : public D3D12ResourceTestBase {
ASSERT_EQ(hr, S_OK); ASSERT_EQ(hr, S_OK);
// The fence starts with 0 signaled, but that won't capture the render target view clear // The fence starts with 0 signaled, but that won't capture the render target view clear
// above, so signal explicitly with 1 and make the next Dawn access wait on 1. // above, so signal explicitly with 1 and make the next Dawn access wait on 1.
d3d11DeviceContext4->Signal(d3d11Fence.Get(), kFenceSignalValue); d3d11DeviceContext4->Signal(d3d11Fence, fenceSignalValue);
} }
}
void WaitAndWrapD3D11Texture(
const wgpu::TextureDescriptor& dawnDescriptor,
HANDLE sharedHandle,
HANDLE fenceSharedHandle,
uint64_t fenceWaitValue,
wgpu::Texture* dawnTextureOut,
std::unique_ptr<dawn::native::d3d12::ExternalImageDXGI>* externalImageOut,
bool isInitialized) const {
dawn::native::d3d12::ExternalImageDescriptorDXGISharedHandle externalImageDesc = {}; dawn::native::d3d12::ExternalImageDescriptorDXGISharedHandle externalImageDesc = {};
externalImageDesc.sharedHandle = sharedHandle; externalImageDesc.sharedHandle = sharedHandle;
externalImageDesc.cTextureDescriptor = externalImageDesc.cTextureDescriptor =
@ -476,12 +499,36 @@ class D3D12SharedHandleUsageTests : public D3D12ResourceTestBase {
externalAccessDesc.isInitialized = isInitialized; externalAccessDesc.isInitialized = isInitialized;
externalAccessDesc.usage = static_cast<WGPUTextureUsageFlags>(dawnDescriptor.usage); externalAccessDesc.usage = static_cast<WGPUTextureUsageFlags>(dawnDescriptor.usage);
if (fenceSharedHandle != nullptr) { if (fenceSharedHandle != nullptr) {
externalAccessDesc.waitFences.push_back({fenceSharedHandle, kFenceSignalValue}); externalAccessDesc.waitFences.push_back({fenceSharedHandle, fenceWaitValue});
} }
*dawnTextureOut = wgpu::Texture::Acquire(externalImage->BeginAccess(&externalAccessDesc)); *dawnTextureOut = wgpu::Texture::Acquire(externalImage->BeginAccess(&externalAccessDesc));
*d3d11TextureOut = d3d11Texture.Detach();
*externalImageOut = std::move(externalImage); *externalImageOut = std::move(externalImage);
}
void WrapAndClearD3D11Texture(
const wgpu::TextureDescriptor& dawnDescriptor,
const D3D11_TEXTURE2D_DESC& d3dDescriptor,
const wgpu::Color& clearColor,
wgpu::Texture* dawnTextureOut,
ID3D11Texture2D** d3d11TextureOut,
std::unique_ptr<dawn::native::d3d12::ExternalImageDXGI>* externalImageOut,
bool isInitialized = true) const {
ComPtr<ID3D11Texture2D> d3d11Texture;
ComPtr<ID3D11Fence> d3d11Fence;
HANDLE sharedHandle = nullptr;
HANDLE fenceSharedHandle = nullptr;
CreateSharedD3D11Texture(d3dDescriptor, &d3d11Texture, &d3d11Fence, &sharedHandle,
&fenceSharedHandle);
constexpr uint64_t kFenceSignalValue = 1;
ClearD3D11Texture(clearColor, d3d11Texture.Get(), d3d11Fence.Get(), kFenceSignalValue);
WaitAndWrapD3D11Texture(dawnDescriptor, sharedHandle, fenceSharedHandle,
/*fenceWaitValue=*/kFenceSignalValue, dawnTextureOut,
externalImageOut, isInitialized);
*d3d11TextureOut = d3d11Texture.Detach();
if (fenceSharedHandle != nullptr) { if (fenceSharedHandle != nullptr) {
::CloseHandle(fenceSharedHandle); ::CloseHandle(fenceSharedHandle);
@ -1123,6 +1170,69 @@ TEST_P(D3D12SharedHandleMultithreadTests, DestroyDeviceAndUseImageInParallel) {
thread2.join(); thread2.join();
} }
// 1. Create and clear a D3D11 texture
// 2. On 2nd thread: Wrap it in a Dawn texture and clear it to a different color
// 3. Readback the texture with D3D11 and ensure we receive the color we cleared with Dawn.
TEST_P(D3D12SharedHandleMultithreadTests, ClearInD3D12ReadbackInD3D11_TwoThreads) {
// TODO(crbug.com/dawn/735): This test appears to hang for
// D3D12_Microsoft_Basic_Render_Driver_CPU when validation is enabled.
DAWN_SUPPRESS_TEST_IF(IsD3D12() && IsWARP() && IsBackendValidationEnabled());
// KeyedMutex doesn't guarantee the order of commands so skip it.
DAWN_TEST_UNSUPPORTED_IF(GetParam().mSyncMode != SyncMode::kFence);
const wgpu::Color d3d11ClearColor{1.0f, 1.0f, 0.0f, 1.0f};
const wgpu::Color d3d12ClearColor{0.0f, 0.0f, 1.0f, 1.0f};
constexpr uint64_t kD3D11FenceSignalValue = 1;
ComPtr<ID3D11Texture2D> d3d11Texture;
ComPtr<ID3D11Fence> d3d11Fence;
HANDLE sharedHandle = nullptr;
HANDLE fenceSharedHandle = nullptr;
CreateSharedD3D11Texture(baseD3dDescriptor, &d3d11Texture, &d3d11Fence, &sharedHandle,
&fenceSharedHandle);
dawn::native::d3d12::ExternalImageDXGIFenceDescriptor d3d12SignalFence;
std::thread d3d12Thread([=, &d3d12SignalFence] {
wgpu::Texture dawnTexture;
std::unique_ptr<dawn::native::d3d12::ExternalImageDXGI> externalImage;
WaitAndWrapD3D11Texture(baseDawnDescriptor, sharedHandle, fenceSharedHandle,
/*fenceWaitValue=*/kD3D11FenceSignalValue, &dawnTexture,
&externalImage, /*isInitialized=*/true);
ASSERT_NE(dawnTexture.Get(), nullptr);
EXPECT_PIXEL_RGBA8_EQ(utils::RGBA8(d3d11ClearColor.r * 255, d3d11ClearColor.g * 255,
d3d11ClearColor.b * 255, d3d11ClearColor.a * 255),
dawnTexture, 0, 0);
ClearImage(dawnTexture, d3d12ClearColor, device);
externalImage->EndAccess(dawnTexture.Get(), &d3d12SignalFence);
dawnTexture.Destroy();
});
ClearD3D11Texture(d3d11ClearColor, d3d11Texture.Get(), d3d11Fence.Get(),
/*fenceSignalValue=*/kD3D11FenceSignalValue);
d3d12Thread.join();
// Now that Dawn (via D3D12) has finished writing to the texture, we should be
// able to read it back by copying it to a staging texture and verifying the
// color matches the D3D12 clear color.
ExpectPixelRGBA8EQ(d3d11Texture.Get(), d3d12ClearColor, &d3d12SignalFence);
if (sharedHandle != nullptr) {
::CloseHandle(sharedHandle);
}
if (fenceSharedHandle != nullptr) {
::CloseHandle(fenceSharedHandle);
}
}
DAWN_INSTANTIATE_TEST_P(D3D12SharedHandleValidation, DAWN_INSTANTIATE_TEST_P(D3D12SharedHandleValidation,
{D3D12Backend()}, {D3D12Backend()},
{SyncMode::kKeyedMutex, SyncMode::kFence}); {SyncMode::kKeyedMutex, SyncMode::kFence});

View File

@ -211,6 +211,47 @@ TEST_P(DeviceLifetimeTests, DroppedThenMapBuffer) {
} }
} }
// 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<UserData*>(userdataPtr);
// Second mapping.
userdata->buffer.MapAsync(
wgpu::MapMode::Read, 0, wgpu::kWholeMapSize,
[](WGPUBufferMapAsyncStatus status, void* userdataPtr) {
EXPECT_EQ(status, WGPUBufferMapAsyncStatus_DeviceLost);
*static_cast<bool*>(userdataPtr) = true;
},
&userdata->done);
},
&userData);
while (!userData.done) {
WaitABit();
}
}
// Test that the device can be dropped inside a buffer map callback. // Test that the device can be dropped inside a buffer map callback.
TEST_P(DeviceLifetimeTests, DroppedInsideBufferMapCallback) { TEST_P(DeviceLifetimeTests, DroppedInsideBufferMapCallback) {
wgpu::BufferDescriptor desc = {}; wgpu::BufferDescriptor desc = {};

View File

@ -16,6 +16,10 @@
#include <CoreVideo/CVPixelBuffer.h> #include <CoreVideo/CVPixelBuffer.h>
#include <IOSurface/IOSurface.h> #include <IOSurface/IOSurface.h>
#include <memory>
#include <thread>
#include <vector>
#include "dawn/tests/DawnTest.h" #include "dawn/tests/DawnTest.h"
#include "dawn/native/MetalBackend.h" #include "dawn/native/MetalBackend.h"
@ -588,5 +592,66 @@ TEST_P(IOSurfaceUsageTests, WriteThenConcurrentReadThenWrite) {
EXPECT_TRUE(endWriteAccessDesc.isInitialized); EXPECT_TRUE(endWriteAccessDesc.isInitialized);
} }
class IOSurfaceMultithreadTests : public IOSurfaceUsageTests {
protected:
std::vector<wgpu::FeatureName> GetRequiredFeatures() override {
std::vector<wgpu::FeatureName> features;
// TODO(crbug.com/dawn/1678): DawnWire doesn't support thread safe API yet.
if (!UsesWire()) {
features.push_back(wgpu::FeatureName::ImplicitDeviceSynchronization);
}
return features;
}
void SetUp() override {
IOSurfaceUsageTests::SetUp();
// TODO(crbug.com/dawn/1678): DawnWire doesn't support thread safe API yet.
DAWN_TEST_UNSUPPORTED_IF(UsesWire());
}
};
// Test that texture with color is cleared when isInitialized = false. There shoudn't be any data
// race if multiple of them are created on multiple threads.
TEST_P(IOSurfaceMultithreadTests, UninitializedTexturesAreCleared_OnMultipleThreads) {
utils::RunInParallel(10, [this](uint32_t) {
ScopedIOSurfaceRef ioSurface =
CreateSinglePlaneIOSurface(1, 1, kCVPixelFormatType_32RGBA, 4);
uint32_t data = 0x04030201;
IOSurfaceLock(ioSurface.get(), 0, nullptr);
memcpy(IOSurfaceGetBaseAddress(ioSurface.get()), &data, sizeof(data));
IOSurfaceUnlock(ioSurface.get(), 0, nullptr);
wgpu::TextureDescriptor textureDescriptor;
textureDescriptor.dimension = wgpu::TextureDimension::e2D;
textureDescriptor.format = wgpu::TextureFormat::RGBA8Unorm;
textureDescriptor.size = {1, 1, 1};
textureDescriptor.sampleCount = 1;
textureDescriptor.mipLevelCount = 1;
textureDescriptor.usage =
wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc;
// wrap ioSurface and ensure color is not visible when isInitialized set to false
wgpu::Texture ioSurfaceTexture = WrapIOSurface(&textureDescriptor, ioSurface.get(), false);
EXPECT_PIXEL_RGBA8_EQ(utils::RGBA8(0, 0, 0, 0), ioSurfaceTexture, 0, 0);
dawn::native::metal::ExternalImageIOSurfaceEndAccessDescriptor endAccessDesc;
dawn::native::metal::IOSurfaceEndAccess(ioSurfaceTexture.Get(), &endAccessDesc);
EXPECT_TRUE(endAccessDesc.isInitialized);
});
}
// Test that wrapping multiple IOSurface and clear them on multiple threads work.
TEST_P(IOSurfaceMultithreadTests, WrapAndClear_OnMultipleThreads) {
utils::RunInParallel(10, [this](uint32_t) {
ScopedIOSurfaceRef ioSurface =
CreateSinglePlaneIOSurface(1, 1, kCVPixelFormatType_32BGRA, 4);
uint32_t data = 0x04010203;
DoClearTest(ioSurface.get(), wgpu::TextureFormat::BGRA8Unorm, &data, sizeof(data));
});
}
DAWN_INSTANTIATE_TEST(IOSurfaceValidationTests, MetalBackend()); DAWN_INSTANTIATE_TEST(IOSurfaceValidationTests, MetalBackend());
DAWN_INSTANTIATE_TEST(IOSurfaceUsageTests, MetalBackend()); DAWN_INSTANTIATE_TEST(IOSurfaceUsageTests, MetalBackend());
DAWN_INSTANTIATE_TEST(IOSurfaceMultithreadTests, MetalBackend());

View File

@ -12,28 +12,50 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
#include <atomic>
#include <condition_variable>
#include <functional> #include <functional>
#include <limits> #include <limits>
#include <memory> #include <memory>
#include <mutex>
#include <sstream>
#include <string>
#include <thread> #include <thread>
#include <utility>
#include <vector> #include <vector>
#include "dawn/common/Constants.h" #include "dawn/common/Constants.h"
#include "dawn/common/Math.h" #include "dawn/common/Math.h"
#include "dawn/common/Mutex.h"
#include "dawn/tests/DawnTest.h" #include "dawn/tests/DawnTest.h"
#include "dawn/utils/ComboRenderPipelineDescriptor.h" #include "dawn/utils/ComboRenderPipelineDescriptor.h"
#include "dawn/utils/TestUtils.h" #include "dawn/utils/TestUtils.h"
#include "dawn/utils/TextureUtils.h" #include "dawn/utils/TextureUtils.h"
#include "dawn/utils/WGPUHelpers.h" #include "dawn/utils/WGPUHelpers.h"
#define LOCKED_CMD(CMD) \
do { \
dawn::Mutex::AutoLock lk(&mutex); \
CMD; \
} while (0)
namespace { namespace {
template <typename Step>
class LockStep {
public:
LockStep() = delete;
explicit LockStep(Step startStep) : mStep(startStep) {}
void Signal(Step step) {
std::lock_guard<std::mutex> lg(mMutex);
mStep = step;
mCv.notify_all();
}
void Wait(Step step) {
std::unique_lock<std::mutex> lg(mMutex);
mCv.wait(lg, [=] { return mStep == step; });
}
private:
Step mStep;
std::mutex mMutex;
std::condition_variable mCv;
};
class MultithreadTests : public DawnTest { class MultithreadTests : public DawnTest {
protected: protected:
std::vector<wgpu::FeatureName> GetRequiredFeatures() override { std::vector<wgpu::FeatureName> GetRequiredFeatures() override {
@ -75,21 +97,309 @@ class MultithreadTests : public DawnTest {
texDescriptor.sampleCount = sampleCount; texDescriptor.sampleCount = sampleCount;
return device.CreateTexture(&texDescriptor); return device.CreateTexture(&texDescriptor);
} }
};
void RunInParallel(uint32_t numThreads, const std::function<void(uint32_t)>& workerFunc) { // Test that dropping a device's last ref on another thread won't crash Instance::ProcessEvents.
std::vector<std::unique_ptr<std::thread>> threads(numThreads); TEST_P(MultithreadTests, Device_DroppedOnAnotherThread) {
std::vector<wgpu::Device> devices(5);
for (uint32_t i = 0; i < threads.size(); ++i) { // Create devices.
threads[i] = std::make_unique<std::thread>([i, workerFunc] { workerFunc(i); }); for (size_t i = 0; i < devices.size(); ++i) {
} devices[i] = CreateDevice();
for (auto& thread : threads) {
thread->join();
}
} }
dawn::Mutex mutex; std::atomic<uint32_t> numAliveDevices = static_cast<uint32_t>(devices.size());
};
// Create threads
utils::RunInParallel(
numAliveDevices.load(),
[&devices, &numAliveDevices](uint32_t index) {
EXPECT_NE(devices[index].Get(), nullptr);
// Drop device.
devices[index] = nullptr;
numAliveDevices--;
},
[this, &numAliveDevices] {
while (numAliveDevices.load() > 0) {
// main thread process events from all devices
WaitABit();
}
});
}
// Test that dropping a device's last ref inside a callback on another thread won't crash
// Instance::ProcessEvents.
TEST_P(MultithreadTests, Device_DroppedInCallback_OnAnotherThread) {
std::vector<wgpu::Device> devices(10);
// Create devices.
for (auto& device : devices) {
device = CreateDevice();
}
// Create threads
utils::RunInParallel(static_cast<uint32_t>(devices.size()), [&devices, this](uint32_t index) {
auto additionalDevice = std::move(devices[index]);
struct UserData {
wgpu::Device device2ndRef;
std::atomic_bool isCompleted{false};
} userData;
userData.device2ndRef = additionalDevice;
// Drop the last ref inside a callback.
additionalDevice.PushErrorScope(wgpu::ErrorFilter::Validation);
additionalDevice.PopErrorScope(
[](WGPUErrorType type, const char*, void* userdataPtr) {
auto userdata = static_cast<UserData*>(userdataPtr);
userdata->device2ndRef = nullptr;
userdata->isCompleted = true;
},
&userData);
// main ref dropped.
additionalDevice = nullptr;
do {
WaitABit();
} while (!userData.isCompleted.load());
EXPECT_EQ(userData.device2ndRef, nullptr);
});
}
// Test that multiple buffers being created and mapped on multiple threads won't interfere with
// each other.
TEST_P(MultithreadTests, Buffers_MapInParallel) {
constexpr uint32_t kDataSize = 1000;
std::vector<uint32_t> myData;
for (uint32_t i = 0; i < kDataSize; ++i) {
myData.push_back(i);
}
constexpr uint32_t kSize = static_cast<uint32_t>(kDataSize * sizeof(uint32_t));
utils::RunInParallel(10, [=, &myData = std::as_const(myData)](uint32_t) {
wgpu::Buffer buffer;
std::atomic<bool> mapCompleted(false);
// Create buffer and request mapping.
buffer = CreateBuffer(kSize, wgpu::BufferUsage::MapWrite | wgpu::BufferUsage::CopySrc);
buffer.MapAsync(
wgpu::MapMode::Write, 0, kSize,
[](WGPUBufferMapAsyncStatus status, void* userdata) {
EXPECT_EQ(WGPUBufferMapAsyncStatus_Success, status);
(*static_cast<std::atomic<bool>*>(userdata)) = true;
},
&mapCompleted);
// Wait for the mapping to complete
while (!mapCompleted.load()) {
device.Tick();
FlushWire();
}
// Buffer is mapped, write into it and unmap .
memcpy(buffer.GetMappedRange(0, kSize), myData.data(), kSize);
buffer.Unmap();
// Check the content of the buffer.
EXPECT_BUFFER_U32_RANGE_EQ(myData.data(), buffer, 0, kDataSize);
});
}
// Test CreateComputePipelineAsync on multiple threads.
TEST_P(MultithreadTests, CreateComputePipelineAsyncInParallel) {
// TODO(crbug.com/dawn/1766): TSAN reported race conditions in NVIDIA's vk driver.
DAWN_SUPPRESS_TEST_IF(IsVulkan() && IsNvidia() && IsTsan());
std::vector<wgpu::ComputePipeline> pipelines(10);
std::vector<std::string> shaderSources(pipelines.size());
std::vector<uint32_t> expectedValues(shaderSources.size());
for (uint32_t i = 0; i < pipelines.size(); ++i) {
expectedValues[i] = i + 1;
std::ostringstream ss;
ss << R"(
struct SSBO {
value : u32
}
@group(0) @binding(0) var<storage, read_write> ssbo : SSBO;
@compute @workgroup_size(1) fn main() {
ssbo.value =
)";
ss << expectedValues[i];
ss << ";}";
shaderSources[i] = ss.str();
}
// Create pipelines in parallel
utils::RunInParallel(static_cast<uint32_t>(pipelines.size()), [&](uint32_t index) {
wgpu::ComputePipelineDescriptor csDesc;
csDesc.compute.module = utils::CreateShaderModule(device, shaderSources[index].c_str());
csDesc.compute.entryPoint = "main";
struct Task {
wgpu::ComputePipeline computePipeline;
std::atomic<bool> isCompleted{false};
} task;
device.CreateComputePipelineAsync(
&csDesc,
[](WGPUCreatePipelineAsyncStatus status, WGPUComputePipeline returnPipeline,
const char* message, void* userdata) {
EXPECT_EQ(WGPUCreatePipelineAsyncStatus::WGPUCreatePipelineAsyncStatus_Success,
status);
auto task = static_cast<Task*>(userdata);
task->computePipeline = wgpu::ComputePipeline::Acquire(returnPipeline);
task->isCompleted = true;
},
&task);
while (!task.isCompleted.load()) {
WaitABit();
}
pipelines[index] = task.computePipeline;
});
// Verify pipelines' executions
for (uint32_t i = 0; i < pipelines.size(); ++i) {
wgpu::Buffer ssbo =
CreateBuffer(sizeof(uint32_t), wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopySrc);
wgpu::CommandBuffer commands;
{
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::ComputePassEncoder pass = encoder.BeginComputePass();
ASSERT_NE(nullptr, pipelines[i].Get());
wgpu::BindGroup bindGroup =
utils::MakeBindGroup(device, pipelines[i].GetBindGroupLayout(0),
{
{0, ssbo, 0, sizeof(uint32_t)},
});
pass.SetBindGroup(0, bindGroup);
pass.SetPipeline(pipelines[i]);
pass.DispatchWorkgroups(1);
pass.End();
commands = encoder.Finish();
}
queue.Submit(1, &commands);
EXPECT_BUFFER_U32_EQ(expectedValues[i], ssbo, 0);
}
}
// Test CreateRenderPipelineAsync on multiple threads.
TEST_P(MultithreadTests, CreateRenderPipelineAsyncInParallel) {
// TODO(crbug.com/dawn/1766): TSAN reported race conditions in NVIDIA's vk driver.
DAWN_SUPPRESS_TEST_IF(IsVulkan() && IsNvidia() && IsTsan());
constexpr uint32_t kNumThreads = 10;
constexpr wgpu::TextureFormat kRenderAttachmentFormat = wgpu::TextureFormat::RGBA8Unorm;
constexpr uint8_t kColorStep = 250 / kNumThreads;
std::vector<wgpu::RenderPipeline> pipelines(kNumThreads);
std::vector<std::string> fragmentShaderSources(kNumThreads);
std::vector<utils::RGBA8> minExpectedValues(kNumThreads);
std::vector<utils::RGBA8> maxExpectedValues(kNumThreads);
for (uint32_t i = 0; i < kNumThreads; ++i) {
// Due to floating point precision, we need to use min & max values to compare the
// expectations.
auto expectedGreen = kColorStep * i;
minExpectedValues[i] =
utils::RGBA8(0, expectedGreen == 0 ? 0 : (expectedGreen - 2), 0, 255);
maxExpectedValues[i] =
utils::RGBA8(0, expectedGreen == 255 ? 255 : (expectedGreen + 2), 0, 255);
std::ostringstream ss;
ss << R"(
@fragment fn main() -> @location(0) vec4f {
return vec4f(0.0,
)";
ss << expectedGreen / 255.0;
ss << ", 0.0, 1.0);}";
fragmentShaderSources[i] = ss.str();
}
// Create pipelines in parallel
utils::RunInParallel(kNumThreads, [&](uint32_t index) {
utils::ComboRenderPipelineDescriptor renderPipelineDescriptor;
wgpu::ShaderModule vsModule = utils::CreateShaderModule(device, R"(
@vertex fn main() -> @builtin(position) vec4f {
return vec4f(0.0, 0.0, 0.0, 1.0);
})");
wgpu::ShaderModule fsModule =
utils::CreateShaderModule(device, fragmentShaderSources[index].c_str());
renderPipelineDescriptor.vertex.module = vsModule;
renderPipelineDescriptor.cFragment.module = fsModule;
renderPipelineDescriptor.cTargets[0].format = kRenderAttachmentFormat;
renderPipelineDescriptor.primitive.topology = wgpu::PrimitiveTopology::PointList;
struct Task {
wgpu::RenderPipeline renderPipeline;
std::atomic<bool> isCompleted{false};
} task;
device.CreateRenderPipelineAsync(
&renderPipelineDescriptor,
[](WGPUCreatePipelineAsyncStatus status, WGPURenderPipeline returnPipeline,
const char* message, void* userdata) {
EXPECT_EQ(WGPUCreatePipelineAsyncStatus::WGPUCreatePipelineAsyncStatus_Success,
status);
auto* task = static_cast<Task*>(userdata);
task->renderPipeline = wgpu::RenderPipeline::Acquire(returnPipeline);
task->isCompleted = true;
},
&task);
while (!task.isCompleted) {
WaitABit();
}
pipelines[index] = task.renderPipeline;
});
// Verify pipelines' executions
for (uint32_t i = 0; i < pipelines.size(); ++i) {
wgpu::Texture outputTexture =
CreateTexture(1, 1, kRenderAttachmentFormat,
wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc);
utils::ComboRenderPassDescriptor renderPassDescriptor({outputTexture.CreateView()});
renderPassDescriptor.cColorAttachments[0].loadOp = wgpu::LoadOp::Clear;
renderPassDescriptor.cColorAttachments[0].clearValue = {1.f, 0.f, 0.f, 1.f};
wgpu::CommandBuffer commands;
{
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder renderPassEncoder =
encoder.BeginRenderPass(&renderPassDescriptor);
ASSERT_NE(nullptr, pipelines[i].Get());
renderPassEncoder.SetPipeline(pipelines[i]);
renderPassEncoder.Draw(1);
renderPassEncoder.End();
commands = encoder.Finish();
}
queue.Submit(1, &commands);
EXPECT_PIXEL_RGBA8_BETWEEN(minExpectedValues[i], maxExpectedValues[i], outputTexture, 0, 0);
}
}
class MultithreadCachingTests : public MultithreadTests { class MultithreadCachingTests : public MultithreadTests {
protected: protected:
@ -116,7 +426,7 @@ class MultithreadCachingTests : public MultithreadTests {
// Test that creating a same shader module (which will return the cached shader module) and release // Test that creating a same shader module (which will return the cached shader module) and release
// it on multiple threads won't race. // it on multiple threads won't race.
TEST_P(MultithreadCachingTests, RefAndReleaseCachedShaderModulesInParallel) { TEST_P(MultithreadCachingTests, RefAndReleaseCachedShaderModulesInParallel) {
RunInParallel(100, [this](uint32_t) { utils::RunInParallel(100, [this](uint32_t) {
wgpu::ShaderModule csModule = CreateComputeShaderModule(); wgpu::ShaderModule csModule = CreateComputeShaderModule();
EXPECT_NE(nullptr, csModule.Get()); EXPECT_NE(nullptr, csModule.Get());
}); });
@ -134,7 +444,7 @@ TEST_P(MultithreadCachingTests, RefAndReleaseCachedComputePipelinesInParallel) {
csDesc.compute.entryPoint = "main"; csDesc.compute.entryPoint = "main";
csDesc.layout = pipelineLayout; csDesc.layout = pipelineLayout;
RunInParallel(100, [&, this](uint32_t) { utils::RunInParallel(100, [&, this](uint32_t) {
wgpu::ComputePipeline pipeline = device.CreateComputePipeline(&csDesc); wgpu::ComputePipeline pipeline = device.CreateComputePipeline(&csDesc);
EXPECT_NE(nullptr, pipeline.Get()); EXPECT_NE(nullptr, pipeline.Get());
}); });
@ -143,7 +453,7 @@ TEST_P(MultithreadCachingTests, RefAndReleaseCachedComputePipelinesInParallel) {
// Test that creating a same bind group layout (which will return the cached layout) and // Test that creating a same bind group layout (which will return the cached layout) and
// release it on multiple threads won't race. // release it on multiple threads won't race.
TEST_P(MultithreadCachingTests, RefAndReleaseCachedBindGroupLayoutsInParallel) { TEST_P(MultithreadCachingTests, RefAndReleaseCachedBindGroupLayoutsInParallel) {
RunInParallel(100, [&, this](uint32_t) { utils::RunInParallel(100, [&, this](uint32_t) {
wgpu::BindGroupLayout layout = CreateComputeBindGroupLayout(); wgpu::BindGroupLayout layout = CreateComputeBindGroupLayout();
EXPECT_NE(nullptr, layout.Get()); EXPECT_NE(nullptr, layout.Get());
}); });
@ -154,7 +464,7 @@ TEST_P(MultithreadCachingTests, RefAndReleaseCachedBindGroupLayoutsInParallel) {
TEST_P(MultithreadCachingTests, RefAndReleaseCachedPipelineLayoutsInParallel) { TEST_P(MultithreadCachingTests, RefAndReleaseCachedPipelineLayoutsInParallel) {
wgpu::BindGroupLayout bglayout = CreateComputeBindGroupLayout(); wgpu::BindGroupLayout bglayout = CreateComputeBindGroupLayout();
RunInParallel(100, [&, this](uint32_t) { utils::RunInParallel(100, [&, this](uint32_t) {
wgpu::PipelineLayout pipelineLayout = utils::MakePipelineLayout(device, {bglayout}); wgpu::PipelineLayout pipelineLayout = utils::MakePipelineLayout(device, {bglayout});
EXPECT_NE(nullptr, pipelineLayout.Get()); EXPECT_NE(nullptr, pipelineLayout.Get());
}); });
@ -177,7 +487,7 @@ TEST_P(MultithreadCachingTests, RefAndReleaseCachedRenderPipelinesInParallel) {
renderPipelineDescriptor.cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm; renderPipelineDescriptor.cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm;
renderPipelineDescriptor.primitive.topology = wgpu::PrimitiveTopology::PointList; renderPipelineDescriptor.primitive.topology = wgpu::PrimitiveTopology::PointList;
RunInParallel(100, [&, this](uint32_t) { utils::RunInParallel(100, [&, this](uint32_t) {
wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&renderPipelineDescriptor); wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&renderPipelineDescriptor);
EXPECT_NE(nullptr, pipeline.Get()); EXPECT_NE(nullptr, pipeline.Get());
}); });
@ -187,7 +497,7 @@ TEST_P(MultithreadCachingTests, RefAndReleaseCachedRenderPipelinesInParallel) {
// on multiple threads won't race. // on multiple threads won't race.
TEST_P(MultithreadCachingTests, RefAndReleaseCachedSamplersInParallel) { TEST_P(MultithreadCachingTests, RefAndReleaseCachedSamplersInParallel) {
wgpu::SamplerDescriptor desc = {}; wgpu::SamplerDescriptor desc = {};
RunInParallel(100, [&, this](uint32_t) { utils::RunInParallel(100, [&, this](uint32_t) {
wgpu::Sampler sampler = device.CreateSampler(&desc); wgpu::Sampler sampler = device.CreateSampler(&desc);
EXPECT_NE(nullptr, sampler.Get()); EXPECT_NE(nullptr, sampler.Get());
}); });
@ -213,7 +523,7 @@ TEST_P(MultithreadEncodingTests, RenderPassEncodersInParallel) {
std::vector<wgpu::CommandBuffer> commandBuffers(kNumThreads); std::vector<wgpu::CommandBuffer> commandBuffers(kNumThreads);
RunInParallel(kNumThreads, [=, &commandBuffers](uint32_t index) { utils::RunInParallel(kNumThreads, [=, &commandBuffers](uint32_t index) {
wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
// Clear the renderTarget to red. // Clear the renderTarget to red.
@ -263,7 +573,7 @@ TEST_P(MultithreadEncodingTests, ComputePassEncodersInParallel) {
std::vector<wgpu::CommandBuffer> commandBuffers(kNumThreads); std::vector<wgpu::CommandBuffer> commandBuffers(kNumThreads);
RunInParallel(kNumThreads, [=, &commandBuffers](uint32_t index) { utils::RunInParallel(kNumThreads, [=, &commandBuffers](uint32_t index) {
wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::ComputePassEncoder pass = encoder.BeginComputePass(); wgpu::ComputePassEncoder pass = encoder.BeginComputePass();
pass.SetPipeline(pipeline); pass.SetPipeline(pipeline);
@ -284,6 +594,494 @@ TEST_P(MultithreadEncodingTests, ComputePassEncodersInParallel) {
} }
} }
class MultithreadTextureCopyTests : public MultithreadTests {
protected:
void SetUp() override {
MultithreadTests::SetUp();
// TODO(crbug.com/dawn/1291): These tests are failing on GLES (both native and ANGLE)
// when using Tint/GLSL.
DAWN_TEST_UNSUPPORTED_IF(IsOpenGLES());
}
wgpu::Texture CreateAndWriteTexture(uint32_t width,
uint32_t height,
wgpu::TextureFormat format,
wgpu::TextureUsage usage,
const void* data,
size_t dataSize) {
auto texture = CreateTexture(width, height, format, wgpu::TextureUsage::CopyDst | usage);
wgpu::Extent3D textureSize = {width, height, 1};
wgpu::ImageCopyTexture imageCopyTexture =
utils::CreateImageCopyTexture(texture, 0, {0, 0, 0}, wgpu::TextureAspect::All);
wgpu::TextureDataLayout textureDataLayout =
utils::CreateTextureDataLayout(0, dataSize / height);
queue.WriteTexture(&imageCopyTexture, data, dataSize, &textureDataLayout, &textureSize);
return texture;
}
uint32_t BufferSizeForTextureCopy(uint32_t width, uint32_t height, wgpu::TextureFormat format) {
uint32_t bytesPerRow = utils::GetMinimumBytesPerRow(format, width);
return utils::RequiredBytesInCopy(bytesPerRow, height, {width, height, 1}, format);
}
void CopyTextureToTextureHelper(
const wgpu::Texture& srcTexture,
const wgpu::ImageCopyTexture& dst,
const wgpu::Extent3D& dstSize,
const wgpu::CommandEncoder& encoder,
const wgpu::CopyTextureForBrowserOptions* copyForBrowerOptions = nullptr) {
wgpu::ImageCopyTexture srcView =
utils::CreateImageCopyTexture(srcTexture, 0, {0, 0, 0}, wgpu::TextureAspect::All);
if (copyForBrowerOptions == nullptr) {
encoder.CopyTextureToTexture(&srcView, &dst, &dstSize);
wgpu::CommandBuffer commands = encoder.Finish();
queue.Submit(1, &commands);
} else {
// Don't need encoder
ASSERT(encoder == nullptr);
queue.CopyTextureForBrowser(&srcView, &dst, &dstSize, copyForBrowerOptions);
}
}
void CopyBufferToTextureHelper(const wgpu::Buffer& srcBuffer,
uint32_t srcBytesPerRow,
const wgpu::ImageCopyTexture& dst,
const wgpu::Extent3D& dstSize,
const wgpu::CommandEncoder& encoder) {
wgpu::ImageCopyBuffer srcView =
utils::CreateImageCopyBuffer(srcBuffer, 0, srcBytesPerRow, dstSize.height);
encoder.CopyBufferToTexture(&srcView, &dst, &dstSize);
wgpu::CommandBuffer commands = encoder.Finish();
queue.Submit(1, &commands);
}
};
// Test that depth texture's CopyTextureToTexture() can work in parallel with other commands (such
// resources creation and texture to buffer copy for texture expectations).
// This test is needed since most of command encoder's commands are not synchronized, but
// CopyTextureToTexture() command might internally allocate resources and we need to make sure that
// it won't race with other threads' works.
TEST_P(MultithreadTextureCopyTests, CopyDepthToDepthNoRace) {
enum class Step {
Begin,
WriteTexture,
};
constexpr uint32_t kWidth = 4;
constexpr uint32_t kHeight = 4;
const std::vector<float> kExpectedData32 = {
0, 0, 0, 0, //
0, 0, 0.4f, 0, //
1.0f, 1.0f, 0, 0, //
1.0f, 1.0f, 0, 0, //
};
std::vector<uint16_t> kExpectedData16(kExpectedData32.size());
for (size_t i = 0; i < kExpectedData32.size(); ++i) {
kExpectedData16[i] = kExpectedData32[i] * std::numeric_limits<uint16_t>::max();
}
const size_t kExpectedDataSize16 = kExpectedData16.size() * sizeof(kExpectedData16[0]);
LockStep<Step> lockStep(Step::Begin);
wgpu::Texture depthTexture;
std::thread writeThread([&] {
depthTexture = CreateAndWriteTexture(
kWidth, kHeight, wgpu::TextureFormat::Depth16Unorm,
wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::RenderAttachment,
kExpectedData16.data(), kExpectedDataSize16);
lockStep.Signal(Step::WriteTexture);
// Verify the initial data
ExpectAttachmentDepthTestData(depthTexture, wgpu::TextureFormat::Depth16Unorm, kWidth,
kHeight, 0, /*mipLevel=*/0, kExpectedData32);
});
std::thread copyThread([&] {
auto destTexture =
CreateTexture(kWidth * 2, kHeight * 2, wgpu::TextureFormat::Depth16Unorm,
wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopyDst |
wgpu::TextureUsage::CopySrc,
/*mipLevelCount=*/2);
// Copy from depthTexture to destTexture.
const wgpu::Extent3D dstSize = {kWidth, kHeight, 1};
wgpu::ImageCopyTexture dest = utils::CreateImageCopyTexture(
destTexture, /*dstMipLevel=*/1, {0, 0, 0}, wgpu::TextureAspect::All);
auto encoder = device.CreateCommandEncoder();
lockStep.Wait(Step::WriteTexture);
CopyTextureToTextureHelper(depthTexture, dest, dstSize, encoder);
// Verify the copied data
ExpectAttachmentDepthTestData(destTexture, wgpu::TextureFormat::Depth16Unorm, kWidth,
kHeight, 0, /*mipLevel=*/1, kExpectedData32);
});
writeThread.join();
copyThread.join();
}
// Test that depth texture's CopyBufferToTexture() can work in parallel with other commands (such
// resources creation and texture to buffer copy for texture expectations).
// This test is needed since most of command encoder's commands are not synchronized, but
// CopyBufferToTexture() command might internally allocate resources and we need to make sure that
// it won't race with other threads' works.
TEST_P(MultithreadTextureCopyTests, CopyBufferToDepthNoRace) {
enum class Step {
Begin,
WriteBuffer,
};
constexpr uint32_t kWidth = 16;
constexpr uint32_t kHeight = 1;
const std::vector<float> kExpectedData32 = {
0, 0, 0, 0, //
0, 0, 0.4f, 0, //
1.0f, 1.0f, 0, 0, //
1.0f, 1.0f, 0, 0, //
};
std::vector<uint16_t> kExpectedData16(kExpectedData32.size());
for (size_t i = 0; i < kExpectedData32.size(); ++i) {
kExpectedData16[i] = kExpectedData32[i] * std::numeric_limits<uint16_t>::max();
}
const uint32_t kExpectedDataSize16 = kExpectedData16.size() * sizeof(kExpectedData16[0]);
const wgpu::Extent3D kSize = {kWidth, kHeight, 1};
LockStep<Step> lockStep(Step::Begin);
wgpu::Buffer buffer;
std::thread writeThread([&] {
buffer = CreateBuffer(
BufferSizeForTextureCopy(kWidth, kHeight, wgpu::TextureFormat::Depth16Unorm),
wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::CopySrc);
queue.WriteBuffer(buffer, 0, kExpectedData16.data(), kExpectedDataSize16);
device.Tick();
lockStep.Signal(Step::WriteBuffer);
EXPECT_BUFFER_U16_RANGE_EQ(kExpectedData16.data(), buffer, 0, kExpectedData16.size());
});
std::thread copyThread([&] {
auto destTexture =
CreateTexture(kWidth, kHeight, wgpu::TextureFormat::Depth16Unorm,
wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopyDst |
wgpu::TextureUsage::CopySrc);
auto encoder = device.CreateCommandEncoder();
wgpu::ImageCopyTexture dest = utils::CreateImageCopyTexture(
destTexture, /*dstMipLevel=*/0, {0, 0, 0}, wgpu::TextureAspect::All);
// Wait until src buffer is written.
lockStep.Wait(Step::WriteBuffer);
CopyBufferToTextureHelper(buffer, kTextureBytesPerRowAlignment, dest, kSize, encoder);
// Verify the copied data
ExpectAttachmentDepthTestData(destTexture, wgpu::TextureFormat::Depth16Unorm, kWidth,
kHeight, 0, /*mipLevel=*/0, kExpectedData32);
});
writeThread.join();
copyThread.join();
}
// Test that stencil texture's CopyTextureToTexture() can work in parallel with other commands (such
// resources creation and texture to buffer copy for texture expectations).
// This test is needed since most of command encoder's commands are not synchronized, but
// CopyTextureToTexture() command might internally allocate resources and we need to make sure that
// it won't race with other threads' works.
TEST_P(MultithreadTextureCopyTests, CopyStencilToStencilNoRace) {
// TODO(crbug.com/dawn/1497): glReadPixels: GL error: HIGH: Invalid format and type
// combination.
DAWN_SUPPRESS_TEST_IF(IsANGLE());
// TODO(crbug.com/dawn/667): Work around the fact that some platforms are unable to read
// stencil.
DAWN_TEST_UNSUPPORTED_IF(HasToggleEnabled("disable_depth_stencil_read"));
enum class Step {
Begin,
WriteTexture,
};
constexpr uint32_t kWidth = 1;
constexpr uint32_t kHeight = 1;
constexpr uint8_t kExpectedData = 177;
constexpr size_t kExpectedDataSize = sizeof(kExpectedData);
LockStep<Step> lockStep(Step::Begin);
wgpu::Texture stencilTexture;
std::thread writeThread([&] {
stencilTexture = CreateAndWriteTexture(
kWidth, kHeight, wgpu::TextureFormat::Stencil8,
wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::RenderAttachment, &kExpectedData,
kExpectedDataSize);
lockStep.Signal(Step::WriteTexture);
// Verify the initial data
ExpectAttachmentStencilTestData(stencilTexture, wgpu::TextureFormat::Stencil8, kWidth,
kHeight, 0, /*mipLevel=*/0, kExpectedData);
});
std::thread copyThread([&] {
auto destTexture =
CreateTexture(kWidth * 2, kHeight * 2, wgpu::TextureFormat::Stencil8,
wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopyDst |
wgpu::TextureUsage::CopySrc,
/*mipLevelCount=*/2);
// Copy from stencilTexture to destTexture.
const wgpu::Extent3D dstSize = {kWidth, kHeight, 1};
wgpu::ImageCopyTexture dest = utils::CreateImageCopyTexture(
destTexture, /*dstMipLevel=*/1, {0, 0, 0}, wgpu::TextureAspect::All);
auto encoder = device.CreateCommandEncoder();
lockStep.Wait(Step::WriteTexture);
CopyTextureToTextureHelper(stencilTexture, dest, dstSize, encoder);
// Verify the copied data
ExpectAttachmentStencilTestData(destTexture, wgpu::TextureFormat::Stencil8, kWidth, kHeight,
0, /*mipLevel=*/1, kExpectedData);
});
writeThread.join();
copyThread.join();
}
// Test that stencil texture's CopyBufferToTexture() can work in parallel with other commands (such
// resources creation and texture to buffer copy for texture expectations).
// This test is needed since most of command encoder's commands are not synchronized, but
// CopyBufferToTexture() command might internally allocate resources and we need to make sure that
// it won't race with other threads' works.
TEST_P(MultithreadTextureCopyTests, CopyBufferToStencilNoRace) {
enum class Step {
Begin,
WriteBuffer,
};
constexpr uint32_t kWidth = 1;
constexpr uint32_t kHeight = 1;
constexpr uint8_t kExpectedData = 177;
const wgpu::Extent3D kSize = {kWidth, kHeight, 1};
LockStep<Step> lockStep(Step::Begin);
wgpu::Buffer buffer;
std::thread writeThread([&] {
const auto kBufferSize = kTextureBytesPerRowAlignment;
buffer = CreateBuffer(kBufferSize, wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::CopySrc);
std::vector<uint8_t> bufferData(kBufferSize);
bufferData[0] = kExpectedData;
queue.WriteBuffer(buffer.Get(), 0, bufferData.data(), kBufferSize);
device.Tick();
lockStep.Signal(Step::WriteBuffer);
EXPECT_BUFFER_U8_EQ(kExpectedData, buffer, 0);
});
std::thread copyThread([&] {
auto destTexture =
CreateTexture(kWidth, kHeight, wgpu::TextureFormat::Stencil8,
wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopyDst |
wgpu::TextureUsage::CopySrc);
auto encoder = device.CreateCommandEncoder();
wgpu::ImageCopyTexture dest = utils::CreateImageCopyTexture(
destTexture, /*dstMipLevel=*/0, {0, 0, 0}, wgpu::TextureAspect::All);
// Wait until src buffer is written.
lockStep.Wait(Step::WriteBuffer);
CopyBufferToTextureHelper(buffer, kTextureBytesPerRowAlignment, dest, kSize, encoder);
// Verify the copied data
ExpectAttachmentStencilTestData(destTexture, wgpu::TextureFormat::Stencil8, kWidth, kHeight,
0, /*mipLevel=*/0, kExpectedData);
});
writeThread.join();
copyThread.join();
}
// Test that color texture's CopyTextureForBrowser() can work in parallel with other commands (such
// resources creation and texture to buffer copy for texture expectations).
// This test is needed since CopyTextureForBrowser() command might internally allocate resources and
// we need to make sure that it won't race with other threads' works.
TEST_P(MultithreadTextureCopyTests, CopyTextureForBrowserNoRace) {
// TODO(crbug.com/dawn/1232): Program link error on OpenGLES backend
DAWN_SUPPRESS_TEST_IF(IsOpenGLES());
DAWN_SUPPRESS_TEST_IF(IsOpenGL() && IsLinux());
enum class Step {
Begin,
WriteTexture,
};
constexpr uint32_t kWidth = 4;
constexpr uint32_t kHeight = 4;
const std::vector<utils::RGBA8> kExpectedData = {
utils::RGBA8::kBlack, utils::RGBA8::kBlack, utils::RGBA8::kBlack, utils::RGBA8::kBlack, //
utils::RGBA8::kBlack, utils::RGBA8::kBlack, utils::RGBA8::kGreen, utils::RGBA8::kBlack, //
utils::RGBA8::kRed, utils::RGBA8::kRed, utils::RGBA8::kBlack, utils::RGBA8::kBlack, //
utils::RGBA8::kRed, utils::RGBA8::kBlue, utils::RGBA8::kBlack, utils::RGBA8::kBlack, //
};
const std::vector<utils::RGBA8> kExpectedFlippedData = {
utils::RGBA8::kRed, utils::RGBA8::kBlue, utils::RGBA8::kBlack, utils::RGBA8::kBlack, //
utils::RGBA8::kRed, utils::RGBA8::kRed, utils::RGBA8::kBlack, utils::RGBA8::kBlack, //
utils::RGBA8::kBlack, utils::RGBA8::kBlack, utils::RGBA8::kGreen, utils::RGBA8::kBlack, //
utils::RGBA8::kBlack, utils::RGBA8::kBlack, utils::RGBA8::kBlack, utils::RGBA8::kBlack, //
};
const size_t kExpectedDataSize = kExpectedData.size() * sizeof(kExpectedData[0]);
LockStep<Step> lockStep(Step::Begin);
wgpu::Texture srcTexture;
std::thread writeThread([&] {
srcTexture =
CreateAndWriteTexture(kWidth, kHeight, wgpu::TextureFormat::RGBA8Unorm,
wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::TextureBinding,
kExpectedData.data(), kExpectedDataSize);
lockStep.Signal(Step::WriteTexture);
// Verify the initial data
EXPECT_TEXTURE_EQ(kExpectedData.data(), srcTexture, {0, 0}, {kWidth, kHeight});
});
std::thread copyThread([&] {
auto destTexture =
CreateTexture(kWidth, kHeight, wgpu::TextureFormat::RGBA8Unorm,
wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopyDst |
wgpu::TextureUsage::CopySrc);
// Copy from srcTexture to destTexture.
const wgpu::Extent3D dstSize = {kWidth, kHeight, 1};
wgpu::ImageCopyTexture dest = utils::CreateImageCopyTexture(
destTexture, /*dstMipLevel=*/0, {0, 0, 0}, wgpu::TextureAspect::All);
wgpu::CopyTextureForBrowserOptions options;
options.flipY = true;
lockStep.Wait(Step::WriteTexture);
CopyTextureToTextureHelper(srcTexture, dest, dstSize, nullptr, &options);
// Verify the copied data
EXPECT_TEXTURE_EQ(kExpectedFlippedData.data(), destTexture, {0, 0}, {kWidth, kHeight});
});
writeThread.join();
copyThread.join();
}
// Test that error from CopyTextureForBrowser() won't cause deadlock.
TEST_P(MultithreadTextureCopyTests, CopyTextureForBrowserErrorNoDeadLock) {
// TODO(crbug.com/dawn/1232): Program link error on OpenGLES backend
DAWN_SUPPRESS_TEST_IF(IsOpenGLES());
DAWN_SUPPRESS_TEST_IF(IsOpenGL() && IsLinux());
DAWN_TEST_UNSUPPORTED_IF(HasToggleEnabled("skip_validation"));
enum class Step {
Begin,
WriteTexture,
};
constexpr uint32_t kWidth = 4;
constexpr uint32_t kHeight = 4;
const std::vector<utils::RGBA8> kExpectedData = {
utils::RGBA8::kBlack, utils::RGBA8::kBlack, utils::RGBA8::kBlack, utils::RGBA8::kBlack, //
utils::RGBA8::kBlack, utils::RGBA8::kBlack, utils::RGBA8::kGreen, utils::RGBA8::kBlack, //
utils::RGBA8::kRed, utils::RGBA8::kRed, utils::RGBA8::kBlack, utils::RGBA8::kBlack, //
utils::RGBA8::kRed, utils::RGBA8::kBlue, utils::RGBA8::kBlack, utils::RGBA8::kBlack, //
};
const size_t kExpectedDataSize = kExpectedData.size() * sizeof(kExpectedData[0]);
LockStep<Step> lockStep(Step::Begin);
wgpu::Texture srcTexture;
std::thread writeThread([&] {
srcTexture =
CreateAndWriteTexture(kWidth, kHeight, wgpu::TextureFormat::RGBA8Unorm,
wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::TextureBinding,
kExpectedData.data(), kExpectedDataSize);
lockStep.Signal(Step::WriteTexture);
// Verify the initial data
EXPECT_TEXTURE_EQ(kExpectedData.data(), srcTexture, {0, 0}, {kWidth, kHeight});
});
std::thread copyThread([&] {
wgpu::Texture invalidSrcTexture;
invalidSrcTexture = CreateTexture(kWidth, kHeight, wgpu::TextureFormat::RGBA8Unorm,
wgpu::TextureUsage::CopySrc);
auto destTexture =
CreateTexture(kWidth, kHeight, wgpu::TextureFormat::RGBA8Unorm,
wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopyDst |
wgpu::TextureUsage::CopySrc);
// Copy from srcTexture to destTexture.
const wgpu::Extent3D dstSize = {kWidth, kHeight, 1};
wgpu::ImageCopyTexture dest = utils::CreateImageCopyTexture(
destTexture, /*dstMipLevel=*/0, {0, 0, 0}, wgpu::TextureAspect::All);
wgpu::CopyTextureForBrowserOptions options = {};
device.PushErrorScope(wgpu::ErrorFilter::Validation);
// The first copy should be an error because of missing TextureBinding from src texture.
lockStep.Wait(Step::WriteTexture);
CopyTextureToTextureHelper(invalidSrcTexture, dest, dstSize, nullptr, &options);
std::atomic<bool> errorThrown(false);
device.PopErrorScope(
[](WGPUErrorType type, char const* message, void* userdata) {
EXPECT_EQ(type, WGPUErrorType_Validation);
auto error = static_cast<std::atomic<bool>*>(userdata);
*error = true;
},
&errorThrown);
device.Tick();
EXPECT_TRUE(errorThrown.load());
// Second copy is valid.
CopyTextureToTextureHelper(srcTexture, dest, dstSize, nullptr, &options);
// Verify the copied data
EXPECT_TEXTURE_EQ(kExpectedData.data(), destTexture, {0, 0}, {kWidth, kHeight});
});
writeThread.join();
copyThread.join();
}
class MultithreadDrawIndexedIndirectTests : public MultithreadTests { class MultithreadDrawIndexedIndirectTests : public MultithreadTests {
protected: protected:
void SetUp() override { void SetUp() override {
@ -376,8 +1174,8 @@ class MultithreadDrawIndexedIndirectTests : public MultithreadTests {
wgpu::CommandBuffer commands) { wgpu::CommandBuffer commands) {
queue.Submit(1, &commands); queue.Submit(1, &commands);
LOCKED_CMD(EXPECT_PIXEL_RGBA8_EQ(bottomLeftExpected, renderPass.color, 1, 3)); EXPECT_PIXEL_RGBA8_EQ(bottomLeftExpected, renderPass.color, 1, 3);
LOCKED_CMD(EXPECT_PIXEL_RGBA8_EQ(topRightExpected, renderPass.color, 3, 1)); EXPECT_PIXEL_RGBA8_EQ(topRightExpected, renderPass.color, 3, 1);
} }
wgpu::RenderPipeline pipeline; wgpu::RenderPipeline pipeline;
@ -397,7 +1195,7 @@ TEST_P(MultithreadDrawIndexedIndirectTests, IndirectOffsetInParallel) {
utils::RGBA8 filled(0, 255, 0, 255); utils::RGBA8 filled(0, 255, 0, 255);
utils::RGBA8 notFilled(0, 0, 0, 0); utils::RGBA8 notFilled(0, 0, 0, 0);
RunInParallel(10, [=](uint32_t) { utils::RunInParallel(10, [=](uint32_t) {
// Test an offset draw call, with indirect buffer containing 2 calls: // Test an offset draw call, with indirect buffer containing 2 calls:
// 1) first 3 indices of the second quad (top right triangle) // 1) first 3 indices of the second quad (top right triangle)
// 2) last 3 indices of the second quad // 2) last 3 indices of the second quad
@ -473,7 +1271,7 @@ TEST_P(MultithreadTimestampQueryTests, ResolveQuerySets_InParallel) {
destinations[i] = CreateResolveBuffer(kQueryCount * sizeof(uint64_t)); destinations[i] = CreateResolveBuffer(kQueryCount * sizeof(uint64_t));
} }
RunInParallel(kNumThreads, [&](uint32_t index) { utils::RunInParallel(kNumThreads, [&](uint32_t index) {
const auto& querySet = querySets[index]; const auto& querySet = querySets[index];
const auto& destination = destinations[index]; const auto& destination = destinations[index];
wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
@ -483,13 +1281,19 @@ TEST_P(MultithreadTimestampQueryTests, ResolveQuerySets_InParallel) {
wgpu::CommandBuffer commands = encoder.Finish(); wgpu::CommandBuffer commands = encoder.Finish();
queue.Submit(1, &commands); queue.Submit(1, &commands);
LOCKED_CMD(EXPECT_BUFFER(destination, 0, kQueryCount * sizeof(uint64_t), EXPECT_BUFFER(destination, 0, kQueryCount * sizeof(uint64_t), new TimestampExpectation);
new TimestampExpectation));
}); });
} }
} // namespace } // namespace
DAWN_INSTANTIATE_TEST(MultithreadTests,
D3D12Backend(),
MetalBackend(),
OpenGLBackend(),
OpenGLESBackend(),
VulkanBackend());
DAWN_INSTANTIATE_TEST(MultithreadCachingTests, DAWN_INSTANTIATE_TEST(MultithreadCachingTests,
D3D12Backend(), D3D12Backend(),
MetalBackend(), MetalBackend(),
@ -504,6 +1308,17 @@ DAWN_INSTANTIATE_TEST(MultithreadEncodingTests,
OpenGLESBackend(), OpenGLESBackend(),
VulkanBackend()); VulkanBackend());
DAWN_INSTANTIATE_TEST(
MultithreadTextureCopyTests,
D3D12Backend(),
MetalBackend(),
MetalBackend({"use_blit_for_buffer_to_depth_texture_copy",
"use_blit_for_depth_texture_to_texture_copy_to_nonzero_subresource"}),
MetalBackend({"use_blit_for_buffer_to_stencil_texture_copy"}),
OpenGLBackend(),
OpenGLESBackend(),
VulkanBackend());
DAWN_INSTANTIATE_TEST(MultithreadDrawIndexedIndirectTests, DAWN_INSTANTIATE_TEST(MultithreadDrawIndexedIndirectTests,
D3D12Backend(), D3D12Backend(),
MetalBackend(), MetalBackend(),

View File

@ -13,7 +13,9 @@
// limitations under the License. // limitations under the License.
#include <algorithm> #include <algorithm>
#include <memory>
#include <ostream> #include <ostream>
#include <thread>
#include <vector> #include <vector>
#include "dawn/common/Assert.h" #include "dawn/common/Assert.h"
@ -190,4 +192,22 @@ uint32_t VertexFormatSize(wgpu::VertexFormat format) {
UNREACHABLE(); UNREACHABLE();
} }
void RunInParallel(uint32_t numThreads,
const std::function<void(uint32_t)>& workerFunc,
const std::function<void()>& mainThreadFunc) {
std::vector<std::unique_ptr<std::thread>> threads(numThreads);
for (uint32_t i = 0; i < threads.size(); ++i) {
threads[i] = std::make_unique<std::thread>([i, workerFunc] { workerFunc(i); });
}
if (mainThreadFunc != nullptr) {
mainThreadFunc();
}
for (auto& thread : threads) {
thread->join();
}
}
} // namespace utils } // namespace utils

View File

@ -15,6 +15,7 @@
#ifndef SRC_DAWN_UTILS_TESTUTILS_H_ #ifndef SRC_DAWN_UTILS_TESTUTILS_H_
#define SRC_DAWN_UTILS_TESTUTILS_H_ #define SRC_DAWN_UTILS_TESTUTILS_H_
#include <functional>
#include <ostream> #include <ostream>
#include "dawn/webgpu_cpp.h" #include "dawn/webgpu_cpp.h"
@ -84,6 +85,10 @@ void UnalignDynamicUploader(wgpu::Device device);
uint32_t VertexFormatSize(wgpu::VertexFormat format); uint32_t VertexFormatSize(wgpu::VertexFormat format);
void RunInParallel(uint32_t numThreads,
const std::function<void(uint32_t)>& workerFunc,
const std::function<void()>& mainThreadFunc = nullptr);
} // namespace utils } // namespace utils
#endif // SRC_DAWN_UTILS_TESTUTILS_H_ #endif // SRC_DAWN_UTILS_TESTUTILS_H_