| /* | |
| * Copyright 2025 Rive | |
| */ | |
| #include "testing_window.hpp" | |
| #if !defined(RIVE_WEBGPU) || (RIVE_WEBGPU < 2) | |
| TestingWindow* TestingWindow::MakeWGPU(const BackendParams&) { return nullptr; } | |
| #else | |
| #include "common/offscreen_render_target.hpp" | |
| #include "rive/renderer/rive_renderer.hpp" | |
| #include "rive/renderer/webgpu/render_context_webgpu_impl.hpp" | |
| #include <algorithm> | |
| #ifdef RIVE_WAGYU | |
| #include <webgpu/webgpu_wagyu.h> | |
| #endif | |
| #ifdef __EMSCRIPTEN__ | |
| #include <emscripten/emscripten.h> | |
| #include <emscripten/html5.h> | |
| #endif | |
| namespace rive::gpu | |
| { | |
| static const char* wgpu_backend_name(wgpu::BackendType backendType) | |
| { | |
| switch (backendType) | |
| { | |
| case wgpu::BackendType::Undefined: | |
| return "<unknown backend>"; | |
| case wgpu::BackendType::Null: | |
| return "<null backend>"; | |
| case wgpu::BackendType::WebGPU: | |
| return "WebGPU"; | |
| case wgpu::BackendType::D3D11: | |
| return "D3D11"; | |
| case wgpu::BackendType::D3D12: | |
| return "D3D12"; | |
| case wgpu::BackendType::Metal: | |
| return "Metal"; | |
| case wgpu::BackendType::Vulkan: | |
| return "Vulkan"; | |
| case wgpu::BackendType::OpenGL: | |
| return "OpenGL"; | |
| case wgpu::BackendType::OpenGLES: | |
| return "OpenGLES"; | |
| } | |
| RIVE_UNREACHABLE(); | |
| } | |
| static const char* pls_impl_name( | |
| const RenderContextWebGPUImpl::Capabilities& capabilities) | |
| { | |
| #ifdef RIVE_WAGYU | |
| switch (capabilities.plsType) | |
| { | |
| case RenderContextWebGPUImpl::PixelLocalStorageType:: | |
| GL_EXT_shader_pixel_local_storage: | |
| return "GL_EXT_shader_pixel_local_storage"; | |
| case RenderContextWebGPUImpl::PixelLocalStorageType:: | |
| VK_EXT_rasterization_order_attachment_access: | |
| return "VK_EXT_rasterization_order_attachment_access"; | |
| case RenderContextWebGPUImpl::PixelLocalStorageType::none: | |
| break; | |
| } | |
| #endif | |
| return "<no pixel local storage>"; | |
| } | |
| class TestingWindowWGPU : public TestingWindow | |
| { | |
| public: | |
| TestingWindowWGPU(const BackendParams& backendParams) : | |
| m_backendParams(backendParams) | |
| { | |
| m_instance = wgpu::CreateInstance(nullptr); | |
| assert(m_instance); | |
| wgpu::RequestAdapterOptions requestAdapterOptions = { | |
| .powerPreference = | |
| backendParams.gpuNameFilter == "i" || | |
| backendParams.gpuNameFilter == "integrated" | |
| ? wgpu::PowerPreference::LowPower | |
| : wgpu::PowerPreference::HighPerformance, | |
| }; | |
| #ifdef RIVE_WAGYU | |
| // Wagyu's OpenGL ES backend requires WebGPU compatibility mode. | |
| const bool compatibilityMode = | |
| wgpuWagyuInstanceGetBackend(m_instance.Get()) == | |
| WGPUBackendType_OpenGLES; | |
| if (compatibilityMode) | |
| { | |
| requestAdapterOptions.featureLevel = | |
| wgpu::FeatureLevel::Compatibility; | |
| } | |
| #endif | |
| m_instance.RequestAdapter( | |
| &requestAdapterOptions, | |
| wgpu::CallbackMode::AllowSpontaneous, | |
| [](wgpu::RequestAdapterStatus status, | |
| wgpu::Adapter adapter, | |
| wgpu::StringView message, | |
| TestingWindowWGPU* this_) { | |
| assert(status == wgpu::RequestAdapterStatus::Success); | |
| this_->m_adapter = adapter; | |
| }, | |
| this); | |
| while (!m_adapter) | |
| { | |
| emscripten_sleep(1); | |
| } | |
| wgpu::DeviceDescriptor deviceDesc = {}; | |
| #ifdef RIVE_WAGYU | |
| wgpu::Limits requiredLimits = {}; | |
| wgpu::CompatibilityModeLimits compatLimits = {}; | |
| if (compatibilityMode) | |
| { | |
| // Rive uses storage buffers in the vertex shader. In compatibility | |
| // mode this isn't allowed by default, so explicitly request it if | |
| // supported. Otherwise, the renderer can fall back to polyfilling | |
| // vertex storage buffers via textures. | |
| wgpu::Limits adapterLimits = {}; | |
| wgpu::CompatibilityModeLimits adapterCompatLimits = {}; | |
| adapterLimits.nextInChain = &adapterCompatLimits; | |
| m_adapter.GetLimits(&adapterLimits); | |
| if (adapterCompatLimits.maxStorageBuffersInVertexStage != | |
| wgpu::kLimitU32Undefined) | |
| { | |
| compatLimits.maxStorageBuffersInVertexStage = | |
| std::min<uint32_t>( | |
| gpu::kMaxStorageBuffers, | |
| adapterCompatLimits.maxStorageBuffersInVertexStage); | |
| requiredLimits.nextInChain = &compatLimits; | |
| deviceDesc.requiredLimits = &requiredLimits; | |
| } | |
| } | |
| #endif | |
| std::vector<wgpu::FeatureName> requiredFeatures; | |
| #ifdef RIVE_WAGYU | |
| // Request coherent advanced blend when the adapter advertises it. This | |
| // enables Rive to take the "supportsBlendAdvancedCoherentKHR" path on | |
| // MSAA, and skip the framebuffer copies and renderPass interruptions | |
| // for advanced blend. | |
| if (m_adapter.HasFeature(static_cast<wgpu::FeatureName>( | |
| WGPUFeatureName_WagyuBlendEquationAdvancedCoherent))) | |
| { | |
| requiredFeatures.push_back(static_cast<wgpu::FeatureName>( | |
| WGPUFeatureName_WagyuBlendEquationAdvancedCoherent)); | |
| } | |
| #endif | |
| if (m_adapter.HasFeature(wgpu::FeatureName::ClipDistances)) | |
| { | |
| requiredFeatures.push_back(wgpu::FeatureName::ClipDistances); | |
| } | |
| deviceDesc.requiredFeatureCount = requiredFeatures.size(); | |
| deviceDesc.requiredFeatures = requiredFeatures.data(); | |
| m_adapter.RequestDevice( | |
| &deviceDesc, | |
| wgpu::CallbackMode::AllowSpontaneous, | |
| [](wgpu::RequestDeviceStatus status, | |
| wgpu::Device device, | |
| wgpu::StringView message, | |
| TestingWindowWGPU* this_) { | |
| assert(status == wgpu::RequestDeviceStatus::Success); | |
| this_->m_device = device; | |
| }, | |
| this); | |
| while (!m_device) | |
| { | |
| emscripten_sleep(1); | |
| } | |
| m_queue = m_device.GetQueue(); | |
| assert(m_queue); | |
| { | |
| wgpu::EmscriptenSurfaceSourceCanvasHTMLSelector htmlSelector; | |
| htmlSelector.selector = "#canvas"; | |
| wgpu::SurfaceDescriptor surfaceDesc = { | |
| .nextInChain = &htmlSelector, | |
| }; | |
| m_surface = m_instance.CreateSurface(&surfaceDesc); | |
| assert(m_surface); | |
| int w, h; | |
| emscripten_get_canvas_element_size("#canvas", &w, &h); | |
| m_width = w; | |
| m_height = h; | |
| } | |
| { | |
| wgpu::SurfaceCapabilities capabilities; | |
| m_surface.GetCapabilities(m_adapter, &capabilities); | |
| assert(capabilities.formatCount > 0); | |
| m_format = capabilities.formats[0]; | |
| assert(m_format != wgpu::TextureFormat::Undefined); | |
| if (m_format != wgpu::TextureFormat::RGBA8Unorm && | |
| m_format != wgpu::TextureFormat::BGRA8Unorm) | |
| { | |
| m_format = wgpu::TextureFormat::RGBA8Unorm; | |
| } | |
| } | |
| { | |
| wgpu::SurfaceConfiguration conf = { | |
| .device = m_device, | |
| .format = m_format, | |
| #ifndef RIVE_WAGYU | |
| // CopySrc so endFrame can read pixels back from the canvas | |
| // texture; CopyDst so the overflow texture can be blitted in. | |
| .usage = wgpu::TextureUsage::RenderAttachment | | |
| wgpu::TextureUsage::CopySrc | | |
| wgpu::TextureUsage::CopyDst, | |
| .width = static_cast<uint32_t>(m_width), | |
| .height = static_cast<uint32_t>(m_height), | |
| // Opaque so that pixels with alpha are more obvious, rather | |
| // than being composited into the browser below. | |
| .alphaMode = wgpu::CompositeAlphaMode::Opaque, | |
| #endif | |
| }; | |
| m_surface.Configure(&conf); | |
| } | |
| RenderContextWebGPUImpl::ContextOptions contextOptions; | |
| #ifdef RIVE_WAGYU | |
| contextOptions.compatibilityMode = compatibilityMode; | |
| #endif | |
| m_renderContext = RenderContextWebGPUImpl::MakeContext(m_adapter, | |
| m_device, | |
| m_queue, | |
| contextOptions); | |
| wgpu::AdapterInfo adapterInfo; | |
| m_adapter.GetInfo(&adapterInfo); | |
| printf("==== WGPU device: %.*s %.*s %.*s (%s", | |
| static_cast<int>(adapterInfo.vendor.length), | |
| adapterInfo.vendor.data, | |
| static_cast<int>(adapterInfo.device.length), | |
| adapterInfo.device.data, | |
| static_cast<int>(adapterInfo.description.length), | |
| adapterInfo.description.data, | |
| wgpu_backend_name(impl()->capabilities().backendType)); | |
| #ifdef RIVE_WAGYU | |
| switch (impl()->capabilities().plsType) | |
| { | |
| case RenderContextWebGPUImpl::PixelLocalStorageType:: | |
| GL_EXT_shader_pixel_local_storage: | |
| printf(", GL_EXT_shader_pixel_local_storage"); | |
| break; | |
| case RenderContextWebGPUImpl::PixelLocalStorageType:: | |
| VK_EXT_rasterization_order_attachment_access: | |
| printf(", VK_EXT_rasterization_order_attachment_access"); | |
| break; | |
| case RenderContextWebGPUImpl::PixelLocalStorageType::none: | |
| break; | |
| } | |
| #endif | |
| if (m_renderContext->platformFeatures() | |
| .supportsBlendAdvancedCoherentKHR) | |
| { | |
| printf(", WagyuBlendEquationAdvancedCoherent"); | |
| } | |
| if (m_renderContext->platformFeatures().supportsClipPlanes) | |
| { | |
| printf(", WGPUFeatureName_ClipDistances"); | |
| } | |
| printf(") ====\n"); | |
| } | |
| rive::Factory* factory() override { return m_renderContext.get(); } | |
| rive::gpu::RenderContext* renderContext() const override | |
| { | |
| return m_renderContext.get(); | |
| } | |
| rcp<rive_tests::OffscreenRenderTarget> makeOffscreenRenderTarget( | |
| uint32_t width, | |
| uint32_t height, | |
| bool riveRenderable) const override | |
| { | |
| return rive_tests::OffscreenRenderTarget::MakeWebGPU(impl(), | |
| width, | |
| height); | |
| } | |
| void resize(int width, int height) override | |
| { | |
| if (m_width != width || m_height != height) | |
| { | |
| m_overflowTexture = {}; | |
| m_overflowTextureView = {}; | |
| m_pixelReadBuff = {}; | |
| TestingWindow::resize(width, height); | |
| #ifndef RIVE_WAGYU | |
| // Match the canvas drawing buffer to the new size and | |
| // reconfigure the surface. Without this the canvas keeps its | |
| // default backing size and only a corner is ever presented. | |
| emscripten_set_canvas_element_size("#canvas", width, height); | |
| wgpu::SurfaceConfiguration conf = { | |
| .device = m_device, | |
| .format = m_format, | |
| .usage = wgpu::TextureUsage::RenderAttachment | | |
| wgpu::TextureUsage::CopySrc | | |
| wgpu::TextureUsage::CopyDst | | |
| wgpu::TextureUsage::TextureBinding, | |
| .width = static_cast<uint32_t>(width), | |
| .height = static_cast<uint32_t>(height), | |
| .alphaMode = wgpu::CompositeAlphaMode::Opaque, | |
| }; | |
| m_surface.Configure(&conf); | |
| #endif | |
| } | |
| } | |
| std::unique_ptr<rive::Renderer> beginFrame( | |
| const FrameOptions& options) override | |
| { | |
| assert(m_currentCanvasTexture == nullptr); | |
| wgpu::SurfaceTexture surfaceTexture; | |
| m_surface.GetCurrentTexture(&surfaceTexture); | |
| m_currentCanvasTexture = surfaceTexture.texture; | |
| uint32_t surfaceWidth = m_currentCanvasTexture.GetWidth(); | |
| uint32_t surfaceHeight = m_currentCanvasTexture.GetHeight(); | |
| wgpu::TextureViewDescriptor textureViewDesc = { | |
| .format = m_format, | |
| .dimension = wgpu::TextureViewDimension::e2D, | |
| }; | |
| m_currentCanvasTextureView = | |
| m_currentCanvasTexture.CreateView(&textureViewDesc); | |
| if (surfaceWidth < m_width || surfaceHeight < m_height) | |
| { | |
| if (!m_overflowTexture) | |
| { | |
| wgpu::TextureDescriptor overflowTextureDesc = { | |
| .usage = wgpu::TextureUsage::RenderAttachment | | |
| wgpu::TextureUsage::CopySrc | | |
| wgpu::TextureUsage::TextureBinding, | |
| .dimension = wgpu::TextureDimension::e2D, | |
| .size = {m_width, m_height}, | |
| .format = m_format, | |
| }; | |
| m_overflowTexture = | |
| m_device.CreateTexture(&overflowTextureDesc); | |
| m_overflowTextureView = m_overflowTexture.CreateView(); | |
| } | |
| assert(m_overflowTexture.GetWidth() == m_width); | |
| assert(m_overflowTexture.GetHeight() == m_height); | |
| m_renderTarget = | |
| m_renderContext->static_impl_cast<RenderContextWebGPUImpl>() | |
| ->makeRenderTarget(m_format, m_width, m_height); | |
| m_renderTarget->setTargetTextureView(m_overflowTextureView, | |
| m_overflowTexture); | |
| } | |
| else | |
| { | |
| m_renderTarget = | |
| m_renderContext->static_impl_cast<RenderContextWebGPUImpl>() | |
| ->makeRenderTarget(m_format, surfaceWidth, surfaceHeight); | |
| m_renderTarget->setTargetTextureView(m_currentCanvasTextureView, | |
| m_currentCanvasTexture); | |
| } | |
| rive::gpu::RenderContext::FrameDescriptor frameDescriptor = { | |
| .renderTargetWidth = m_renderTarget->width(), | |
| .renderTargetHeight = m_renderTarget->height(), | |
| .loadAction = options.doClear | |
| ? rive::gpu::LoadAction::clear | |
| : rive::gpu::LoadAction::preserveRenderTarget, | |
| .clearColor = options.clearColor, | |
| .msaaSampleCount = m_backendParams.msaa ? 4u : 0u, | |
| .disableRasterOrdering = options.disableRasterOrdering, | |
| .triangulationThresholds = options.triangulationThresholds, | |
| .wireframe = options.wireframe, | |
| .fillsDisabled = options.fillsDisabled, | |
| .strokesDisabled = options.strokesDisabled, | |
| .clockwiseFillOverride = | |
| m_backendParams.clockwise || options.clockwiseFillOverride, | |
| .synthesizedFailureType = options.synthesizedFailureType, | |
| }; | |
| m_renderContext->beginFrame(frameDescriptor); | |
| return std::make_unique<RiveRenderer>(m_renderContext.get()); | |
| } | |
| void flushPLSContext(RenderTarget* offscreenRenderTarget) final | |
| { | |
| wgpu::CommandEncoder encoder = m_device.CreateCommandEncoder(); | |
| m_renderContext->flush({ | |
| .renderTarget = offscreenRenderTarget != nullptr | |
| ? offscreenRenderTarget | |
| : m_renderTarget.get(), | |
| .externalCommandBuffer = encoder.Get(), | |
| }); | |
| wgpu::CommandBuffer commands = encoder.Finish(); | |
| m_queue.Submit(1, &commands); | |
| } | |
| void endFrame(std::vector<uint8_t>* pixelData) override | |
| { | |
| flushPLSContext(nullptr); | |
| assert(m_currentCanvasTexture != nullptr); | |
| if (m_overflowTexture) | |
| { | |
| // Blit the overflow texture back to the canvas. | |
| wgpu::CommandEncoder encoder = m_device.CreateCommandEncoder(); | |
| wgpu::TexelCopyTextureInfo src = { | |
| .texture = m_overflowTexture, | |
| .origin = {0, 0, 0}, | |
| }; | |
| wgpu::TexelCopyTextureInfo dst = { | |
| .texture = m_currentCanvasTexture, | |
| .origin = {0, 0, 0}, | |
| }; | |
| wgpu::Extent3D copySize{ | |
| std::min(m_width, m_currentCanvasTexture.GetWidth()), | |
| std::min(m_height, m_currentCanvasTexture.GetHeight()), | |
| }; | |
| encoder.CopyTextureToTexture(&src, &dst, ©Size); | |
| wgpu::CommandBuffer commands = encoder.Finish(); | |
| m_queue.Submit(1, &commands); | |
| } | |
| if (pixelData != nullptr) | |
| { | |
| assert(m_format == wgpu::TextureFormat::RGBA8Unorm || | |
| m_format == wgpu::TextureFormat::BGRA8Unorm); | |
| bool invertY = false; | |
| #ifdef RIVE_WAGYU | |
| invertY = | |
| impl()->capabilities().backendType == | |
| wgpu::BackendType::OpenGLES && | |
| wgpuWagyuTextureIsSwapchain(m_currentCanvasTexture.Get()) && | |
| m_overflowTexture == nullptr; | |
| #endif | |
| const uint32_t rowBytesInReadBuff = | |
| math::round_up_to_multiple_of<256>(m_width * 4); | |
| // Create a buffer to receive the pixels. | |
| if (!m_pixelReadBuff) | |
| { | |
| wgpu::BufferDescriptor buffDesc{ | |
| .usage = | |
| wgpu::BufferUsage::MapRead | wgpu::BufferUsage::CopyDst, | |
| .size = m_height * rowBytesInReadBuff, | |
| }; | |
| m_pixelReadBuff = m_device.CreateBuffer(&buffDesc); | |
| } | |
| assert(m_pixelReadBuff.GetSize() == m_height * rowBytesInReadBuff); | |
| // Blit the framebuffer into m_pixelReadBuff. | |
| wgpu::CommandEncoder readEncoder = m_device.CreateCommandEncoder(); | |
| wgpu::TexelCopyTextureInfo srcTexture = { | |
| .texture = m_overflowTexture != nullptr | |
| ? m_overflowTexture | |
| : m_currentCanvasTexture, | |
| .origin = {0, | |
| invertY ? m_renderTarget->height() - m_height : 0, | |
| 0}, | |
| }; | |
| wgpu::TexelCopyBufferInfo dstBuffer = { | |
| .layout = | |
| { | |
| .offset = 0, | |
| .bytesPerRow = rowBytesInReadBuff, | |
| }, | |
| .buffer = m_pixelReadBuff, | |
| }; | |
| wgpu::Extent3D copySize = { | |
| .width = m_width, | |
| .height = m_height, | |
| }; | |
| readEncoder.CopyTextureToBuffer(&srcTexture, &dstBuffer, ©Size); | |
| wgpu::CommandBuffer commands = readEncoder.Finish(NULL); | |
| m_queue.Submit(1, &commands); | |
| { | |
| // Map m_pixelReadBuff. | |
| bool mappingFinished = false; | |
| m_pixelReadBuff.MapAsync( | |
| wgpu::MapMode::Read, | |
| 0, | |
| m_height * rowBytesInReadBuff, | |
| wgpu::CallbackMode::AllowSpontaneous, | |
| [](wgpu::MapAsyncStatus status, | |
| wgpu::StringView message, | |
| bool* mappingFinished) { | |
| if (status != wgpu::MapAsyncStatus::Success) | |
| { | |
| fprintf(stderr, | |
| "failed to map m_pixelReadBuff: %s\n", | |
| message.data); | |
| abort(); | |
| } | |
| *mappingFinished = true; | |
| }, | |
| &mappingFinished); | |
| while (!mappingFinished) | |
| { | |
| emscripten_sleep(1); | |
| } | |
| } | |
| // Copy the image data from m_pixelReadBuff to pixelData. | |
| const size_t rowBytesInDst = m_width * 4; | |
| pixelData->resize(m_height * rowBytesInDst); | |
| const uint8_t* pixelReadBuffData = reinterpret_cast<const uint8_t*>( | |
| m_pixelReadBuff.GetConstMappedRange()); | |
| for (size_t y = 0; y < m_height; ++y) | |
| { | |
| const uint8_t* src; | |
| if (invertY) | |
| { | |
| src = &pixelReadBuffData[y * rowBytesInReadBuff]; | |
| } | |
| else | |
| { | |
| src = &pixelReadBuffData[(m_height - y - 1) * | |
| rowBytesInReadBuff]; | |
| } | |
| uint8_t* dst = &(*pixelData)[y * rowBytesInDst]; | |
| if (m_format == wgpu::TextureFormat::RGBA8Unorm) | |
| { | |
| memcpy(dst, src, rowBytesInDst); | |
| } | |
| else | |
| { | |
| assert(m_format == wgpu::TextureFormat::BGRA8Unorm); | |
| for (size_t x = 0; x < rowBytesInDst; x += 4) | |
| { | |
| // BGBRA -> RGBA. | |
| dst[x + 0] = src[x + 2]; | |
| dst[x + 1] = src[x + 1]; | |
| dst[x + 2] = src[x + 0]; | |
| dst[x + 3] = src[x + 3]; | |
| } | |
| } | |
| } | |
| m_pixelReadBuff.Unmap(); | |
| } | |
| m_currentCanvasTextureView = {}; | |
| m_currentCanvasTexture = {}; | |
| } | |
| void* getCurrentCommandBuffer() const override { return &m_oreEncoder; } | |
| void* getOreContext() const override | |
| { | |
| return m_renderContext->getOreContext(); | |
| } | |
| void beginOreFrame() override | |
| { | |
| auto oreContext = | |
| static_cast<rive::ore::Context*>(m_renderContext->getOreContext()); | |
| m_oreEncoder = m_device.CreateCommandEncoder(); | |
| // Ore move-takes the encoder, pass a copy so we retain our ref. | |
| wgpu::CommandEncoder borrowed = m_oreEncoder; | |
| oreContext->beginFrame({.externalCommandBuffer = &borrowed}); | |
| } | |
| void endOreFrame() override | |
| { | |
| auto oreContext = | |
| static_cast<rive::ore::Context*>(m_renderContext->getOreContext()); | |
| oreContext->endFrame(); | |
| wgpu::CommandBuffer commands = m_oreEncoder.Finish(); | |
| m_queue.Submit(1, &commands); | |
| m_oreEncoder = nullptr; | |
| } | |
| private: | |
| RenderContextWebGPUImpl* impl() const | |
| { | |
| return m_renderContext->static_impl_cast<RenderContextWebGPUImpl>(); | |
| } | |
| const BackendParams m_backendParams; | |
| wgpu::Instance m_instance = nullptr; | |
| wgpu::Adapter m_adapter; | |
| wgpu::Device m_device; | |
| wgpu::Surface m_surface; | |
| wgpu::TextureFormat m_format = wgpu::TextureFormat::Undefined; | |
| wgpu::Queue m_queue; | |
| wgpu::Texture m_currentCanvasTexture; | |
| wgpu::TextureView m_currentCanvasTextureView; | |
| wgpu::Texture m_overflowTexture; | |
| wgpu::TextureView m_overflowTextureView; | |
| wgpu::Buffer m_pixelReadBuff; | |
| mutable wgpu::CommandEncoder m_oreEncoder; | |
| std::unique_ptr<RenderContext> m_renderContext; | |
| rcp<RenderTargetWebGPU> m_renderTarget; | |
| }; // namespace rive::gpu | |
| }; // namespace rive::gpu | |
| TestingWindow* TestingWindow::MakeWGPU(const BackendParams& backendParams) | |
| { | |
| return new rive::gpu::TestingWindowWGPU(backendParams); | |
| } | |
| #endif |