#include "WebGPURenderInterface.hpp" #include "FileInterface_SDL.h" #include "RuntimeTextureProvider.hpp" #include "pipeline.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "../logging.hpp" #include "../webgpu/gpu.hpp" #include "../gfx/texture.hpp" namespace aurora::rmlui { namespace { Module Log("aurora::rmlui::RenderInterface"); constexpr size_t rmlBufferOffsetAlignment = 4; constexpr float FilterEpsilon = 0.0001f; struct Image { std::unique_ptr data; size_t size; uint32_t width; uint32_t height; }; struct ShaderGeometryData { std::vector vertices; std::vector indices; }; struct ShaderTextureData { wgpu::Texture m_texture; wgpu::TextureView m_textureView; std::vector m_pendingUpload; wgpu::Extent3D m_size{}; uint32_t m_rowBytes = 0; bool m_uploaded = false; }; struct CompiledShaderData { GradientUniformBlock gradient; }; enum class FilterType { Opacity, Blur, DropShadow, ColorMatrix, MaskImage, }; struct CompiledFilter { FilterType type = FilterType::Blur; float opacity = 1.f; float sigma = 0.f; Rml::Vector2f offset; Rml::ColourbPremultiplied color; Rml::Matrix4f colorMatrix; }; Image get_image(const Rml::String& source) { FileInterface_SDL fileInterface; const Rml::FileHandle file = fileInterface.Open(source); if (file == Rml::FileHandle{}) { return {}; } auto* stream = reinterpret_cast(file); SDL_Surface* loadedSurface = SDL_LoadPNG_IO(stream, true); if (loadedSurface == nullptr) { Log.warn("Failed to load image '{}': {}", source, SDL_GetError()); return {}; } SDL_Surface* rgbaSurface = SDL_ConvertSurface(loadedSurface, SDL_PIXELFORMAT_RGBA32); SDL_DestroySurface(loadedSurface); if (rgbaSurface == nullptr) { Log.warn("Failed to convert image '{}': {}", source, SDL_GetError()); return {}; } const auto iconWidth = static_cast(rgbaSurface->w); const auto iconHeight = static_cast(rgbaSurface->h); const size_t rowSize = static_cast(iconWidth) * 4; const size_t size = rowSize * static_cast(iconHeight); auto ptr = std::make_unique(size); for (uint32_t row = 0; row < iconHeight; ++row) { const auto* src = static_cast(rgbaSurface->pixels) + static_cast(row) * static_cast(rgbaSurface->pitch); auto* dst = ptr.get() + static_cast(row) * rowSize; std::memcpy(dst, src, rowSize); // Convert colors to premultiplied alpha, which is necessary for correct alpha compositing. for (size_t col = 0; col < rowSize; col += 4) { const uint8_t alpha = dst[col + 3]; for (size_t channel = 0; channel < 3; ++channel) { dst[col + channel] = static_cast((static_cast(dst[col + channel]) * static_cast(alpha)) / 255); } } } SDL_DestroySurface(rgbaSurface); return Image{ .data = std::move(ptr), .size = size, .width = iconWidth, .height = iconHeight, }; } void sigma_to_params(float desiredSigma, int& passLevel, float& sigma) { constexpr int MaxPasses = 10; constexpr float MaxSinglePassSigma = 3.f; const int downsampleHint = static_cast(desiredSigma * (2.f / MaxSinglePassSigma)); passLevel = downsampleHint > 0 ? static_cast(std::log2(static_cast(downsampleHint))) : 0; passLevel = std::clamp(passLevel, 0, MaxPasses); sigma = std::clamp(desiredSigma / static_cast(1 << passLevel), 0.f, MaxSinglePassSigma); } Rml::Vector4f blur_weights(float sigma, uint32_t radius) { std::array scalarWeights = {}; float normalization = 0.f; radius = std::min(radius, MaxBlurRadius); for (uint32_t i = 0; i <= radius; ++i) { if (std::abs(sigma) < 0.1f) { scalarWeights[i] = i == 0 ? 1.f : 0.f; } else { const float x = static_cast(i); scalarWeights[i] = std::exp(-(x * x) / (2.f * sigma * sigma)); } normalization += (i == 0 ? 1.f : 2.f) * scalarWeights[i]; } if (normalization > 0.f) { for (uint32_t i = 0; i <= radius; ++i) { scalarWeights[i] /= normalization; } } Rml::Vector4f weights = {}; for (uint32_t i = 0; i <= radius; ++i) { weights[i] = scalarWeights[i]; } return weights; } Rml::Vector4f to_colorf(Rml::ColourbPremultiplied color) { constexpr float InvByte = 1.f / 255.f; return { static_cast(color.red) * InvByte, static_cast(color.green) * InvByte, static_cast(color.blue) * InvByte, static_cast(color.alpha) * InvByte, }; } Rml::ColumnMajorMatrix4f to_shader_matrix(const Rml::Matrix4f& matrix) { if constexpr (std::is_same_v) { return matrix.Transpose(); } else { return matrix; } } bool is_identity_matrix(const Rml::Matrix4f& matrix) { const Rml::Matrix4f identity = Rml::Matrix4f::Identity(); const auto* matrixData = matrix.data(); const auto* identityData = identity.data(); for (size_t i = 0; i < 16; ++i) { if (std::abs(matrixData[i] - identityData[i]) > FilterEpsilon) { return false; } } return true; } bool is_identity_filter(const CompiledFilter& filter) { switch (filter.type) { case FilterType::Opacity: return std::abs(filter.opacity - 1.f) <= FilterEpsilon; case FilterType::Blur: return filter.sigma < 0.5f; case FilterType::ColorMatrix: return is_identity_matrix(filter.colorMatrix); case FilterType::DropShadow: case FilterType::MaskImage: default: return false; } } std::vector active_filters(Rml::Span filters) { std::vector activeFilters; activeFilters.reserve(filters.size()); for (Rml::CompiledFilterHandle filterHandle : filters) { const auto* filter = reinterpret_cast(filterHandle); if (filter != nullptr && !is_identity_filter(*filter)) { activeFilters.push_back(filterHandle); } } return activeFilters; } bool try_fold_simple_filters(Rml::Span filters, Rml::Matrix4f& colorMatrix, float& opacity) { colorMatrix = Rml::Matrix4f::Identity(); opacity = 1.f; for (Rml::CompiledFilterHandle filterHandle : filters) { const auto* filter = reinterpret_cast(filterHandle); if (filter == nullptr) { continue; } switch (filter->type) { case FilterType::Opacity: opacity *= filter->opacity; break; case FilterType::ColorMatrix: colorMatrix = filter->colorMatrix * colorMatrix; break; case FilterType::Blur: case FilterType::DropShadow: case FilterType::MaskImage: default: return false; } } return true; } Rml::Rectanglei downsample_scissor(Rml::Rectanglei scissor) { scissor.p0 = (scissor.p0 + Rml::Vector2i(1)) / 2; scissor.p1 = Rml::Math::Max(scissor.p1 / 2, scissor.p0); return scissor; } PipelineConfig make_pipeline_config(PipelineKind kind, wgpu::TextureFormat colorFormat, uint32_t sampleCount, VertexLayoutKind vertexLayout, StencilMode stencilMode, BlendMode blendMode, wgpu::ColorWriteMask colorWriteMask = wgpu::ColorWriteMask::All) { return { .kind = static_cast(kind), .colorFormat = static_cast(colorFormat), .sampleCount = sampleCount, .vertexLayout = static_cast(vertexLayout), .stencilFormat = static_cast(WebGPURenderInterface::ClipMaskStencilFormat), .stencilMode = static_cast(stencilMode), .blendMode = static_cast(blendMode), .colorWriteMask = static_cast(colorWriteMask), }; } gfx::PipelineRef geometry_pipeline(wgpu::TextureFormat colorFormat, uint32_t sampleCount, WebGPURenderInterface::PipelineType type) { StencilMode stencilMode = StencilMode::AlwaysKeep; auto colorWriteMask = wgpu::ColorWriteMask::All; switch (type) { case WebGPURenderInterface::PipelineType::Masked: stencilMode = StencilMode::EqualKeep; break; case WebGPURenderInterface::PipelineType::ClipReplace: stencilMode = StencilMode::ClipReplace; colorWriteMask = wgpu::ColorWriteMask::None; break; case WebGPURenderInterface::PipelineType::ClipIntersect: stencilMode = StencilMode::ClipIntersect; colorWriteMask = wgpu::ColorWriteMask::None; break; case WebGPURenderInterface::PipelineType::Normal: case WebGPURenderInterface::PipelineType::Count: default: break; } return gfx::pipeline_ref(make_pipeline_config(PipelineKind::Geometry, colorFormat, sampleCount, VertexLayoutKind::Geometry, stencilMode, BlendMode::Premultiplied, colorWriteMask)); } gfx::PipelineRef gradient_pipeline(wgpu::TextureFormat colorFormat, uint32_t sampleCount, bool masked) { return gfx::pipeline_ref( make_pipeline_config(PipelineKind::Gradient, colorFormat, sampleCount, VertexLayoutKind::Geometry, masked ? StencilMode::EqualKeep : StencilMode::AlwaysKeep, BlendMode::Premultiplied)); } gfx::PipelineRef blit_pipeline(wgpu::TextureFormat colorFormat, uint32_t sampleCount, WebGPURenderInterface::BlitPipelineType type, bool useStencil) { const bool blend = type == WebGPURenderInterface::BlitPipelineType::Blend || type == WebGPURenderInterface::BlitPipelineType::BlendMasked; const bool masked = type == WebGPURenderInterface::BlitPipelineType::BlendMasked || type == WebGPURenderInterface::BlitPipelineType::ReplaceMasked; return gfx::pipeline_ref( make_pipeline_config(PipelineKind::Blit, colorFormat, sampleCount, VertexLayoutKind::Fullscreen, masked ? StencilMode::EqualKeep : (useStencil ? StencilMode::AlwaysKeep : StencilMode::None), blend ? BlendMode::Premultiplied : BlendMode::None)); } gfx::PipelineRef simple_filter_pipeline(wgpu::TextureFormat colorFormat, uint32_t sampleCount, WebGPURenderInterface::BlitPipelineType type, bool useStencil) { const bool blend = type == WebGPURenderInterface::BlitPipelineType::Blend || type == WebGPURenderInterface::BlitPipelineType::BlendMasked; const bool masked = type == WebGPURenderInterface::BlitPipelineType::BlendMasked || type == WebGPURenderInterface::BlitPipelineType::ReplaceMasked; return gfx::pipeline_ref( make_pipeline_config(PipelineKind::SimpleFilter, colorFormat, sampleCount, VertexLayoutKind::Fullscreen, masked ? StencilMode::EqualKeep : (useStencil ? StencilMode::AlwaysKeep : StencilMode::None), blend ? BlendMode::Premultiplied : BlendMode::None)); } gfx::PipelineRef seed_resample_pipeline(wgpu::TextureFormat colorFormat, uint32_t sampleCount, bool useStencil) { return gfx::pipeline_ref( make_pipeline_config(PipelineKind::SeedResample, colorFormat, sampleCount, VertexLayoutKind::Fullscreen, useStencil ? StencilMode::AlwaysKeep : StencilMode::None, BlendMode::None)); } gfx::PipelineRef filter_pipeline(PipelineKind kind, wgpu::TextureFormat colorFormat, VertexLayoutKind vertexLayout = VertexLayoutKind::Fullscreen, BlendMode blendMode = BlendMode::None) { return gfx::pipeline_ref(make_pipeline_config(kind, colorFormat, 1, vertexLayout, StencilMode::None, blendMode)); } void queue_texture_upload_if_needed(ShaderTextureData& texture) { if (texture.m_uploaded || texture.m_pendingUpload.empty()) { return; } const wgpu::TexelCopyTextureInfo dst{ .texture = texture.m_texture, .aspect = wgpu::TextureAspect::All, }; gfx::queue_texture_upload_data(texture.m_pendingUpload.data(), texture.m_rowBytes, texture.m_size.height, dst, texture.m_size); texture.m_pendingUpload.clear(); texture.m_pendingUpload.shrink_to_fit(); texture.m_uploaded = true; } } // namespace Rml::CompiledGeometryHandle WebGPURenderInterface::CompileGeometry(Rml::Span vertices, Rml::Span indices) { auto* geometryData = new ShaderGeometryData(); geometryData->vertices.assign(vertices.begin(), vertices.end()); geometryData->indices.reserve(indices.size()); for (const int index : indices) { geometryData->indices.push_back(static_cast(index)); } return reinterpret_cast(geometryData); } void WebGPURenderInterface::RenderGeometry(Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation, Rml::TextureHandle texture) { DrawGeometry(geometry, translation, texture, geometry_pipeline(m_renderTargetFormat, LayerSampleCount, m_clipMaskEnabled ? PipelineType::Masked : PipelineType::Normal)); } void WebGPURenderInterface::DrawGeometry(Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation, Rml::TextureHandle texture, gfx::PipelineRef pipeline) { EnsureActiveLayerPass("RmlUi resumed geometry layer pass"); if (!m_passActive) { return; } auto* geometryData = reinterpret_cast(geometry); auto* textureData = reinterpret_cast(texture != 0 ? texture : m_nullTexture); if (geometryData == nullptr || textureData == nullptr) { return; } queue_texture_upload_if_needed(*textureData); const auto uniformRange = SetupRenderState(translation); const auto vertexRange = gfx::push_verts(reinterpret_cast(geometryData->vertices.data()), geometryData->vertices.size() * sizeof(Rml::Vertex), rmlBufferOffsetAlignment); const auto indexRange = gfx::push_indices(reinterpret_cast(geometryData->indices.data()), geometryData->indices.size() * sizeof(uint32_t), rmlBufferOffsetAlignment); gfx::push_draw_command(DrawData{ .pipeline = pipeline, .vertexRange = vertexRange, .indexRange = indexRange, .uniformRange = uniformRange, .bindGroup1 = texture_bind_group_ref(textureData->m_textureView), .drawKind = static_cast(DrawKind::Geometry), .indexCount = static_cast(geometryData->indices.size()), .stencilRef = m_stencilRef, .blendConstant = {0.f, 0.f, 0.f, 0.f}, .hasBlendConstant = 1, }); } void WebGPURenderInterface::ReleaseGeometry(Rml::CompiledGeometryHandle geometry) { delete reinterpret_cast(geometry); } Rml::TextureHandle WebGPURenderInterface::LoadTexture(Rml::Vector2i& dimensions, const Rml::String& source) { if (const auto runtimeTexture = load_runtime_texture(source)) { const size_t size = static_cast(runtimeTexture->width) * static_cast(runtimeTexture->height) * 4; if (runtimeTexture->width == 0 || runtimeTexture->height == 0 || runtimeTexture->rgba8.size() < size) { Log.error("Runtime texture provider returned invalid texture! Path: {}", source); return 0; } const auto* texels = reinterpret_cast(runtimeTexture->rgba8.data()); std::vector premultiplied; if (!runtimeTexture->premultipliedAlpha) { premultiplied.assign(texels, texels + size); for (size_t offset = 0; offset < premultiplied.size(); offset += 4) { const uint8_t alpha = premultiplied[offset + 3]; for (size_t channel = 0; channel < 3; ++channel) { premultiplied[offset + channel] = static_cast( (static_cast(premultiplied[offset + channel]) * static_cast(alpha)) / 255); } } texels = premultiplied.data(); } dimensions.x = static_cast(runtimeTexture->width); dimensions.y = static_cast(runtimeTexture->height); return GenerateTexture({texels, size}, dimensions); } // load texels from image source const auto image = get_image(source); if (image.size == 0) { Log.error("Failed to load texture! Path: {}", source); return 0; } dimensions.x = static_cast(image.width); dimensions.y = static_cast(image.height); return GenerateTexture({image.data.get(), image.size}, dimensions); } Rml::TextureHandle WebGPURenderInterface::GenerateTexture(Rml::Span source, Rml::Vector2i source_dimensions) { auto* texData = new ShaderTextureData(); const wgpu::Extent3D size{ .width = static_cast(source_dimensions.x), .height = static_cast(source_dimensions.y), .depthOrArrayLayers = 1, }; const wgpu::TextureDescriptor textureDesc{ .label = "RmlUi Texture", .usage = wgpu::TextureUsage::CopyDst | wgpu::TextureUsage::TextureBinding, .dimension = wgpu::TextureDimension::e2D, .size = size, .format = wgpu::TextureFormat::RGBA8Unorm, }; texData->m_texture = webgpu::g_device.CreateTexture(&textureDesc); texData->m_textureView = texData->m_texture.CreateView(nullptr); constexpr uint32_t BytesPerPixel = 4; texData->m_size = size; texData->m_rowBytes = static_cast(source_dimensions.x) * BytesPerPixel; texData->m_pendingUpload.assign(source.begin(), source.end()); texData->m_uploaded = texData->m_pendingUpload.empty(); return reinterpret_cast(texData); } void WebGPURenderInterface::ReleaseTexture(Rml::TextureHandle texture) { delete reinterpret_cast(texture); } void WebGPURenderInterface::EnableScissorRegion(bool enable) { m_enableScissorRegion = enable; ApplyScissorRegion(); } void WebGPURenderInterface::SetScissorRegion(Rml::Rectanglei region) { m_scissorRegion = region; ApplyScissorRegion(); } void WebGPURenderInterface::ApplyScissorRegion() { if (!m_passActive) { return; } ApplyScissorRegion(GetActiveScissorRegion()); } void WebGPURenderInterface::ApplyScissorRegion(Rml::Rectanglei region) const { const int maxWidth = static_cast(m_frameSize.width); const int maxHeight = static_cast(m_frameSize.height); const uint32_t x = static_cast(std::clamp(region.Left(), 0, maxWidth)); const uint32_t y = static_cast(std::clamp(region.Top(), 0, maxHeight)); const uint32_t width = static_cast(std::clamp(region.Width(), 0, maxWidth - static_cast(x))); const uint32_t height = static_cast(std::clamp(region.Height(), 0, maxHeight - static_cast(y))); gfx::set_scissor( {static_cast(x), static_cast(y), static_cast(width), static_cast(height)}); } Rml::Rectanglei WebGPURenderInterface::GetActiveScissorRegion() const { const int maxWidth = m_windowSize.x > 0 ? m_windowSize.x : static_cast(m_frameSize.width); const int maxHeight = m_windowSize.y > 0 ? m_windowSize.y : static_cast(m_frameSize.height); if (!m_enableScissorRegion || !m_scissorRegion.Valid()) { return Rml::Rectanglei::FromSize({std::max(maxWidth, 0), std::max(maxHeight, 0)}); } const int left = std::clamp(m_scissorRegion.Left(), 0, maxWidth); const int top = std::clamp(m_scissorRegion.Top(), 0, maxHeight); const int right = std::clamp(m_scissorRegion.Right(), left, maxWidth); const int bottom = std::clamp(m_scissorRegion.Bottom(), top, maxHeight); return Rml::Rectanglei::FromCorners({left, top}, {right, bottom}); } void WebGPURenderInterface::EnableClipMask(bool enable) { m_clipMaskEnabled = enable; if (!enable) { m_stencilRef = 0; } } void WebGPURenderInterface::RenderToClipMask(Rml::ClipMaskOperation operation, Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation) { EnsureActiveLayerPass("RmlUi resumed clip mask layer pass"); if (!m_passActive) { return; } EnsureClipResetGeometry(); const Rml::Matrix4f prevMatrix = m_translationMatrix; switch (operation) { case Rml::ClipMaskOperation::Set: m_translationMatrix = Rml::Matrix4f::Identity(); m_stencilRef = 0; DrawGeometry(m_clipResetGeometry, {}, 0, geometry_pipeline(m_renderTargetFormat, LayerSampleCount, PipelineType::ClipReplace)); m_translationMatrix = prevMatrix; m_stencilRef = 1; DrawGeometry(geometry, translation, 0, geometry_pipeline(m_renderTargetFormat, LayerSampleCount, PipelineType::ClipReplace)); break; case Rml::ClipMaskOperation::SetInverse: m_translationMatrix = Rml::Matrix4f::Identity(); m_stencilRef = 1; DrawGeometry(m_clipResetGeometry, {}, 0, geometry_pipeline(m_renderTargetFormat, LayerSampleCount, PipelineType::ClipReplace)); m_translationMatrix = prevMatrix; m_stencilRef = 1; m_stencilRef = 0; DrawGeometry(geometry, translation, 0, geometry_pipeline(m_renderTargetFormat, LayerSampleCount, PipelineType::ClipReplace)); m_stencilRef = 1; break; case Rml::ClipMaskOperation::Intersect: if (m_stencilRef == std::numeric_limits::max()) { Log.warn("RmlUi clip mask nesting exceeded stencil capacity; further nested clipping may be incorrect"); break; } DrawGeometry(geometry, translation, 0, geometry_pipeline(m_renderTargetFormat, LayerSampleCount, PipelineType::ClipIntersect)); ++m_stencilRef; break; } } void WebGPURenderInterface::SetTransform(const Rml::Matrix4f* transform) { if (transform == nullptr) { m_translationMatrix = Rml::Matrix4f::Identity(); } else { m_translationMatrix = *transform; } } void WebGPURenderInterface::EnsureRenderTarget(RenderTarget& target, const char* label, const wgpu::Extent3D& size, bool multisampled) { const bool useMultisampling = multisampled && LayerSampleCount > 1; if (target.view && target.size == size && static_cast(target.multisampleView) == useMultisampling) { return; } target = {}; target.size = size; const wgpu::TextureDescriptor textureDesc{ .label = label, .usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc, .dimension = wgpu::TextureDimension::e2D, .size = size, .format = m_renderTargetFormat, .mipLevelCount = 1, .sampleCount = 1, }; target.texture = webgpu::g_device.CreateTexture(&textureDesc); target.view = target.texture.CreateView(nullptr); if (useMultisampling) { const wgpu::TextureDescriptor multisampleTextureDesc{ .label = "RmlUi Multisampled Render Target", .usage = wgpu::TextureUsage::RenderAttachment, .dimension = wgpu::TextureDimension::e2D, .size = size, .format = m_renderTargetFormat, .mipLevelCount = 1, .sampleCount = LayerSampleCount, }; target.multisampleTexture = webgpu::g_device.CreateTexture(&multisampleTextureDesc); target.multisampleView = target.multisampleTexture.CreateView(nullptr); } } void WebGPURenderInterface::EnsureFrameTargets(const wgpu::Extent3D& size) { if (m_layers.empty()) { m_layers.resize(1); } EnsureRenderTarget(m_postprocessTargets[0], "RmlUi Postprocess A", size); EnsureRenderTarget(m_postprocessTargets[1], "RmlUi Postprocess B", size); EnsureRenderTarget(m_postprocessTargets[2], "RmlUi Postprocess C", size); EnsureRenderTarget(m_blendMaskTarget, "RmlUi Blend Mask", size); } TexCoordLimits WebGPURenderInterface::GetPostprocessTexCoordLimits() const { return GetPostprocessTexCoordLimits(GetActiveScissorRegion()); } TexCoordLimits WebGPURenderInterface::GetPostprocessTexCoordLimits(Rml::Rectanglei region) const { const float viewportWidth = std::max(m_viewport.width, 1.f); const float viewportHeight = std::max(m_viewport.height, 1.f); const int maxWidth = m_windowSize.x > 0 ? m_windowSize.x : static_cast(viewportWidth); const int maxHeight = m_windowSize.y > 0 ? m_windowSize.y : static_cast(viewportHeight); const int left = std::clamp(region.Left(), 0, maxWidth); const int top = std::clamp(region.Top(), 0, maxHeight); const int right = std::clamp(region.Right(), left, maxWidth); const int bottom = std::clamp(region.Bottom(), top, maxHeight); if (right <= left || bottom <= top) { const Rml::Vector2f empty = {0.5f / viewportWidth, 0.5f / viewportHeight}; return { .min = empty, .max = empty, }; } const Rml::Vector2f viewportOrigin{m_viewport.left, m_viewport.top}; const Rml::Vector2f viewportSize{viewportWidth, viewportHeight}; const Rml::Vector2f minLimit{0.5f / viewportWidth, 0.5f / viewportHeight}; const Rml::Vector2f maxLimit{1.f - minLimit.x, 1.f - minLimit.y}; const auto clamp_limits = [minLimit, maxLimit](Rml::Vector2f value) { return Rml::Vector2f{ std::clamp(value.x, minLimit.x, maxLimit.x), std::clamp(value.y, minLimit.y, maxLimit.y), }; }; const Rml::Vector2f min = (Rml::Vector2f(static_cast(left), static_cast(top)) - viewportOrigin + Rml::Vector2f(0.5f)) / viewportSize; const Rml::Vector2f max = (Rml::Vector2f(static_cast(right), static_cast(bottom)) - viewportOrigin - Rml::Vector2f(0.5f)) / viewportSize; return { .min = clamp_limits(min), .max = clamp_limits(max), }; } void WebGPURenderInterface::ApplyViewport() const { gfx::set_viewport(m_viewport); } void WebGPURenderInterface::ApplyFullFrameScissor() const { gfx::set_scissor({0, 0, static_cast(m_frameSize.width), static_cast(m_frameSize.height)}); } void WebGPURenderInterface::BeginRenderTargetPass(const wgpu::TextureView& view, wgpu::LoadOp loadOp, const char* label, bool clearStencil) { EndActivePass(); gfx::begin_color_pass({ .label = label, .colorView = view, .depthStencilView = clearStencil ? GetClipMaskStencilView(m_frameSize) : wgpu::TextureView{}, .targetSize = m_frameSize, .sampleCount = 1, .colorLoadOp = loadOp, .colorStoreOp = wgpu::StoreOp::Store, .clearColor = {0.f, 0.f, 0.f, 0.f}, .hasStencil = clearStencil, .stencilLoadOp = clearStencil ? wgpu::LoadOp::Clear : wgpu::LoadOp::Undefined, .stencilStoreOp = clearStencil ? wgpu::StoreOp::Store : wgpu::StoreOp::Undefined, .stencilClearValue = 0, }); m_passActive = true; ApplyViewport(); ApplyScissorRegion(); } void WebGPURenderInterface::BeginLayerPass(Rml::LayerHandle layer, wgpu::LoadOp loadOp, const char* label, bool clearStencil, bool resolveMultisampled) { m_activeLayer = layer; EndActivePass(); const RenderTarget& target = m_layers[layer]; const bool multisampled = LayerSampleCount > 1 && static_cast(target.multisampleView); gfx::begin_color_pass({ .label = label, .colorView = multisampled ? target.multisampleView : target.view, .resolveView = multisampled && resolveMultisampled ? target.view : wgpu::TextureView{}, .depthStencilView = GetClipMaskStencilView(m_frameSize), .targetSize = m_frameSize, .sampleCount = multisampled ? LayerSampleCount : 1, .colorLoadOp = loadOp, .colorStoreOp = wgpu::StoreOp::Store, .clearColor = {0.f, 0.f, 0.f, 0.f}, .hasStencil = true, .stencilLoadOp = clearStencil ? wgpu::LoadOp::Clear : wgpu::LoadOp::Load, .stencilStoreOp = wgpu::StoreOp::Store, .stencilClearValue = 0, }); m_passActive = true; ApplyViewport(); ApplyScissorRegion(); } void WebGPURenderInterface::EnsureFrameRenderingStarted() { if (m_frameRenderingStarted || !m_frameActive || m_layers.empty() || !m_layers[0].view) { return; } m_frameRenderingStarted = true; if (m_baseLayerContent == BaseLayerContent::Transparent) { BeginLayerPass(0, wgpu::LoadOp::Clear, "RmlUi transparent base layer pass", true); return; } const auto seedBindGroup = texture_bind_group_ref(m_frameSeedView); const auto seedUniformRange = gfx::push_uniform(SeedResampleUniformBlock{ .samplerMode = sampler_mode(), .frameWidth = static_cast(m_frameSize.width), .frameHeight = static_cast(m_frameSize.height), }); if (m_layers[0].multisampleView) { BeginLayerPass(0, wgpu::LoadOp::Clear, "RmlUi base layer seed pass", true); ApplyFullFrameScissor(); DrawFullscreenTexture(seedBindGroup, seed_resample_pipeline(m_renderTargetFormat, LayerSampleCount, true), uniform_bind_group_ref(), seedUniformRange); BeginLayerPass(0, wgpu::LoadOp::Load, "RmlUi base layer pass"); } else { BeginRenderTargetPass(m_layers[0].view, wgpu::LoadOp::Clear, "RmlUi game frame copy pass"); ApplyFullFrameScissor(); DrawFullscreenTexture(seedBindGroup, seed_resample_pipeline(m_renderTargetFormat, 1, false), uniform_bind_group_ref(), seedUniformRange); BeginLayerPass(0, wgpu::LoadOp::Load, "RmlUi base layer pass", true); } } void WebGPURenderInterface::EnsureActiveLayerPass(const char* label) { EnsureFrameRenderingStarted(); if (m_passActive || !m_frameActive || m_activeLayer >= m_layers.size() || !m_layers[m_activeLayer].view) { return; } BeginLayerPass(m_activeLayer, wgpu::LoadOp::Load, label); } void WebGPURenderInterface::EndActivePass() { if (m_passActive) { gfx::end_color_pass(); m_passActive = false; } } void WebGPURenderInterface::DrawFullscreenTexture(gfx::BindGroupRef bindGroup, gfx::PipelineRef pipeline, gfx::BindGroupRef extraBindGroup, gfx::Range extraUniformRange, bool extraBindGroupHasDynamicOffset, std::array blendConstant, bool hasBlendConstant) { if (!m_passActive) { return; } gfx::push_draw_command(DrawData{ .pipeline = pipeline, .uniformRange = {}, .bindGroup1 = bindGroup, .bindGroup2 = extraBindGroup, .bindGroup2DynamicOffset = extraUniformRange.offset, .dynamicBindGroupMask = extraBindGroup != 0 && extraBindGroupHasDynamicOffset ? (1u << 2u) : 0u, .drawKind = static_cast(DrawKind::Fullscreen), .vertexCount = 3, .stencilRef = m_stencilRef, .blendConstant = blendConstant, .hasBlendConstant = hasBlendConstant ? 1u : 0u, }); } void WebGPURenderInterface::CompositeToTarget(gfx::BindGroupRef bindGroup, const wgpu::TextureView& view, wgpu::LoadOp loadOp, gfx::PipelineRef pipeline, const char* label, gfx::BindGroupRef extraBindGroup, gfx::Range extraUniformRange, bool extraBindGroupHasDynamicOffset, std::array blendConstant, bool hasBlendConstant) { BeginRenderTargetPass(view, loadOp, label); DrawFullscreenTexture(bindGroup, pipeline, extraBindGroup, extraUniformRange, extraBindGroupHasDynamicOffset, blendConstant, hasBlendConstant); } void WebGPURenderInterface::RenderBlur(float sigma, const RenderTarget& sourceDestination, const RenderTarget& temp) { sigma = std::max(sigma, 0.f); if (sigma < 0.5f) { return; } const Rml::Rectanglei originalScissor = GetActiveScissorRegion(); if (originalScissor.Width() <= 0 || originalScissor.Height() <= 0) { return; } auto write_blur_uniform = [&](Rml::Vector2f texelOffset, Rml::Rectanglei texCoordRegion, float radius, Rml::Vector4f weights) { const TexCoordLimits texCoordLimits = GetPostprocessTexCoordLimits(texCoordRegion); const BlurUniformBlock uniform{ .texelOffset = texelOffset, .radius = radius, .padding = 0.f, .texCoordMin = texCoordLimits.min, .texCoordMax = texCoordLimits.max, .weights = weights, }; return gfx::push_uniform(uniform); }; auto write_region_blit_uniform = [&](Rml::Rectanglei texCoordRegion, Rml::Vector4f weights) { const float viewportWidth = std::max(m_viewport.width, 1.f); const float viewportHeight = std::max(m_viewport.height, 1.f); const int maxWidth = m_windowSize.x > 0 ? m_windowSize.x : static_cast(viewportWidth); const int maxHeight = m_windowSize.y > 0 ? m_windowSize.y : static_cast(viewportHeight); const int left = std::clamp(texCoordRegion.Left(), 0, maxWidth); const int top = std::clamp(texCoordRegion.Top(), 0, maxHeight); const int right = std::clamp(texCoordRegion.Right(), left, maxWidth); const int bottom = std::clamp(texCoordRegion.Bottom(), top, maxHeight); const Rml::Vector2f viewportOrigin{m_viewport.left, m_viewport.top}; const Rml::Vector2f viewportSize{viewportWidth, viewportHeight}; const BlurUniformBlock uniform{ .texelOffset = {}, .radius = 0.f, .padding = 0.f, .texCoordMin = (Rml::Vector2f(static_cast(left), static_cast(top)) - viewportOrigin) / viewportSize, .texCoordMax = (Rml::Vector2f(static_cast(right), static_cast(bottom)) - viewportOrigin) / viewportSize, .weights = weights, }; return gfx::push_uniform(uniform); }; int passLevel = 0; sigma_to_params(sigma, passLevel, sigma); if (sigma == 0.f) { return; } Rml::Rectanglei scissor = originalScissor; const auto weights = blur_weights(sigma, MaxBlurRadius); constexpr auto radius = static_cast(MaxBlurRadius); for (int i = 0; i < passLevel; ++i) { scissor = downsample_scissor(scissor); const bool fromSource = (i % 2) == 0; const RenderTarget& source = fromSource ? sourceDestination : temp; const RenderTarget& destination = fromSource ? temp : sourceDestination; BeginRenderTargetPass(destination.view, wgpu::LoadOp::Clear, "RmlUi blur downsample pass"); gfx::set_viewport({m_viewport.left, m_viewport.top, std::max(m_viewport.width * 0.5f, 1.f), std::max(m_viewport.height * 0.5f, 1.f), m_viewport.znear, m_viewport.zfar}); ApplyScissorRegion(scissor); DrawFullscreenTexture(texture_bind_group_ref(source.view), blit_pipeline(m_renderTargetFormat, 1, BlitPipelineType::Replace, false)); } if ((passLevel % 2) == 0) { BeginRenderTargetPass(temp.view, wgpu::LoadOp::Clear, "RmlUi blur transfer pass"); ApplyViewport(); ApplyScissorRegion(scissor); DrawFullscreenTexture(texture_bind_group_ref(sourceDestination.view), blit_pipeline(m_renderTargetFormat, 1, BlitPipelineType::Replace, false)); } const auto verticalRange = write_blur_uniform({0.f, 1.f / std::max(m_viewport.height, 1.f)}, scissor, radius, weights); BeginRenderTargetPass(sourceDestination.view, wgpu::LoadOp::Clear, "RmlUi vertical blur pass"); ApplyScissorRegion(scissor); DrawFullscreenTexture(texture_bind_group_ref(temp.view), filter_pipeline(PipelineKind::Blur, m_renderTargetFormat, VertexLayoutKind::BlurFullscreen), uniform_bind_group_ref(), verticalRange); const auto horizontalRange = write_blur_uniform({1.f / std::max(m_viewport.width, 1.f), 0.f}, scissor, radius, weights); BeginRenderTargetPass(temp.view, wgpu::LoadOp::Clear, "RmlUi horizontal blur pass"); ApplyScissorRegion(scissor); DrawFullscreenTexture(texture_bind_group_ref(sourceDestination.view), filter_pipeline(PipelineKind::Blur, m_renderTargetFormat, VertexLayoutKind::BlurFullscreen), uniform_bind_group_ref(), horizontalRange); const auto upscaleRange = write_region_blit_uniform(scissor, weights); BeginRenderTargetPass(sourceDestination.view, wgpu::LoadOp::Clear, "RmlUi blur upscale pass"); gfx::set_viewport({static_cast(originalScissor.Left()), static_cast(originalScissor.Top()), static_cast(std::max(originalScissor.Width(), 1)), static_cast(std::max(originalScissor.Height(), 1)), 0.f, 1.f}); ApplyScissorRegion(originalScissor); DrawFullscreenTexture(texture_bind_group_ref(temp.view), filter_pipeline(PipelineKind::RegionBlit, m_renderTargetFormat), uniform_bind_group_ref(), upscaleRange); const auto targetMin = scissor.p0 * (1 << passLevel); const auto targetMax = scissor.p1 * (1 << passLevel); const auto targetRegion = Rml::Rectanglei::FromCorners(targetMin, targetMax); if (targetRegion.p0 != originalScissor.p0 || targetRegion.p1 != originalScissor.p1) { BeginRenderTargetPass(sourceDestination.view, wgpu::LoadOp::Load, "RmlUi blur power-of-two upscale pass"); gfx::set_viewport({static_cast(targetRegion.Left()), static_cast(targetRegion.Top()), static_cast(std::max(targetRegion.Width(), 1)), static_cast(std::max(targetRegion.Height(), 1)), 0.f, 1.f}); ApplyScissorRegion(targetRegion); DrawFullscreenTexture(texture_bind_group_ref(temp.view), filter_pipeline(PipelineKind::RegionBlit, m_renderTargetFormat), uniform_bind_group_ref(), upscaleRange); } } size_t WebGPURenderInterface::RenderFilters(Rml::Span filters) { size_t sourceIndex = 0; constexpr size_t tempIndex = 2; for (Rml::CompiledFilterHandle filterHandle : filters) { const auto* filter = reinterpret_cast(filterHandle); if (filter == nullptr) { continue; } const size_t scratchIndex = sourceIndex == 0 ? 1 : 0; switch (filter->type) { case FilterType::Opacity: { const SimpleFilterUniformBlock simpleFilterUniform{ .matrix = to_shader_matrix(Rml::Matrix4f::Identity()), .opacity = {filter->opacity, 0.f, 0.f, 0.f}, }; const auto simpleFilterRange = gfx::push_uniform(simpleFilterUniform); BeginRenderTargetPass(m_postprocessTargets[scratchIndex].view, wgpu::LoadOp::Clear, "RmlUi opacity pass"); DrawFullscreenTexture(texture_bind_group_ref(m_postprocessTargets[sourceIndex].view), filter_pipeline(PipelineKind::SimpleFilter, m_renderTargetFormat), uniform_bind_group_ref(), simpleFilterRange); sourceIndex = scratchIndex; break; } case FilterType::Blur: RenderBlur(filter->sigma, m_postprocessTargets[sourceIndex], m_postprocessTargets[tempIndex]); break; case FilterType::DropShadow: { TexCoordLimits texCoordLimits = GetPostprocessTexCoordLimits(); if (filter->offset.x > 0.f) texCoordLimits.min.x += 1.f / std::max(m_viewport.width, 1.f); else if (filter->offset.x < 0.f) texCoordLimits.max.x -= 1.f / std::max(m_viewport.width, 1.f); const auto to_float_color = [](Rml::ColourbPremultiplied color) { constexpr float InvByte = 1.f / 255.f; return Rml::Vector4f(static_cast(color.red) * InvByte, static_cast(color.green) * InvByte, static_cast(color.blue) * InvByte, static_cast(color.alpha) * InvByte); }; const DropShadowUniformBlock dropShadowUniform{ .color = to_float_color(filter->color), .uvOffset = { filter->offset.x / std::max(m_viewport.width, 1.f), filter->offset.y / std::max(m_viewport.height, 1.f), }, .texCoordMin = texCoordLimits.min, .texCoordMax = texCoordLimits.max, }; const auto dropShadowRange = gfx::push_uniform(dropShadowUniform); CompositeToTarget(texture_bind_group_ref(m_postprocessTargets[sourceIndex].view), m_postprocessTargets[scratchIndex].view, wgpu::LoadOp::Clear, filter_pipeline(PipelineKind::DropShadow, m_renderTargetFormat), "RmlUi drop shadow pass", uniform_bind_group_ref(), dropShadowRange); RenderBlur(filter->sigma, m_postprocessTargets[scratchIndex], m_postprocessTargets[tempIndex]); CompositeToTarget(texture_bind_group_ref(m_postprocessTargets[sourceIndex].view), m_postprocessTargets[scratchIndex].view, wgpu::LoadOp::Load, blit_pipeline(m_renderTargetFormat, 1, BlitPipelineType::Blend, false), "RmlUi drop shadow source composite pass"); sourceIndex = scratchIndex; break; } case FilterType::ColorMatrix: { const SimpleFilterUniformBlock simpleFilterUniform{ .matrix = to_shader_matrix(filter->colorMatrix), .opacity = {1.f, 0.f, 0.f, 0.f}, }; const auto simpleFilterRange = gfx::push_uniform(simpleFilterUniform); CompositeToTarget(texture_bind_group_ref(m_postprocessTargets[sourceIndex].view), m_postprocessTargets[scratchIndex].view, wgpu::LoadOp::Clear, filter_pipeline(PipelineKind::SimpleFilter, m_renderTargetFormat), "RmlUi color matrix pass", uniform_bind_group_ref(), simpleFilterRange); sourceIndex = scratchIndex; break; } case FilterType::MaskImage: { CompositeToTarget(texture_bind_group_ref(m_postprocessTargets[sourceIndex].view), m_postprocessTargets[scratchIndex].view, wgpu::LoadOp::Clear, filter_pipeline(PipelineKind::MaskImage, m_renderTargetFormat), "RmlUi mask image pass", texture_bind_group_ref(m_blendMaskTarget.view), {}, false); sourceIndex = scratchIndex; break; } } } EndActivePass(); return sourceIndex; } void WebGPURenderInterface::BeginFrame(const webgpu::TextureWithSampler& target, const webgpu::TextureWithSampler& sceneTarget, BaseLayerContent baseLayerContent) { m_frameSeedView = sceneTarget.view; m_frameSize = target.size; m_viewport = { .left = 0.f, .top = 0.f, .width = static_cast(target.size.width), .height = static_cast(target.size.height), .znear = 0.f, .zfar = 1.f, }; m_nextLayer = 1; m_layerStack = {0}; m_activeLayer = 0; m_frameRenderingStarted = false; m_frameActive = true; m_passActive = false; m_baseLayerContent = baseLayerContent; NewFrame(); EnsureFrameTargets(target.size); wgpu::Texture multisampleTexture; wgpu::TextureView multisampleView; if constexpr (LayerSampleCount > 1) { if (m_layers[0].multisampleView && m_layers[0].size == target.size) { multisampleTexture = m_layers[0].multisampleTexture; multisampleView = m_layers[0].multisampleView; } else { const wgpu::TextureDescriptor multisampleTextureDesc{ .label = "RmlUi Multisampled Base Layer", .usage = wgpu::TextureUsage::RenderAttachment, .dimension = wgpu::TextureDimension::e2D, .size = target.size, .format = m_renderTargetFormat, .mipLevelCount = 1, .sampleCount = LayerSampleCount, }; multisampleTexture = webgpu::g_device.CreateTexture(&multisampleTextureDesc); multisampleView = multisampleTexture.CreateView(nullptr); } } m_layers[0] = { .texture = target.texture, .view = target.view, .multisampleTexture = multisampleTexture, .multisampleView = multisampleView, .size = target.size, }; } bool WebGPURenderInterface::EndFrame() { const bool rendered = m_frameRenderingStarted; EndActivePass(); m_layerStack.clear(); if (!m_layers.empty()) { m_layers[0] = { .multisampleTexture = m_layers[0].multisampleTexture, .multisampleView = m_layers[0].multisampleView, .size = m_layers[0].size, }; } m_frameActive = false; m_frameSeedView = {}; m_frameRenderingStarted = false; m_baseLayerContent = BaseLayerContent::Transparent; return rendered; } Rml::LayerHandle WebGPURenderInterface::PushLayer() { EnsureFrameRenderingStarted(); const Rml::LayerHandle layer = m_nextLayer++; if (static_cast(layer) >= m_layers.size()) { m_layers.resize(static_cast(layer) + 1); } EnsureRenderTarget(m_layers[static_cast(layer)], "RmlUi Layer", m_frameSize, true); m_layerStack.push_back(layer); BeginLayerPass(layer, wgpu::LoadOp::Clear, "RmlUi pushed layer pass"); return layer; } void WebGPURenderInterface::CompositeLayers(Rml::LayerHandle source, Rml::LayerHandle destination, Rml::BlendMode blendMode, Rml::Span filters) { if (source >= m_layers.size() || destination >= m_layers.size()) { Log.warn("RmlUi requested composite with invalid layer handles: {} -> {}", source, destination); return; } EnsureFrameRenderingStarted(); const Rml::LayerHandle topLayer = m_layerStack.empty() ? 0 : m_layerStack.back(); const std::vector activeFilterHandles = active_filters(filters); const Rml::Span activeFilters{activeFilterHandles.data(), activeFilterHandles.size()}; const bool replace = blendMode == Rml::BlendMode::Replace; const BlitPipelineType pipelineType = replace ? (m_clipMaskEnabled ? BlitPipelineType::ReplaceMasked : BlitPipelineType::Replace) : (m_clipMaskEnabled ? BlitPipelineType::BlendMasked : BlitPipelineType::Blend); if (activeFilters.empty() && source != destination) { BeginLayerPass(destination, wgpu::LoadOp::Load, "RmlUi layer direct composite pass"); DrawFullscreenTexture(texture_bind_group_ref(m_layers[source].view), blit_pipeline(m_renderTargetFormat, LayerSampleCount, pipelineType, true)); if (destination != topLayer) { EndActivePass(); m_activeLayer = topLayer; } return; } Rml::Matrix4f simpleFilterMatrix; float simpleFilterOpacity = 1.f; if (source != destination && try_fold_simple_filters(activeFilters, simpleFilterMatrix, simpleFilterOpacity)) { const SimpleFilterUniformBlock simpleFilterUniform{ .matrix = to_shader_matrix(simpleFilterMatrix), .opacity = {simpleFilterOpacity, 0.f, 0.f, 0.f}, }; const auto simpleFilterRange = gfx::push_uniform(simpleFilterUniform); BeginLayerPass(destination, wgpu::LoadOp::Load, "RmlUi simple filter composite pass"); DrawFullscreenTexture(texture_bind_group_ref(m_layers[source].view), simple_filter_pipeline(m_renderTargetFormat, LayerSampleCount, pipelineType, true), uniform_bind_group_ref(), simpleFilterRange); if (destination != topLayer) { EndActivePass(); m_activeLayer = topLayer; } return; } CompositeToTarget(texture_bind_group_ref(m_layers[source].view), m_postprocessTargets[0].view, wgpu::LoadOp::Clear, blit_pipeline(m_renderTargetFormat, 1, BlitPipelineType::Replace, false), "RmlUi layer copy pass"); const size_t filteredIndex = RenderFilters(activeFilters); BeginLayerPass(destination, wgpu::LoadOp::Load, "RmlUi layer composite pass"); DrawFullscreenTexture(texture_bind_group_ref(m_postprocessTargets[filteredIndex].view), blit_pipeline(m_renderTargetFormat, LayerSampleCount, pipelineType, true)); if (destination != topLayer) { EndActivePass(); m_activeLayer = topLayer; } } void WebGPURenderInterface::PopLayer() { if (m_layerStack.size() <= 1) { Log.warn("RmlUi requested PopLayer with no pushed layer"); return; } m_layerStack.pop_back(); EndActivePass(); m_activeLayer = m_layerStack.back(); } Rml::TextureHandle WebGPURenderInterface::SaveLayerAsTexture() { if (!m_frameActive) { Log.warn("RmlUi requested SaveLayerAsTexture outside a frame"); return 0; } if (!m_scissorRegion.Valid()) { Log.warn("RmlUi requested SaveLayerAsTexture without a valid scissor region"); return 0; } EnsureFrameRenderingStarted(); const Rml::LayerHandle layer = m_layerStack.empty() ? m_activeLayer : m_layerStack.back(); if (layer >= m_layers.size() || !m_layers[layer].texture) { Log.warn("RmlUi requested SaveLayerAsTexture with invalid layer handle {}", layer); return 0; } const int left = std::clamp(m_scissorRegion.Left(), 0, static_cast(m_frameSize.width)); const int top = std::clamp(m_scissorRegion.Top(), 0, static_cast(m_frameSize.height)); const int right = std::clamp(m_scissorRegion.Right(), left, static_cast(m_frameSize.width)); const int bottom = std::clamp(m_scissorRegion.Bottom(), top, static_cast(m_frameSize.height)); if (right <= left || bottom <= top) { return 0; } auto* texData = new ShaderTextureData(); const wgpu::Extent3D textureSize{ .width = static_cast(right - left), .height = static_cast(bottom - top), .depthOrArrayLayers = 1, }; const wgpu::TextureDescriptor textureDesc{ .label = "RmlUi Saved Layer Texture", .usage = wgpu::TextureUsage::CopyDst | wgpu::TextureUsage::TextureBinding, .dimension = wgpu::TextureDimension::e2D, .size = textureSize, .format = m_renderTargetFormat, .mipLevelCount = 1, .sampleCount = 1, }; texData->m_texture = webgpu::g_device.CreateTexture(&textureDesc); texData->m_textureView = texData->m_texture.CreateView(nullptr); texData->m_size = textureSize; texData->m_rowBytes = textureSize.width * 4; texData->m_uploaded = true; EndActivePass(); const wgpu::TexelCopyTextureInfo src{ .texture = m_layers[layer].texture, .origin = wgpu::Origin3D{ .x = static_cast(left), .y = static_cast(top), }, .aspect = wgpu::TextureAspect::All, }; const wgpu::TexelCopyTextureInfo dst{ .texture = texData->m_texture, .aspect = wgpu::TextureAspect::All, }; gfx::queue_texture_copy(src, dst, textureSize); BeginLayerPass(layer, wgpu::LoadOp::Load, "RmlUi saved layer restore pass"); return reinterpret_cast(texData); } Rml::CompiledFilterHandle WebGPURenderInterface::SaveLayerAsMaskImage() { if (!m_frameActive) { Log.warn("RmlUi requested SaveLayerAsMaskImage outside a frame"); return {}; } const Rml::LayerHandle layer = m_layerStack.empty() ? m_activeLayer : m_layerStack.back(); if (layer >= m_layers.size() || !m_layers[layer].texture) { Log.warn("RmlUi requested SaveLayerAsMaskImage with invalid layer handle {}", layer); return {}; } EnsureFrameRenderingStarted(); CompositeToTarget(texture_bind_group_ref(m_layers[layer].view), m_postprocessTargets[0].view, wgpu::LoadOp::Clear, blit_pipeline(m_renderTargetFormat, 1, BlitPipelineType::Replace, false), "RmlUi mask source copy pass"); CompositeToTarget(texture_bind_group_ref(m_postprocessTargets[0].view), m_blendMaskTarget.view, wgpu::LoadOp::Clear, blit_pipeline(m_renderTargetFormat, 1, BlitPipelineType::Replace, false), "RmlUi mask image save pass"); BeginLayerPass(layer, wgpu::LoadOp::Load, "RmlUi mask image restore pass"); auto* filter = new CompiledFilter{ .type = FilterType::MaskImage, }; return reinterpret_cast(filter); } Rml::CompiledFilterHandle WebGPURenderInterface::CompileFilter(const Rml::String& name, const Rml::Dictionary& parameters) { if (name == "opacity") { auto* filter = new CompiledFilter{ .type = FilterType::Opacity, .opacity = Rml::Get(parameters, "value", 1.0f), }; return reinterpret_cast(filter); } if (name == "blur") { auto* filter = new CompiledFilter{ .type = FilterType::Blur, .sigma = Rml::Get(parameters, "sigma", 1.0f), }; return reinterpret_cast(filter); } if (name == "drop-shadow") { auto* filter = new CompiledFilter{ .type = FilterType::DropShadow, .sigma = Rml::Get(parameters, "sigma", 0.f), .offset = Rml::Get(parameters, "offset", Rml::Vector2f(0.f)), .color = Rml::Get(parameters, "color", Rml::Colourb()).ToPremultiplied(), }; return reinterpret_cast(filter); } CompiledFilter colorMatrixFilter = { .type = FilterType::ColorMatrix, }; if (name == "brightness") { const float value = Rml::Get(parameters, "value", 1.0f); colorMatrixFilter.colorMatrix = Rml::Matrix4f::Diag(value, value, value, 1.f); } else if (name == "contrast") { const float value = Rml::Get(parameters, "value", 1.0f); const float grayness = 0.5f - 0.5f * value; colorMatrixFilter.colorMatrix = Rml::Matrix4f::Diag(value, value, value, 1.f); colorMatrixFilter.colorMatrix.SetColumn(3, Rml::Vector4f(grayness, grayness, grayness, 1.f)); } else if (name == "invert") { const float value = Rml::Math::Clamp(Rml::Get(parameters, "value", 1.0f), 0.f, 1.f); const float inverted = 1.f - 2.f * value; colorMatrixFilter.colorMatrix = Rml::Matrix4f::Diag(inverted, inverted, inverted, 1.f); colorMatrixFilter.colorMatrix.SetColumn(3, Rml::Vector4f(value, value, value, 1.f)); } else if (name == "grayscale") { const float value = Rml::Get(parameters, "value", 1.0f); const float revValue = 1.f - value; const Rml::Vector3f gray = value * Rml::Vector3f(0.2126f, 0.7152f, 0.0722f); colorMatrixFilter.colorMatrix = Rml::Matrix4f::FromRows({gray.x + revValue, gray.y, gray.z, 0.f}, {gray.x, gray.y + revValue, gray.z, 0.f}, {gray.x, gray.y, gray.z + revValue, 0.f}, {0.f, 0.f, 0.f, 1.f}); } else if (name == "sepia") { const float value = Rml::Get(parameters, "value", 1.0f); const float revValue = 1.f - value; const Rml::Vector3f rMix = value * Rml::Vector3f(0.393f, 0.769f, 0.189f); const Rml::Vector3f gMix = value * Rml::Vector3f(0.349f, 0.686f, 0.168f); const Rml::Vector3f bMix = value * Rml::Vector3f(0.272f, 0.534f, 0.131f); colorMatrixFilter.colorMatrix = Rml::Matrix4f::FromRows({rMix.x + revValue, rMix.y, rMix.z, 0.f}, {gMix.x, gMix.y + revValue, gMix.z, 0.f}, {bMix.x, bMix.y, bMix.z + revValue, 0.f}, {0.f, 0.f, 0.f, 1.f}); } else if (name == "hue-rotate") { const float value = Rml::Get(parameters, "value", 1.0f); const float s = Rml::Math::Sin(value); const float c = Rml::Math::Cos(value); colorMatrixFilter.colorMatrix = Rml::Matrix4f::FromRows( {0.213f + 0.787f * c - 0.213f * s, 0.715f - 0.715f * c - 0.715f * s, 0.072f - 0.072f * c + 0.928f * s, 0.f}, {0.213f - 0.213f * c + 0.143f * s, 0.715f + 0.285f * c + 0.140f * s, 0.072f - 0.072f * c - 0.283f * s, 0.f}, {0.213f - 0.213f * c - 0.787f * s, 0.715f - 0.715f * c + 0.715f * s, 0.072f + 0.928f * c + 0.072f * s, 0.f}, {0.f, 0.f, 0.f, 1.f}); } else if (name == "saturate") { const float value = Rml::Get(parameters, "value", 1.0f); colorMatrixFilter.colorMatrix = Rml::Matrix4f::FromRows( {0.213f + 0.787f * value, 0.715f - 0.715f * value, 0.072f - 0.072f * value, 0.f}, {0.213f - 0.213f * value, 0.715f + 0.285f * value, 0.072f - 0.072f * value, 0.f}, {0.213f - 0.213f * value, 0.715f - 0.715f * value, 0.072f + 0.928f * value, 0.f}, {0.f, 0.f, 0.f, 1.f}); } else { Log.warn("Unsupported RmlUi filter '{}'", name); return {}; } return reinterpret_cast(new CompiledFilter(std::move(colorMatrixFilter))); } void WebGPURenderInterface::ReleaseFilter(Rml::CompiledFilterHandle filter) { delete reinterpret_cast(filter); } Rml::CompiledShaderHandle WebGPURenderInterface::CompileShader(const Rml::String& name, const Rml::Dictionary& parameters) { const bool supportedGradient = name == "linear-gradient" || name == "radial-gradient" || name == "conic-gradient"; if (!supportedGradient) { Log.warn("Unsupported RmlUi shader '{}'", name); return {}; } const auto it = parameters.find("color_stop_list"); if (it == parameters.end() || it->second.GetType() != Rml::Variant::COLORSTOPLIST) { Log.warn("RmlUi shader '{}' missing color stop list", name); return {}; } const Rml::ColorStopList& colorStopList = it->second.GetReference(); const size_t numStops = std::min(colorStopList.size(), MaxGradientStops); if (numStops == 0) { return {}; } auto* shader = new CompiledShaderData(); const bool repeating = Rml::Get(parameters, "repeating", false); constexpr int32_t Linear = 0; constexpr int32_t Radial = 1; constexpr int32_t Conic = 2; constexpr int32_t RepeatingOffset = 3; if (name == "linear-gradient") { shader->gradient.function = Linear + (repeating ? RepeatingOffset : 0); shader->gradient.p = Rml::Get(parameters, "p0", Rml::Vector2f(0.f)); shader->gradient.v = Rml::Get(parameters, "p1", Rml::Vector2f(0.f)) - shader->gradient.p; } else if (name == "radial-gradient") { shader->gradient.function = Radial + (repeating ? RepeatingOffset : 0); shader->gradient.p = Rml::Get(parameters, "center", Rml::Vector2f(0.f)); shader->gradient.v = Rml::Vector2f(1.f) / Rml::Get(parameters, "radius", Rml::Vector2f(1.f)); } else if (name == "conic-gradient") { shader->gradient.function = Conic + (repeating ? RepeatingOffset : 0); shader->gradient.p = Rml::Get(parameters, "center", Rml::Vector2f(0.f)); const float angle = Rml::Get(parameters, "angle", 0.f); shader->gradient.v = {Rml::Math::Cos(angle), Rml::Math::Sin(angle)}; } else { Log.warn("Unsupported RmlUi shader '{}'", name); delete shader; return {}; } shader->gradient.numStops = static_cast(numStops); for (size_t i = 0; i < numStops; ++i) { const Rml::ColorStop& stop = colorStopList[i]; shader->gradient.stopColors[i] = to_colorf(stop.color); shader->gradient.stopPositions[i / 4][i % 4] = stop.position.number; } return reinterpret_cast(shader); } void WebGPURenderInterface::RenderShader(Rml::CompiledShaderHandle shader, Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation, Rml::TextureHandle) { const auto* shaderData = reinterpret_cast(shader); const auto* geometryData = reinterpret_cast(geometry); if (shaderData == nullptr || geometryData == nullptr) { return; } EnsureActiveLayerPass("RmlUi resumed shader layer pass"); if (!m_passActive) { return; } const auto uniformRange = SetupRenderState(translation); const auto shaderRange = gfx::push_uniform(shaderData->gradient); const auto vertexRange = gfx::push_verts(reinterpret_cast(geometryData->vertices.data()), geometryData->vertices.size() * sizeof(Rml::Vertex), rmlBufferOffsetAlignment); const auto indexRange = gfx::push_indices(reinterpret_cast(geometryData->indices.data()), geometryData->indices.size() * sizeof(uint32_t), rmlBufferOffsetAlignment); gfx::push_draw_command(DrawData{ .pipeline = gradient_pipeline(m_renderTargetFormat, LayerSampleCount, m_clipMaskEnabled), .vertexRange = vertexRange, .indexRange = indexRange, .uniformRange = uniformRange, .bindGroup1 = uniform_bind_group_ref(), .bindGroup1DynamicOffset = shaderRange.offset, .dynamicBindGroupMask = 1u << 1u, .drawKind = static_cast(DrawKind::Geometry), .indexCount = static_cast(geometryData->indices.size()), .stencilRef = m_stencilRef, .blendConstant = {0.f, 0.f, 0.f, 0.f}, .hasBlendConstant = 1, }); } void WebGPURenderInterface::ReleaseShader(Rml::CompiledShaderHandle shader) { delete reinterpret_cast(shader); } void WebGPURenderInterface::CreateDeviceObjects() { switch (m_renderTargetFormat) { case wgpu::TextureFormat::ASTC10x10UnormSrgb: case wgpu::TextureFormat::ASTC10x5UnormSrgb: case wgpu::TextureFormat::ASTC10x6UnormSrgb: case wgpu::TextureFormat::ASTC10x8UnormSrgb: case wgpu::TextureFormat::ASTC12x10UnormSrgb: case wgpu::TextureFormat::ASTC12x12UnormSrgb: case wgpu::TextureFormat::ASTC4x4UnormSrgb: case wgpu::TextureFormat::ASTC5x5UnormSrgb: case wgpu::TextureFormat::ASTC6x5UnormSrgb: case wgpu::TextureFormat::ASTC6x6UnormSrgb: case wgpu::TextureFormat::ASTC8x5UnormSrgb: case wgpu::TextureFormat::ASTC8x6UnormSrgb: case wgpu::TextureFormat::ASTC8x8UnormSrgb: case wgpu::TextureFormat::BC1RGBAUnormSrgb: case wgpu::TextureFormat::BC2RGBAUnormSrgb: case wgpu::TextureFormat::BC3RGBAUnormSrgb: case wgpu::TextureFormat::BC7RGBAUnormSrgb: case wgpu::TextureFormat::BGRA8UnormSrgb: case wgpu::TextureFormat::ETC2RGB8A1UnormSrgb: case wgpu::TextureFormat::ETC2RGB8UnormSrgb: case wgpu::TextureFormat::ETC2RGBA8UnormSrgb: case wgpu::TextureFormat::RGBA8UnormSrgb: m_gamma = 2.2f; break; default: m_gamma = 1.0f; } CreateNullTexture(); } gfx::Range WebGPURenderInterface::SetupRenderState(const Rml::Vector2f& translation) { const float L = 0.f; const float R = static_cast(std::max(m_windowSize.x, 1)); const float T = 0.f; const float B = static_cast(std::max(m_windowSize.y, 1)); const float near = -10000.f; const float far = 10000.f; const Rml::Matrix4f proj = Rml::Matrix4f::FromColumns( {2.0f / (R - L), 0.0f, 0.0f, 0.0f}, {0.0f, 2.0f / (T - B), 0.0f, 0.0f}, {0.0f, 0.0f, 1.0f / (far - near), 0.0f}, {(R + L) / (L - R), (T + B) / (B - T), -near / (far - near), 1.0f}); const UniformBlock ubo{ .MVP = proj * m_translationMatrix, .translation = {translation.x, translation.y, 0.0f, 1.0f}, .Gamma = m_gamma, }; return gfx::push_uniform(ubo); } void WebGPURenderInterface::CreateNullTexture() { constexpr std::array tex_bytes{0xFF, 0xFF, 0xFF, 0xFF}; const Rml::Span tex(tex_bytes.data(), tex_bytes.size()); m_nullTexture = GenerateTexture(tex, {1, 1}); } void WebGPURenderInterface::EnsureClipResetGeometry() { if (m_clipResetGeometry != 0 && m_clipResetGeometrySize == m_windowSize) { return; } if (m_clipResetGeometry != 0) { ReleaseGeometry(m_clipResetGeometry); m_clipResetGeometry = 0; } const float width = static_cast(std::max(m_windowSize.x, 1)); const float height = static_cast(std::max(m_windowSize.y, 1)); const auto colour = Rml::ColourbPremultiplied(255, 255, 255, 255); const std::array vertices{ Rml::Vertex{.position = {0.f, 0.f}, .colour = colour}, Rml::Vertex{.position = {width, 0.f}, .colour = colour}, Rml::Vertex{.position = {width, height}, .colour = colour}, Rml::Vertex{.position = {0.f, height}, .colour = colour}, }; const std::array indices = {0, 1, 2, 0, 2, 3}; m_clipResetGeometry = CompileGeometry({vertices.data(), vertices.size()}, {indices.data(), indices.size()}); m_clipResetGeometrySize = m_windowSize; } void WebGPURenderInterface::NewFrame() { m_clipMaskEnabled = false; m_stencilRef = 0; } wgpu::TextureView WebGPURenderInterface::GetClipMaskStencilView(const wgpu::Extent3D& size) { if (m_clipMaskStencilView && m_clipMaskStencilSize == size) { return m_clipMaskStencilView; } m_clipMaskStencilSize = size; const wgpu::TextureDescriptor textureDesc{ .label = "RmlUi Clip Mask Stencil", .usage = wgpu::TextureUsage::RenderAttachment, .dimension = wgpu::TextureDimension::e2D, .size = size, .format = ClipMaskStencilFormat, .mipLevelCount = 1, .sampleCount = LayerSampleCount, }; m_clipMaskStencilTexture = webgpu::g_device.CreateTexture(&textureDesc); constexpr wgpu::TextureViewDescriptor viewDesc{ .label = "RmlUi Clip Mask Stencil View", .format = ClipMaskStencilFormat, .dimension = wgpu::TextureViewDimension::e2D, .aspect = wgpu::TextureAspect::StencilOnly, }; m_clipMaskStencilView = m_clipMaskStencilTexture.CreateView(&viewDesc); return m_clipMaskStencilView; } } // namespace aurora::rmlui