| // Copyright 2026 The Dawn & Tint Authors |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are met: |
| // |
| // 1. Redistributions of source code must retain the above copyright notice, this |
| // list of conditions and the following disclaimer. |
| // |
| // 2. Redistributions in binary form must reproduce the above copyright notice, |
| // this list of conditions and the following disclaimer in the documentation |
| // and/or other materials provided with the distribution. |
| // |
| // 3. Neither the name of the copyright holder nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| #include <span> |
| |
| #include "dawn/wire/Wire.h" |
| #include "dawn/wire/WireClient.h" |
| #include "dawn/wire/WireCmd_autogen.h" |
| #include "dawn/wire/WireServer.h" |
| #include "src/dawn/common/StringViewUtils.h" |
| #include "src/dawn/tests/MockCallback.h" |
| #include "src/dawn/tests/StringViewMatchers.h" |
| #include "src/dawn/tests/unittests/wire/WireTest.h" |
| #include "src/dawn/utils/TerribleCommandBuffer.h" |
| #include "src/dawn/wire/BufferConsumer.h" |
| #include "src/dawn/wire/ChunkedCommandSerializer.h" |
| #include "src/dawn/wire/client/Client.h" |
| #include "src/dawn/wire/server/Server.h" |
| |
| namespace dawn::wire { |
| namespace { |
| |
| using testing::_; |
| using testing::EmptySizedString; |
| using testing::InvokeWithoutArgs; |
| using testing::IsNull; |
| using testing::MockCppCallback; |
| using testing::NonEmptySizedString; |
| using testing::NotNull; |
| using testing::Return; |
| using testing::WithArg; |
| |
| // Fixture that helps execute specific commands through the wire that may not be possible to trigger |
| // through usage of the dawn::wire::client. It is even more change detecting than regular dawn::wire |
| // tests so we should use it only when there are no alternatives. |
| class WireSpecificCommandTests : public WireTest { |
| protected: |
| template <typename Cmd> |
| void AddSpecificServerCmd(const Cmd& cmd) { |
| CommandSerializer* c2s = GetC2SSerializer(); |
| ChunkedCommandSerializer serializer(c2s); |
| |
| serializer.SerializeCommand(cmd, *GetWireClient()->GetImplForTesting()); |
| } |
| |
| // Intercept a command that will be sent from the client to the server. This involves first |
| // capturing the command, deserializing it as if we were the server, and then re-injecting it |
| // back into the client command buffer. Note that this does not currently handle object |
| // serialization correctly, so when modifying commands with descriptors that include other |
| // WebGPU objects, users may need to manually update those members. This is currently a |
| // limitation that is hard to address because of the way the commands are [de]serialized in the |
| // wire. |
| template <typename Cmd, typename ToIntercept, typename Modifier> |
| void InterceptServerCmd(ToIntercept toIntercept, Modifier modifier) { |
| Cmd cmd; |
| utils::TerribleCommandBuffer* c2sBuf = GetC2SCommandBuffer(); |
| size_t startOffset = c2sBuf->GetOffsetForTesting(); |
| toIntercept(); |
| size_t endOffset = c2sBuf->GetOffsetForTesting(); |
| auto subrange = c2sBuf->GetContentSubrange(startOffset, endOffset); |
| dawn::wire::DeserializeBuffer deserializeBuffer(subrange); |
| EXPECT_EQ(WireResult::Success, cmd.Deserialize(&deserializeBuffer, &mAllocator, |
| *GetWireServer()->GetImplForTesting())); |
| |
| modifier(&cmd); |
| c2sBuf->SetOffsetForTesting(startOffset); |
| AddSpecificServerCmd(cmd); |
| } |
| |
| private: |
| WireDeserializeAllocator mAllocator; |
| }; |
| |
| // Regression test for https://issues.chromium.org/492139412 where a server receiving |
| // Device::Destroy wouldn't realize that the buffers got unmapped and would try to write into them. |
| // While it's not exactly possible to replicate the issue with WireTests since there is no |
| // dawn::native backend that will unmap buffers on destroy, we can check that the ordering of |
| // commands in the server is such that it will check that the buffer is mapped before writing into |
| // it. |
| TEST_F(WireSpecificCommandTests, UpdateMappedDataAfterDeviceDestroy_MappedAtCreation) { |
| // Create a mapped buffer. |
| wgpu::BufferDescriptor descriptor = {}; |
| descriptor.size = 4; |
| descriptor.usage = wgpu::BufferUsage::CopySrc; |
| descriptor.mappedAtCreation = true; |
| wgpu::Buffer buffer = device.CreateBuffer(&descriptor); |
| |
| WGPUBuffer apiBuffer = api.GetNewBuffer(); |
| EXPECT_CALL(api, DeviceCreateBuffer(apiDevice, _)) |
| .WillOnce(Return(apiBuffer)) |
| .RetiresOnSaturation(); |
| FlushClient(); |
| |
| // Force a device destroy without giving the wire::client a chance to unmap client-side buffers. |
| DeviceDestroyCmd cmd; |
| cmd.self = device.Get(); |
| AddSpecificServerCmd(cmd); |
| |
| EXPECT_CALL(api, DeviceDestroy(apiDevice)).Times(1); |
| FlushClient(); |
| |
| // A call to unmap will get a nullptr mapped range and should not write to it! (if it were, we'd |
| // see a crash here since it would write to nullptr). |
| EXPECT_CALL(api, BufferGetMappedRange(apiBuffer, 0, 4)).WillOnce(Return(nullptr)); |
| EXPECT_CALL(api, BufferUnmap(apiBuffer)).Times(1); |
| buffer.Unmap(); |
| FlushClient(); |
| } |
| |
| // The same test at an offset, to check that it doesn't allow bypassing the null check. It was a |
| // bug found during review of the fix. |
| TEST_F(WireSpecificCommandTests, UpdateMappedDataAfterDeviceDestroy_MapWriteOffsetNonZero) { |
| // Create a mapped buffer. |
| wgpu::BufferDescriptor descriptor = {}; |
| descriptor.size = 8; |
| descriptor.usage = wgpu::BufferUsage::MapWrite; |
| wgpu::Buffer buffer = device.CreateBuffer(&descriptor); |
| |
| WGPUBuffer apiBuffer = api.GetNewBuffer(); |
| EXPECT_CALL(api, DeviceCreateBuffer(apiDevice, _)) |
| .WillOnce(Return(apiBuffer)) |
| .RetiresOnSaturation(); |
| FlushClient(); |
| |
| // Map the buffer |
| buffer.MapAsync(wgpu::MapMode::Write, 4, 4, wgpu::CallbackMode::AllowProcessEvents, |
| [](wgpu::MapAsyncStatus status, wgpu::StringView) {}); |
| EXPECT_CALL(api, OnBufferMapAsync(apiBuffer, WGPUMapMode_Write, 4, 4, _)) |
| .WillOnce(InvokeWithoutArgs([&] { |
| api.CallBufferMapAsyncCallback(apiBuffer, WGPUMapAsyncStatus_Success, |
| kEmptyOutputStringView); |
| })); |
| |
| FlushClient(); |
| FlushServer(); |
| instance.ProcessEvents(); |
| |
| // Force a device destroy without giving the wire::client a chance to unmap client-side buffers. |
| DeviceDestroyCmd cmd; |
| cmd.self = device.Get(); |
| AddSpecificServerCmd(cmd); |
| |
| EXPECT_CALL(api, DeviceDestroy(apiDevice)).Times(1); |
| FlushClient(); |
| |
| // A call to unmap will get a nullptr mapped range and should not write to it! (if it were, we'd |
| // see a crash here since it would write to nullptr). |
| EXPECT_CALL(api, BufferGetMappedRange(apiBuffer, 4, 4)).WillOnce(Return(nullptr)); |
| EXPECT_CALL(api, BufferUnmap(apiBuffer)).Times(1); |
| buffer.Unmap(); |
| FlushClient(); |
| } |
| |
| // This is a regression test for https://issues.chromium.org/508092644 where a compromised client |
| // could potentially ask the server to Unregister a reserved, but not backed Device. |
| TEST_F(WireSpecificCommandTests, RequestDeviceIdReuseAfterInjectedUnregister) { |
| // Set up all the mock callbacks and objects. |
| MockCppCallback<wgpu::DeviceLostCallback<void>*> deviceLostCb; |
| MockCppCallback<wgpu::RequestDeviceCallback<void>*> requestDeviceCb; |
| WGPUDevice apiDeviceA = api.GetNewDevice(); |
| WGPUFuture futureA; |
| WGPUDevice apiDeviceB = api.GetNewDevice(); |
| WGPUFuture futureB; |
| auto SetDeviceCallbacks = [](WGPUDevice apiDevice, const WGPUDeviceDescriptor* desc) { |
| ProcTableAsClass::Object* object = reinterpret_cast<ProcTableAsClass::Object*>(apiDevice); |
| object->mDeviceLostCallback = desc->deviceLostCallbackInfo.callback; |
| object->mDeviceLostUserdata1 = desc->deviceLostCallbackInfo.userdata1; |
| object->mDeviceLostUserdata2 = desc->deviceLostCallbackInfo.userdata2; |
| }; |
| |
| // Add the first request for a device and capture the C->S command for it. |
| wgpu::DeviceDescriptor deviceDescA = {}; |
| deviceDescA.SetDeviceLostCallback(wgpu::CallbackMode::AllowSpontaneous, |
| deviceLostCb.Callback()); |
| AdapterRequestDeviceCmd requestA = {}; |
| InterceptServerCmd<AdapterRequestDeviceCmd>( |
| [&]() { |
| adapter.RequestDevice(&deviceDescA, wgpu::CallbackMode::AllowSpontaneous, |
| requestDeviceCb.Callback()); |
| }, |
| [&](AdapterRequestDeviceCmd* cmd) { requestA = *cmd; }); |
| |
| // Immediately unregister the device that was reserved for the first request. This is not |
| // normally possible, so we inject the command directly. |
| UnregisterObjectCmd unregisterA = {}; |
| unregisterA.objectType = ObjectType::Device; |
| unregisterA.objectId = requestA.deviceObjectHandle.id; |
| AddSpecificServerCmd(unregisterA); |
| |
| // Add a second request for a device that attempts to reuse the same id that was originally |
| // reserved for the first request. |
| wgpu::DeviceDescriptor deviceDescB = {}; |
| deviceDescB.SetDeviceLostCallback(wgpu::CallbackMode::AllowSpontaneous, |
| deviceLostCb.Callback()); |
| AdapterRequestDeviceCmd requestB = {}; |
| InterceptServerCmd<AdapterRequestDeviceCmd>( |
| [&]() { |
| adapter.RequestDevice(&deviceDescB, wgpu::CallbackMode::AllowSpontaneous, |
| requestDeviceCb.Callback()); |
| }, |
| [&](AdapterRequestDeviceCmd* cmd) { |
| cmd->deviceObjectHandle.id = requestA.deviceObjectHandle.id; |
| cmd->deviceObjectHandle.generation = requestA.deviceObjectHandle.generation + 1; |
| requestB = *cmd; |
| }); |
| |
| EXPECT_CALL(api, OnAdapterRequestDevice(apiAdapter, NotNull(), _)) |
| .WillOnce(WithArg<1>([&](const WGPUDeviceDescriptor* desc) { |
| SetDeviceCallbacks(apiDeviceA, desc); |
| futureA = api.GetLastFuture(); |
| })) |
| .WillOnce(WithArg<1>([&](const WGPUDeviceDescriptor* desc) { |
| SetDeviceCallbacks(apiDeviceB, desc); |
| futureB = api.GetLastFuture(); |
| })); |
| FlushClient(); |
| |
| // Emulate the backend server completing the first request for a device successfully. Even |
| // though the backend successfully created the device, because the client has already asked to |
| // unregister that reserved slot, the server should just immediately release the successfully |
| // created device. |
| EXPECT_CALL(api, DeviceRelease(apiDeviceA)).Times(1); |
| api.CallAdapterRequestDeviceCallback(apiAdapter, WGPURequestDeviceStatus_Success, apiDeviceA, |
| kEmptyOutputStringView, futureA); |
| // Additionally verify that the server does not back the device. |
| EXPECT_EQ(GetWireServer()->GetDevice(requestA.deviceObjectHandle.id, |
| requestA.deviceObjectHandle.generation), |
| nullptr); |
| |
| // When we flush the server, the RequestDevice callback for the first request should be called |
| // with callback cancelled, and the DeviceLost callback should also fire once for the first |
| // requested device. |
| EXPECT_CALL(requestDeviceCb, Call(wgpu::RequestDeviceStatus::CallbackCancelled, IsNull(), |
| NonEmptySizedString())); |
| EXPECT_CALL(deviceLostCb, Call).Times(1); |
| FlushServer(); |
| |
| // Emulate the backend server completing the second request for a device successfully. |
| api.CallAdapterRequestDeviceCallback(apiAdapter, WGPURequestDeviceStatus_Success, apiDeviceB, |
| kEmptyOutputStringView, futureB); |
| // Verify that the server backs the device. |
| EXPECT_EQ(GetWireServer()->GetDevice(requestB.deviceObjectHandle.id, |
| requestB.deviceObjectHandle.generation), |
| apiDeviceB); |
| |
| // When the server completes the second request for a device, the RequestDevice callback for |
| // the second request should be called with success, and the DeviceLost callback should also |
| // fire once for the second requested device because we don't save the device from the |
| // RequestDevice callback. |
| EXPECT_CALL(requestDeviceCb, |
| Call(wgpu::RequestDeviceStatus::Success, NotNull(), EmptySizedString())); |
| EXPECT_CALL(deviceLostCb, Call).Times(1); |
| FlushServer(); |
| |
| // Finally, since we dropped the device on the client side, it should be Unregistered from the |
| // client, and correspondingly released on the server side once the commands are flushed. Note |
| // that the client flush should still fail though because we injected a different ID for the |
| // device than the client frontend actually allocated for the second device. This means that the |
| // wire client frontend will try to Unregister a non-existent object as a result of the modified |
| // and injected second request which will cause the server to fail. |
| EXPECT_CALL(api, DeviceRelease(apiDeviceB)).Times(1); |
| FlushClient(false); |
| } |
| |
| // This is a regression test for https://issues.chromium.org/508092644 where a compromised client |
| // could potentially ask the server to Unregister a reserved, but not backed Adapter. |
| TEST_F(WireSpecificCommandTests, RequestAdapterIdReuseAfterInjectedUnregister) { |
| // Set up all the mock callbacks and objects. |
| MockCppCallback<wgpu::RequestAdapterCallback<void>*> requestAdapterCb; |
| WGPUAdapter apiAdapterA = api.GetNewAdapter(); |
| WGPUFuture futureA; |
| WGPUAdapter apiAdapterB = api.GetNewAdapter(); |
| WGPUFuture futureB; |
| |
| // Add the first request for an adapter. |
| InstanceRequestAdapterCmd requestA = {}; |
| InterceptServerCmd<InstanceRequestAdapterCmd>( |
| [&]() { |
| instance.RequestAdapter(nullptr, wgpu::CallbackMode::AllowSpontaneous, |
| requestAdapterCb.Callback()); |
| }, |
| [&](InstanceRequestAdapterCmd* cmd) { requestA = *cmd; }); |
| |
| // Immediately unregister the adapter that was reserved for the first request. This is not |
| // normally possible, so we inject the command directly. |
| UnregisterObjectCmd unregisterA = {}; |
| unregisterA.objectType = ObjectType::Adapter; |
| unregisterA.objectId = requestA.adapterObjectHandle.id; |
| AddSpecificServerCmd(unregisterA); |
| |
| // Add a second request for an adapter that attempts to reuse the same id that was originally |
| // reserved for the first request. |
| InstanceRequestAdapterCmd requestB = {}; |
| InterceptServerCmd<InstanceRequestAdapterCmd>( |
| [&]() { |
| instance.RequestAdapter(nullptr, wgpu::CallbackMode::AllowSpontaneous, |
| requestAdapterCb.Callback()); |
| }, |
| [&](InstanceRequestAdapterCmd* cmd) { |
| cmd->adapterObjectHandle.id = requestA.adapterObjectHandle.id; |
| cmd->adapterObjectHandle.generation = requestA.adapterObjectHandle.generation + 1; |
| requestB = *cmd; |
| }); |
| |
| EXPECT_CALL(api, OnInstanceRequestAdapter(apiInstance, IsNull(), _)) |
| .WillOnce([&]() { futureA = api.GetLastFuture(); }) |
| .WillOnce([&]() { futureB = api.GetLastFuture(); }); |
| FlushClient(); |
| |
| // Emulate the backend server completing the first request for an adapter successfully. Even |
| // though the backend successfully created the adapter, because the client has already asked to |
| // unregister that reserved slot, the server should just immediately release the successfully |
| // created adapter. |
| EXPECT_CALL(api, AdapterRelease(apiAdapterA)).Times(1); |
| api.CallInstanceRequestAdapterCallback(apiInstance, WGPURequestAdapterStatus_Success, |
| apiAdapterA, kEmptyOutputStringView, futureA); |
| |
| // When we flush the server, the RequestAdapter callback for the first request should be called |
| // with callback cancelled. |
| EXPECT_CALL(requestAdapterCb, Call(wgpu::RequestAdapterStatus::CallbackCancelled, IsNull(), |
| NonEmptySizedString())); |
| FlushServer(); |
| |
| // Emulate the backend server completing the second request for an adapter successfully. |
| api.CallInstanceRequestAdapterCallback(apiInstance, WGPURequestAdapterStatus_Success, |
| apiAdapterB, kEmptyOutputStringView, futureB); |
| |
| // When the server completes the second request for an adapter, the RequestAdapter callback for |
| // the second request should be called with success. |
| EXPECT_CALL(requestAdapterCb, |
| Call(wgpu::RequestAdapterStatus::Success, NotNull(), EmptySizedString())); |
| FlushServer(); |
| |
| // Finally, since we dropped the adapter on the client side, it should be Unregistered from the |
| // client, and correspondingly released on the server side once the commands are flushed. Note |
| // that the client flush should still fail though because we injected a different ID for the |
| // adapter than the client frontend actually allocated for the second adapter. This means that |
| // the wire client frontend will try to Unregister a non-existent object as a result of the |
| // modified and injected second request which will cause the server to fail. |
| EXPECT_CALL(api, AdapterRelease(apiAdapterB)).Times(1); |
| FlushClient(false); |
| } |
| |
| // This is a regression test for https://issues.chromium.org/508092644 where a compromised client |
| // could potentially ask the server to Unregister a reserved, but not backed ComputePipeline. See |
| // the RequestAdapterIdReuseAfterInjectedUnregister test above for more equivalent descriptions for |
| // each step throughout the test. |
| TEST_F(WireSpecificCommandTests, CreateComputePipelineAsyncIdReuseAfterInjectedUnregister) { |
| wgpu::ShaderModuleDescriptor shaderDesc = {}; |
| wgpu::ShaderModule shader = device.CreateShaderModule(&shaderDesc); |
| WGPUShaderModule apiShader = api.GetNewShaderModule(); |
| EXPECT_CALL(api, DeviceCreateShaderModule(apiDevice, _)).WillOnce(Return(apiShader)); |
| FlushClient(); |
| |
| wgpu::ComputePipelineDescriptor descriptor = {}; |
| descriptor.compute.module = shader; |
| |
| MockCppCallback<wgpu::CreateComputePipelineAsyncCallback<void>*> createComputePipelineCb; |
| WGPUComputePipeline apiPipelineA = api.GetNewComputePipeline(); |
| WGPUFuture futureA; |
| WGPUComputePipeline apiPipelineB = api.GetNewComputePipeline(); |
| WGPUFuture futureB; |
| |
| DeviceCreateComputePipelineAsyncCmd requestA = {}; |
| InterceptServerCmd<DeviceCreateComputePipelineAsyncCmd>( |
| [&]() { |
| device.CreateComputePipelineAsync(&descriptor, wgpu::CallbackMode::AllowSpontaneous, |
| createComputePipelineCb.Callback()); |
| }, |
| [&](DeviceCreateComputePipelineAsyncCmd* cmd) { |
| requestA = *cmd; |
| // Manually fix the shader module since the [de]serialization can't handle objects. |
| const_cast<WGPUComputePipelineDescriptor*>(cmd->descriptor)->compute.module = |
| shader.Get(); |
| }); |
| |
| UnregisterObjectCmd unregisterA = {}; |
| unregisterA.objectType = ObjectType::ComputePipeline; |
| unregisterA.objectId = requestA.pipelineObjectHandle.id; |
| AddSpecificServerCmd(unregisterA); |
| |
| DeviceCreateComputePipelineAsyncCmd requestB = {}; |
| InterceptServerCmd<DeviceCreateComputePipelineAsyncCmd>( |
| [&]() { |
| device.CreateComputePipelineAsync(&descriptor, wgpu::CallbackMode::AllowSpontaneous, |
| createComputePipelineCb.Callback()); |
| }, |
| [&](DeviceCreateComputePipelineAsyncCmd* cmd) { |
| cmd->pipelineObjectHandle.id = requestA.pipelineObjectHandle.id; |
| cmd->pipelineObjectHandle.generation = requestA.pipelineObjectHandle.generation + 1; |
| requestB = *cmd; |
| // Manually fix the shader module since the [de]serialization can't handle objects. |
| const_cast<WGPUComputePipelineDescriptor*>(cmd->descriptor)->compute.module = |
| shader.Get(); |
| }); |
| |
| EXPECT_CALL(api, OnDeviceCreateComputePipelineAsync(apiDevice, NotNull(), _)) |
| .WillOnce([&]() { futureA = api.GetLastFuture(); }) |
| .WillOnce([&]() { futureB = api.GetLastFuture(); }); |
| FlushClient(); |
| |
| EXPECT_CALL(api, ComputePipelineRelease(apiPipelineA)).Times(1); |
| api.CallDeviceCreateComputePipelineAsyncCallback(apiDevice, |
| WGPUCreatePipelineAsyncStatus_Success, |
| apiPipelineA, kEmptyOutputStringView, futureA); |
| |
| EXPECT_CALL(createComputePipelineCb, Call(wgpu::CreatePipelineAsyncStatus::CallbackCancelled, |
| IsNull(), NonEmptySizedString())); |
| FlushServer(); |
| |
| api.CallDeviceCreateComputePipelineAsyncCallback(apiDevice, |
| WGPUCreatePipelineAsyncStatus_Success, |
| apiPipelineB, kEmptyOutputStringView, futureB); |
| |
| EXPECT_CALL(createComputePipelineCb, |
| Call(wgpu::CreatePipelineAsyncStatus::Success, NotNull(), EmptySizedString())); |
| FlushServer(); |
| |
| EXPECT_CALL(api, ComputePipelineRelease(apiPipelineB)).Times(1); |
| FlushClient(false); |
| } |
| |
| // This is a regression test for https://issues.chromium.org/508092644 where a compromised client |
| // could potentially ask the server to Unregister a reserved, but not backed RenderPipeline. See the |
| // RequestAdapterIdReuseAfterInjectedUnregister test above for more equivalent descriptions for |
| // each step throughout the test. |
| TEST_F(WireSpecificCommandTests, CreateRenderPipelineAsyncIdReuseAfterInjectedUnregister) { |
| wgpu::ShaderModuleDescriptor shaderDesc = {}; |
| wgpu::ShaderModule shader = device.CreateShaderModule(&shaderDesc); |
| WGPUShaderModule apiShader = api.GetNewShaderModule(); |
| EXPECT_CALL(api, DeviceCreateShaderModule(apiDevice, _)).WillOnce(Return(apiShader)); |
| FlushClient(); |
| |
| wgpu::FragmentState fragment = {}; |
| fragment.module = shader; |
| wgpu::RenderPipelineDescriptor descriptor = {}; |
| descriptor.vertex.module = shader; |
| descriptor.fragment = &fragment; |
| |
| MockCppCallback<wgpu::CreateRenderPipelineAsyncCallback<void>*> createRenderPipelineCb; |
| WGPURenderPipeline apiPipelineA = api.GetNewRenderPipeline(); |
| WGPUFuture futureA; |
| WGPURenderPipeline apiPipelineB = api.GetNewRenderPipeline(); |
| WGPUFuture futureB; |
| |
| DeviceCreateRenderPipelineAsyncCmd requestA = {}; |
| InterceptServerCmd<DeviceCreateRenderPipelineAsyncCmd>( |
| [&]() { |
| device.CreateRenderPipelineAsync(&descriptor, wgpu::CallbackMode::AllowSpontaneous, |
| createRenderPipelineCb.Callback()); |
| }, |
| [&](DeviceCreateRenderPipelineAsyncCmd* cmd) { |
| requestA = *cmd; |
| // Manually fix the shader module since the [de]serialization can't handle objects. |
| auto* desc = const_cast<WGPURenderPipelineDescriptor*>(cmd->descriptor); |
| desc->vertex.module = shader.Get(); |
| const_cast<WGPUFragmentState*>(desc->fragment)->module = shader.Get(); |
| }); |
| |
| UnregisterObjectCmd unregisterA = {}; |
| unregisterA.objectType = ObjectType::RenderPipeline; |
| unregisterA.objectId = requestA.pipelineObjectHandle.id; |
| AddSpecificServerCmd(unregisterA); |
| |
| DeviceCreateRenderPipelineAsyncCmd requestB = {}; |
| InterceptServerCmd<DeviceCreateRenderPipelineAsyncCmd>( |
| [&]() { |
| device.CreateRenderPipelineAsync(&descriptor, wgpu::CallbackMode::AllowSpontaneous, |
| createRenderPipelineCb.Callback()); |
| }, |
| [&](DeviceCreateRenderPipelineAsyncCmd* cmd) { |
| cmd->pipelineObjectHandle.id = requestA.pipelineObjectHandle.id; |
| cmd->pipelineObjectHandle.generation = requestA.pipelineObjectHandle.generation + 1; |
| requestB = *cmd; |
| // Manually fix the shader module since the [de]serialization can't handle objects. |
| auto* desc = const_cast<WGPURenderPipelineDescriptor*>(cmd->descriptor); |
| desc->vertex.module = shader.Get(); |
| const_cast<WGPUFragmentState*>(desc->fragment)->module = shader.Get(); |
| }); |
| |
| EXPECT_CALL(api, OnDeviceCreateRenderPipelineAsync(apiDevice, NotNull(), _)) |
| .WillOnce([&]() { futureA = api.GetLastFuture(); }) |
| .WillOnce([&]() { futureB = api.GetLastFuture(); }); |
| FlushClient(); |
| |
| EXPECT_CALL(api, RenderPipelineRelease(apiPipelineA)).Times(1); |
| api.CallDeviceCreateRenderPipelineAsyncCallback(apiDevice, |
| WGPUCreatePipelineAsyncStatus_Success, |
| apiPipelineA, kEmptyOutputStringView, futureA); |
| |
| EXPECT_CALL(createRenderPipelineCb, Call(wgpu::CreatePipelineAsyncStatus::CallbackCancelled, |
| IsNull(), NonEmptySizedString())); |
| FlushServer(); |
| |
| api.CallDeviceCreateRenderPipelineAsyncCallback(apiDevice, |
| WGPUCreatePipelineAsyncStatus_Success, |
| apiPipelineB, kEmptyOutputStringView, futureB); |
| |
| EXPECT_CALL(createRenderPipelineCb, |
| Call(wgpu::CreatePipelineAsyncStatus::Success, NotNull(), EmptySizedString())); |
| FlushServer(); |
| |
| EXPECT_CALL(api, RenderPipelineRelease(apiPipelineB)).Times(1); |
| FlushClient(false); |
| } |
| |
| } // anonymous namespace |
| } // namespace dawn::wire |