D3D12: Support creating render pipeline asynchronously

This patch implements the asynchronous path of CreateRenderPipelineAsync
on D3D12 backend.
1. Call the constructor of dawn_native::d3d12::RenderPipeline in main
   thread.
2. Execute dawn_native::RenderPipelineBase::Initialize() (a virtual function)
   asynchronously.
3. Ensure every operation in dawn_native::d3d12::RenderPipeline::Initialize()
   is thread-safe.
4. Save all the return values (pipeline object or error message, userdata, etc)
   in a CreateRenderPipelineAsyncWaitableCallbackTask object and insert this
   callback task into CallbackTaskManager.
5. In Callback.Finish():
- Insert the pipeline object into the pipeline cache if necessary
- Call WGPUCreateRenderPipelineAsyncCallback

This patch also removes FlatRenderPipelineDescriptor as it is not needed
right now.

BUG=dawn:529
TEST=dawn_end2end_tests

Change-Id: I7fd30339ab7bea599c483dea4bd1100359982409
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/64440
Commit-Queue: Jiawei Shao <jiawei.shao@intel.com>
Reviewed-by: Austin Eng <enga@chromium.org>
This commit is contained in:
Jiawei Shao 2021-09-17 07:39:00 +00:00 committed by Dawn LUCI CQ
parent 110cac1d09
commit 4ecfc58777
14 changed files with 243 additions and 136 deletions

View File

@ -106,7 +106,7 @@ namespace dawn_native {
size_t blueprintHash,
WGPUCreateComputePipelineAsyncCallback callback,
void* userdata)
: mComputePipeline(nonInitializedComputePipeline),
: mComputePipeline(std::move(nonInitializedComputePipeline)),
mBlueprintHash(blueprintHash),
mCallback(callback),
mUserdata(userdata) {
@ -139,4 +139,41 @@ namespace dawn_native {
device->GetAsyncTaskManager()->PostTask(std::move(asyncTask));
}
CreateRenderPipelineAsyncTask::CreateRenderPipelineAsyncTask(
Ref<RenderPipelineBase> nonInitializedRenderPipeline,
size_t blueprintHash,
WGPUCreateRenderPipelineAsyncCallback callback,
void* userdata)
: mRenderPipeline(std::move(nonInitializedRenderPipeline)),
mBlueprintHash(blueprintHash),
mCallback(callback),
mUserdata(userdata) {
ASSERT(mRenderPipeline != nullptr);
}
void CreateRenderPipelineAsyncTask::Run() {
MaybeError maybeError = mRenderPipeline->Initialize();
std::string errorMessage;
if (maybeError.IsError()) {
mRenderPipeline = nullptr;
errorMessage = maybeError.AcquireError()->GetMessage();
}
mRenderPipeline->GetDevice()->AddRenderPipelineAsyncCallbackTask(
mRenderPipeline, errorMessage, mCallback, mUserdata, mBlueprintHash);
}
void CreateRenderPipelineAsyncTask::RunAsync(
std::unique_ptr<CreateRenderPipelineAsyncTask> task) {
DeviceBase* device = task->mRenderPipeline->GetDevice();
// Using "taskPtr = std::move(task)" causes compilation error while it should be supported
// since C++14:
// https://docs.microsoft.com/en-us/cpp/cpp/lambda-expressions-in-cpp?view=msvc-160
auto asyncTask = [taskPtr = task.release()] {
std::unique_ptr<CreateRenderPipelineAsyncTask> innerTaskPtr(taskPtr);
innerTaskPtr->Run();
};
device->GetAsyncTaskManager()->PostTask(std::move(asyncTask));
}
} // namespace dawn_native

View File

@ -52,17 +52,17 @@ namespace dawn_native {
WGPUCreateComputePipelineAsyncCallback mCreateComputePipelineAsyncCallback;
};
struct CreateRenderPipelineAsyncCallbackTask final : CreatePipelineAsyncCallbackTaskBase {
struct CreateRenderPipelineAsyncCallbackTask : CreatePipelineAsyncCallbackTaskBase {
CreateRenderPipelineAsyncCallbackTask(Ref<RenderPipelineBase> pipeline,
std::string errorMessage,
WGPUCreateRenderPipelineAsyncCallback callback,
void* userdata);
void Finish() final;
void Finish() override;
void HandleShutDown() final;
void HandleDeviceLoss() final;
private:
protected:
Ref<RenderPipelineBase> mPipeline;
WGPUCreateRenderPipelineAsyncCallback mCreateRenderPipelineAsyncCallback;
};
@ -76,18 +76,37 @@ namespace dawn_native {
WGPUCreateComputePipelineAsyncCallback callback,
void* userdata);
virtual ~CreateComputePipelineAsyncTask() = default;
void Run();
static void RunAsync(std::unique_ptr<CreateComputePipelineAsyncTask> task);
protected:
private:
Ref<ComputePipelineBase> mComputePipeline;
size_t mBlueprintHash;
WGPUCreateComputePipelineAsyncCallback mCallback;
void* mUserdata;
};
// CreateRenderPipelineAsyncTask defines all the inputs and outputs of
// CreateRenderPipelineAsync() tasks, which are the same among all the backends.
class CreateRenderPipelineAsyncTask {
public:
CreateRenderPipelineAsyncTask(Ref<RenderPipelineBase> nonInitializedRenderPipeline,
size_t blueprintHash,
WGPUCreateRenderPipelineAsyncCallback callback,
void* userdata);
void Run();
static void RunAsync(std::unique_ptr<CreateRenderPipelineAsyncTask> task);
private:
Ref<RenderPipelineBase> mRenderPipeline;
size_t mBlueprintHash;
WGPUCreateRenderPipelineAsyncCallback mCallback;
void* mUserdata;
};
} // namespace dawn_native
#endif // DAWNNATIVE_CREATEPIPELINEASYNCTASK_H_

View File

@ -1338,9 +1338,8 @@ namespace dawn_native {
// Otherwise we will create the pipeline object in CreateRenderPipelineAsyncImpl(),
// where the pipeline object may be created asynchronously and the result will be saved
// to mCreatePipelineAsyncTracker.
FlatRenderPipelineDescriptor appliedFlatDescriptor(&descriptorWithPipelineLayout);
const size_t blueprintHash = pipelineAndBlueprintFromCache.second;
CreateRenderPipelineAsyncImpl(&appliedFlatDescriptor, blueprintHash, callback,
CreateRenderPipelineAsyncImpl(&descriptorWithPipelineLayout, blueprintHash, callback,
userdata);
}
@ -1530,8 +1529,8 @@ namespace dawn_native {
}
void Finish() final {
// TODO(jiawei.shao@intel.com): call AddOrGetCachedComputePipeline() asynchronously
// in CreateComputePipelineAsyncTaskImpl::Run() when the front-end pipeline cache is
// TODO(dawn:529): call AddOrGetCachedComputePipeline() asynchronously in
// CreateComputePipelineAsyncTaskImpl::Run() when the front-end pipeline cache is
// thread-safe.
if (mPipeline.Get() != nullptr) {
mPipeline = mPipeline->GetDevice()->AddOrGetCachedComputePipeline(
@ -1550,6 +1549,50 @@ namespace dawn_native {
std::move(pipeline), errorMessage, callback, userdata, blueprintHash));
}
void DeviceBase::AddRenderPipelineAsyncCallbackTask(
Ref<RenderPipelineBase> pipeline,
std::string errorMessage,
WGPUCreateRenderPipelineAsyncCallback callback,
void* userdata,
size_t blueprintHash) {
// CreateRenderPipelineAsyncWaitableCallbackTask is declared as an internal class as it
// needs to call the private member function DeviceBase::AddOrGetCachedRenderPipeline().
struct CreateRenderPipelineAsyncWaitableCallbackTask final
: CreateRenderPipelineAsyncCallbackTask {
CreateRenderPipelineAsyncWaitableCallbackTask(
Ref<RenderPipelineBase> pipeline,
std::string errorMessage,
WGPUCreateRenderPipelineAsyncCallback callback,
void* userdata,
size_t blueprintHash)
: CreateRenderPipelineAsyncCallbackTask(std::move(pipeline),
errorMessage,
callback,
userdata),
mBlueprintHash(blueprintHash) {
}
void Finish() final {
// TODO(dawn:529): call AddOrGetCachedRenderPipeline() asynchronously in
// CreateRenderPipelineAsyncTaskImpl::Run() when the front-end pipeline cache is
// thread-safe.
if (mPipeline.Get() != nullptr) {
mPipeline = mPipeline->GetDevice()->AddOrGetCachedRenderPipeline(
mPipeline, mBlueprintHash);
}
CreateRenderPipelineAsyncCallbackTask::Finish();
}
private:
size_t mBlueprintHash;
};
mCallbackTaskManager->AddCallbackTask(
std::make_unique<CreateRenderPipelineAsyncWaitableCallbackTask>(
std::move(pipeline), errorMessage, callback, userdata, blueprintHash));
}
PipelineCompatibilityToken DeviceBase::GetNextPipelineCompatibilityToken() {
return PipelineCompatibilityToken(mNextPipelineCompatibilityToken++);
}

View File

@ -299,6 +299,11 @@ namespace dawn_native {
WGPUCreateComputePipelineAsyncCallback callback,
void* userdata,
size_t blueprintHash);
void AddRenderPipelineAsyncCallbackTask(Ref<RenderPipelineBase> pipeline,
std::string errorMessage,
WGPUCreateRenderPipelineAsyncCallback callback,
void* userdata,
size_t blueprintHash);
PipelineCompatibilityToken GetNextPipelineCompatibilityToken();

View File

@ -364,72 +364,6 @@ namespace dawn_native {
}
} // anonymous namespace
// FlatRenderPipelineDescriptor
FlatRenderPipelineDescriptor::FlatRenderPipelineDescriptor(
const RenderPipelineDescriptor* descriptor)
: mLabel(descriptor->label != nullptr ? descriptor->label : ""),
mLayout(descriptor->layout),
mVertexModule(descriptor->vertex.module),
mVertexEntryPoint(descriptor->vertex.entryPoint) {
label = mLabel.c_str();
ASSERT(descriptor->layout != nullptr);
layout = mLayout.Get();
vertex.module = mVertexModule.Get();
vertex.entryPoint = mVertexEntryPoint.c_str();
vertex.bufferCount = descriptor->vertex.bufferCount;
vertex.buffers = mVertexBuffers.data();
uint32_t vertexAttributeCount = 0;
for (uint32_t vertexBufferIndex = 0; vertexBufferIndex < vertex.bufferCount;
++vertexBufferIndex) {
const VertexBufferLayout& vertexLayout = descriptor->vertex.buffers[vertexBufferIndex];
mVertexBuffers[vertexBufferIndex] = vertexLayout;
mVertexBuffers[vertexBufferIndex].attributes = &mVertexAttributes[vertexAttributeCount];
for (uint32_t attributeIndex = 0; attributeIndex < vertexLayout.attributeCount;
++attributeIndex) {
mVertexAttributes[vertexAttributeCount + attributeIndex] =
vertexLayout.attributes[attributeIndex];
}
vertexAttributeCount += vertexLayout.attributeCount;
}
primitive = descriptor->primitive;
if (descriptor->depthStencil != nullptr) {
mDepthStencilState = *(descriptor->depthStencil);
depthStencil = &mDepthStencilState;
}
multisample = descriptor->multisample;
if (descriptor->fragment != nullptr) {
mFragmentModule = descriptor->fragment->module;
mFragmentState.module = mFragmentModule.Get();
mFragmentEntryPoint = descriptor->fragment->entryPoint;
mFragmentState.entryPoint = mFragmentEntryPoint.c_str();
mFragmentState.targetCount = descriptor->fragment->targetCount;
mFragmentState.targets = mColorTargetStates.data();
for (uint32_t colorTargetIndex = 0; colorTargetIndex < mFragmentState.targetCount;
++colorTargetIndex) {
mColorTargetStates[colorTargetIndex] =
descriptor->fragment->targets[colorTargetIndex];
if (descriptor->fragment->targets[colorTargetIndex].blend != nullptr) {
mColorTargetStates[colorTargetIndex].blend = &mBlendStates[colorTargetIndex];
mBlendStates[colorTargetIndex] =
*(descriptor->fragment->targets[colorTargetIndex].blend);
}
}
fragment = &mFragmentState;
}
}
// Helper functions
size_t IndexFormatSize(wgpu::IndexFormat format) {
switch (format) {
@ -954,4 +888,8 @@ namespace dawn_native {
return true;
}
MaybeError RenderPipelineBase::Initialize() {
return {};
}
} // namespace dawn_native

View File

@ -29,31 +29,6 @@ namespace dawn_native {
class DeviceBase;
// TODO(dawn:529): Use FlatRenderPipelineDescriptor to keep all the members of
// RenderPipelineDescriptor (especially the members in pointers) valid when the creation of the
// render pipeline is executed asynchronously.
struct FlatRenderPipelineDescriptor : public RenderPipelineDescriptor, public NonMovable {
public:
explicit FlatRenderPipelineDescriptor(const RenderPipelineDescriptor* descriptor);
private:
std::string mLabel;
Ref<PipelineLayoutBase> mLayout;
Ref<ShaderModuleBase> mVertexModule;
std::string mVertexEntryPoint;
std::array<VertexBufferLayout, kMaxVertexBuffers> mVertexBuffers;
std::array<VertexAttribute, kMaxVertexAttributes> mVertexAttributes;
FragmentState mFragmentState;
Ref<ShaderModuleBase> mFragmentModule;
std::string mFragmentEntryPoint;
std::array<ColorTargetState, kMaxColorAttachments> mColorTargetStates;
std::array<BlendState, kMaxColorAttachments> mBlendStates;
DepthStencilState mDepthStencilState;
};
MaybeError ValidateRenderPipelineDescriptor(DeviceBase* device,
const RenderPipelineDescriptor* descriptor);
@ -130,6 +105,11 @@ namespace dawn_native {
private:
RenderPipelineBase(DeviceBase* device, ObjectBase::ErrorTag tag);
// CreateRenderPipelineAsyncTask is declared as a friend of RenderPipelineBase as it
// needs to call the private member function RenderPipelineBase::Initialize().
friend class CreateRenderPipelineAsyncTask;
virtual MaybeError Initialize();
// TODO(dawn:529): store all the following members in a FlatRenderPipelineDescriptor object
// Vertex state
uint32_t mVertexBufferCount;

View File

@ -382,6 +382,12 @@ namespace dawn_native { namespace d3d12 {
void* userdata) {
ComputePipeline::CreateAsync(this, descriptor, blueprintHash, callback, userdata);
}
void Device::CreateRenderPipelineAsyncImpl(const RenderPipelineDescriptor* descriptor,
size_t blueprintHash,
WGPUCreateRenderPipelineAsyncCallback callback,
void* userdata) {
RenderPipeline::CreateAsync(this, descriptor, blueprintHash, callback, userdata);
}
ResultOrError<std::unique_ptr<StagingBufferBase>> Device::CreateStagingBuffer(size_t size) {
std::unique_ptr<StagingBufferBase> stagingBuffer =

View File

@ -177,6 +177,10 @@ namespace dawn_native { namespace d3d12 {
size_t blueprintHash,
WGPUCreateComputePipelineAsyncCallback callback,
void* userdata) override;
void CreateRenderPipelineAsyncImpl(const RenderPipelineDescriptor* descriptor,
size_t blueprintHash,
WGPUCreateRenderPipelineAsyncCallback callback,
void* userdata) override;
void ShutDownImpl() override;
MaybeError WaitForIdleForDestruction() override;

View File

@ -16,6 +16,8 @@
#include "common/Assert.h"
#include "common/Log.h"
#include "dawn_native/AsyncTask.h"
#include "dawn_native/CreatePipelineAsyncTask.h"
#include "dawn_native/d3d12/D3D12Error.h"
#include "dawn_native/d3d12/DeviceD3D12.h"
#include "dawn_native/d3d12/PipelineLayoutD3D12.h"
@ -471,4 +473,16 @@ namespace dawn_native { namespace d3d12 {
return inputLayoutDescriptor;
}
void RenderPipeline::CreateAsync(Device* device,
const RenderPipelineDescriptor* descriptor,
size_t blueprintHash,
WGPUCreateRenderPipelineAsyncCallback callback,
void* userdata) {
Ref<RenderPipeline> pipeline = AcquireRef(new RenderPipeline(device, descriptor));
std::unique_ptr<CreateRenderPipelineAsyncTask> asyncTask =
std::make_unique<CreateRenderPipelineAsyncTask>(pipeline, blueprintHash, callback,
userdata);
CreateRenderPipelineAsyncTask::RunAsync(std::move(asyncTask));
}
}} // namespace dawn_native::d3d12

View File

@ -29,6 +29,11 @@ namespace dawn_native { namespace d3d12 {
static ResultOrError<Ref<RenderPipeline>> Create(
Device* device,
const RenderPipelineDescriptor* descriptor);
static void CreateAsync(Device* device,
const RenderPipelineDescriptor* descriptor,
size_t blueprintHash,
WGPUCreateRenderPipelineAsyncCallback callback,
void* userdata);
RenderPipeline() = delete;
D3D12_PRIMITIVE_TOPOLOGY GetD3D12PrimitiveTopology() const;
@ -42,7 +47,7 @@ namespace dawn_native { namespace d3d12 {
private:
~RenderPipeline() override;
using RenderPipelineBase::RenderPipelineBase;
MaybeError Initialize();
MaybeError Initialize() override;
D3D12_INPUT_LAYOUT_DESC ComputeInputLayout(
std::array<D3D12_INPUT_ELEMENT_DESC, kMaxVertexAttributes>* inputElementDescriptors);

View File

@ -47,7 +47,7 @@ namespace dawn_native { namespace metal {
private:
using RenderPipelineBase::RenderPipelineBase;
MaybeError Initialize();
MaybeError Initialize() override;
MTLVertexDescriptor* MakeVertexDesc();

View File

@ -42,7 +42,7 @@ namespace dawn_native { namespace opengl {
private:
RenderPipeline(Device* device, const RenderPipelineDescriptor* descriptor);
~RenderPipeline() override;
MaybeError Initialize();
MaybeError Initialize() override;
void CreateVAOForVertexState();

View File

@ -38,7 +38,7 @@ namespace dawn_native { namespace vulkan {
private:
~RenderPipeline() override;
using RenderPipelineBase::RenderPipelineBase;
MaybeError Initialize();
MaybeError Initialize() override;
struct PipelineVertexInputStateCreateInfoTemporaryAllocations {
std::array<VkVertexInputBindingDescription, kMaxVertexBuffers> bindings;

View File

@ -68,6 +68,47 @@ class CreatePipelineAsyncTest : public DawnTest {
ValidateCreateComputePipelineAsync(&task);
}
void ValidateCreateRenderPipelineAsync(CreatePipelineAsyncTask* currentTask) {
constexpr wgpu::TextureFormat kRenderAttachmentFormat = wgpu::TextureFormat::RGBA8Unorm;
wgpu::TextureDescriptor textureDescriptor;
textureDescriptor.size = {1, 1, 1};
textureDescriptor.format = kRenderAttachmentFormat;
textureDescriptor.usage =
wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc;
wgpu::Texture outputTexture = device.CreateTexture(&textureDescriptor);
utils::ComboRenderPassDescriptor renderPassDescriptor({outputTexture.CreateView()});
renderPassDescriptor.cColorAttachments[0].loadOp = wgpu::LoadOp::Clear;
renderPassDescriptor.cColorAttachments[0].clearColor = {1.f, 0.f, 0.f, 1.f};
wgpu::CommandBuffer commands;
{
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder renderPassEncoder =
encoder.BeginRenderPass(&renderPassDescriptor);
while (!currentTask->isCompleted) {
WaitABit();
}
ASSERT_TRUE(currentTask->message.empty());
ASSERT_NE(nullptr, currentTask->renderPipeline.Get());
renderPassEncoder.SetPipeline(currentTask->renderPipeline);
renderPassEncoder.Draw(1);
renderPassEncoder.EndPass();
commands = encoder.Finish();
}
queue.Submit(1, &commands);
EXPECT_PIXEL_RGBA8_EQ(RGBA8(0, 255, 0, 255), outputTexture, 0, 0);
}
void ValidateCreateRenderPipelineAsync() {
ValidateCreateRenderPipelineAsync(&task);
}
void DoCreateRenderPipelineAsync(
const utils::ComboRenderPipelineDescriptor& renderPipelineDescriptor) {
device.CreateRenderPipelineAsync(
@ -212,36 +253,7 @@ TEST_P(CreatePipelineAsyncTest, BasicUseOfCreateRenderPipelineAsync) {
DoCreateRenderPipelineAsync(renderPipelineDescriptor);
wgpu::TextureDescriptor textureDescriptor;
textureDescriptor.size = {1, 1, 1};
textureDescriptor.format = kRenderAttachmentFormat;
textureDescriptor.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc;
wgpu::Texture outputTexture = device.CreateTexture(&textureDescriptor);
utils::ComboRenderPassDescriptor renderPassDescriptor({outputTexture.CreateView()});
renderPassDescriptor.cColorAttachments[0].loadOp = wgpu::LoadOp::Clear;
renderPassDescriptor.cColorAttachments[0].clearColor = {1.f, 0.f, 0.f, 1.f};
wgpu::CommandBuffer commands;
{
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder renderPassEncoder = encoder.BeginRenderPass(&renderPassDescriptor);
while (!task.isCompleted) {
WaitABit();
}
ASSERT_TRUE(task.message.empty());
ASSERT_NE(nullptr, task.renderPipeline.Get());
renderPassEncoder.SetPipeline(task.renderPipeline);
renderPassEncoder.Draw(1);
renderPassEncoder.EndPass();
commands = encoder.Finish();
}
queue.Submit(1, &commands);
EXPECT_PIXEL_RGBA8_EQ(RGBA8(0, 255, 0, 255), outputTexture, 0, 0);
ValidateCreateRenderPipelineAsync();
}
// Verify the render pipeline created with CreateRenderPipelineAsync() still works when the entry
@ -448,7 +460,7 @@ TEST_P(CreatePipelineAsyncTest, CreateSameComputePipelineTwice) {
// Verify creating compute pipeline with same descriptor and CreateComputePipelineAsync() at the
// same time works correctly.
TEST_P(CreatePipelineAsyncTest, CreateSamePipelineTwiceAtSameTime) {
TEST_P(CreatePipelineAsyncTest, CreateSameComputePipelineTwiceAtSameTime) {
wgpu::BindGroupLayoutEntry binding = {};
binding.binding = 0;
binding.buffer.type = wgpu::BufferBindingType::Storage;
@ -505,6 +517,50 @@ TEST_P(CreatePipelineAsyncTest, CreateSamePipelineTwiceAtSameTime) {
}
}
// Verify the basic use of CreateRenderPipelineAsync() works on all backends.
TEST_P(CreatePipelineAsyncTest, CreateSameRenderPipelineTwiceAtSameTime) {
constexpr wgpu::TextureFormat kRenderAttachmentFormat = wgpu::TextureFormat::RGBA8Unorm;
utils::ComboRenderPipelineDescriptor renderPipelineDescriptor;
wgpu::ShaderModule vsModule = utils::CreateShaderModule(device, R"(
[[stage(vertex)]] fn main() -> [[builtin(position)]] vec4<f32> {
return vec4<f32>(0.0, 0.0, 0.0, 1.0);
})");
wgpu::ShaderModule fsModule = utils::CreateShaderModule(device, R"(
[[stage(fragment)]] fn main() -> [[location(0)]] vec4<f32> {
return vec4<f32>(0.0, 1.0, 0.0, 1.0);
})");
renderPipelineDescriptor.vertex.module = vsModule;
renderPipelineDescriptor.cFragment.module = fsModule;
renderPipelineDescriptor.cTargets[0].format = kRenderAttachmentFormat;
renderPipelineDescriptor.primitive.topology = wgpu::PrimitiveTopology::PointList;
auto callback = [](WGPUCreatePipelineAsyncStatus status, WGPURenderPipeline returnPipeline,
const char* message, void* userdata) {
EXPECT_EQ(WGPUCreatePipelineAsyncStatus::WGPUCreatePipelineAsyncStatus_Success, status);
CreatePipelineAsyncTask* task = static_cast<CreatePipelineAsyncTask*>(userdata);
task->renderPipeline = wgpu::RenderPipeline::Acquire(returnPipeline);
task->isCompleted = true;
task->message = message;
};
// Create two render pipelines with same descriptor.
CreatePipelineAsyncTask anotherTask;
device.CreateRenderPipelineAsync(&renderPipelineDescriptor, callback, &task);
device.CreateRenderPipelineAsync(&renderPipelineDescriptor, callback, &anotherTask);
// Verify task.renderPipeline and anotherTask.renderPipeline are both created correctly.
ValidateCreateRenderPipelineAsync(&task);
ValidateCreateRenderPipelineAsync(&anotherTask);
// Verify task.renderPipeline and anotherTask.renderPipeline are pointing to the same Dawn
// object.
if (!UsesWire()) {
EXPECT_EQ(task.renderPipeline.Get(), anotherTask.renderPipeline.Get());
}
}
// Verify calling CreateRenderPipelineAsync() with valid VertexBufferLayouts works on all backends.
TEST_P(CreatePipelineAsyncTest, CreateRenderPipelineAsyncWithVertexBufferLayouts) {
wgpu::TextureDescriptor textureDescriptor;