blob: e926cc096226f9d9fc9810373d2de470888099f1 [file] [log] [blame] [edit]
/*
* 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/rive_render_image.hpp"
#include "rive/renderer/webgpu/render_context_webgpu_impl.hpp"
#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);
m_instance.RequestAdapter(
{},
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);
}
m_adapter.RequestDevice(
{},
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,
};
m_surface.Configure(&conf);
}
RenderContextWebGPUImpl::ContextOptions contextOptions;
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, %s) ====\n",
adapterInfo.vendor.data,
adapterInfo.device.data,
adapterInfo.description.data,
wgpu_backend_name(impl()->capabilities().backendType),
pls_impl_name(impl()->capabilities()));
}
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);
}
}
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,
.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,
.wireframe = options.wireframe,
.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 = 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, &copySize);
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, &copySize);
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 = {};
}
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;
std::unique_ptr<RenderContext> m_renderContext;
rcp<RenderTargetWebGPU> m_renderTarget;
};
}; // namespace rive::gpu
TestingWindow* TestingWindow::MakeWGPU(const BackendParams& backendParams)
{
return new rive::gpu::TestingWindowWGPU(backendParams);
}
#endif