From 71d84625573d0505ffbd10d0e810f7132443c276 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Thu, 4 Jun 2026 23:43:13 -0600 Subject: [PATCH] Introduce render worker & revamp RmlUi backend --- cmake/aurora_gx.cmake | 8 + lib/aurora.cpp | 165 +-- lib/device.cpp | 2 +- lib/dolphin/gx/GXCpu2Efb.cpp | 2 - lib/dolphin/gx/GXDispList.cpp | 11 +- lib/dolphin/gx/GXFrameBuffer.cpp | 2 + lib/dolphin/gx/GXVert.cpp | 7 +- lib/gfx/common.cpp | 1031 ++++++++++++----- lib/gfx/common.hpp | 52 +- lib/gfx/depth_peek.cpp | 8 +- lib/gfx/depth_peek.hpp | 1 - lib/gfx/pipeline_cache.cpp | 34 + lib/gfx/pipeline_cache.hpp | 4 + lib/gfx/render_worker.cpp | 270 +++++ lib/gfx/render_worker.hpp | 88 ++ lib/gfx/texture.cpp | 18 +- lib/gfx/texture.hpp | 8 +- lib/gfx/texture_convert.cpp | 1 + lib/gx/command_processor.cpp | 57 +- lib/gx/pipeline.cpp | 6 +- lib/gx/shader_info.cpp | 7 +- lib/imgui.cpp | 78 +- lib/imgui.hpp | 21 +- lib/rmlui.cpp | 20 +- lib/rmlui.hpp | 8 +- lib/rmlui/WebGPURenderInterface.cpp | 1663 ++++++--------------------- lib/rmlui/WebGPURenderInterface.hpp | 74 +- lib/rmlui/pipeline.cpp | 917 +++++++++++++++ lib/rmlui/pipeline.hpp | 105 ++ lib/webgpu/gpu.cpp | 6 + tests/CMakeLists.txt | 16 + tests/gx_test_stubs.cpp | 10 +- tests/render_worker_test.cpp | 107 ++ 33 files changed, 2979 insertions(+), 1828 deletions(-) create mode 100644 lib/gfx/render_worker.cpp create mode 100644 lib/gfx/render_worker.hpp create mode 100644 lib/rmlui/pipeline.cpp create mode 100644 lib/rmlui/pipeline.hpp create mode 100644 tests/render_worker_test.cpp diff --git a/cmake/aurora_gx.cmake b/cmake/aurora_gx.cmake index 7aab6cc..491387e 100644 --- a/cmake/aurora_gx.cmake +++ b/cmake/aurora_gx.cmake @@ -3,6 +3,7 @@ add_library(aurora_gx STATIC lib/gfx/common.cpp lib/gfx/depth_peek.cpp lib/gfx/pipeline_cache.cpp + lib/gfx/render_worker.cpp lib/gfx/dds_io.cpp lib/gfx/tex_copy_conv.cpp lib/gfx/tex_palette_conv.cpp @@ -43,3 +44,10 @@ set_target_properties(aurora_gx PROPERTIES FOLDER "aurora") target_link_libraries(aurora_gx PUBLIC aurora::core dawn::webgpu_dawn xxhash) target_link_libraries(aurora_gx PRIVATE absl::btree absl::flat_hash_map sqlite3 TracyClient PNG::PNG) target_compile_definitions(aurora_gx PRIVATE WEBGPU_DAWN) + +if (AURORA_ENABLE_RMLUI) + target_sources(aurora_gx PRIVATE + lib/rmlui/pipeline.cpp + lib/rmlui/pipeline.hpp + ) +endif () diff --git a/lib/aurora.cpp b/lib/aurora.cpp index 8cef877..dc5ae5b 100644 --- a/lib/aurora.cpp +++ b/lib/aurora.cpp @@ -176,16 +176,11 @@ AuroraInfo initialize(int argc, char* argv[], const AuroraConfig& config) noexce }; } -#ifdef AURORA_ENABLE_GX -wgpu::TextureView g_currentView; -#endif - void shutdown() noexcept { #ifdef AURORA_ENABLE_RMLUI rmlui::shutdown(); #endif #ifdef AURORA_ENABLE_GX - g_currentView = {}; imgui::shutdown(); gfx::shutdown(); webgpu::shutdown(); @@ -221,36 +216,10 @@ bool begin_frame() noexcept { return false; } } - wgpu::SurfaceTexture surfaceTexture; - g_surface.GetCurrentTexture(&surfaceTexture); - switch (surfaceTexture.status) { - case wgpu::SurfaceGetCurrentTextureStatus::SuccessOptimal: - g_currentView = surfaceTexture.texture.CreateView(); - break; - case wgpu::SurfaceGetCurrentTextureStatus::Timeout: - Log.warn("Surface texture acquisition timed out"); - return false; - case wgpu::SurfaceGetCurrentTextureStatus::SuccessSuboptimal: - case wgpu::SurfaceGetCurrentTextureStatus::Outdated: - Log.info("Surface texture is {}, reconfiguring swapchain", magic_enum::enum_name(surfaceTexture.status)); - webgpu::refresh_surface(false); - return false; - case wgpu::SurfaceGetCurrentTextureStatus::Lost: - Log.warn("Surface texture is {}, releasing surface", magic_enum::enum_name(surfaceTexture.status)); - webgpu::release_surface(); - case wgpu::SurfaceGetCurrentTextureStatus::Error: - Log.warn("Surface texture is {}, dropping surface", magic_enum::enum_name(surfaceTexture.status)); - g_surface = {}; - return false; - default: - Log.error("Failed to get surface texture: {}", magic_enum::enum_name(surfaceTexture.status)); - return false; - } } imgui::new_frame(window::get_window_size()); if (!gfx::begin_frame()) { - g_currentView = {}; return false; } #endif @@ -261,33 +230,51 @@ void end_frame() noexcept { ZoneScoped; #ifdef AURORA_ENABLE_GX gx::fifo::drain(); - const auto encoderDescriptor = wgpu::CommandEncoderDescriptor{ - .label = "Redraw encoder", - }; - auto encoder = g_device.CreateCommandEncoder(&encoderDescriptor); - gfx::end_frame(encoder); - gfx::render(encoder); - { + gfx::finish(); + auto imguiDrawData = imgui::freeze(); + + const auto& presentSource = webgpu::present_source(); + const auto viewport = webgpu::calculate_present_viewport(webgpu::g_graphicsConfig.surfaceConfiguration.width, + webgpu::g_graphicsConfig.surfaceConfiguration.height, + presentSource.size.width, presentSource.size.height); + + wgpu::BindGroup rmlBindGroup; +#if AURORA_ENABLE_RMLUI + if (rmlui::is_initialized()) { + rmlBindGroup = rmlui::record_frame(viewport); + } +#endif + + gfx::end_frame([rmlBindGroup = std::move(rmlBindGroup), viewport, + imguiDrawData = std::move(imguiDrawData)](wgpu::CommandEncoder& encoder) { window::SurfaceLock surfaceLock; - if (window::is_presentable() && g_surface && g_currentView) { - const auto& presentSource = webgpu::present_source(); - auto viewport = webgpu::calculate_present_viewport(webgpu::g_graphicsConfig.surfaceConfiguration.width, - webgpu::g_graphicsConfig.surfaceConfiguration.height, - presentSource.size.width, presentSource.size.height); - const auto& resampledSource = webgpu::resample_present_source(encoder, viewport); - wgpu::BindGroup presentBindGroup = webgpu::create_copy_bind_group(resampledSource); - #if AURORA_ENABLE_RMLUI - if (rmlui::is_initialized()) { - const auto rmlOutput = rmlui::render(encoder, viewport, resampledSource); - if (rmlOutput.texture != nullptr) { - presentBindGroup = rmlOutput.copyBindGroup; - } + wgpu::Texture currentTexture; + wgpu::TextureView currentView; + auto surfaceStatus = wgpu::SurfaceGetCurrentTextureStatus::Error; + if (window::is_presentable() && g_surface) { + ZoneScopedN("Acquire texture"); + wgpu::SurfaceTexture surfaceTexture; + g_surface.GetCurrentTexture(&surfaceTexture); + surfaceStatus = surfaceTexture.status; + if (surfaceStatus == wgpu::SurfaceGetCurrentTextureStatus::SuccessOptimal) { + currentTexture = std::move(surfaceTexture.texture); + currentView = currentTexture.CreateView(); + } + } + + const bool canPresent = currentTexture && currentView; + if (canPresent) { + wgpu::BindGroup presentBindGroup; + if (rmlBindGroup) { + presentBindGroup = rmlBindGroup; + } else { + const auto& resampledSource = webgpu::resample_present_source(encoder, viewport); + presentBindGroup = webgpu::create_copy_bind_group(resampledSource); } - #endif { const std::array attachments{ wgpu::RenderPassColorAttachment{ - .view = g_currentView, + .view = currentView, .loadOp = wgpu::LoadOp::Clear, .storeOp = wgpu::StoreOp::Store, }, @@ -309,7 +296,7 @@ void end_frame() noexcept { { const std::array attachments{ wgpu::RenderPassColorAttachment{ - .view = g_currentView, + .view = currentView, .loadOp = wgpu::LoadOp::Load, .storeOp = wgpu::StoreOp::Store, }, @@ -322,44 +309,70 @@ void end_frame() noexcept { const auto pass = encoder.BeginRenderPass(&renderPassDescriptor); pass.SetViewport(0.f, 0.f, static_cast(webgpu::g_graphicsConfig.surfaceConfiguration.width), static_cast(webgpu::g_graphicsConfig.surfaceConfiguration.height), 0.f, 1.f); - imgui::render(pass); + imgui::render(pass, imguiDrawData); pass.End(); } } else { Log.info("Skipping present; window not presentable"); - webgpu::release_surface(); } const wgpu::CommandBufferDescriptor cmdBufDescriptor{.label = "Redraw command buffer"}; const auto buffer = encoder.Finish(&cmdBufDescriptor); - g_queue.Submit(1, &buffer); - gfx::after_submit(); - if (window::is_presentable() && g_surface) { + { + ZoneScopedN("Queue Submit"); + g_queue.Submit(1, &buffer); + } + if (canPresent && g_surface) { + ZoneScopedN("Present"); auto presentStatus = g_surface.Present(); if (presentStatus != wgpu::Status::Success) { Log.warn("Surface present failed: {}", static_cast(presentStatus)); webgpu::release_surface(); } } else if (g_surface) { - webgpu::release_surface(); + switch (surfaceStatus) { + case wgpu::SurfaceGetCurrentTextureStatus::Timeout: + Log.warn("Surface texture acquisition timed out"); + break; + case wgpu::SurfaceGetCurrentTextureStatus::SuccessSuboptimal: + case wgpu::SurfaceGetCurrentTextureStatus::Outdated: + Log.info("Surface texture is {}, reconfiguring swapchain", magic_enum::enum_name(surfaceStatus)); + webgpu::refresh_surface(false); + break; + case wgpu::SurfaceGetCurrentTextureStatus::Lost: + Log.warn("Surface texture is {}, releasing surface", magic_enum::enum_name(surfaceStatus)); + webgpu::release_surface(); + break; + case wgpu::SurfaceGetCurrentTextureStatus::Error: + Log.warn("Surface texture is {}, dropping surface", magic_enum::enum_name(surfaceStatus)); + g_surface = {}; + break; + default: + if (!window::is_presentable()) { + webgpu::release_surface(); + } else { + Log.error("Failed to get surface texture: {}", magic_enum::enum_name(surfaceStatus)); + } + break; + } } - g_currentView = {}; - } + gfx::after_submit(); - TracyPlotConfig("aurora: lastVertSize", tracy::PlotFormatType::Memory, false, true, 0); - TracyPlotConfig("aurora: lastUniformSize", tracy::PlotFormatType::Memory, false, true, 0); - TracyPlotConfig("aurora: lastIndexSize", tracy::PlotFormatType::Memory, false, true, 0); - TracyPlotConfig("aurora: lastStorageSize", tracy::PlotFormatType::Memory, false, true, 0); - TracyPlotConfig("aurora: lastTextureUploadSize", tracy::PlotFormatType::Memory, false, true, 0); + TracyPlotConfig("aurora: lastVertSize", tracy::PlotFormatType::Memory, false, true, 0); + TracyPlotConfig("aurora: lastUniformSize", tracy::PlotFormatType::Memory, false, true, 0); + TracyPlotConfig("aurora: lastIndexSize", tracy::PlotFormatType::Memory, false, true, 0); + TracyPlotConfig("aurora: lastStorageSize", tracy::PlotFormatType::Memory, false, true, 0); + TracyPlotConfig("aurora: lastTextureUploadSize", tracy::PlotFormatType::Memory, false, true, 0); - TracyPlot("aurora: queuedPipelines", static_cast(gfx::g_stats.queuedPipelines)); - TracyPlot("aurora: createdPipelines", static_cast(gfx::g_stats.createdPipelines)); - TracyPlot("aurora: drawCallCount", static_cast(gfx::g_stats.drawCallCount)); - TracyPlot("aurora: mergedDrawCallCount", static_cast(gfx::g_stats.mergedDrawCallCount)); - TracyPlot("aurora: lastVertSize", static_cast(gfx::g_stats.lastVertSize)); - TracyPlot("aurora: lastUniformSize", static_cast(gfx::g_stats.lastUniformSize)); - TracyPlot("aurora: lastIndexSize", static_cast(gfx::g_stats.lastIndexSize)); - TracyPlot("aurora: lastStorageSize", static_cast(gfx::g_stats.lastStorageSize)); - TracyPlot("aurora: lastTextureUploadSize", static_cast(gfx::g_stats.lastTextureUploadSize)); + TracyPlot("aurora: queuedPipelines", static_cast(gfx::g_stats.queuedPipelines)); + TracyPlot("aurora: createdPipelines", static_cast(gfx::g_stats.createdPipelines)); + TracyPlot("aurora: drawCallCount", static_cast(gfx::g_stats.drawCallCount)); + TracyPlot("aurora: mergedDrawCallCount", static_cast(gfx::g_stats.mergedDrawCallCount)); + TracyPlot("aurora: lastVertSize", static_cast(gfx::g_stats.lastVertSize)); + TracyPlot("aurora: lastUniformSize", static_cast(gfx::g_stats.lastUniformSize)); + TracyPlot("aurora: lastIndexSize", static_cast(gfx::g_stats.lastIndexSize)); + TracyPlot("aurora: lastStorageSize", static_cast(gfx::g_stats.lastStorageSize)); + TracyPlot("aurora: lastTextureUploadSize", static_cast(gfx::g_stats.lastTextureUploadSize)); + }); #endif } diff --git a/lib/device.cpp b/lib/device.cpp index c2c868e..b1a0d8b 100644 --- a/lib/device.cpp +++ b/lib/device.cpp @@ -198,7 +198,7 @@ void shutdown_rumble() noexcept { } } } // namespace detail -#elif !defined(SDL_PLATFORM_IOS) +#elif !defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_TVOS) bool rumble_available() noexcept { return false; } void rumble(const uint16_t lowFreq, const uint16_t highFreq, const uint16_t durationMs) noexcept { diff --git a/lib/dolphin/gx/GXCpu2Efb.cpp b/lib/dolphin/gx/GXCpu2Efb.cpp index 9815bd4..2128662 100644 --- a/lib/dolphin/gx/GXCpu2Efb.cpp +++ b/lib/dolphin/gx/GXCpu2Efb.cpp @@ -5,8 +5,6 @@ #include void GXPeekZ(u16 x, u16 y, u32* z) { - aurora::gfx::depth_peek::poll(); - if (z != nullptr) { u32 value = 0; if (aurora::gfx::depth_peek::read_latest(x, y, value)) { diff --git a/lib/dolphin/gx/GXDispList.cpp b/lib/dolphin/gx/GXDispList.cpp index 98c0339..e165cd1 100644 --- a/lib/dolphin/gx/GXDispList.cpp +++ b/lib/dolphin/gx/GXDispList.cpp @@ -58,13 +58,12 @@ void GXCallDisplayList(const void* data, u32 nbytes) { __GXSendFlushPrim(); } - // Drain the internal FIFO so that any pending CP register writes - // (VCD, VAT, etc.) are processed into g_gxState before the display - // list's draw commands reference them. - aurora::gx::fifo::drain(); + // Write display list contents to the FIFO + aurora::gx::fifo::write_data(data, nbytes); - // Process the display list through the command processor - aurora::gx::fifo::process(static_cast(data), nbytes, true); + // TEMP: debugging aid + // aurora::gx::fifo::drain(); + // aurora::gx::fifo::process(static_cast(data), nbytes, true); } void GXCallDisplayListLE(const void* data, u32 nbytes) { diff --git a/lib/dolphin/gx/GXFrameBuffer.cpp b/lib/dolphin/gx/GXFrameBuffer.cpp index 102e40b..c196cf6 100644 --- a/lib/dolphin/gx/GXFrameBuffer.cpp +++ b/lib/dolphin/gx/GXFrameBuffer.cpp @@ -148,6 +148,8 @@ void GXSetDispCopyGamma(GXGamma gamma) {} void GXCopyDisp(void* dest, GXBool clear) {} void GXCopyTex(void* dest, GXBool clear) { + aurora::gx::fifo::drain(); + const auto rect = aurora::gx::map_logical_scissor(g_gxState.texCopySrc); const auto [dstWidth, dstHeight] = scale_copy_dst(g_gxState.texCopyDstWidth, g_gxState.texCopyDstHeight); const auto texCopyFmt = g_gxState.texCopyFmt; diff --git a/lib/dolphin/gx/GXVert.cpp b/lib/dolphin/gx/GXVert.cpp index f3e0b32..4ebf42e 100644 --- a/lib/dolphin/gx/GXVert.cpp +++ b/lib/dolphin/gx/GXVert.cpp @@ -48,9 +48,10 @@ void GXEnd() { } sInBegin = false; } - if (!aurora::gx::fifo::in_display_list()) { - aurora::gx::fifo::drain(); - } + // TEMP: debugging aid + // if (!aurora::gx::fifo::in_display_list()) { + // aurora::gx::fifo::drain(); + // } } void GXPosition3f32(f32 x, f32 y, f32 z) { diff --git a/lib/gfx/common.cpp b/lib/gfx/common.cpp index de7ce7b..eff0c65 100644 --- a/lib/gfx/common.cpp +++ b/lib/gfx/common.cpp @@ -5,7 +5,11 @@ #include "../internal.hpp" #include "../webgpu/gpu.hpp" #include "../gx/pipeline.hpp" +#ifdef AURORA_ENABLE_RMLUI +#include "../rmlui/pipeline.hpp" +#endif #include "pipeline_cache.hpp" +#include "render_worker.hpp" #include "tex_copy_conv.hpp" #include "tex_palette_conv.hpp" #include "texture_replacement.hpp" @@ -13,8 +17,13 @@ #include "../window.hpp" #include +#include +#include +#include #include #include +#include +#include #include #include @@ -35,12 +44,26 @@ std::vector g_debugMarkers; constexpr uint64_t StagingBufferSize = UniformBufferSize + VertexBufferSize + IndexBufferSize + StorageBufferSize + (UseTextureBuffer ? TextureUploadSize : 0); +constexpr size_t FrameSlotCount = 2; +constexpr size_t StagingBufferCount = FrameSlotCount + 2; + +struct StagingHighWater { + uint32_t verts = 0; + uint32_t uniforms = 0; + uint32_t indices = 0; + uint32_t storage = 0; + uint32_t textureUpload = 0; + size_t textureUploadCount = 0; +}; struct ShaderDrawCommand { ShaderType type; union { clear::DrawData clear; gx::DrawData gx; +#ifdef AURORA_ENABLE_RMLUI + rmlui::DrawData rml; +#endif }; }; enum class CommandType { @@ -95,24 +118,20 @@ constexpr uint32_t BindGroupCacheSweepPeriod = 16; static absl::flat_hash_map g_cachedBindGroups; static absl::flat_hash_map g_cachedSamplers; +static std::mutex g_bindGroupCacheMutex; +static std::mutex g_samplerCacheMutex; -static ByteBuffer g_verts; -static ByteBuffer g_uniforms; -static ByteBuffer g_indices; -static ByteBuffer g_storage; -static ByteBuffer g_textureUpload; wgpu::Buffer g_vertexBuffer; wgpu::Buffer g_uniformBuffer; wgpu::Buffer g_indexBuffer; wgpu::Buffer g_storageBuffer; -static std::array g_stagingBuffers; -static size_t currentStagingBuffer = 0; enum class BufferMapState { Unmapped, Mapping, Mapped, }; -static std::atomic s_mappingState{BufferMapState::Unmapped}; +static std::array g_stagingBuffers; +static std::array, StagingBufferCount> s_mappingStates; static wgpu::Limits g_cachedLimits; static uint32_t g_frameIndex = UINT32_MAX; static PipelineRef g_currentPipeline; @@ -128,9 +147,10 @@ uint32_t g_mergedDrawCallCount = 0; using CommandList = std::vector; struct RenderPass { + std::string label; wgpu::TextureView colorView; wgpu::TextureView resolveView; // MSAA resolve target; null if msaaSamples == 1 - wgpu::TextureView depthView; + wgpu::TextureView depthStencilView; wgpu::Texture copySourceTexture; wgpu::TextureView copySourceView; wgpu::TextureView copySourceDepthView; @@ -143,12 +163,72 @@ struct RenderPass { Range resolveUniformRange; Vec4 clearColorValue{0.f, 0.f, 0.f, 0.f}; float clearDepthValue = gx::UseReversedZ ? 0.f : 1.f; + wgpu::LoadOp colorLoadOp = wgpu::LoadOp::Undefined; + wgpu::StoreOp colorStoreOp = wgpu::StoreOp::Store; + wgpu::LoadOp depthLoadOp = wgpu::LoadOp::Undefined; + wgpu::StoreOp depthStoreOp = wgpu::StoreOp::Store; + wgpu::LoadOp stencilLoadOp = wgpu::LoadOp::Undefined; + wgpu::StoreOp stencilStoreOp = wgpu::StoreOp::Undefined; + uint32_t stencilClearValue = 0; CommandList commands; bool clearColor = true; bool clearDepth = true; + bool hasDepth = true; + bool hasStencil = false; + bool offscreen = false; + bool observable = false; + bool captureDepthSnapshot = false; + bool sealed = false; std::vector paletteConvs; }; -static std::vector g_renderPasses; + +struct TextureCopy { + wgpu::TexelCopyTextureInfo src; + wgpu::TexelCopyTextureInfo dst; + wgpu::Extent3D size; +}; + +enum class FrameOpType : uint8_t { + RenderPass, + TextureCopy, +}; + +struct FrameOp { + FrameOpType type = FrameOpType::RenderPass; + uint32_t index = 0; + RenderPass* renderPass = nullptr; + TextureCopy* textureCopy = nullptr; + StagingHighWater highWater; + std::vector textureUploads; +}; + +using RenderPassList = std::deque; +struct FramePacket { + RenderPassList renderPasses; + std::deque textureCopies; + std::deque ops; + std::deque textureUploads; + ByteBuffer verts; + ByteBuffer uniforms; + ByteBuffer indices; + ByteBuffer storage; + ByteBuffer textureUpload; + wgpu::CommandEncoder encoder; + uint64_t frameId = 0; + uint32_t frameIndex = 0; + size_t stagingBuffer = 0; + StagingHighWater copied; + AuroraStats stats{}; + uint32_t drawCallCount = 0; + uint32_t mergedDrawCallCount = 0; +}; + +static std::array g_framePackets; +static FramePacket* g_recordingFrame = nullptr; +static size_t g_recordingFrameSlot = 0; +static uint64_t g_nextFrameId = 1; +static render_worker::FrameSlotPool g_frameSlots{FrameSlotCount}; +static render_worker::FrameSlotPool g_stagingSlots{StagingBufferCount}; static u32 g_currentRenderPass = UINT32_MAX; static bool g_inOffscreen = false; static std::optional g_suspendedEfbPass; @@ -156,11 +236,40 @@ static Viewport g_suspendedEfbViewport; static ClipRect g_suspendedEfbScissor; static webgpu::TextureWithSampler g_offscreenColor; static webgpu::TextureWithSampler g_offscreenDepth; +static Viewport g_cachedViewport; +static ClipRect g_cachedScissor; + +static void map_staging_buffer(size_t slot, bool releaseSlotOnCompletion = false) { + auto expected = BufferMapState::Unmapped; + if (!s_mappingStates[slot].compare_exchange_strong(expected, BufferMapState::Mapping, std::memory_order_acq_rel, + std::memory_order_acquire)) { + return; + } + + g_stagingBuffers[slot].MapAsync( + wgpu::MapMode::Write, 0, StagingBufferSize, wgpu::CallbackMode::AllowSpontaneous, + [slot, releaseSlotOnCompletion](wgpu::MapAsyncStatus status, wgpu::StringView message) { + if (status == wgpu::MapAsyncStatus::CallbackCancelled || status == wgpu::MapAsyncStatus::Aborted) { + Log.warn("Buffer mapping {}: {}", magic_enum::enum_name(status), message); + s_mappingStates[slot].store(BufferMapState::Unmapped, std::memory_order_release); + if (releaseSlotOnCompletion) { + g_stagingSlots.release(slot); + } + return; + } + ASSERT(status == wgpu::MapAsyncStatus::Success, "Buffer mapping failed: {} {}", magic_enum::enum_name(status), + message); + s_mappingStates[slot].store(BufferMapState::Mapped, std::memory_order_release); + if (releaseSlotOnCompletion) { + g_stagingSlots.release(slot); + } + }); +} static void set_efb_targets(RenderPass& pass) { pass.colorView = webgpu::g_frameBuffer.view; pass.resolveView = webgpu::g_graphicsConfig.msaaSamples > 1 ? webgpu::g_frameBufferResolved.view : nullptr; - pass.depthView = webgpu::g_depthBuffer.view; + pass.depthStencilView = webgpu::g_depthBuffer.view; pass.copySourceTexture = webgpu::g_graphicsConfig.msaaSamples > 1 ? webgpu::g_frameBufferResolved.texture : webgpu::g_frameBuffer.texture; pass.copySourceView = @@ -168,6 +277,8 @@ static void set_efb_targets(RenderPass& pass) { pass.copySourceDepthView = webgpu::g_depthBuffer.view; pass.targetSize = webgpu::g_frameBuffer.size; pass.msaaSamples = webgpu::g_graphicsConfig.msaaSamples; + pass.hasDepth = true; + pass.hasStencil = false; } struct OffscreenCacheKey { @@ -185,7 +296,201 @@ struct OffscreenCacheEntry { webgpu::TextureWithSampler depth; }; static absl::flat_hash_map g_offscreenCache; -std::vector g_textureUploads; + +static FramePacket& current_frame_packet() { + CHECK(g_recordingFrame != nullptr, "No active frame packet"); + return *g_recordingFrame; +} + +static RenderPassList& current_render_passes() { return current_frame_packet().renderPasses; } + +static StagingHighWater current_high_water(const FramePacket& frame) noexcept { + return { + .verts = static_cast(frame.verts.size()), + .uniforms = static_cast(frame.uniforms.size()), + .indices = static_cast(frame.indices.size()), + .storage = static_cast(frame.storage.size()), + .textureUpload = static_cast(frame.textureUpload.size()), + .textureUploadCount = frame.textureUploads.size(), + }; +} + +static FrameOp capture_frame_op(FramePacket& frame, FrameOpType type, uint32_t index) { + FrameOp op{ + .type = type, + .index = index, + .renderPass = + type == FrameOpType::RenderPass && index < frame.renderPasses.size() ? &frame.renderPasses[index] : nullptr, + .textureCopy = type == FrameOpType::TextureCopy && index < frame.textureCopies.size() + ? &frame.textureCopies[index] + : nullptr, + .highWater = current_high_water(frame), + }; + op.textureUploads.reserve(op.highWater.textureUploadCount); + for (size_t i = 0; i < op.highWater.textureUploadCount; ++i) { + op.textureUploads.push_back(&frame.textureUploads[i]); + } + return op; +} + +static void seal_pass(FramePacket& frame, uint32_t passIndex) { + if (passIndex >= frame.renderPasses.size()) { + return; + } + auto& pass = frame.renderPasses[passIndex]; + if (pass.sealed) { + return; + } + pass.sealed = true; +} + +static void encode_op(wgpu::CommandEncoder& cmd, FramePacket& frame, const FrameOp& op); +static void render(wgpu::CommandEncoder& cmd, FramePacket& frame, RenderPass& passInfo, uint32_t passIndex); +static void render_pass(const wgpu::RenderPassEncoder& pass, FramePacket& frame, const RenderPass& passInfo); +static void expire_cached_bind_groups(); +static void push_command(CommandType type, const Command::Data& data); + +static void enqueue_op(FramePacket& frame, size_t frameSlot, uint32_t opIndex) { + if (opIndex >= frame.ops.size()) { + return; + } + auto op = frame.ops[opIndex]; + render_worker::enqueue_encode_pass(frame.frameId, opIndex, [frameSlot, op = std::move(op)] { + if (op.renderPass == nullptr && op.textureCopy == nullptr) { + return; + } + auto& packet = g_framePackets[frameSlot]; + encode_op(packet.encoder, packet, op); + }); +} + +static void enqueue_pass(FramePacket& frame, size_t frameSlot, uint32_t passIndex) { + seal_pass(frame, passIndex); + const auto opIndex = static_cast(frame.ops.size()); + frame.ops.emplace_back(capture_frame_op(frame, FrameOpType::RenderPass, passIndex)); + enqueue_op(frame, frameSlot, opIndex); +} + +void queue_texture_upload(TextureUpload upload) { + if (g_currentRenderPass != UINT32_MAX) { + ASSERT(!current_render_passes()[g_currentRenderPass].sealed, + "Attempted to append texture upload to sealed render pass {}", g_currentRenderPass); + } + current_frame_packet().textureUploads.emplace_back(std::move(upload)); +} + +void queue_texture_upload_data(const uint8_t* data, size_t length, uint32_t bytesPerRow, uint32_t rowsPerImage, + wgpu::TexelCopyTextureInfo tex, wgpu::Extent3D size) { + const auto copyBytesPerRow = AURORA_ALIGN(bytesPerRow, 256); + auto& frame = current_frame_packet(); + if (frame.textureUpload.size() + copyBytesPerRow * rowsPerImage <= TextureUploadSize) { + const auto range = push_texture_data(data, length, bytesPerRow, rowsPerImage); + const wgpu::TexelCopyBufferLayout layout{ + .offset = range.offset, + .bytesPerRow = bytesPerRow, + .rowsPerImage = rowsPerImage, + }; + queue_texture_upload(TextureUpload{layout, std::move(tex), size}); + return; + } + + const uint64_t uploadSize = copyBytesPerRow * rowsPerImage; + const wgpu::BufferDescriptor descriptor{ + .label = "Overflow Texture Upload Buffer", + .usage = wgpu::BufferUsage::MapWrite | wgpu::BufferUsage::CopySrc, + .size = uploadSize, + .mappedAtCreation = true, + }; + auto buffer = g_device.CreateBuffer(&descriptor); + auto* dst = static_cast(buffer.GetMappedRange(0, uploadSize)); + for (uint32_t row = 0; row < rowsPerImage; ++row) { + memcpy(dst, data, bytesPerRow); + data += bytesPerRow; + dst += copyBytesPerRow; + } + buffer.Unmap(); + + const wgpu::TexelCopyBufferLayout layout{ + .offset = 0, + .bytesPerRow = bytesPerRow, + .rowsPerImage = rowsPerImage, + }; + queue_texture_upload(TextureUpload{layout, std::move(tex), size, std::move(buffer)}); +} + +void queue_texture_copy(wgpu::TexelCopyTextureInfo src, wgpu::TexelCopyTextureInfo dst, wgpu::Extent3D size) { + ZoneScoped; + auto& frame = current_frame_packet(); + if (g_currentRenderPass != UINT32_MAX) { + enqueue_pass(frame, g_recordingFrameSlot, g_currentRenderPass); + g_currentRenderPass = UINT32_MAX; + } + + const auto copyIndex = static_cast(frame.textureCopies.size()); + frame.textureCopies.emplace_back(TextureCopy{ + .src = std::move(src), + .dst = std::move(dst), + .size = size, + }); + const auto opIndex = static_cast(frame.ops.size()); + frame.ops.emplace_back(capture_frame_op(frame, FrameOpType::TextureCopy, copyIndex)); + enqueue_op(frame, g_recordingFrameSlot, opIndex); +} + +void begin_color_pass(const ColorPassDescriptor& desc) { + ZoneScoped; + auto& frame = current_frame_packet(); + if (g_currentRenderPass != UINT32_MAX) { + enqueue_pass(frame, g_recordingFrameSlot, g_currentRenderPass); + } + + RenderPass pass{ + .label = desc.label != nullptr ? desc.label : "", + .colorView = desc.colorView, + .resolveView = desc.resolveView, + .depthStencilView = desc.depthStencilView, + .targetSize = desc.targetSize, + .msaaSamples = desc.sampleCount, + .clearColorValue = + { + static_cast(desc.clearColor.r), + static_cast(desc.clearColor.g), + static_cast(desc.clearColor.b), + static_cast(desc.clearColor.a), + }, + .clearDepthValue = desc.depthClearValue, + .colorLoadOp = desc.colorLoadOp, + .colorStoreOp = desc.colorStoreOp, + .depthLoadOp = desc.depthLoadOp, + .depthStoreOp = desc.depthStoreOp, + .stencilLoadOp = desc.stencilLoadOp, + .stencilStoreOp = desc.stencilStoreOp, + .stencilClearValue = desc.stencilClearValue, + .clearColor = desc.colorLoadOp == wgpu::LoadOp::Clear, + .clearDepth = desc.depthLoadOp == wgpu::LoadOp::Clear, + .hasDepth = desc.hasDepth, + .hasStencil = desc.hasStencil, + .observable = desc.observable, + }; + pass.commands.reserve(128); + frame.renderPasses.emplace_back(std::move(pass)); + g_currentRenderPass = static_cast(frame.renderPasses.size() - 1); + + g_cachedViewport = {0.f, 0.f, static_cast(desc.targetSize.width), static_cast(desc.targetSize.height), + 0.f, 1.f}; + g_cachedScissor = {0, 0, static_cast(desc.targetSize.width), static_cast(desc.targetSize.height)}; + push_command(CommandType::SetViewport, Command::Data{.setViewport = g_cachedViewport}); + push_command(CommandType::SetScissor, Command::Data{.setScissor = g_cachedScissor}); +} + +void end_color_pass() { + ZoneScoped; + if (g_currentRenderPass == UINT32_MAX) { + return; + } + enqueue_pass(current_frame_packet(), g_recordingFrameSlot, g_currentRenderPass); + g_currentRenderPass = UINT32_MAX; +} static inline void push_command(CommandType type, const Command::Data& data) { if (g_currentRenderPass == UINT32_MAX) @@ -193,7 +498,10 @@ static inline void push_command(CommandType type, const Command::Data& data) { Log.warn("Dropping command {}", magic_enum::enum_name(type)); return; } - g_renderPasses[g_currentRenderPass].commands.push_back({ + auto& renderPass = current_render_passes()[g_currentRenderPass]; + ASSERT(!renderPass.sealed, "Attempted to append command {} to sealed render pass {}", magic_enum::enum_name(type), + g_currentRenderPass); + renderPass.commands.push_back({ .type = type, #ifdef AURORA_GFX_DEBUG_GROUPS .debugGroupStack = g_debugGroupStack, @@ -204,10 +512,10 @@ static inline void push_command(CommandType type, const Command::Data& data) { template <> gx::DrawData* get_last_draw_command() { - if (g_currentRenderPass >= g_renderPasses.size()) { + if (g_currentRenderPass >= current_render_passes().size()) { return nullptr; } - auto& last = g_renderPasses[g_currentRenderPass].commands.back(); + auto& last = current_render_passes()[g_currentRenderPass].commands.back(); if (last.type != CommandType::Draw || last.data.draw.type != ShaderType::GX) { return nullptr; } @@ -220,15 +528,14 @@ static void push_draw_command(ShaderDrawCommand data) { } Vec2 get_render_target_size() noexcept { - if (g_currentRenderPass < g_renderPasses.size()) { - const auto& size = g_renderPasses[g_currentRenderPass].targetSize; + if (g_currentRenderPass < current_render_passes().size()) { + const auto& size = current_render_passes()[g_currentRenderPass].targetSize; return {size.width, size.height}; } const auto windowSize = window::get_window_size(); return {windowSize.fb_width, windowSize.fb_height}; } -static Viewport g_cachedViewport; void set_viewport(const Viewport& cmd) noexcept { if (cmd != g_cachedViewport) { push_command(CommandType::SetViewport, Command::Data{.setViewport = cmd}); @@ -236,7 +543,6 @@ void set_viewport(const Viewport& cmd) noexcept { } } -static ClipRect g_cachedScissor; void set_scissor(const ClipRect& cmd) noexcept { if (cmd != g_cachedScissor) { push_command(CommandType::SetScissor, Command::Data{.setScissor = cmd}); @@ -257,7 +563,7 @@ PipelineRef pipeline_ref(const clear::PipelineConfig& config) { void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool clearAlpha, bool clearDepth, Vec4 clearColorValue, float clearDepthValue, GXTexFmt resolveFormat) { // Resolve current render pass - auto& prevPass = g_renderPasses[g_currentRenderPass]; + auto& prevPass = current_render_passes()[g_currentRenderPass]; prevPass.resolveTarget = std::move(texture); prevPass.resolveRect = rect; prevPass.resolveFormat = resolveFormat; @@ -271,13 +577,14 @@ void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool cl static_cast(rect.height) / srcH, }; prevPass.resolveUniformRange = push_uniform(uvTransform); + enqueue_pass(current_frame_packet(), g_recordingFrameSlot, g_currentRenderPass); // Populate new render pass from previous const auto msaaSamples = prevPass.msaaSamples; RenderPass newPass{ .colorView = prevPass.colorView, .resolveView = prevPass.resolveView, - .depthView = prevPass.depthView, + .depthStencilView = prevPass.depthStencilView, .copySourceTexture = prevPass.copySourceTexture, .copySourceView = prevPass.copySourceView, .copySourceDepthView = prevPass.copySourceDepthView, @@ -287,8 +594,11 @@ void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool cl .clearDepthValue = clearDepthValue, .clearColor = clearColor && clearAlpha, .clearDepth = clearDepth, + .hasDepth = prevPass.hasDepth, + .hasStencil = prevPass.hasStencil, }; - g_renderPasses.emplace_back(std::move(newPass)); + newPass.commands.reserve(2048); + current_render_passes().emplace_back(std::move(newPass)); ++g_currentRenderPass; if (!newPass.clearColor && (clearColor || clearAlpha)) { @@ -314,18 +624,21 @@ void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool cl } void queue_palette_conv(tex_palette_conv::ConvRequest req) { - g_renderPasses[g_currentRenderPass].paletteConvs.push_back(std::move(req)); + auto& renderPass = current_render_passes()[g_currentRenderPass]; + ASSERT(!renderPass.sealed, "Attempted to append palette conversion to sealed render pass {}", g_currentRenderPass); + renderPass.paletteConvs.push_back(std::move(req)); } bool is_offscreen() noexcept { return g_inOffscreen; } uint32_t get_sample_count() noexcept { CHECK(g_currentRenderPass != UINT32_MAX, "get_sample_count called outside of a frame"); - return g_renderPasses[g_currentRenderPass].msaaSamples; + return current_render_passes()[g_currentRenderPass].msaaSamples; } void clear_caches() noexcept { g_offscreenCache.clear(); + std::lock_guard lock{g_bindGroupCacheMutex}; g_cachedBindGroups.clear(); } @@ -387,11 +700,13 @@ void begin_offscreen(uint32_t width, uint32_t height) { // If the current EFB pass has no resolve target, its output is unobservable. // Suspend it so that we can resume it after the offscreen pass. if (!g_inOffscreen) { - auto& currentPass = g_renderPasses[g_currentRenderPass]; + auto& currentPass = current_render_passes()[g_currentRenderPass]; if (!currentPass.resolveTarget) { g_suspendedEfbPass = std::move(currentPass); - g_renderPasses.pop_back(); + current_render_passes().pop_back(); --g_currentRenderPass; + } else { + enqueue_pass(current_frame_packet(), g_recordingFrameSlot, g_currentRenderPass); } g_suspendedEfbViewport = g_cachedViewport; g_suspendedEfbScissor = g_cachedScissor; @@ -405,7 +720,7 @@ void begin_offscreen(uint32_t width, uint32_t height) { // Start a new pass with offscreen targets RenderPass newPass{ .colorView = g_offscreenColor.view, - .depthView = g_offscreenDepth.view, + .depthStencilView = g_offscreenDepth.view, .copySourceTexture = g_offscreenColor.texture, .copySourceView = g_offscreenColor.view, .copySourceDepthView = g_offscreenDepth.view, @@ -415,8 +730,11 @@ void begin_offscreen(uint32_t width, uint32_t height) { .clearDepthValue = gx::UseReversedZ ? 0.f : 1.f, .clearColor = true, .clearDepth = true, + .hasDepth = true, + .hasStencil = false, + .offscreen = true, }; - g_renderPasses.emplace_back(std::move(newPass)); + current_render_passes().emplace_back(std::move(newPass)); ++g_currentRenderPass; g_inOffscreen = true; @@ -431,21 +749,23 @@ void end_offscreen() { ZoneScoped; CHECK(g_inOffscreen, "end_offscreen called without begin_offscreen"); + enqueue_pass(current_frame_packet(), g_recordingFrameSlot, g_currentRenderPass); + g_inOffscreen = false; g_offscreenColor = {}; g_offscreenDepth = {}; // Resume suspended EFB pass, or start a new one (load existing content) if (g_suspendedEfbPass) { - g_renderPasses.emplace_back(std::move(*g_suspendedEfbPass)); + current_render_passes().emplace_back(std::move(*g_suspendedEfbPass)); g_suspendedEfbPass.reset(); } else { - auto& pass = g_renderPasses.emplace_back(); + auto& pass = current_render_passes().emplace_back(); pass.clearColor = false; pass.clearDepth = false; } ++g_currentRenderPass; - set_efb_targets(g_renderPasses[g_currentRenderPass]); + set_efb_targets(current_render_passes()[g_currentRenderPass]); g_cachedViewport = g_suspendedEfbViewport; g_cachedScissor = g_suspendedEfbScissor; @@ -458,13 +778,33 @@ void push_draw_command(gx::DrawData data) { push_draw_command(ShaderDrawCommand{.type = ShaderType::GX, .gx = data}); } +#ifdef AURORA_ENABLE_RMLUI +template <> +void push_draw_command(rmlui::DrawData data) { + push_draw_command(ShaderDrawCommand{.type = ShaderType::Rml, .rml = data}); +} +#endif + template <> PipelineRef pipeline_ref(const gx::PipelineConfig& config) { return find_pipeline(ShaderType::GX, config, [=] { return create_pipeline(config); }); } +#ifdef AURORA_ENABLE_RMLUI +template <> +PipelineRef pipeline_ref(const rmlui::PipelineConfig& config) { + return find_pipeline(ShaderType::Rml, config, [=] { return rmlui::create_pipeline(config); }); +} +#endif + void initialize() { g_frameIndex = 0; + render_worker::initialize(); + // render_worker::set_event_pump([] { + // if (g_instance) { + // g_instance.ProcessEvents(); + // } + // }); depth_peek::initialize(); tex_copy_conv::initialize(); tex_palette_conv::initialize(); @@ -486,20 +826,23 @@ void initialize() { }; createBuffer(g_uniformBuffer, wgpu::BufferUsage::Uniform | wgpu::BufferUsage::CopyDst, UniformBufferSize, "Shared Uniform Buffer"); - createBuffer(g_vertexBuffer, wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopyDst, VertexBufferSize, - "Shared Vertex Buffer"); + createBuffer(g_vertexBuffer, wgpu::BufferUsage::Storage | wgpu::BufferUsage::Vertex | wgpu::BufferUsage::CopyDst, + VertexBufferSize, "Shared Vertex Buffer"); createBuffer(g_indexBuffer, wgpu::BufferUsage::Index | wgpu::BufferUsage::CopyDst, IndexBufferSize, "Shared Index Buffer"); createBuffer(g_storageBuffer, wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopyDst, StorageBufferSize, "Shared Storage Buffer"); - for (int i = 0; i < g_stagingBuffers.size(); ++i) { + for (size_t i = 0; i < g_stagingBuffers.size(); ++i) { const auto label = fmt::format("Staging Buffer {}", i); createBuffer(g_stagingBuffers[i], wgpu::BufferUsage::MapWrite | wgpu::BufferUsage::CopySrc, StagingBufferSize, label.c_str()); } - currentStagingBuffer = 0; - s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release); - map_staging_buffer(); + for (auto& state : s_mappingStates) { + state.store(BufferMapState::Unmapped, std::memory_order_release); + } + for (size_t slot = 0; slot < g_stagingBuffers.size(); ++slot) { + map_staging_buffer(slot); + } { constexpr std::array layoutEntries{ @@ -583,26 +926,42 @@ void initialize() { } gx::initialize(); +#ifdef AURORA_ENABLE_RMLUI + rmlui::initialize_pipeline(); +#endif initialize_pipeline_cache(); } void shutdown() { + render_worker::synchronize(); + render_worker::shutdown(); shutdown_pipeline_cache(); depth_peek::shutdown(); tex_copy_conv::shutdown(); tex_palette_conv::shutdown(); texture_replacement::shutdown(); gx::shutdown(); +#ifdef AURORA_ENABLE_RMLUI + rmlui::shutdown_pipeline(); +#endif - g_textureUploads.clear(); - g_cachedBindGroups.clear(); - g_cachedSamplers.clear(); + { + std::lock_guard lock{g_bindGroupCacheMutex}; + g_cachedBindGroups.clear(); + } + { + std::lock_guard lock{g_samplerCacheMutex}; + g_cachedSamplers.clear(); + } g_vertexBuffer = {}; g_uniformBuffer = {}; g_indexBuffer = {}; g_storageBuffer = {}; g_stagingBuffers.fill({}); - g_renderPasses.clear(); + for (auto& packet : g_framePackets) { + packet = {}; + } + g_recordingFrame = nullptr; g_currentRenderPass = UINT32_MAX; g_offscreenCache.clear(); g_offscreenColor = {}; @@ -613,49 +972,83 @@ void shutdown() { g_uniformBindGroupLayout = {}; g_inOffscreen = false; g_frameIndex = UINT32_MAX; - currentStagingBuffer = 0; - s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release); + g_frameSlots.reset(); + g_stagingSlots.reset(); + for (auto& state : s_mappingStates) { + state.store(BufferMapState::Unmapped, std::memory_order_release); + } } -void map_staging_buffer() { - auto expected = BufferMapState::Unmapped; - if (!s_mappingState.compare_exchange_strong(expected, BufferMapState::Mapping, std::memory_order_acq_rel, - std::memory_order_acquire)) { - return; +static bool wait_for_staging_buffer(size_t slot) { + ZoneScopedN("Wait for buffer map"); + map_staging_buffer(slot); + while (true) { + const auto mappingState = s_mappingStates[slot].load(std::memory_order_acquire); + if (mappingState == BufferMapState::Mapped) { + return true; + } + if (mappingState == BufferMapState::Unmapped) { + return false; + } + if (render_worker::is_idle()) { + g_instance.ProcessEvents(); + } else { + std::this_thread::sleep_for(std::chrono::milliseconds{1}); + } } +} - g_stagingBuffers[currentStagingBuffer].MapAsync( - wgpu::MapMode::Write, 0, StagingBufferSize, wgpu::CallbackMode::AllowSpontaneous, - [](wgpu::MapAsyncStatus status, wgpu::StringView message) { - if (status == wgpu::MapAsyncStatus::CallbackCancelled || status == wgpu::MapAsyncStatus::Aborted) { - Log.warn("Buffer mapping {}: {}", magic_enum::enum_name(status), message); - s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release); - return; - } - ASSERT(status == wgpu::MapAsyncStatus::Success, "Buffer mapping failed: {} {}", magic_enum::enum_name(status), - message); - s_mappingState.store(BufferMapState::Mapped, std::memory_order_release); - }); +static size_t acquire_frame_slot() { + ZoneScopedN("Acquire frame slot"); + while (true) { + if (auto slot = g_frameSlots.try_acquire()) { + return *slot; + } + if (render_worker::is_idle()) { + g_instance.ProcessEvents(); + } else { + std::this_thread::sleep_for(std::chrono::microseconds{100}); + } + } +} + +static std::optional acquire_mapped_staging_buffer() { + ZoneScopedN("Acquire mapped staging buffer"); + while (true) { + if (auto slot = g_stagingSlots.try_acquire()) { + if (wait_for_staging_buffer(*slot)) { + return *slot; + } + g_stagingSlots.release(*slot); + return std::nullopt; + } + if (render_worker::is_idle()) { + g_instance.ProcessEvents(); + } else { + std::this_thread::sleep_for(std::chrono::microseconds{100}); + } + } } bool begin_frame() { ZoneScoped; - { - ZoneScopedN("Wait for buffer map"); - map_staging_buffer(); - while (true) { - const auto mappingState = s_mappingState.load(std::memory_order_acquire); - if (mappingState == BufferMapState::Mapped) { - break; - } - if (mappingState == BufferMapState::Unmapped) { - return false; - } - g_instance.ProcessEvents(); - } + const size_t frameSlot = acquire_frame_slot(); + const auto stagingSlot = acquire_mapped_staging_buffer(); + if (!stagingSlot) { + g_frameSlots.release(frameSlot); + return false; } + + auto& frame = g_framePackets[frameSlot]; + frame = {}; + frame.frameId = g_nextFrameId++; + frame.frameIndex = g_frameIndex; + frame.stagingBuffer = *stagingSlot; + g_recordingFrame = &frame; + g_recordingFrameSlot = frameSlot; + size_t bufferOffset = 0; - const auto& stagingBuf = g_stagingBuffers[currentStagingBuffer]; + const auto& stagingBuf = g_stagingBuffers[*stagingSlot]; const auto mapBuffer = [&](ByteBuffer& buf, uint64_t size) { if (size <= 0) { return; @@ -663,22 +1056,23 @@ bool begin_frame() { buf = ByteBuffer{static_cast(stagingBuf.GetMappedRange(bufferOffset, size)), static_cast(size)}; bufferOffset += size; }; - mapBuffer(g_verts, VertexBufferSize); - mapBuffer(g_uniforms, UniformBufferSize); - mapBuffer(g_indices, IndexBufferSize); - mapBuffer(g_storage, StorageBufferSize); + mapBuffer(frame.verts, VertexBufferSize); + mapBuffer(frame.uniforms, UniformBufferSize); + mapBuffer(frame.indices, IndexBufferSize); + mapBuffer(frame.storage, StorageBufferSize); if constexpr (UseTextureBuffer) { - mapBuffer(g_textureUpload, TextureUploadSize); + mapBuffer(frame.textureUpload, TextureUploadSize); } g_drawCallCount = 0; g_mergedDrawCallCount = 0; g_suspendedEfbPass.reset(); - g_renderPasses.emplace_back(); - set_efb_targets(g_renderPasses[0]); - g_renderPasses[0].clearColorValue = gx::g_gxState.clearColor; - g_renderPasses[0].clearDepthValue = gx::clear_depth_value(); + current_render_passes().emplace_back(); + auto& pass = current_render_passes()[0]; + set_efb_targets(pass); + pass.clearColorValue = gx::g_gxState.clearColor; + pass.clearDepthValue = gx::clear_depth_value(); g_currentRenderPass = 0; // Refresh render viewport/scissor from logical in case FB size changed g_cachedViewport = gx::map_logical_viewport(gx::g_gxState.logicalViewport); @@ -686,64 +1080,96 @@ bool begin_frame() { push_command(CommandType::SetViewport, Command::Data{.setViewport = g_cachedViewport}); push_command(CommandType::SetScissor, Command::Data{.setScissor = g_cachedScissor}); begin_pipeline_frame(); + render_worker::enqueue_begin_frame(frame.frameId, [frameSlot] { + const auto encoderDescriptor = wgpu::CommandEncoderDescriptor{ + .label = "Redraw encoder", + }; + g_framePackets[frameSlot].encoder = g_device.CreateCommandEncoder(&encoderDescriptor); + }); return true; } -void end_frame(const wgpu::CommandEncoder& cmd) { +void finish() { + ZoneScoped; + if (g_recordingFrame == nullptr) { + return; + } + ASSERT(!g_inOffscreen, "finish called while offscreen rendering is active"); + if (g_currentRenderPass != UINT32_MAX) { + auto& frame = current_frame_packet(); + frame.uniforms.append_zeroes(gx::MaxUniformSize); // Pad the end of the GX uniform buffer slice. + auto& pass = frame.renderPasses[g_currentRenderPass]; + pass.observable = true; + pass.captureDepthSnapshot = true; + enqueue_pass(frame, g_recordingFrameSlot, g_currentRenderPass); + g_currentRenderPass = UINT32_MAX; + } +} + +void end_frame(EndFrameCallback callback) { ZoneScoped; ASSERT(!g_inOffscreen, "end_frame called while offscreen rendering is active"); - g_uniforms.append_zeroes(gx::MaxUniformSize); // Pad the end of the buffer - uint64_t bufferOffset = 0; - const auto writeBuffer = [&](ByteBuffer& buf, wgpu::Buffer& out, uint64_t size, std::string_view label) { - const auto writeSize = buf.size(); // Only need to copy this many bytes - if (writeSize > 0) { - cmd.CopyBufferToBuffer(g_stagingBuffers[currentStagingBuffer], bufferOffset, out, 0, AURORA_ALIGN(writeSize, 4)); - buf.release(); - } - bufferOffset += size; - return writeSize; - }; - g_stagingBuffers[currentStagingBuffer].Unmap(); - s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release); - g_stats.drawCallCount = g_drawCallCount; - g_stats.mergedDrawCallCount = g_mergedDrawCallCount; - g_stats.lastVertSize = writeBuffer(g_verts, g_vertexBuffer, VertexBufferSize, "Vertex"); - g_stats.lastUniformSize = writeBuffer(g_uniforms, g_uniformBuffer, UniformBufferSize, "Uniform"); - g_stats.lastIndexSize = writeBuffer(g_indices, g_indexBuffer, IndexBufferSize, "Index"); - g_stats.lastStorageSize = writeBuffer(g_storage, g_storageBuffer, StorageBufferSize, "Storage"); - if constexpr (UseTextureBuffer) { - g_stats.lastTextureUploadSize = g_textureUpload.size(); - { - // Perform texture copies - for (const auto& item : g_textureUploads) { - const wgpu::TexelCopyBufferInfo buf{ - .layout = - wgpu::TexelCopyBufferLayout{ - .offset = item.layout.offset + bufferOffset, - .bytesPerRow = AURORA_ALIGN(item.layout.bytesPerRow, 256), - .rowsPerImage = item.layout.rowsPerImage, - }, - .buffer = g_stagingBuffers[currentStagingBuffer], - }; - cmd.CopyBufferToTexture(&buf, &item.tex, &item.size); - } - g_textureUploads.clear(); - g_textureUpload.release(); - } - } - currentStagingBuffer = (currentStagingBuffer + 1) % g_stagingBuffers.size(); - map_staging_buffer(); + ASSERT(g_currentRenderPass == UINT32_MAX, "end_frame called before finish finalized the current render pass"); + auto& frame = current_frame_packet(); + frame.stats.drawCallCount = g_drawCallCount; + frame.stats.mergedDrawCallCount = g_mergedDrawCallCount; + frame.stats.lastVertSize = frame.verts.size(); + frame.stats.lastUniformSize = frame.uniforms.size(); + frame.stats.lastIndexSize = frame.indices.size(); + frame.stats.lastStorageSize = frame.storage.size(); + frame.stats.lastTextureUploadSize = frame.textureUpload.size(); + + const size_t frameSlot = g_recordingFrameSlot; + const uint64_t frameId = frame.frameId; g_currentRenderPass = UINT32_MAX; for (auto& array : gx::g_gxState.arrays) { array.cachedRange = {}; } end_pipeline_frame(); ++g_frameIndex; + g_recordingFrame = nullptr; + +#if defined(AURORA_GFX_DEBUG_GROUPS) + if (!g_debugGroupStack.empty()) { + for (auto& it : std::ranges::reverse_view(g_debugGroupStack)) { + Log.warn("Debug group was not popped at end of frame: {}", it); + } + g_debugGroupStack.clear(); + } + + if (g_debugMarkers.size() > 0) { + g_debugMarkers.clear(); + } +#endif + + const size_t stagingSlot = frame.stagingBuffer; + render_worker::enqueue_end_frame(frameId, [frameSlot, stagingSlot, callback = std::move(callback)]() mutable { + auto& packet = g_framePackets[frameSlot]; + g_stagingBuffers[stagingSlot].Unmap(); + s_mappingStates[stagingSlot].store(BufferMapState::Unmapped, std::memory_order_release); + auto encoder = std::move(packet.encoder); + const auto stats = packet.stats; + packet = {}; + g_frameSlots.release(frameSlot); + g_stats.drawCallCount = stats.drawCallCount; + g_stats.mergedDrawCallCount = stats.mergedDrawCallCount; + g_stats.lastVertSize = stats.lastVertSize; + g_stats.lastUniformSize = stats.lastUniformSize; + g_stats.lastIndexSize = stats.lastIndexSize; + g_stats.lastStorageSize = stats.lastStorageSize; + g_stats.lastTextureUploadSize = stats.lastTextureUploadSize; + if (callback) { + callback(encoder); + } + expire_cached_bind_groups(); + map_staging_buffer(stagingSlot, true); + }); } uint32_t current_frame() noexcept { return g_frameIndex; } static void expire_cached_bind_groups() { + std::lock_guard lock{g_bindGroupCacheMutex}; if (g_cachedBindGroups.empty() || g_frameIndex == UINT32_MAX || g_frameIndex % BindGroupCacheSweepPeriod != 0) { return; } @@ -758,118 +1184,181 @@ static void expire_cached_bind_groups() { } } -void render(wgpu::CommandEncoder& cmd) { - ZoneScoped; - for (u32 i = 0; i < g_renderPasses.size(); ++i) { - const auto& passInfo = g_renderPasses[i]; - for (const auto& conv : passInfo.paletteConvs) { - tex_palette_conv::run(cmd, conv); - } - if (i == g_renderPasses.size() - 1) { - ASSERT(!passInfo.resolveTarget, "Final render pass must not have resolve target"); - } else if (!passInfo.resolveTarget) { - // Skip intermediate render passes without resolve target - continue; - } +static constexpr uint64_t VertexStagingOffset = 0; +static constexpr uint64_t UniformStagingOffset = VertexStagingOffset + VertexBufferSize; +static constexpr uint64_t IndexStagingOffset = UniformStagingOffset + UniformBufferSize; +static constexpr uint64_t StorageStagingOffset = IndexStagingOffset + IndexBufferSize; +static constexpr uint64_t TextureUploadStagingOffset = StorageStagingOffset + StorageBufferSize; - const std::array attachments{ - wgpu::RenderPassColorAttachment{ - .view = passInfo.colorView, - .resolveTarget = passInfo.resolveView, - .loadOp = passInfo.clearColor ? wgpu::LoadOp::Clear : wgpu::LoadOp::Load, - .storeOp = wgpu::StoreOp::Store, - .clearValue = - { - .r = passInfo.clearColorValue.x(), - .g = passInfo.clearColorValue.y(), - .b = passInfo.clearColorValue.z(), - .a = passInfo.clearColorValue.w(), - }, - }, - }; - const wgpu::RenderPassDepthStencilAttachment depthStencilAttachment{ - .view = passInfo.depthView, - .depthLoadOp = passInfo.clearDepth ? wgpu::LoadOp::Clear : wgpu::LoadOp::Load, - .depthStoreOp = wgpu::StoreOp::Store, - .depthClearValue = passInfo.clearDepthValue, - }; - const auto label = fmt::format("Render pass {}", i); - const wgpu::RenderPassDescriptor renderPassDescriptor{ - .label = label.c_str(), - .colorAttachmentCount = attachments.size(), - .colorAttachments = attachments.data(), - .depthStencilAttachment = &depthStencilAttachment, - }; +static constexpr uint32_t align_down_copy_offset(uint32_t value) noexcept { return value & ~3u; } - auto pass = cmd.BeginRenderPass(&renderPassDescriptor); - render_pass(pass, i); - pass.End(); +static void copy_staging_buffer_range(wgpu::CommandEncoder& cmd, const FramePacket& frame, uint32_t& copied, + uint32_t highWater, uint64_t stagingOffset, const wgpu::Buffer& dst) { + if (highWater <= copied) { + return; + } + const uint32_t copyStart = align_down_copy_offset(copied); + const uint32_t copyEnd = AURORA_ALIGN(highWater, 4); + cmd.CopyBufferToBuffer(g_stagingBuffers[frame.stagingBuffer], stagingOffset + copyStart, dst, copyStart, + copyEnd - copyStart); + copied = highWater; +} - if (i == g_renderPasses.size() - 1) { - depth_peek::encode_frame_snapshot(cmd, passInfo.copySourceDepthView, passInfo.targetSize, passInfo.msaaSamples); - } +static void copy_staging_to_high_water(wgpu::CommandEncoder& cmd, FramePacket& frame, const FrameOp& op) { + const auto& highWater = op.highWater; + copy_staging_buffer_range(cmd, frame, frame.copied.verts, highWater.verts, VertexStagingOffset, g_vertexBuffer); + copy_staging_buffer_range(cmd, frame, frame.copied.uniforms, highWater.uniforms, UniformStagingOffset, + g_uniformBuffer); + copy_staging_buffer_range(cmd, frame, frame.copied.indices, highWater.indices, IndexStagingOffset, g_indexBuffer); + copy_staging_buffer_range(cmd, frame, frame.copied.storage, highWater.storage, StorageStagingOffset, g_storageBuffer); - if (passInfo.resolveTarget) { - const auto& dstSize = passInfo.resolveTarget->size; - const bool needsConversion = tex_copy_conv::needs_conversion(passInfo.resolveFormat); - const bool needsScaling = dstSize.width != static_cast(passInfo.resolveRect.width) || - dstSize.height != static_cast(passInfo.resolveRect.height); - const bool isDepth = gx::is_depth_format(passInfo.resolveFormat); - if (isDepth && passInfo.msaaSamples > 1) { - Log.fatal("Depth tex copies from multisampled EFB targets are not supported"); - } - const tex_copy_conv::ConvRequest convReq{ - .fmt = passInfo.resolveFormat, - .srcView = isDepth ? passInfo.copySourceDepthView : passInfo.copySourceView, - .uniformRange = passInfo.resolveUniformRange, - .dst = passInfo.resolveTarget, - .sampleFilter = needsScaling ? tex_copy_conv::SampleFilter::Linear : tex_copy_conv::SampleFilter::Nearest, + if constexpr (UseTextureBuffer) { + for (size_t i = frame.copied.textureUploadCount; i < op.textureUploads.size(); ++i) { + const auto& item = *op.textureUploads[i]; + const wgpu::TexelCopyBufferInfo buf{ + .layout = + wgpu::TexelCopyBufferLayout{ + .offset = item.buffer ? item.layout.offset : item.layout.offset + TextureUploadStagingOffset, + .bytesPerRow = AURORA_ALIGN(item.layout.bytesPerRow, 256), + .rowsPerImage = item.layout.rowsPerImage, + }, + .buffer = item.buffer ? item.buffer : g_stagingBuffers[frame.stagingBuffer], }; - if (needsConversion) { - tex_copy_conv::run(cmd, convReq); - } else if (needsScaling) { - tex_copy_conv::blit(cmd, convReq); - } else { - const wgpu::TexelCopyTextureInfo src{ - .texture = passInfo.copySourceTexture, - .origin = - wgpu::Origin3D{ - .x = static_cast(passInfo.resolveRect.x), - .y = static_cast(passInfo.resolveRect.y), - }, - }; - const wgpu::TexelCopyTextureInfo dst{ - .texture = passInfo.resolveTarget->texture, - }; - const wgpu::Extent3D size{ - .width = static_cast(passInfo.resolveRect.width), - .height = static_cast(passInfo.resolveRect.height), - .depthOrArrayLayers = 1, - }; - cmd.CopyTextureToTexture(&src, &dst, &size); - } + cmd.CopyBufferToTexture(&buf, &item.tex, &item.size); } + frame.copied.textureUpload = highWater.textureUpload; + frame.copied.textureUploadCount = op.textureUploads.size(); } - g_renderPasses.clear(); - expire_cached_bind_groups(); +} -#if defined(AURORA_GFX_DEBUG_GROUPS) - if (!g_debugGroupStack.empty()) { - for (auto& it : std::ranges::reverse_view(g_debugGroupStack)) { - Log.warn("Debug group was not popped at end of frame: {}", it); +static void encode_op(wgpu::CommandEncoder& cmd, FramePacket& frame, const FrameOp& op) { + copy_staging_to_high_water(cmd, frame, op); + switch (op.type) { + case FrameOpType::RenderPass: + if (op.renderPass != nullptr) { + render(cmd, frame, *op.renderPass, op.index); } - g_debugGroupStack.clear(); + break; + case FrameOpType::TextureCopy: + if (op.textureCopy != nullptr) { + cmd.CopyTextureToTexture(&op.textureCopy->src, &op.textureCopy->dst, &op.textureCopy->size); + } + break; + } +} + +static void render(wgpu::CommandEncoder& cmd, FramePacket& frame, RenderPass& passInfo, uint32_t passIndex) { + ZoneScoped; + if (!passInfo.sealed) { + return; } - if (g_debugMarkers.size() > 0) { - g_debugMarkers.clear(); + for (const auto& conv : passInfo.paletteConvs) { + tex_palette_conv::run(cmd, conv); + } + if (!passInfo.observable && !passInfo.resolveTarget && !passInfo.offscreen) { + // Skip intermediate EFB render passes without observable output. + return; + } + + const std::array attachments{ + wgpu::RenderPassColorAttachment{ + .view = passInfo.colorView, + .resolveTarget = passInfo.resolveView, + .loadOp = passInfo.colorLoadOp != wgpu::LoadOp::Undefined + ? passInfo.colorLoadOp + : (passInfo.clearColor ? wgpu::LoadOp::Clear : wgpu::LoadOp::Load), + .storeOp = passInfo.colorStoreOp, + .clearValue = + { + .r = passInfo.clearColorValue.x(), + .g = passInfo.clearColorValue.y(), + .b = passInfo.clearColorValue.z(), + .a = passInfo.clearColorValue.w(), + }, + }, + }; + wgpu::RenderPassDepthStencilAttachment depthStencilAttachment{}; + const wgpu::RenderPassDepthStencilAttachment* depthStencilAttachmentPtr = nullptr; + if (passInfo.depthStencilView) { + depthStencilAttachment = { + .view = passInfo.depthStencilView, + .depthLoadOp = passInfo.hasDepth ? (passInfo.depthLoadOp != wgpu::LoadOp::Undefined + ? passInfo.depthLoadOp + : (passInfo.clearDepth ? wgpu::LoadOp::Clear : wgpu::LoadOp::Load)) + : wgpu::LoadOp::Undefined, + .depthStoreOp = passInfo.hasDepth ? passInfo.depthStoreOp : wgpu::StoreOp::Undefined, + .depthClearValue = passInfo.clearDepthValue, + .stencilLoadOp = passInfo.hasStencil ? passInfo.stencilLoadOp : wgpu::LoadOp::Undefined, + .stencilStoreOp = passInfo.hasStencil ? passInfo.stencilStoreOp : wgpu::StoreOp::Undefined, + .stencilClearValue = passInfo.stencilClearValue, + }; + depthStencilAttachmentPtr = &depthStencilAttachment; + } + const auto label = fmt::format("Render pass {}", passIndex); + const wgpu::RenderPassDescriptor renderPassDescriptor{ + .label = passInfo.label.empty() ? label.c_str() : passInfo.label.c_str(), + .colorAttachmentCount = attachments.size(), + .colorAttachments = attachments.data(), + .depthStencilAttachment = depthStencilAttachmentPtr, + }; + + auto pass = cmd.BeginRenderPass(&renderPassDescriptor); + render_pass(pass, frame, passInfo); + pass.End(); + + if (passInfo.captureDepthSnapshot) { + depth_peek::encode_frame_snapshot(cmd, passInfo.copySourceDepthView, passInfo.targetSize, passInfo.msaaSamples); + } + + if (passInfo.resolveTarget) { + const auto& dstSize = passInfo.resolveTarget->size; + const bool needsConversion = tex_copy_conv::needs_conversion(passInfo.resolveFormat); + const bool needsScaling = dstSize.width != static_cast(passInfo.resolveRect.width) || + dstSize.height != static_cast(passInfo.resolveRect.height); + const bool isDepth = gx::is_depth_format(passInfo.resolveFormat); + if (isDepth && passInfo.msaaSamples > 1) { + Log.fatal("Depth tex copies from multisampled EFB targets are not supported"); + } + const tex_copy_conv::ConvRequest convReq{ + .fmt = passInfo.resolveFormat, + .srcView = isDepth ? passInfo.copySourceDepthView : passInfo.copySourceView, + .uniformRange = passInfo.resolveUniformRange, + .dst = passInfo.resolveTarget, + .sampleFilter = needsScaling ? tex_copy_conv::SampleFilter::Linear : tex_copy_conv::SampleFilter::Nearest, + }; + if (needsConversion) { + tex_copy_conv::run(cmd, convReq); + } else if (needsScaling) { + tex_copy_conv::blit(cmd, convReq); + } else { + const wgpu::TexelCopyTextureInfo src{ + .texture = passInfo.copySourceTexture, + .origin = + wgpu::Origin3D{ + .x = static_cast(passInfo.resolveRect.x), + .y = static_cast(passInfo.resolveRect.y), + }, + }; + const wgpu::TexelCopyTextureInfo dst{ + .texture = passInfo.resolveTarget->texture, + }; + const wgpu::Extent3D size{ + .width = static_cast(passInfo.resolveRect.width), + .height = static_cast(passInfo.resolveRect.height), + .depthOrArrayLayers = 1, + }; + cmd.CopyTextureToTexture(&src, &dst, &size); + } } -#endif } void after_submit() noexcept { depth_peek::after_submit(); } -void render_pass(const wgpu::RenderPassEncoder& pass, u32 idx) { +void gpu_synchronize() { render_worker::synchronize(); } + +static void render_pass(const wgpu::RenderPassEncoder& pass, FramePacket& frame, const RenderPass& passInfo) { + ZoneScoped; g_currentPipeline = UINTPTR_MAX; #ifdef AURORA_GFX_DEBUG_GROUPS std::vector lastDebugGroupStack; @@ -879,7 +1368,7 @@ void render_pass(const wgpu::RenderPassEncoder& pass, u32 idx) { pass.SetBindGroup(0, g_staticBindGroup); pass.SetBindGroup(2, gx::g_emptyTextureBindGroup); - for (const auto& cmd : g_renderPasses[idx].commands) { + for (const auto& cmd : passInfo.commands) { #ifdef AURORA_GFX_DEBUG_GROUPS { size_t firstDiff = lastDebugGroupStack.size(); @@ -907,7 +1396,7 @@ void render_pass(const wgpu::RenderPassEncoder& pass, u32 idx) { } break; case CommandType::SetScissor: { const auto& sc = cmd.data.setScissor; - const auto& size = g_renderPasses[idx].targetSize; + const auto& size = passInfo.targetSize; const auto x = std::clamp(static_cast(sc.x), 0u, size.width); const auto y = std::clamp(static_cast(sc.y), 0u, size.height); const auto w = std::clamp(static_cast(sc.width), 0u, size.width - x); @@ -918,11 +1407,16 @@ void render_pass(const wgpu::RenderPassEncoder& pass, u32 idx) { const auto& draw = cmd.data.draw; switch (draw.type) { case ShaderType::Clear: - clear::render(draw.clear, pass, g_renderPasses[idx].targetSize); + clear::render(draw.clear, pass, passInfo.targetSize); break; case ShaderType::GX: gx::render(draw.gx, pass); break; +#ifdef AURORA_ENABLE_RMLUI + case ShaderType::Rml: + rmlui::render(draw.rml, pass); + break; +#endif } } break; case CommandType::DebugMarker: { @@ -940,6 +1434,11 @@ void render_pass(const wgpu::RenderPassEncoder& pass, u32 idx) { #endif } +void render_pass(const wgpu::RenderPassEncoder& pass, u32 idx) { + auto& frame = current_frame_packet(); + render_pass(pass, frame, frame.renderPasses[idx]); +} + bool bind_pipeline(PipelineRef ref, const wgpu::RenderPassEncoder& pass) { if (ref == g_currentPipeline) { return true; @@ -953,54 +1452,55 @@ bool bind_pipeline(PipelineRef ref, const wgpu::RenderPassEncoder& pass) { return true; } -static inline Range push(ByteBuffer& target, const uint8_t* data, size_t length, size_t alignment) { - size_t padding = 0; +static Range push(ByteBuffer& target, const uint8_t* data, size_t length, size_t alignment) { if (alignment != 0) { - const size_t remainder = length % alignment; - if (remainder != 0) { - padding = alignment - remainder; + const size_t begin = target.size(); + const size_t alignedBegin = AURORA_ALIGN(begin, alignment); + if (alignedBegin > begin) { + target.append_zeroes(alignedBegin - begin); } } - auto begin = target.size(); - if (length == 0) { - length = alignment; - target.append_zeroes(alignment); - } else { + const auto begin = target.size(); + if (length > 0) { target.append(data, length); - if (padding > 0) { - target.append_zeroes(padding); - } } - return {static_cast(begin), static_cast(length + padding)}; + return {static_cast(begin), static_cast(length)}; } -static inline Range map(ByteBuffer& target, size_t length, size_t alignment) { - size_t padding = 0; +static Range map(ByteBuffer& target, size_t length, size_t alignment) { if (alignment != 0) { - const size_t remainder = length % alignment; - if (remainder != 0) { - padding = alignment - remainder; + const size_t begin = target.size(); + const size_t alignedBegin = AURORA_ALIGN(begin, alignment); + if (alignedBegin > begin) { + target.append_zeroes(alignedBegin - begin); } } - if (length == 0) { - length = alignment; - } auto begin = target.size(); - target.append_zeroes(length + padding); - return {static_cast(begin), static_cast(length + padding)}; + if (length > 0) { + target.append_zeroes(length); + } + return {static_cast(begin), static_cast(length)}; +} +Range push_verts(const uint8_t* data, size_t length, size_t alignment) { + ZoneScoped; + return push(current_frame_packet().verts, data, length, alignment); +} +Range push_indices(const uint8_t* data, size_t length, size_t alignment) { + ZoneScoped; + return push(current_frame_packet().indices, data, length, alignment); } -Range push_verts(const uint8_t* data, size_t length) { return push(g_verts, data, length, 0); } -Range push_indices(const uint8_t* data, size_t length) { return push(g_indices, data, length, 0); } Range push_uniform(const uint8_t* data, size_t length) { - return push(g_uniforms, data, length, g_cachedLimits.minUniformBufferOffsetAlignment); + ZoneScoped; + return push(current_frame_packet().uniforms, data, length, g_cachedLimits.minUniformBufferOffsetAlignment); } Range push_storage(const uint8_t* data, size_t length) { - return push(g_storage, data, length, g_cachedLimits.minStorageBufferOffsetAlignment); + ZoneScoped; + return push(current_frame_packet().storage, data, length, g_cachedLimits.minStorageBufferOffsetAlignment); } Range push_texture_data(const uint8_t* data, size_t length, u32 bytesPerRow, u32 rowsPerImage) { // For CopyBufferToTexture, we need an alignment of 256 per row (see Dawn kTextureBytesPerRowAlignment) const auto copyBytesPerRow = AURORA_ALIGN(bytesPerRow, 256); - const auto range = map(g_textureUpload, copyBytesPerRow * rowsPerImage, 0); - u8* dst = g_textureUpload.data() + range.offset; + const auto range = map(current_frame_packet().textureUpload, copyBytesPerRow * rowsPerImage, 0); + u8* dst = current_frame_packet().textureUpload.data() + range.offset; for (u32 i = 0; i < rowsPerImage; ++i) { memcpy(dst, data, bytesPerRow); data += bytesPerRow; @@ -1009,24 +1509,25 @@ Range push_texture_data(const uint8_t* data, size_t length, u32 bytesPerRow, u32 return range; } std::pair map_verts(size_t length) { - const auto range = map(g_verts, length, 4); - return {ByteBuffer{g_verts.data() + range.offset, range.size}, range}; + const auto range = map(current_frame_packet().verts, length, 4); + return {ByteBuffer{current_frame_packet().verts.data() + range.offset, range.size}, range}; } std::pair map_indices(size_t length) { - const auto range = map(g_indices, length, 4); - return {ByteBuffer{g_indices.data() + range.offset, range.size}, range}; + const auto range = map(current_frame_packet().indices, length, 4); + return {ByteBuffer{current_frame_packet().indices.data() + range.offset, range.size}, range}; } std::pair map_uniform(size_t length) { - const auto range = map(g_uniforms, length, g_cachedLimits.minUniformBufferOffsetAlignment); - return {ByteBuffer{g_uniforms.data() + range.offset, range.size}, range}; + const auto range = map(current_frame_packet().uniforms, length, g_cachedLimits.minUniformBufferOffsetAlignment); + return {ByteBuffer{current_frame_packet().uniforms.data() + range.offset, range.size}, range}; } std::pair map_storage(size_t length) { - const auto range = map(g_storage, length, g_cachedLimits.minStorageBufferOffsetAlignment); - return {ByteBuffer{g_storage.data() + range.offset, range.size}, range}; + const auto range = map(current_frame_packet().storage, length, g_cachedLimits.minStorageBufferOffsetAlignment); + return {ByteBuffer{current_frame_packet().storage.data() + range.offset, range.size}, range}; } BindGroupRef bind_group_ref(const WGPUBindGroupDescriptor& descriptor) { const auto id = xxh3_hash(descriptor); + std::lock_guard lock{g_bindGroupCacheMutex}; const auto it = g_cachedBindGroups.find(id); if (it == g_cachedBindGroups.end()) { auto bg = wgpu::BindGroup::Acquire(wgpuDeviceCreateBindGroup(g_device.Get(), &descriptor)); @@ -1040,14 +1541,16 @@ BindGroupRef bind_group_ref(const WGPUBindGroupDescriptor& descriptor) { return id; } -wgpu::BindGroup& find_bind_group(BindGroupRef id) { +wgpu::BindGroup find_bind_group(BindGroupRef id) { + std::lock_guard lock{g_bindGroupCacheMutex}; const auto it = g_cachedBindGroups.find(id); CHECK(it != g_cachedBindGroups.end(), "get_bind_group: failed to locate {:x}", id); return it->second.bindGroup; } -wgpu::Sampler& sampler_ref(const wgpu::SamplerDescriptor& descriptor) { +wgpu::Sampler sampler_ref(const wgpu::SamplerDescriptor& descriptor) { const auto id = xxh3_hash(descriptor); + std::lock_guard lock{g_samplerCacheMutex}; auto it = g_cachedSamplers.find(id); if (it == g_cachedSamplers.end()) { it = g_cachedSamplers.try_emplace(id, g_device.CreateSampler(&descriptor)).first; diff --git a/lib/gfx/common.hpp b/lib/gfx/common.hpp index 45608a8..db245d0 100644 --- a/lib/gfx/common.hpp +++ b/lib/gfx/common.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -170,7 +171,7 @@ private: } // namespace aurora namespace aurora::gfx { -inline constexpr bool UseTextureBuffer = false; +inline constexpr bool UseTextureBuffer = true; inline constexpr uint64_t UniformBufferSize = 25165824; // 24mb inline constexpr uint64_t VertexBufferSize = 3145728; // 3mb inline constexpr uint64_t IndexBufferSize = 1048576; // 1mb @@ -215,25 +216,52 @@ using webgpu::Viewport; struct TextureRef; using TextureHandle = std::shared_ptr; +using EndFrameCallback = std::function; enum class ShaderType : uint8_t { Clear = 0, GX = 1, + Rml = 2, }; void initialize(); void shutdown(); bool begin_frame(); -void end_frame(const wgpu::CommandEncoder& cmd); +void finish(); +void end_frame(EndFrameCallback callback); uint32_t current_frame() noexcept; -void render(wgpu::CommandEncoder& cmd); void render_pass(const wgpu::RenderPassEncoder& pass, uint32_t idx); void after_submit() noexcept; -void map_staging_buffer(); +void gpu_synchronize(); void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool clearAlpha, bool clearDepth, Vec4 clearColorValue, float clearDepthValue, GXTexFmt resolveFormat = GX_TF_RGBA8); +struct ColorPassDescriptor { + const char* label = nullptr; + wgpu::TextureView colorView; + wgpu::TextureView resolveView; + wgpu::TextureView depthStencilView; + wgpu::Extent3D targetSize; + uint32_t sampleCount = 1; + wgpu::LoadOp colorLoadOp = wgpu::LoadOp::Clear; + wgpu::StoreOp colorStoreOp = wgpu::StoreOp::Store; + wgpu::Color clearColor{0.f, 0.f, 0.f, 0.f}; + bool hasDepth = false; + wgpu::LoadOp depthLoadOp = wgpu::LoadOp::Undefined; + wgpu::StoreOp depthStoreOp = wgpu::StoreOp::Undefined; + float depthClearValue = 0.f; + bool hasStencil = false; + wgpu::LoadOp stencilLoadOp = wgpu::LoadOp::Undefined; + wgpu::StoreOp stencilStoreOp = wgpu::StoreOp::Undefined; + uint32_t stencilClearValue = 0; + bool observable = true; +}; + +void begin_color_pass(const ColorPassDescriptor& desc); +void end_color_pass(); +void queue_texture_copy(wgpu::TexelCopyTextureInfo src, wgpu::TexelCopyTextureInfo dst, wgpu::Extent3D size); + void begin_offscreen(uint32_t width, uint32_t height); void end_offscreen(); bool is_offscreen() noexcept; @@ -245,15 +273,15 @@ struct ConvRequest; } // namespace tex_palette_conv void queue_palette_conv(tex_palette_conv::ConvRequest req); -Range push_verts(const uint8_t* data, size_t length); +Range push_verts(const uint8_t* data, size_t length, size_t alignment); template -static Range push_verts(ArrayRef data) { - return push_verts(reinterpret_cast(data.data()), data.size() * sizeof(T)); +static Range push_verts(ArrayRef data, size_t alignment) { + return push_verts(reinterpret_cast(data.data()), data.size() * sizeof(T), alignment); } -Range push_indices(const uint8_t* data, size_t length); +Range push_indices(const uint8_t* data, size_t length, size_t alignment); template -static Range push_indices(ArrayRef data) { - return push_indices(reinterpret_cast(data.data()), data.size() * sizeof(T)); +static Range push_indices(ArrayRef data, size_t alignment) { + return push_indices(reinterpret_cast(data.data()), data.size() * sizeof(T), alignment); } Range push_uniform(const uint8_t* data, size_t length); template @@ -287,9 +315,9 @@ PipelineRef pipeline_ref(const PipelineConfig& config); bool bind_pipeline(PipelineRef ref, const wgpu::RenderPassEncoder& pass); BindGroupRef bind_group_ref(const WGPUBindGroupDescriptor& descriptor); -wgpu::BindGroup& find_bind_group(BindGroupRef id); +wgpu::BindGroup find_bind_group(BindGroupRef id); -wgpu::Sampler& sampler_ref(const wgpu::SamplerDescriptor& descriptor); +wgpu::Sampler sampler_ref(const wgpu::SamplerDescriptor& descriptor); uint32_t align_uniform(uint32_t value); diff --git a/lib/gfx/depth_peek.cpp b/lib/gfx/depth_peek.cpp index 66a7d5e..6e7f888 100644 --- a/lib/gfx/depth_peek.cpp +++ b/lib/gfx/depth_peek.cpp @@ -2,6 +2,7 @@ #include "../dolphin/vi/vi_internal.hpp" #include "../gx/gx.hpp" +#include "../gfx/render_worker.hpp" #include "../webgpu/gpu.hpp" #include @@ -330,12 +331,6 @@ bool read_latest(uint16_t x, uint16_t y, uint32_t& z) noexcept { return true; } -void poll() noexcept { - if (g_instance) { - g_instance.ProcessEvents(); - } -} - void encode_frame_snapshot(const wgpu::CommandEncoder& cmd, const wgpu::TextureView& depthView, wgpu::Extent3D sourceSize, uint32_t msaaSamples) noexcept { ZoneScoped; @@ -375,6 +370,7 @@ void encode_frame_snapshot(const wgpu::CommandEncoder& cmd, const wgpu::TextureV byteSize = slot->byteSize; } + ASSERT(render_worker::is_worker_thread(), "Depth peek queue write must run on the render worker"); g_queue.WriteBuffer(paramsBuffer, 0, ¶ms, sizeof(params)); const std::array bindGroupEntries{ diff --git a/lib/gfx/depth_peek.hpp b/lib/gfx/depth_peek.hpp index 0f6fec9..b9ca78e 100644 --- a/lib/gfx/depth_peek.hpp +++ b/lib/gfx/depth_peek.hpp @@ -11,7 +11,6 @@ void shutdown(); void request_snapshot() noexcept; bool read_latest(uint16_t x, uint16_t y, uint32_t& z) noexcept; -void poll() noexcept; void encode_frame_snapshot(const wgpu::CommandEncoder& cmd, const wgpu::TextureView& depthView, wgpu::Extent3D sourceSize, uint32_t msaaSamples) noexcept; diff --git a/lib/gfx/pipeline_cache.cpp b/lib/gfx/pipeline_cache.cpp index 9c8fc5c..3379476 100644 --- a/lib/gfx/pipeline_cache.cpp +++ b/lib/gfx/pipeline_cache.cpp @@ -3,6 +3,9 @@ #include "clear.hpp" #include "../fs_helper.hpp" #include "../gx/pipeline.hpp" +#ifdef AURORA_ENABLE_RMLUI +#include "../rmlui/pipeline.hpp" +#endif #include "../sqlite_utils.hpp" #include "../webgpu/gpu.hpp" @@ -140,12 +143,18 @@ static PendingPipeline* touch_pending_pipeline(PipelineRef hash, bool prioritize return &g_priorityPipelines.back(); } +static PipelineRef g_lastPipelineRef = std::numeric_limits::max(); + template static PipelineRef find_pipeline_impl(ShaderType type, const PipelineConfig& config, NewPipelineCallback&& cb, bool persist, std::optional firstFrameUsedOverride) { ZoneScoped; const PipelineRef hash = xxh3_hash(config, static_cast(type)); + if (hash == g_lastPipelineRef) { + return g_lastPipelineRef; + } + g_lastPipelineRef = hash; const uint32_t firstFrameUsed = firstFrameUsedOverride.value_or(current_frame()); bool notifyWorker = false; std::optional cacheWrite; @@ -352,7 +361,18 @@ static void prune_old_pipeline_cache_versions() { if (ret != SQLITE_OK) { Log.error("Failed to prune GX pipeline cache rows: {}", sqlite3_errmsg(g_pipelineCacheDb)); pipeline_cache_abort(); + return; } + +#ifdef AURORA_ENABLE_RMLUI + const auto rmlDelete = fmt::format("DELETE FROM pipeline_cache WHERE type = {} AND config_version < {}", + underlying(ShaderType::Rml), rmlui::RmlPipelineConfigVersion); + ret = sqlite::exec(g_pipelineCacheDb, rmlDelete.c_str()); + if (ret != SQLITE_OK) { + Log.error("Failed to prune RmlUi pipeline cache rows: {}", sqlite3_errmsg(g_pipelineCacheDb)); + pipeline_cache_abort(); + } +#endif } static bool write_pipeline_cache_record(const PipelineCacheWrite& write) { @@ -552,6 +572,13 @@ static void load_pipeline_cache() { return; } load_pipeline_cache_entries(ShaderType::GX, gx::GXPipelineConfigVersion, gx::create_pipeline); +#ifdef AURORA_ENABLE_RMLUI + if (g_pipelineCacheBroken) { + return; + } + load_pipeline_cache_entries(ShaderType::Rml, rmlui::RmlPipelineConfigVersion, + rmlui::create_pipeline); +#endif } static void start_pipeline_cache_writer() { @@ -588,6 +615,13 @@ PipelineRef find_pipeline(ShaderType type, const gx::PipelineConfig& config, New return find_pipeline_impl(type, config, std::move(cb), true, std::nullopt); } +#ifdef AURORA_ENABLE_RMLUI +template <> +PipelineRef find_pipeline(ShaderType type, const rmlui::PipelineConfig& config, NewPipelineCallback&& cb) { + return find_pipeline_impl(type, config, std::move(cb), true, std::nullopt); +} +#endif + void initialize_pipeline_cache() { g_pipelineCacheBroken = false; g_pipelineCacheWriterStop = false; diff --git a/lib/gfx/pipeline_cache.hpp b/lib/gfx/pipeline_cache.hpp index 06d7a8b..2e87f4c 100644 --- a/lib/gfx/pipeline_cache.hpp +++ b/lib/gfx/pipeline_cache.hpp @@ -12,6 +12,10 @@ namespace aurora::gx { struct PipelineConfig; } // namespace aurora::gx +namespace aurora::rmlui { +struct PipelineConfig; +} // namespace aurora::rmlui + namespace aurora::gfx { using NewPipelineCallback = std::function; diff --git a/lib/gfx/render_worker.cpp b/lib/gfx/render_worker.cpp new file mode 100644 index 0000000..1484076 --- /dev/null +++ b/lib/gfx/render_worker.cpp @@ -0,0 +1,270 @@ +#include "render_worker.hpp" + +#include +#include +#include + +#include + +namespace aurora::gfx::render_worker { +namespace { +constexpr size_t QueueCapacity = 256; +constexpr auto IdlePumpInterval = std::chrono::milliseconds{1}; + +BoundedQueue g_queue{QueueCapacity}; +std::thread g_thread; +std::atomic_bool g_running = false; +std::atomic_size_t g_pendingItems = 0; +std::thread::id g_workerThreadId; + +void complete_sync(const std::shared_ptr& sync) { + if (!sync) { + return; + } + ZoneScoped; + { + std::lock_guard lock{sync->mutex}; + sync->complete = true; + } + sync->cv.notify_all(); +} + +void worker_main() { +#ifdef TRACY_ENABLE + tracy::SetThreadName("Aurora render worker"); +#endif + g_workerThreadId = std::this_thread::get_id(); + + while (true) { + bool closed = false; + auto item = g_queue.pop_for(IdlePumpInterval, closed); + if (!item) { + if (closed) { + break; + } + continue; + } + + if (item->work) { + ZoneScopedN("QueueItem work"); + item->work(); + } + complete_sync(item->sync); + g_pendingItems.fetch_sub(1, std::memory_order_acq_rel); + if (item->type == ItemType::Shutdown) { + break; + } + } + + g_workerThreadId = {}; +} + +void enqueue(QueueItem item) { + ZoneScoped; + if (is_worker_thread()) { + if (item.work) { + item.work(); + } + complete_sync(item.sync); + return; + } + + if (!g_running.load(std::memory_order_acquire)) { + if (item.work) { + item.work(); + } + complete_sync(item.sync); + return; + } + + g_pendingItems.fetch_add(1, std::memory_order_acq_rel); + if (!g_queue.push(std::move(item))) { + g_pendingItems.fetch_sub(1, std::memory_order_acq_rel); + } +} +} // namespace + +BoundedQueue::BoundedQueue(size_t capacity) : m_capacity(capacity) {} + +bool BoundedQueue::push(QueueItem item) { + ZoneScoped; + std::unique_lock lock{m_mutex}; + m_notFull.wait(lock, [&] { return m_closed || m_items.size() < m_capacity; }); + if (m_closed) { + return false; + } + m_items.emplace_back(std::move(item)); + lock.unlock(); + m_notEmpty.notify_one(); + return true; +} + +std::optional BoundedQueue::pop_for(std::chrono::milliseconds timeout, bool& closed) { + std::unique_lock lock{m_mutex}; + m_notEmpty.wait_for(lock, timeout, [&] { return m_closed || !m_items.empty(); }); + closed = m_closed && m_items.empty(); + if (m_items.empty()) { + return std::nullopt; + } + auto item = std::move(m_items.front()); + m_items.pop_front(); + lock.unlock(); + m_notFull.notify_one(); + return item; +} + +void BoundedQueue::close() { + { + std::lock_guard lock{m_mutex}; + m_closed = true; + } + m_notEmpty.notify_all(); + m_notFull.notify_all(); +} + +void BoundedQueue::reset() { + { + std::lock_guard lock{m_mutex}; + m_items.clear(); + m_closed = false; + } + m_notFull.notify_all(); +} + +size_t BoundedQueue::size() const { + std::lock_guard lock{m_mutex}; + return m_items.size(); +} + +FrameSlotPool::FrameSlotPool(size_t slotCount) : m_freeSlots(slotCount, true) {} + +size_t FrameSlotPool::acquire() { + std::unique_lock lock{m_mutex}; + m_cv.wait(lock, [&] { + for (const bool free : m_freeSlots) { + if (free) { + return true; + } + } + return false; + }); + for (size_t i = 0; i < m_freeSlots.size(); ++i) { + if (m_freeSlots[i]) { + m_freeSlots[i] = false; + return i; + } + } + return 0; +} + +std::optional FrameSlotPool::try_acquire() { + std::lock_guard lock{m_mutex}; + for (size_t i = 0; i < m_freeSlots.size(); ++i) { + if (m_freeSlots[i]) { + m_freeSlots[i] = false; + return i; + } + } + return std::nullopt; +} + +void FrameSlotPool::release(size_t slot) { + { + std::lock_guard lock{m_mutex}; + if (slot < m_freeSlots.size()) { + m_freeSlots[slot] = true; + } + } + m_cv.notify_one(); +} + +void FrameSlotPool::reset() { + { + std::lock_guard lock{m_mutex}; + std::fill(m_freeSlots.begin(), m_freeSlots.end(), true); + } + m_cv.notify_all(); +} + +size_t FrameSlotPool::free_count() const { + std::lock_guard lock{m_mutex}; + return static_cast(std::ranges::count(m_freeSlots, true)); +} + +void initialize() { + if (g_running.exchange(true, std::memory_order_acq_rel)) { + return; + } + g_queue.reset(); + g_pendingItems.store(0, std::memory_order_release); + g_thread = std::thread(worker_main); +} + +void shutdown() { + if (!g_running.load(std::memory_order_acquire)) { + return; + } + enqueue({ + .type = ItemType::Shutdown, + }); + if (g_thread.joinable()) { + g_thread.join(); + } + g_running.store(false, std::memory_order_release); + g_queue.close(); + g_queue.reset(); + g_pendingItems.store(0, std::memory_order_release); +} + +void enqueue_begin_frame(uint64_t frameId, WorkCallback work) { + enqueue({ + .type = ItemType::BeginFrame, + .frameId = frameId, + .work = std::move(work), + }); +} + +void enqueue_encode_pass(uint64_t frameId, uint32_t passIndex, WorkCallback work) { + enqueue({ + .type = ItemType::EncodePass, + .frameId = frameId, + .passIndex = passIndex, + .work = std::move(work), + }); +} + +void enqueue_end_frame(uint64_t frameId, WorkCallback work) { + enqueue({ + .type = ItemType::EndFrame, + .frameId = frameId, + .work = std::move(work), + }); +} + +void enqueue_work(WorkCallback work) { + enqueue({ + .type = ItemType::Sync, + .work = std::move(work), + }); +} + +void synchronize() { + if (is_worker_thread()) { + return; + } + + ZoneScoped; + auto sync = std::make_shared(); + enqueue({ + .type = ItemType::Sync, + .sync = sync, + }); + std::unique_lock lock{sync->mutex}; + sync->cv.wait(lock, [&] { return sync->complete; }); +} + +bool is_worker_thread() noexcept { return g_workerThreadId == std::this_thread::get_id(); } + +bool is_idle() noexcept { return g_pendingItems.load(std::memory_order_acquire) == 0; } + +} // namespace aurora::gfx::render_worker diff --git a/lib/gfx/render_worker.hpp b/lib/gfx/render_worker.hpp new file mode 100644 index 0000000..7d652ca --- /dev/null +++ b/lib/gfx/render_worker.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace aurora::gfx::render_worker { + +enum class ItemType : uint8_t { + BeginFrame, + EncodePass, + EndFrame, + Sync, + Shutdown, +}; + +using WorkCallback = std::function; + +struct SyncState { + std::mutex mutex; + std::condition_variable cv; + bool complete = false; +}; + +struct QueueItem { + ItemType type = ItemType::Sync; + uint64_t frameId = 0; + uint32_t passIndex = 0; + WorkCallback work; + std::shared_ptr sync; +}; + +class BoundedQueue { +public: + explicit BoundedQueue(size_t capacity); + + bool push(QueueItem item); + std::optional pop_for(std::chrono::milliseconds timeout, bool& closed); + void close(); + void reset(); + [[nodiscard]] size_t size() const; + +private: + size_t m_capacity = 0; + mutable std::mutex m_mutex; + std::condition_variable m_notEmpty; + std::condition_variable m_notFull; + std::deque m_items; + bool m_closed = false; +}; + +class FrameSlotPool { +public: + explicit FrameSlotPool(size_t slotCount); + + size_t acquire(); + std::optional try_acquire(); + void release(size_t slot); + void reset(); + [[nodiscard]] size_t free_count() const; + +private: + mutable std::mutex m_mutex; + std::condition_variable m_cv; + std::vector m_freeSlots; +}; + +void initialize(); +void shutdown(); + +void enqueue_begin_frame(uint64_t frameId, WorkCallback work); +void enqueue_encode_pass(uint64_t frameId, uint32_t passIndex, WorkCallback work); +void enqueue_end_frame(uint64_t frameId, WorkCallback work); +void enqueue_work(WorkCallback work); +void synchronize(); + +bool is_worker_thread() noexcept; +bool is_idle() noexcept; + +} // namespace aurora::gfx::render_worker diff --git a/lib/gfx/texture.cpp b/lib/gfx/texture.cpp index e788b89..36eb38a 100644 --- a/lib/gfx/texture.cpp +++ b/lib/gfx/texture.cpp @@ -133,13 +133,8 @@ TextureHandle new_static_texture_2d(uint32_t width, uint32_t height, uint32_t mi .mipLevel = mip, }; if constexpr (UseTextureBuffer) { - const auto range = push_texture_data(data.data() + offset, dataSize, bytesPerRow, heightBlocks); - const wgpu::TexelCopyBufferLayout dataLayout{ - .offset = range.offset, - .bytesPerRow = bytesPerRow, - .rowsPerImage = heightBlocks, - }; - g_textureUploads.emplace_back(dataLayout, std::move(dstView), physicalSize); + queue_texture_upload_data(data.data() + offset, dataSize, bytesPerRow, heightBlocks, std::move(dstView), + physicalSize); } else { const wgpu::TexelCopyBufferLayout dataLayout{ .bytesPerRow = bytesPerRow, @@ -288,13 +283,8 @@ void write_texture(TextureRef& ref, ArrayRef data) noexcept { .mipLevel = mip, }; if constexpr (UseTextureBuffer) { - const auto range = push_texture_data(data.data() + offset, dataSize, bytesPerRow, heightBlocks); - const wgpu::TexelCopyBufferLayout dataLayout{ - .offset = range.offset, - .bytesPerRow = bytesPerRow, - .rowsPerImage = heightBlocks, - }; - g_textureUploads.emplace_back(dataLayout, std::move(dstView), physicalSize); + queue_texture_upload_data(data.data() + offset, dataSize, bytesPerRow, heightBlocks, std::move(dstView), + physicalSize); } else { const wgpu::TexelCopyBufferLayout dataLayout{ .bytesPerRow = bytesPerRow, diff --git a/lib/gfx/texture.hpp b/lib/gfx/texture.hpp index 858a9dc..14d89c9 100644 --- a/lib/gfx/texture.hpp +++ b/lib/gfx/texture.hpp @@ -10,11 +10,17 @@ struct TextureUpload { wgpu::TexelCopyBufferLayout layout; wgpu::TexelCopyTextureInfo tex; wgpu::Extent3D size; + wgpu::Buffer buffer; TextureUpload(wgpu::TexelCopyBufferLayout layout, wgpu::TexelCopyTextureInfo tex, wgpu::Extent3D size) noexcept : layout(layout), tex(std::move(tex)), size(size) {} + TextureUpload(wgpu::TexelCopyBufferLayout layout, wgpu::TexelCopyTextureInfo tex, wgpu::Extent3D size, + wgpu::Buffer buffer) noexcept + : layout(layout), tex(std::move(tex)), size(size), buffer(std::move(buffer)) {} }; -extern std::vector g_textureUploads; +void queue_texture_upload(TextureUpload upload); +void queue_texture_upload_data(const uint8_t* data, size_t length, uint32_t bytesPerRow, uint32_t rowsPerImage, + wgpu::TexelCopyTextureInfo tex, wgpu::Extent3D size); struct TextureFormatInfo { uint8_t blockWidth; diff --git a/lib/gfx/texture_convert.cpp b/lib/gfx/texture_convert.cpp index 91db40d..199806b 100644 --- a/lib/gfx/texture_convert.cpp +++ b/lib/gfx/texture_convert.cpp @@ -558,6 +558,7 @@ static ByteBuffer BuildRGBA8FromBC1(uint32_t width, uint32_t height, uint32_t mi } ConvertedTexture convert_texture(u32 format, uint32_t width, uint32_t height, uint32_t mips, ArrayRef data) { + ZoneScoped; ByteBuffer converted; switch (format) { DEFAULT_FATAL("convert_texture: unknown texture format {}", format); diff --git a/lib/gx/command_processor.cpp b/lib/gx/command_processor.cpp index 3f7ff75..149d823 100644 --- a/lib/gx/command_processor.cpp +++ b/lib/gx/command_processor.cpp @@ -1562,32 +1562,47 @@ static void handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndi if (pos + totalVtxBytes > size) UNLIKELY { handle_draw_overrun(totalVtxBytes, data, pos, size); } - // Push raw vertex data to buffer - gfx::Range vertRange = gfx::push_verts(data + pos, totalVtxBytes); + auto* lastDraw = !g_gxState.stateDirty ? gfx::get_last_draw_command() : nullptr; + const bool canMerge = lastDraw != nullptr && prim != GX_LINES && prim != GX_LINESTRIP && prim != GX_POINTS && + lastDraw->instanceCount == 1; + + // Push raw vertex data to buffer. Merged draws must remain byte-contiguous with the previous range. + gfx::Range vertRange = gfx::push_verts(data + pos, totalVtxBytes, canMerge ? 0 : 4); pos += totalVtxBytes; // Try to merge with previous draw call - if (!g_gxState.stateDirty) LIKELY { - auto* lastDraw = gfx::get_last_draw_command(); + if (canMerge) { // Only if the previous draw call was a single instance draw (no lines/points handling) - if (lastDraw != nullptr && prim != GX_LINES && prim != GX_LINESTRIP && prim != GX_POINTS && - lastDraw->instanceCount == 1) LIKELY { - u32 numIndices = prepare_idx_buffer(handle_draw_idx_buf, prim, lastDraw->vtxCount, vtxCount); - gfx::Range idxRange = gfx::push_indices(handle_draw_idx_buf.data(), handle_draw_idx_buf.size()); + u32 numIndices = 0; + gfx::Range idxRange; + const bool hadIndexRange = lastDraw->idxRange.size != 0; + if (lastDraw->indexCount == 0 && prim != GX_TRIANGLES) { + // Generate triangle index buffer for previous draw + lastDraw->indexCount = prepare_idx_buffer(handle_draw_idx_buf, GX_TRIANGLES, 0, lastDraw->vtxCount); + } + if (lastDraw->indexCount != 0) { + numIndices += prepare_idx_buffer(handle_draw_idx_buf, prim, lastDraw->vtxCount, vtxCount); + idxRange = gfx::push_indices(handle_draw_idx_buf.data(), handle_draw_idx_buf.size(), hadIndexRange ? 0 : 4); handle_draw_idx_buf.clear(); - CHECK(lastDraw->vertRange.offset + lastDraw->vertRange.size == vertRange.offset, - "Non-consecutive vertex ranges ({} < {})", lastDraw->vertRange.offset + lastDraw->vertRange.size, - vertRange.offset); + } + CHECK(lastDraw->vertRange.offset + lastDraw->vertRange.size == vertRange.offset, + "Non-consecutive vertex ranges ({} < {})", lastDraw->vertRange.offset + lastDraw->vertRange.size, + vertRange.offset); + if (hadIndexRange) { CHECK(lastDraw->idxRange.offset + lastDraw->idxRange.size == idxRange.offset, "Non-consecutive index ranges ({} < {})", lastDraw->idxRange.offset + lastDraw->idxRange.size, idxRange.offset); - lastDraw->vertRange.size += vertRange.size; - lastDraw->idxRange.size += idxRange.size; - lastDraw->vtxCount += vtxCount; - lastDraw->indexCount += numIndices; - ++gfx::g_mergedDrawCallCount; - return; } + lastDraw->vertRange.size += vertRange.size; + if (lastDraw->idxRange.size == 0) { + lastDraw->idxRange = idxRange; + } else { + lastDraw->idxRange.size += idxRange.size; + } + lastDraw->vtxCount += vtxCount; + lastDraw->indexCount += numIndices; + ++gfx::g_mergedDrawCallCount; + return; } handle_draw_unmerged(prim, fmt, vtxCount, vertRange); @@ -1600,11 +1615,11 @@ static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, g u32 numIndices = 0; gfx::Range idxRange; - { - ByteBuffer idxBuf; - auto& realBuf = vtxCount < 1000 ? handle_draw_unmerged_idxBuf : idxBuf; + if (prim != GX_TRIANGLES) { + ZoneScopedN("build idx buffer"); + auto& realBuf = handle_draw_unmerged_idxBuf; numIndices = prepare_idx_buffer(realBuf, prim, 0, vtxCount); - idxRange = gfx::push_indices(realBuf.data(), realBuf.size()); + idxRange = gfx::push_indices(realBuf.data(), realBuf.size(), 4); realBuf.clear(); } diff --git a/lib/gx/pipeline.cpp b/lib/gx/pipeline.cpp index aad7efc..a5088fd 100644 --- a/lib/gx/pipeline.cpp +++ b/lib/gx/pipeline.cpp @@ -29,6 +29,10 @@ void render(const DrawData& data, const wgpu::RenderPassEncoder& pass) { const wgpu::Color color{0.f, 0.f, 0.f, data.dstAlpha / 255.f}; pass.SetBlendConstant(&color); } - pass.DrawIndexed(data.indexCount, data.instanceCount); + if (data.indexCount == 0) { + pass.Draw(data.vtxCount, data.instanceCount); + } else { + pass.DrawIndexed(data.indexCount, data.instanceCount); + } } } // namespace aurora::gx diff --git a/lib/gx/shader_info.cpp b/lib/gx/shader_info.cpp index 5512f5a..bb9ddec 100644 --- a/lib/gx/shader_info.cpp +++ b/lib/gx/shader_info.cpp @@ -371,7 +371,10 @@ static u32 line_texcoord_mask() noexcept { gfx::Range build_uniform(const ShaderInfo& info, u32 vtxStart, const BindGroupRanges& ranges) noexcept { ZoneScoped; - auto [buf, range] = gfx::map_uniform(info.uniformSize); + static ByteBuffer buf; + buf.clear(); + buf.reserve_extra(info.uniformSize); + buf.append(vtxStart); buf.append(g_gxState.currentPnMtx); buf.append(g_gxState.renderViewport.width); @@ -479,6 +482,6 @@ gfx::Range build_uniform(const ShaderInfo& info, u32 vtxStart, const BindGroupRa buf.append(texture_size_bias(tex)); } g_gxState.stateDirty = false; - return range; + return gfx::push_uniform(buf.data(), buf.size()); } } // namespace aurora::gx diff --git a/lib/imgui.cpp b/lib/imgui.cpp index 0c31342..02adbe1 100644 --- a/lib/imgui.cpp +++ b/lib/imgui.cpp @@ -2,7 +2,10 @@ #include #include +#include +#include #include +#include #include #include @@ -10,6 +13,7 @@ #include #include "internal.hpp" +#include "gfx/render_worker.hpp" #include "webgpu/gpu.hpp" #include "window.hpp" @@ -28,6 +32,11 @@ static bool g_useSdlRenderer = false; static std::vector g_sdlTextures; static std::vector g_wgpuTextures; +struct DrawData::Impl { + ImDrawData drawData; + std::vector> drawLists; +}; + void create_context() noexcept { IMGUI_CHECKVERSION(); ImGui::CreateContext(); @@ -151,12 +160,33 @@ void new_frame(const AuroraWindowSize& size) noexcept { ImGui::NewFrame(); } -void render(const wgpu::RenderPassEncoder& pass) noexcept { +DrawData freeze() noexcept { ZoneScoped; ImGui::Render(); auto* data = ImGui::GetDrawData(); data->FramebufferScale = ImGui::GetIO().DisplayFramebufferScale; + auto frozen = std::make_shared(); + frozen->drawData = *data; + frozen->drawLists.reserve(data->CmdListsCount); + frozen->drawData.CmdLists.resize(data->CmdListsCount); + for (int i = 0; i < data->CmdListsCount; ++i) { + frozen->drawLists.emplace_back(data->CmdLists[i]->CloneOutput()); + frozen->drawData.CmdLists[i] = frozen->drawLists.back().get(); + } + return DrawData{std::move(frozen)}; +} + +void render(const wgpu::RenderPassEncoder& pass, const DrawData& drawData) noexcept { + ZoneScoped; + + if (!drawData.m_impl) { + return; + } + auto* data = &drawData.m_impl->drawData; + if (data->CmdListsCount == 0) { + return; + } if (g_useSdlRenderer) { SDL_Renderer* renderer = window::get_sdl_renderer(); SDL_RenderClear(renderer); @@ -169,6 +199,47 @@ void render(const wgpu::RenderPassEncoder& pass) noexcept { } } +static wgpu::Buffer create_texture_upload_buffer(uint32_t width, uint32_t height, const uint8_t* data, + uint32_t copyBytesPerRow) { + const uint32_t rowBytes = width * 4; + const uint64_t uploadSize = static_cast(copyBytesPerRow) * height; + const wgpu::BufferDescriptor desc{ + .label = "imgui texture upload buffer", + .usage = wgpu::BufferUsage::CopySrc, + .size = uploadSize, + .mappedAtCreation = true, + }; + auto buffer = webgpu::g_device.CreateBuffer(&desc); + auto* dst = static_cast(buffer.GetMappedRange(0, uploadSize)); + for (uint32_t row = 0; row < height; ++row) { + std::memcpy(dst, data, rowBytes); + dst += copyBytesPerRow; + data += rowBytes; + } + buffer.Unmap(); + return buffer; +} + +static void enqueue_texture_upload(wgpu::Buffer buffer, wgpu::TexelCopyTextureInfo dst, + wgpu::TexelCopyBufferLayout layout, wgpu::Extent3D size) { + gfx::render_worker::enqueue_work([buffer = std::move(buffer), dst = std::move(dst), layout, size] { + const wgpu::CommandEncoderDescriptor encoderDesc{ + .label = "imgui texture upload encoder", + }; + auto encoder = webgpu::g_device.CreateCommandEncoder(&encoderDesc); + const wgpu::TexelCopyBufferInfo src{ + .layout = layout, + .buffer = buffer, + }; + encoder.CopyBufferToTexture(&src, &dst, &size); + const wgpu::CommandBufferDescriptor commandBufferDesc{ + .label = "imgui texture upload command buffer", + }; + auto commandBuffer = encoder.Finish(&commandBufferDesc); + webgpu::g_queue.Submit(1, &commandBuffer); + }); +} + ImTextureID add_texture(uint32_t width, uint32_t height, const uint8_t* data) noexcept { if (SDL_Renderer* renderer = window::get_sdl_renderer()) { SDL_Texture* texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STATIC, width, height); @@ -204,11 +275,12 @@ ImTextureID add_texture(uint32_t width, uint32_t height, const uint8_t* data) no const wgpu::TexelCopyTextureInfo dstView{ .texture = texture, }; + const uint32_t copyBytesPerRow = AURORA_ALIGN(width * 4, 256); const wgpu::TexelCopyBufferLayout dataLayout{ - .bytesPerRow = 4 * width, + .bytesPerRow = copyBytesPerRow, .rowsPerImage = height, }; - webgpu::g_queue.WriteTexture(&dstView, data, width * height * 4, &dataLayout, &size); + enqueue_texture_upload(create_texture_upload_buffer(width, height, data, copyBytesPerRow), dstView, dataLayout, size); } g_wgpuTextures.push_back(texture); return reinterpret_cast(textureView.MoveToCHandle()); diff --git a/lib/imgui.hpp b/lib/imgui.hpp index f83771d..a11a7f8 100644 --- a/lib/imgui.hpp +++ b/lib/imgui.hpp @@ -2,6 +2,9 @@ #include +#include +#include + union SDL_Event; namespace wgpu { @@ -9,6 +12,21 @@ class RenderPassEncoder; } // namespace wgpu namespace aurora::imgui { +class DrawData { +public: + DrawData() noexcept = default; + [[nodiscard]] explicit operator bool() const noexcept { return m_impl != nullptr; } + +private: + struct Impl; + explicit DrawData(std::shared_ptr impl) noexcept : m_impl(std::move(impl)) {} + + std::shared_ptr m_impl; + + friend DrawData freeze() noexcept; + friend void render(const wgpu::RenderPassEncoder& pass, const DrawData& drawData) noexcept; +}; + void create_context() noexcept; void initialize() noexcept; void shutdown() noexcept; @@ -16,5 +34,6 @@ void shutdown() noexcept; void process_event(const SDL_Event& event) noexcept; bool wants_capture_event(const SDL_Event& event) noexcept; void new_frame(const AuroraWindowSize& size) noexcept; -void render(const wgpu::RenderPassEncoder& pass) noexcept; +DrawData freeze() noexcept; +void render(const wgpu::RenderPassEncoder& pass, const DrawData& drawData) noexcept; } // namespace aurora::imgui diff --git a/lib/rmlui.cpp b/lib/rmlui.cpp index 2183f1e..f553daa 100644 --- a/lib/rmlui.cpp +++ b/lib/rmlui.cpp @@ -1,6 +1,7 @@ #include "rmlui.hpp" #include +#include #include #include @@ -174,8 +175,8 @@ Rml::TouchList touch_list(SDL_FingerID id, Rml::Vector2f position) { return {Rml::Touch{static_cast(id), position}}; } -void dispatch_touch_event( - TrackedTouch& touch, const char* type, Rml::Vector2f position, bool inside, Rml::Vector2f delta = {}) noexcept { +void dispatch_touch_event(TrackedTouch& touch, const char* type, Rml::Vector2f position, bool inside, + Rml::Vector2f delta = {}) noexcept { if (touch.target == nullptr) { return; } @@ -350,9 +351,7 @@ Rml::Context* get_context() noexcept { return g_context; } bool is_initialized() noexcept { return g_context != nullptr; } -void set_ui_scale(float scale) noexcept { - s_uiScale = scale > 0.0f ? std::clamp(scale, 0.25f, 4.0f) : 0.0f; -} +void set_ui_scale(float scale) noexcept { s_uiScale = scale > 0.0f ? std::clamp(scale, 0.25f, 4.0f) : 0.0f; } float get_ui_scale() noexcept { return s_uiScale; } @@ -409,8 +408,7 @@ void handle_event(SDL_Event& event) noexcept { RmlSDL::InputEventHandler(g_context, window::get_sdl_window(), event); } -RenderOutput render(const wgpu::CommandEncoder& encoder, const webgpu::Viewport& presentViewport, - const webgpu::TextureWithSampler& presentSource) noexcept { +wgpu::BindGroup record_frame(const webgpu::Viewport& presentViewport) noexcept { if (g_context == nullptr) { return {}; } @@ -427,7 +425,7 @@ RenderOutput render(const wgpu::CommandEncoder& encoder, const webgpu::Viewport& auto* renderInterface = get_render_interface(); renderInterface->SetWindowSize(g_context->GetDimensions()); - renderInterface->BeginFrame(encoder, s_renderTarget, presentSource); + renderInterface->BeginFrame(s_renderTarget, webgpu::present_source()); Backend::BeginFrame(); g_context->Render(); @@ -437,11 +435,7 @@ RenderOutput render(const wgpu::CommandEncoder& encoder, const webgpu::Viewport& // We didn't render anything return {}; } - - return { - .texture = &s_renderTarget, - .copyBindGroup = s_renderTargetCopyBindGroup, - }; + return s_renderTargetCopyBindGroup; } void shutdown() noexcept { diff --git a/lib/rmlui.hpp b/lib/rmlui.hpp index 7e72e94..2bd4eb7 100644 --- a/lib/rmlui.hpp +++ b/lib/rmlui.hpp @@ -8,14 +8,10 @@ #include namespace aurora::rmlui { -struct RenderOutput { - const webgpu::TextureWithSampler* texture = nullptr; - wgpu::BindGroup copyBindGroup; -}; void initialize(const AuroraWindowSize& size) noexcept; void handle_event(SDL_Event& event) noexcept; -RenderOutput render(const wgpu::CommandEncoder& encoder, const webgpu::Viewport& presentViewport, - const webgpu::TextureWithSampler& presentSource) noexcept; +wgpu::BindGroup record_frame(const webgpu::Viewport& presentViewport) noexcept; void shutdown() noexcept; + } // namespace aurora::rmlui diff --git a/lib/rmlui/WebGPURenderInterface.cpp b/lib/rmlui/WebGPURenderInterface.cpp index c34c93a..b125d3a 100644 --- a/lib/rmlui/WebGPURenderInterface.cpp +++ b/lib/rmlui/WebGPURenderInterface.cpp @@ -2,6 +2,7 @@ #include "FileInterface_SDL.h" #include "RuntimeTextureProvider.hpp" +#include "pipeline.hpp" #include #include @@ -13,20 +14,20 @@ #include #include #include -#include #include #include #include #include "../logging.hpp" #include "../webgpu/gpu.hpp" -#include "../internal.hpp" -#include "../gfx/clear.hpp" +#include "../gfx/texture.hpp" namespace aurora::rmlui { namespace { Module Log("aurora::rmlui::RenderInterface"); +constexpr size_t rmlBufferOffsetAlignment = 4; + struct Image { std::unique_ptr data; size_t size; @@ -35,14 +36,17 @@ struct Image { }; struct ShaderGeometryData { - wgpu::Buffer m_vertexBuffer; - wgpu::Buffer m_indexBuffer; + std::vector vertices; + std::vector indices; }; struct ShaderTextureData { - wgpu::BindGroup m_bindGroup; 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 { @@ -160,435 +164,101 @@ Rml::Rectanglei downsample_scissor(Rml::Rectanglei scissor) { return scissor; } -wgpu::ComputeState compile_shader(const std::string_view& wgslSource, const std::string_view& label) { - const wgpu::ShaderSourceWGSL source{ - wgpu::ShaderSourceWGSL::Init{ - .nextInChain = nullptr, - .code = wgslSource, - }, - }; - const wgpu::ShaderModuleDescriptor desc{ - .nextInChain = &source, - .label = label, - }; +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 { - .module = webgpu::g_device.CreateShaderModule(&desc), - .entryPoint = "main", + .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), }; } -wgpu::StencilFaceState stencil_face(wgpu::CompareFunction compare, wgpu::StencilOperation passOp) { - return { - .compare = compare, - .failOp = wgpu::StencilOperation::Keep, - .depthFailOp = wgpu::StencilOperation::Keep, - .passOp = passOp, - }; +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 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_pendingUpload.size(), 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 -constexpr std::string_view vertexSource = R"( -struct VertexInput { - @location(0) position: vec2, - @location(1) uv: vec2, - @location(2) color: vec4, -}; - -struct VertexOutput { - @builtin(position) position: vec4, - @location(0) color: vec4, - @location(1) uv: vec2, -}; - -struct Uniforms { - mvp: mat4x4, - translation: vec4, - gamma: f32, -}; - -@group(0) @binding(0) var uniforms: Uniforms; - -@vertex -fn main(in: VertexInput) -> VertexOutput { - var out: VertexOutput; - var translatedPos = uniforms.translation.xy + in.position; - - out.position = uniforms.mvp * vec4(translatedPos, 0.0, 1.0); - out.color = in.color; - out.uv = in.uv; - return out; -} -)"sv; - -constexpr std::string_view fragmentSource = R"( -struct VertexOutput { - @builtin(position) position: vec4, - @location(0) color: vec4, - @location(1) uv: vec2, -}; - -struct Uniforms { - mvp: mat4x4, - translation: vec4, - gamma: f32, -}; - -@group(0) @binding(0) var uniforms: Uniforms; -@group(0) @binding(1) var s: sampler; -@group(1) @binding(0) var t: texture_2d; - -@fragment -fn main(in: VertexOutput) -> @location(0) vec4 { - let color = in.color * textureSample(t, s, in.uv); - if (uniforms.gamma == 1.0) { - return color; - } - let corrected_color = pow(color.rgb, vec3(uniforms.gamma)); - return vec4(corrected_color, color.a); -} -)"sv; - -constexpr std::string_view gradientFragmentSource = R"( -struct VertexOutput { - @builtin(position) position: vec4, - @location(0) color: vec4, - @location(1) uv: vec2, -}; - -struct Uniforms { - mvp: mat4x4, - translation: vec4, - gamma: f32, -}; - -struct GradientUniforms { - function: i32, - num_stops: i32, - p: vec2, - v: vec2, - padding: vec2, - stop_colors: array, 16>, - stop_positions: array, 4>, -}; - -@group(0) @binding(0) var uniforms: Uniforms; -@group(1) @binding(0) var gradient: GradientUniforms; - -const LINEAR: i32 = 0; -const RADIAL: i32 = 1; -const CONIC: i32 = 2; -const REPEATING_LINEAR: i32 = 3; -const REPEATING_RADIAL: i32 = 4; -const REPEATING_CONIC: i32 = 5; -const PI: f32 = 3.14159265; - -fn bayer_dither(position: vec4) -> f32 { - let bayer = array( - 0u, 32u, 8u, 40u, 2u, 34u, 10u, 42u, - 48u, 16u, 56u, 24u, 50u, 18u, 58u, 26u, - 12u, 44u, 4u, 36u, 14u, 46u, 6u, 38u, - 60u, 28u, 52u, 20u, 62u, 30u, 54u, 22u, - 3u, 35u, 11u, 43u, 1u, 33u, 9u, 41u, - 51u, 19u, 59u, 27u, 49u, 17u, 57u, 25u, - 15u, 47u, 7u, 39u, 13u, 45u, 5u, 37u, - 63u, 31u, 55u, 23u, 61u, 29u, 53u, 21u - ); - let x = u32(position.x) % 8u; - let y = u32(position.y) % 8u; - return (f32(bayer[x + y * 8u]) / 64.0 - 0.5) / 255.0; -} - -fn stop_position(index: i32) -> f32 { - let stop_index = u32(index); - let group_index = stop_index / 4u; - let component_index = stop_index % 4u; - return gradient.stop_positions[group_index][component_index]; -} - -fn stop_color_mix(t: f32) -> vec4 { - var color = gradient.stop_colors[0]; - - for (var i = 1; i < 16; i = i + 1) { - if (i < gradient.num_stops) { - color = mix(color, gradient.stop_colors[u32(i)], smoothstep(stop_position(i - 1), stop_position(i), t)); - } - } - - return color; -} - -@fragment -fn main(in: VertexOutput) -> @location(0) vec4 { - var t = 0.0; - - if (gradient.function == LINEAR || gradient.function == REPEATING_LINEAR) { - let dist_square = dot(gradient.v, gradient.v); - let v = in.uv - gradient.p; - t = dot(gradient.v, v) / dist_square; - } else if (gradient.function == RADIAL || gradient.function == REPEATING_RADIAL) { - let v = in.uv - gradient.p; - t = length(gradient.v * v); - } else if (gradient.function == CONIC || gradient.function == REPEATING_CONIC) { - let v = in.uv - gradient.p; - let rotated = vec2( - gradient.v.x * v.x + gradient.v.y * v.y, - -gradient.v.y * v.x + gradient.v.x * v.y - ); - t = 0.5 + atan2(-rotated.x, rotated.y) / (2.0 * PI); - } - - if (gradient.function == REPEATING_LINEAR || - gradient.function == REPEATING_RADIAL || - gradient.function == REPEATING_CONIC) { - let t0 = stop_position(0); - let t1 = stop_position(gradient.num_stops - 1); - let span = t1 - t0; - t = t0 + (t - t0) - span * floor((t - t0) / span); - } - - let color = in.color * stop_color_mix(t); - if (uniforms.gamma == 1.0) { - return color; - } - let corrected_color = pow(color.rgb, vec3(uniforms.gamma)); - let dithered_color = clamp(corrected_color + vec3(bayer_dither(in.position)), vec3(0.0), vec3(1.0)); - return vec4(dithered_color, color.a); -} -)"sv; - -constexpr std::string_view fullscreenVertexSource = R"( -struct VertexOutput { - @builtin(position) position: vec4, - @location(0) uv: vec2, -}; - -var pos: array, 3> = array, 3>( - vec2(-1.0, 1.0), - vec2(-1.0, -3.0), - vec2(3.0, 1.0), -); -var uvs: array, 3> = array, 3>( - vec2(0.0, 0.0), - vec2(0.0, 2.0), - vec2(2.0, 0.0), -); - -@vertex -fn main(@builtin(vertex_index) vtxIdx: u32) -> VertexOutput { - var out: VertexOutput; - out.position = vec4(pos[vtxIdx], 0.0, 1.0); - out.uv = uvs[vtxIdx]; - return out; -} -)"sv; - -constexpr std::string_view blurVertexSource = R"( -struct BlurUniforms { - texel_offset: vec2, - radius: f32, - padding: f32, - tex_coord_min: vec2, - tex_coord_max: vec2, - weights: vec4, -}; - -struct VertexOutput { - @builtin(position) position: vec4, - @location(0) uv0: vec2, - @location(1) uv1: vec2, - @location(2) uv2: vec2, - @location(3) uv3: vec2, - @location(4) uv4: vec2, - @location(5) uv5: vec2, - @location(6) uv6: vec2, -}; - -@group(2) @binding(0) var blur: BlurUniforms; - -const BLUR_NUM_WEIGHTS: i32 = 4; - -var pos: array, 3> = array, 3>( - vec2(-1.0, 1.0), - vec2(-1.0, -3.0), - vec2(3.0, 1.0), -); -var uvs: array, 3> = array, 3>( - vec2(0.0, 0.0), - vec2(0.0, 2.0), - vec2(2.0, 0.0), -); - -fn blur_uv(uv: vec2, index: i32) -> vec2 { - return uv - f32(index - BLUR_NUM_WEIGHTS + 1) * blur.texel_offset; -} - -@vertex -fn main(@builtin(vertex_index) vtxIdx: u32) -> VertexOutput { - let uv = uvs[vtxIdx]; - var out: VertexOutput; - out.position = vec4(pos[vtxIdx], 0.0, 1.0); - out.uv0 = blur_uv(uv, 0); - out.uv1 = blur_uv(uv, 1); - out.uv2 = blur_uv(uv, 2); - out.uv3 = blur_uv(uv, 3); - out.uv4 = blur_uv(uv, 4); - out.uv5 = blur_uv(uv, 5); - out.uv6 = blur_uv(uv, 6); - return out; -} -)"sv; - -constexpr std::string_view blitFragmentSource = R"( -@group(0) @binding(1) var s: sampler; -@group(1) @binding(0) var t: texture_2d; - -@fragment -fn main(@location(0) uv: vec2) -> @location(0) vec4 { - return textureSample(t, s, uv); -} -)"sv; - -constexpr std::string_view opaqueBlitFragmentSource = R"( -@group(0) @binding(1) var s: sampler; -@group(1) @binding(0) var t: texture_2d; - -@fragment -fn main(@location(0) uv: vec2) -> @location(0) vec4 { - let color = textureSample(t, s, uv); - return vec4(color.rgb, 1.0); -} -)"sv; - -constexpr std::string_view colorMatrixFragmentSource = R"( -struct ColorMatrixUniforms { - matrix: mat4x4, -}; - -@group(0) @binding(1) var s: sampler; -@group(1) @binding(0) var t: texture_2d; -@group(2) @binding(0) var color_matrix: ColorMatrixUniforms; - -@fragment -fn main(@location(0) uv: vec2) -> @location(0) vec4 { - let tex_color = textureSample(t, s, uv); - let transformed_color = (color_matrix.matrix * tex_color).rgb; - return vec4(transformed_color, tex_color.a); -} -)"sv; - -constexpr std::string_view maskImageFragmentSource = R"( -@group(0) @binding(1) var s: sampler; -@group(1) @binding(0) var t: texture_2d; -@group(2) @binding(0) var mask_t: texture_2d; - -@fragment -fn main(@location(0) uv: vec2) -> @location(0) vec4 { - let tex_color = textureSample(t, s, uv); - let mask_alpha = textureSample(mask_t, s, uv).a; - return tex_color * mask_alpha; -} -)"sv; - -constexpr std::string_view blurFragmentSource = R"( -struct BlurUniforms { - texel_offset: vec2, - radius: f32, - padding: f32, - tex_coord_min: vec2, - tex_coord_max: vec2, - weights: vec4, -}; - -@group(0) @binding(1) var s: sampler; -@group(1) @binding(0) var t: texture_2d; -@group(2) @binding(0) var blur: BlurUniforms; - -fn get_weight(index: i32) -> f32 { - return blur.weights[u32(abs(index))]; -} - -fn sample_blur(sample_uv: vec2, offset_index: i32) -> vec4 { - let in_region = step(blur.tex_coord_min, sample_uv) * step(sample_uv, blur.tex_coord_max); - return textureSample(t, s, sample_uv) * get_weight(offset_index) * in_region.x * in_region.y; -} - -@fragment -fn main(@location(0) uv0: vec2, @location(1) uv1: vec2, @location(2) uv2: vec2, - @location(3) uv3: vec2, @location(4) uv4: vec2, @location(5) uv5: vec2, - @location(6) uv6: vec2) -> @location(0) vec4 { - var color = sample_blur(uv0, -3); - color += sample_blur(uv1, -2); - color += sample_blur(uv2, -1); - color += sample_blur(uv3, 0); - color += sample_blur(uv4, 1); - color += sample_blur(uv5, 2); - color += sample_blur(uv6, 3); - return color; -} -)"sv; - -constexpr std::string_view regionBlitFragmentSource = R"( -struct BlurUniforms { - texel_offset: vec2, - radius: f32, - padding: f32, - tex_coord_min: vec2, - tex_coord_max: vec2, - weights: vec4, -}; - -@group(0) @binding(1) var s: sampler; -@group(1) @binding(0) var t: texture_2d; -@group(2) @binding(0) var blur: BlurUniforms; - -@fragment -fn main(@location(0) uv: vec2) -> @location(0) vec4 { - let sample_uv = mix(blur.tex_coord_min, blur.tex_coord_max, uv); - return textureSample(t, s, sample_uv); -} -)"sv; - -constexpr std::string_view dropShadowFragmentSource = R"( -struct DropShadowUniforms { - color: vec4, - uv_offset: vec2, - tex_coord_min: vec2, - tex_coord_max: vec2, -}; - -@group(0) @binding(1) var s: sampler; -@group(1) @binding(0) var t: texture_2d; -@group(2) @binding(0) var shadow: DropShadowUniforms; - -@fragment -fn main(@location(0) uv: vec2) -> @location(0) vec4 { - let sample_uv = uv - shadow.uv_offset; - let in_region = step(shadow.tex_coord_min, sample_uv) * step(sample_uv, shadow.tex_coord_max); - let alpha = textureSample(t, s, sample_uv).a * in_region.x * in_region.y; - return shadow.color * alpha; -} -)"sv; - -inline constexpr uint64_t UniformBufferSize = 1048576; // 1mb - Rml::CompiledGeometryHandle WebGPURenderInterface::CompileGeometry(Rml::Span vertices, Rml::Span indices) { auto* geometryData = new ShaderGeometryData(); - const wgpu::BufferDescriptor vtxBufferDesc{ - .label = "RmlUi Vertex Buffer", - .usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::Vertex, - .size = AURORA_ALIGN(vertices.size() * sizeof(Rml::Vertex), 4), - }; - geometryData->m_vertexBuffer = webgpu::g_device.CreateBuffer(&vtxBufferDesc); - webgpu::g_queue.WriteBuffer(geometryData->m_vertexBuffer, 0, vertices.data(), sizeof(Rml::Vertex) * vertices.size()); - - const wgpu::BufferDescriptor idxBufferDesc{ - .label = "RmlUi Index Buffer", - .usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::Index, - .size = AURORA_ALIGN(indices.size() * sizeof(int), 4), - }; - geometryData->m_indexBuffer = webgpu::g_device.CreateBuffer(&idxBufferDesc); - webgpu::g_queue.WriteBuffer(geometryData->m_indexBuffer, 0, indices.data(), sizeof(int) * indices.size()); + 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); } @@ -596,30 +266,42 @@ Rml::CompiledGeometryHandle WebGPURenderInterface::CompileGeometry(Rml::Span(m_clipMaskEnabled ? PipelineType::Masked : PipelineType::Normal)]); + geometry_pipeline(m_renderTargetFormat, LayerSampleCount, + m_clipMaskEnabled ? PipelineType::Masked : PipelineType::Normal)); } void WebGPURenderInterface::DrawGeometry(Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation, - Rml::TextureHandle texture, const wgpu::RenderPipeline& pipeline) { + Rml::TextureHandle texture, gfx::PipelineRef pipeline) { EnsureActiveLayerPass("RmlUi resumed geometry layer pass"); - if (m_pass == nullptr) { + if (!m_passActive) { return; } - SetupRenderState(translation); - 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); - m_pass.SetVertexBuffer(0, geometryData->m_vertexBuffer, 0, geometryData->m_vertexBuffer.GetSize()); - m_pass.SetIndexBuffer(geometryData->m_indexBuffer, wgpu::IndexFormat::Uint32, 0, - geometryData->m_indexBuffer.GetSize()); - m_pass.SetPipeline(pipeline); - m_pass.SetBindGroup(0, m_commonBindGroup, 1, &m_uniformCurrentOffset); - m_pass.SetBindGroup(1, textureData->m_bindGroup); - m_pass.DrawIndexed(geometryData->m_indexBuffer.GetSize() / sizeof(int)); - - m_uniformCurrentOffset += AURORA_ALIGN(sizeof(UniformBlock), 256); + 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) { @@ -684,31 +366,10 @@ Rml::TextureHandle WebGPURenderInterface::GenerateTexture(Rml::Spanm_textureView = texData->m_texture.CreateView(nullptr); constexpr uint32_t BytesPerPixel = 4; - const wgpu::TexelCopyTextureInfo dst{ - .texture = texData->m_texture, - .aspect = wgpu::TextureAspect::All, - }; - const wgpu::TexelCopyBufferLayout layout{ - .offset = 0, - .bytesPerRow = static_cast(source_dimensions.x) * BytesPerPixel, - .rowsPerImage = static_cast(source_dimensions.y), - }; - webgpu::g_queue.WriteTexture(&dst, source.data(), source_dimensions.x * BytesPerPixel * source_dimensions.y, &layout, - &size); - - const std::array bindGroupEntries{ - wgpu::BindGroupEntry{ - .binding = 0, - .textureView = texData->m_textureView, - }, - }; - const wgpu::BindGroupDescriptor bindGroupDesc{ - .layout = m_imageBindGroupLayout, - .entryCount = bindGroupEntries.size(), - .entries = bindGroupEntries.data(), - }; - texData->m_bindGroup = webgpu::g_device.CreateBindGroup(&bindGroupDesc); - + 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); } @@ -727,25 +388,22 @@ void WebGPURenderInterface::SetScissorRegion(Rml::Rectanglei region) { } void WebGPURenderInterface::ApplyScissorRegion() { - if (m_pass == nullptr) { + if (!m_passActive) { return; } ApplyScissorRegion(GetActiveScissorRegion()); } -void WebGPURenderInterface::ApplyScissorRegion(Rml::Rectanglei region) { - if (m_pass == nullptr) { - return; - } - +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))); - m_pass.SetScissorRect(x, y, width, height); + gfx::set_scissor( + {static_cast(x), static_cast(y), static_cast(width), static_cast(height)}); } Rml::Rectanglei WebGPURenderInterface::GetActiveScissorRegion() const { @@ -766,18 +424,13 @@ void WebGPURenderInterface::EnableClipMask(bool enable) { m_clipMaskEnabled = enable; if (!enable) { m_stencilRef = 0; - if (m_pass != nullptr) { - m_pass.SetStencilReference(0); - } - } else if (m_pass != nullptr) { - m_pass.SetStencilReference(m_stencilRef); } } void WebGPURenderInterface::RenderToClipMask(Rml::ClipMaskOperation operation, Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation) { EnsureActiveLayerPass("RmlUi resumed clip mask layer pass"); - if (m_pass == nullptr) { + if (!m_passActive) { return; } @@ -787,23 +440,27 @@ void WebGPURenderInterface::RenderToClipMask(Rml::ClipMaskOperation operation, R switch (operation) { case Rml::ClipMaskOperation::Set: m_translationMatrix = Rml::Matrix4f::Identity(); - m_pass.SetStencilReference(0); - DrawGeometry(m_clipResetGeometry, {}, 0, m_pipelines[static_cast(PipelineType::ClipReplace)]); + m_stencilRef = 0; + DrawGeometry(m_clipResetGeometry, {}, 0, + geometry_pipeline(m_renderTargetFormat, LayerSampleCount, PipelineType::ClipReplace)); m_translationMatrix = prevMatrix; m_stencilRef = 1; - m_pass.SetStencilReference(m_stencilRef); - DrawGeometry(geometry, translation, 0, m_pipelines[static_cast(PipelineType::ClipReplace)]); + DrawGeometry(geometry, translation, 0, + geometry_pipeline(m_renderTargetFormat, LayerSampleCount, PipelineType::ClipReplace)); break; case Rml::ClipMaskOperation::SetInverse: m_translationMatrix = Rml::Matrix4f::Identity(); - m_pass.SetStencilReference(1); - DrawGeometry(m_clipResetGeometry, {}, 0, m_pipelines[static_cast(PipelineType::ClipReplace)]); + m_stencilRef = 1; + DrawGeometry(m_clipResetGeometry, {}, 0, + geometry_pipeline(m_renderTargetFormat, LayerSampleCount, PipelineType::ClipReplace)); m_translationMatrix = prevMatrix; m_stencilRef = 1; - m_pass.SetStencilReference(0); - DrawGeometry(geometry, translation, 0, m_pipelines[static_cast(PipelineType::ClipReplace)]); + 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()) { @@ -811,13 +468,11 @@ void WebGPURenderInterface::RenderToClipMask(Rml::ClipMaskOperation operation, R break; } - m_pass.SetStencilReference(m_stencilRef); - DrawGeometry(geometry, translation, 0, m_pipelines[static_cast(PipelineType::ClipIntersect)]); + DrawGeometry(geometry, translation, 0, + geometry_pipeline(m_renderTargetFormat, LayerSampleCount, PipelineType::ClipIntersect)); ++m_stencilRef; break; } - - m_pass.SetStencilReference(m_stencilRef); } void WebGPURenderInterface::SetTransform(const Rml::Matrix4f* transform) { @@ -848,7 +503,6 @@ void WebGPURenderInterface::EnsureRenderTarget(RenderTarget& target, const char* }; target.texture = webgpu::g_device.CreateTexture(&textureDesc); target.view = target.texture.CreateView(nullptr); - target.bindGroup = CreateImageBindGroup(target.view); if (useMultisampling) { const wgpu::TextureDescriptor multisampleTextureDesc{ @@ -865,21 +519,6 @@ void WebGPURenderInterface::EnsureRenderTarget(RenderTarget& target, const char* } } -wgpu::BindGroup WebGPURenderInterface::CreateImageBindGroup(const wgpu::TextureView& view) const { - const std::array bindGroupEntries{ - wgpu::BindGroupEntry{ - .binding = 0, - .textureView = view, - }, - }; - const wgpu::BindGroupDescriptor bindGroupDesc{ - .layout = m_imageBindGroupLayout, - .entryCount = bindGroupEntries.size(), - .entries = bindGroupEntries.data(), - }; - return webgpu::g_device.CreateBindGroup(&bindGroupDesc); -} - void WebGPURenderInterface::EnsureFrameTargets(const wgpu::Extent3D& size) { if (m_layers.empty()) { m_layers.resize(1); @@ -937,41 +576,33 @@ TexCoordLimits WebGPURenderInterface::GetPostprocessTexCoordLimits(Rml::Rectangl }; } -void WebGPURenderInterface::ApplyViewport() { - if (m_pass != nullptr) { - m_pass.SetViewport(m_viewport.left, m_viewport.top, m_viewport.width, m_viewport.height, m_viewport.znear, - m_viewport.zfar); - } -} +void WebGPURenderInterface::ApplyViewport() const { gfx::set_viewport(m_viewport); } -void WebGPURenderInterface::ApplyFullFrameScissor() { - if (m_pass != nullptr) { - m_pass.SetScissorRect(0, 0, m_frameSize.width, m_frameSize.height); - } +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(); - (void)clearStencil; - - const std::array attachments{ - wgpu::RenderPassColorAttachment{ - .view = view, - .loadOp = loadOp, - .storeOp = wgpu::StoreOp::Store, - .clearValue = {0.f, 0.f, 0.f, 0.f}, - }, - }; - const wgpu::RenderPassDescriptor renderPassDesc{ + gfx::begin_color_pass({ .label = label, - .colorAttachmentCount = attachments.size(), - .colorAttachments = attachments.data(), - }; - m_pass = m_encoder.BeginRenderPass(&renderPassDesc); + .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, + .observable = true, + }); + m_passActive = true; ApplyViewport(); ApplyScissorRegion(); - m_pass.SetStencilReference(m_stencilRef); } void WebGPURenderInterface::BeginLayerPass(Rml::LayerHandle layer, wgpu::LoadOp loadOp, const char* label, @@ -981,57 +612,58 @@ void WebGPURenderInterface::BeginLayerPass(Rml::LayerHandle layer, wgpu::LoadOp const RenderTarget& target = m_layers[layer]; const bool multisampled = LayerSampleCount > 1 && static_cast(target.multisampleView); - const std::array attachments{ - wgpu::RenderPassColorAttachment{ - .view = multisampled ? target.multisampleView : target.view, - .resolveTarget = multisampled && resolveMultisampled ? target.view : nullptr, - .loadOp = loadOp, - .storeOp = wgpu::StoreOp::Store, - .clearValue = {0.f, 0.f, 0.f, 0.f}, - }, - }; - const wgpu::RenderPassDepthStencilAttachment depthStencilAttachment{ - .view = GetClipMaskStencilView(m_frameSize), + 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, - }; - const wgpu::RenderPassDescriptor renderPassDesc{ - .label = label, - .colorAttachmentCount = attachments.size(), - .colorAttachments = attachments.data(), - .depthStencilAttachment = &depthStencilAttachment, - }; - m_pass = m_encoder.BeginRenderPass(&renderPassDesc); + .observable = true, + }); + m_passActive = true; ApplyViewport(); ApplyScissorRegion(); - m_pass.SetStencilReference(m_stencilRef); } void WebGPURenderInterface::EnsureFrameRenderingStarted() { - if (m_frameRenderingStarted || m_encoder == nullptr || m_layers.empty() || !m_layers[0].view) { + if (m_frameRenderingStarted || !m_frameActive || m_layers.empty() || !m_layers[0].view) { return; } m_frameRenderingStarted = true; - const wgpu::BindGroup seedBindGroup = CreateImageBindGroup(m_frameSeedView); + 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, m_layerOpaqueBlitPipeline); + 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, m_opaqueBlitPipeline); + 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_pass != nullptr || m_encoder == nullptr || m_activeLayer >= m_layers.size() || !m_layers[m_activeLayer].view) { + if (m_passActive || !m_frameActive || m_activeLayer >= m_layers.size() || !m_layers[m_activeLayer].view) { return; } @@ -1039,36 +671,43 @@ void WebGPURenderInterface::EnsureActiveLayerPass(const char* label) { } void WebGPURenderInterface::EndActivePass() { - if (m_pass != nullptr) { - m_pass.End(); - m_pass = nullptr; + if (m_passActive) { + gfx::end_color_pass(); + m_passActive = false; } } -void WebGPURenderInterface::DrawFullscreenTexture(const wgpu::BindGroup& bindGroup, - const wgpu::RenderPipeline& pipeline, - const wgpu::BindGroup* extraBindGroup, uint32_t extraDynamicOffset, - bool extraBindGroupHasDynamicOffset) { - constexpr uint32_t uniformOffset = 0; - m_pass.SetPipeline(pipeline); - m_pass.SetBindGroup(0, m_commonBindGroup, 1, &uniformOffset); - m_pass.SetBindGroup(1, bindGroup); - if (extraBindGroup != nullptr) { - if (extraBindGroupHasDynamicOffset) { - m_pass.SetBindGroup(2, *extraBindGroup, 1, &extraDynamicOffset); - } else { - m_pass.SetBindGroup(2, *extraBindGroup); - } +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; } - m_pass.Draw(3); + + 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(const wgpu::BindGroup& bindGroup, const wgpu::TextureView& view, - wgpu::LoadOp loadOp, const wgpu::RenderPipeline& pipeline, - const char* label, const wgpu::BindGroup* extraBindGroup, - uint32_t extraDynamicOffset, bool extraBindGroupHasDynamicOffset) { +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, extraDynamicOffset, extraBindGroupHasDynamicOffset); + DrawFullscreenTexture(bindGroup, pipeline, extraBindGroup, extraUniformRange, extraBindGroupHasDynamicOffset, + blendConstant, hasBlendConstant); } void WebGPURenderInterface::RenderBlur(float sigma, const RenderTarget& sourceDestination, const RenderTarget& temp) { @@ -1084,8 +723,6 @@ void WebGPURenderInterface::RenderBlur(float sigma, const RenderTarget& sourceDe auto write_blur_uniform = [&](Rml::Vector2f texelOffset, Rml::Rectanglei texCoordRegion, float radius, Rml::Vector4f weights) { - const uint32_t offset = m_blurUniformCurrentOffset; - m_blurUniformCurrentOffset += AURORA_ALIGN(sizeof(BlurUniformBlock), 256); const TexCoordLimits texCoordLimits = GetPostprocessTexCoordLimits(texCoordRegion); const BlurUniformBlock uniform{ .texelOffset = texelOffset, @@ -1095,8 +732,7 @@ void WebGPURenderInterface::RenderBlur(float sigma, const RenderTarget& sourceDe .texCoordMax = texCoordLimits.max, .weights = weights, }; - webgpu::g_queue.WriteBuffer(m_blurUniformBuffer, offset, &uniform, sizeof(uniform)); - return offset; + 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); @@ -1109,8 +745,6 @@ void WebGPURenderInterface::RenderBlur(float sigma, const RenderTarget& sourceDe 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 uint32_t offset = m_blurUniformCurrentOffset; - m_blurUniformCurrentOffset += AURORA_ALIGN(sizeof(BlurUniformBlock), 256); const BlurUniformBlock uniform{ .texelOffset = {}, .radius = 0.f, @@ -1121,8 +755,7 @@ void WebGPURenderInterface::RenderBlur(float sigma, const RenderTarget& sourceDe (Rml::Vector2f(static_cast(right), static_cast(bottom)) - viewportOrigin) / viewportSize, .weights = weights, }; - webgpu::g_queue.WriteBuffer(m_blurUniformBuffer, offset, &uniform, sizeof(uniform)); - return offset; + return gfx::push_uniform(uniform); }; int passLevel = 0; @@ -1142,49 +775,59 @@ void WebGPURenderInterface::RenderBlur(float sigma, const RenderTarget& sourceDe const RenderTarget& destination = fromSource ? temp : sourceDestination; BeginRenderTargetPass(destination.view, wgpu::LoadOp::Clear, "RmlUi blur downsample pass"); - m_pass.SetViewport(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); + 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(source.bindGroup, m_blitPipelines[static_cast(BlitPipelineType::Replace)]); + 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(sourceDestination.bindGroup, m_blitPipelines[static_cast(BlitPipelineType::Replace)]); + DrawFullscreenTexture(texture_bind_group_ref(sourceDestination.view), + blit_pipeline(m_renderTargetFormat, 1, BlitPipelineType::Replace, false)); } - const uint32_t verticalOffset = + 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(temp.bindGroup, m_blurPipeline, &m_blurBindGroup, verticalOffset); + DrawFullscreenTexture(texture_bind_group_ref(temp.view), + filter_pipeline(PipelineKind::Blur, m_renderTargetFormat, VertexLayoutKind::BlurFullscreen), + uniform_bind_group_ref(), verticalRange); - const uint32_t horizontalOffset = + 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(sourceDestination.bindGroup, m_blurPipeline, &m_blurBindGroup, horizontalOffset); + DrawFullscreenTexture(texture_bind_group_ref(sourceDestination.view), + filter_pipeline(PipelineKind::Blur, m_renderTargetFormat, VertexLayoutKind::BlurFullscreen), + uniform_bind_group_ref(), horizontalRange); - const uint32_t upscaleOffset = write_region_blit_uniform(scissor, weights); + const auto upscaleRange = write_region_blit_uniform(scissor, weights); BeginRenderTargetPass(sourceDestination.view, wgpu::LoadOp::Clear, "RmlUi blur upscale pass"); - m_pass.SetViewport(static_cast(originalScissor.Left()), static_cast(originalScissor.Top()), + 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); + static_cast(std::max(originalScissor.Height(), 1)), 0.f, 1.f}); ApplyScissorRegion(originalScissor); - DrawFullscreenTexture(temp.bindGroup, m_regionBlitPipeline, &m_blurBindGroup, upscaleOffset); + DrawFullscreenTexture(texture_bind_group_ref(temp.view), + filter_pipeline(PipelineKind::RegionBlit, m_renderTargetFormat), uniform_bind_group_ref(), + upscaleRange); - const Rml::Vector2i targetMin = scissor.p0 * (1 << passLevel); - const Rml::Vector2i targetMax = scissor.p1 * (1 << passLevel); - const Rml::Rectanglei targetRegion = Rml::Rectanglei::FromCorners(targetMin, targetMax); + 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"); - m_pass.SetViewport(static_cast(targetRegion.Left()), static_cast(targetRegion.Top()), + 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); + static_cast(std::max(targetRegion.Height(), 1)), 0.f, 1.f}); ApplyScissorRegion(targetRegion); - DrawFullscreenTexture(temp.bindGroup, m_regionBlitPipeline, &m_blurBindGroup, upscaleOffset); + DrawFullscreenTexture(texture_bind_group_ref(temp.view), + filter_pipeline(PipelineKind::RegionBlit, m_renderTargetFormat), uniform_bind_group_ref(), + upscaleRange); } } @@ -1201,12 +844,14 @@ void WebGPURenderInterface::RenderFilters(Rml::Spantype) { case FilterType::Opacity: { - const wgpu::Color blendColor{filter->opacity, filter->opacity, filter->opacity, filter->opacity}; BeginRenderTargetPass(m_postprocessTargets[shadowIndex].view, wgpu::LoadOp::Clear, "RmlUi opacity pass"); - m_pass.SetBlendConstant(&blendColor); - DrawFullscreenTexture(m_postprocessTargets[sourceIndex].bindGroup, m_opacityPipeline); - CompositeToTarget(m_postprocessTargets[shadowIndex].bindGroup, m_postprocessTargets[sourceIndex].view, - wgpu::LoadOp::Clear, m_blitPipelines[static_cast(BlitPipelineType::Replace)], + DrawFullscreenTexture(texture_bind_group_ref(m_postprocessTargets[sourceIndex].view), + filter_pipeline(PipelineKind::Opacity, m_renderTargetFormat, VertexLayoutKind::Fullscreen, + BlendMode::Opacity), + 0, {}, true, {filter->opacity, filter->opacity, filter->opacity, filter->opacity}, true); + CompositeToTarget(texture_bind_group_ref(m_postprocessTargets[shadowIndex].view), + m_postprocessTargets[sourceIndex].view, wgpu::LoadOp::Clear, + blit_pipeline(m_renderTargetFormat, 1, BlitPipelineType::Replace, false), "RmlUi opacity result copy pass"); break; } @@ -1235,21 +880,21 @@ void WebGPURenderInterface::RenderFilters(Rml::Spansigma, m_postprocessTargets[shadowIndex], m_postprocessTargets[tempIndex]); - CompositeToTarget(m_postprocessTargets[sourceIndex].bindGroup, m_postprocessTargets[shadowIndex].view, - wgpu::LoadOp::Load, m_blitPipelines[static_cast(BlitPipelineType::Blend)], + CompositeToTarget(texture_bind_group_ref(m_postprocessTargets[sourceIndex].view), + m_postprocessTargets[shadowIndex].view, wgpu::LoadOp::Load, + blit_pipeline(m_renderTargetFormat, 1, BlitPipelineType::Blend, false), "RmlUi drop shadow source composite pass"); - CompositeToTarget(m_postprocessTargets[shadowIndex].bindGroup, m_postprocessTargets[sourceIndex].view, - wgpu::LoadOp::Clear, m_blitPipelines[static_cast(BlitPipelineType::Replace)], + CompositeToTarget(texture_bind_group_ref(m_postprocessTargets[shadowIndex].view), + m_postprocessTargets[sourceIndex].view, wgpu::LoadOp::Clear, + blit_pipeline(m_renderTargetFormat, 1, BlitPipelineType::Replace, false), "RmlUi drop shadow result copy pass"); break; } @@ -1257,25 +902,26 @@ void WebGPURenderInterface::RenderFilters(Rml::SpancolorMatrix), }; - const uint32_t colorMatrixOffset = m_shaderUniformCurrentOffset; - m_shaderUniformCurrentOffset += AURORA_ALIGN(sizeof(ColorMatrixUniformBlock), 256); - webgpu::g_queue.WriteBuffer(m_shaderUniformBuffer, colorMatrixOffset, &colorMatrixUniform, - sizeof(colorMatrixUniform)); + const auto colorMatrixRange = gfx::push_uniform(colorMatrixUniform); - CompositeToTarget(m_postprocessTargets[sourceIndex].bindGroup, m_postprocessTargets[shadowIndex].view, - wgpu::LoadOp::Clear, m_colorMatrixPipeline, "RmlUi color matrix pass", &m_shaderBindGroup, - colorMatrixOffset); - CompositeToTarget(m_postprocessTargets[shadowIndex].bindGroup, m_postprocessTargets[sourceIndex].view, - wgpu::LoadOp::Clear, m_blitPipelines[static_cast(BlitPipelineType::Replace)], + CompositeToTarget(texture_bind_group_ref(m_postprocessTargets[sourceIndex].view), + m_postprocessTargets[shadowIndex].view, wgpu::LoadOp::Clear, + filter_pipeline(PipelineKind::ColorMatrix, m_renderTargetFormat), "RmlUi color matrix pass", + uniform_bind_group_ref(), colorMatrixRange); + CompositeToTarget(texture_bind_group_ref(m_postprocessTargets[shadowIndex].view), + m_postprocessTargets[sourceIndex].view, wgpu::LoadOp::Clear, + blit_pipeline(m_renderTargetFormat, 1, BlitPipelineType::Replace, false), "RmlUi color matrix result copy pass"); break; } case FilterType::MaskImage: { - CompositeToTarget(m_postprocessTargets[sourceIndex].bindGroup, m_postprocessTargets[shadowIndex].view, - wgpu::LoadOp::Clear, m_maskImagePipeline, "RmlUi mask image pass", &m_blendMaskTarget.bindGroup, - 0, false); - CompositeToTarget(m_postprocessTargets[shadowIndex].bindGroup, m_postprocessTargets[sourceIndex].view, - wgpu::LoadOp::Clear, m_blitPipelines[static_cast(BlitPipelineType::Replace)], + CompositeToTarget(texture_bind_group_ref(m_postprocessTargets[sourceIndex].view), + m_postprocessTargets[shadowIndex].view, wgpu::LoadOp::Clear, + filter_pipeline(PipelineKind::MaskImage, m_renderTargetFormat), "RmlUi mask image pass", + texture_bind_group_ref(m_blendMaskTarget.view), {}, false); + CompositeToTarget(texture_bind_group_ref(m_postprocessTargets[shadowIndex].view), + m_postprocessTargets[sourceIndex].view, wgpu::LoadOp::Clear, + blit_pipeline(m_renderTargetFormat, 1, BlitPipelineType::Replace, false), "RmlUi mask image result copy pass"); break; } @@ -1285,9 +931,8 @@ void WebGPURenderInterface::RenderFilters(Rml::Span(BlitPipelineType::Replace)], "RmlUi layer copy pass"); + 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 Rml::LayerHandle topLayer = m_layerStack.empty() ? 0 : m_layerStack.back(); RenderFilters(filters); @@ -1387,7 +1033,8 @@ void WebGPURenderInterface::CompositeLayers(Rml::LayerHandle source, Rml::LayerH replace ? (m_clipMaskEnabled ? BlitPipelineType::ReplaceMasked : BlitPipelineType::Replace) : (m_clipMaskEnabled ? BlitPipelineType::BlendMasked : BlitPipelineType::Blend); BeginLayerPass(destination, wgpu::LoadOp::Load, "RmlUi layer composite pass"); - DrawFullscreenTexture(m_postprocessTargets[0].bindGroup, m_layerBlitPipelines[static_cast(pipelineType)]); + DrawFullscreenTexture(texture_bind_group_ref(m_postprocessTargets[0].view), + blit_pipeline(m_renderTargetFormat, LayerSampleCount, pipelineType, true)); if (destination != topLayer) { EndActivePass(); @@ -1407,7 +1054,7 @@ void WebGPURenderInterface::PopLayer() { } Rml::TextureHandle WebGPURenderInterface::SaveLayerAsTexture() { - if (m_encoder == nullptr) { + if (!m_frameActive) { Log.warn("RmlUi requested SaveLayerAsTexture outside a frame"); return 0; } @@ -1450,6 +1097,9 @@ Rml::TextureHandle WebGPURenderInterface::SaveLayerAsTexture() { }; 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(); @@ -1466,27 +1116,14 @@ Rml::TextureHandle WebGPURenderInterface::SaveLayerAsTexture() { .texture = texData->m_texture, .aspect = wgpu::TextureAspect::All, }; - m_encoder.CopyTextureToTexture(&src, &dst, &textureSize); - - const std::array bindGroupEntries{ - wgpu::BindGroupEntry{ - .binding = 0, - .textureView = texData->m_textureView, - }, - }; - const wgpu::BindGroupDescriptor bindGroupDesc{ - .layout = m_imageBindGroupLayout, - .entryCount = bindGroupEntries.size(), - .entries = bindGroupEntries.data(), - }; - texData->m_bindGroup = webgpu::g_device.CreateBindGroup(&bindGroupDesc); + 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_encoder == nullptr) { + if (!m_frameActive) { Log.warn("RmlUi requested SaveLayerAsMaskImage outside a frame"); return {}; } @@ -1499,10 +1136,12 @@ Rml::CompiledFilterHandle WebGPURenderInterface::SaveLayerAsMaskImage() { EnsureFrameRenderingStarted(); - CompositeToTarget(m_layers[layer].bindGroup, m_postprocessTargets[0].view, wgpu::LoadOp::Clear, - m_blitPipelines[static_cast(BlitPipelineType::Replace)], "RmlUi mask source copy pass"); - CompositeToTarget(m_postprocessTargets[0].bindGroup, m_blendMaskTarget.view, wgpu::LoadOp::Clear, - m_blitPipelines[static_cast(BlitPipelineType::Replace)], "RmlUi mask image save pass"); + 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"); @@ -1658,670 +1297,44 @@ Rml::CompiledShaderHandle WebGPURenderInterface::CompileShader(const Rml::String void WebGPURenderInterface::RenderShader(Rml::CompiledShaderHandle shader, Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation, Rml::TextureHandle) { - if (shader == 0 || geometry == 0) { + 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_pass == nullptr) { + if (!m_passActive) { return; } - SetupRenderState(translation); - - const auto* shaderData = reinterpret_cast(shader); - const uint32_t shaderOffset = m_shaderUniformCurrentOffset; - m_shaderUniformCurrentOffset += AURORA_ALIGN(sizeof(GradientUniformBlock), 256); - webgpu::g_queue.WriteBuffer(m_shaderUniformBuffer, shaderOffset, &shaderData->gradient, sizeof(shaderData->gradient)); - - const auto* geometryData = reinterpret_cast(geometry); - m_pass.SetVertexBuffer(0, geometryData->m_vertexBuffer, 0, geometryData->m_vertexBuffer.GetSize()); - m_pass.SetIndexBuffer(geometryData->m_indexBuffer, wgpu::IndexFormat::Uint32, 0, - geometryData->m_indexBuffer.GetSize()); - m_pass.SetPipeline(m_gradientPipelines[m_clipMaskEnabled ? 1 : 0]); - m_pass.SetBindGroup(0, m_commonBindGroup, 1, &m_uniformCurrentOffset); - m_pass.SetBindGroup(1, m_shaderBindGroup, 1, &shaderOffset); - m_pass.DrawIndexed(geometryData->m_indexBuffer.GetSize() / sizeof(int)); - - m_uniformCurrentOffset += AURORA_ALIGN(sizeof(UniformBlock), 256); + 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); } -// code heavily based of imgui wgpu impl void WebGPURenderInterface::CreateDeviceObjects() { - constexpr std::array commonBindGroupLayoutEntries{ - wgpu::BindGroupLayoutEntry{ - .binding = 0, - .visibility = wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment, - .buffer = - { - .type = wgpu::BufferBindingType::Uniform, - .hasDynamicOffset = true, - }, - }, - wgpu::BindGroupLayoutEntry{ - .binding = 1, - .visibility = wgpu::ShaderStage::Fragment, - .sampler = - { - .type = wgpu::SamplerBindingType::Filtering, - }, - }, - }; - const wgpu::BindGroupLayoutDescriptor commonBindGroupLayoutDesc{ - .entryCount = commonBindGroupLayoutEntries.size(), - .entries = commonBindGroupLayoutEntries.data(), - }; - m_commonBindGroupLayout = webgpu::g_device.CreateBindGroupLayout(&commonBindGroupLayoutDesc); - - constexpr std::array imageBindGroupLayoutEntries{ - wgpu::BindGroupLayoutEntry{ - .binding = 0, - .visibility = wgpu::ShaderStage::Fragment, - .texture = - { - .sampleType = wgpu::TextureSampleType::Float, - .viewDimension = wgpu::TextureViewDimension::e2D, - }, - }, - }; - const wgpu::BindGroupLayoutDescriptor imageBindGroupLayoutDesc{ - .entryCount = imageBindGroupLayoutEntries.size(), - .entries = imageBindGroupLayoutEntries.data(), - }; - m_imageBindGroupLayout = webgpu::g_device.CreateBindGroupLayout(&imageBindGroupLayoutDesc); - - constexpr std::array blurBindGroupLayoutEntries{ - wgpu::BindGroupLayoutEntry{ - .binding = 0, - .visibility = wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment, - .buffer = - { - .type = wgpu::BufferBindingType::Uniform, - .hasDynamicOffset = true, - }, - }, - }; - const wgpu::BindGroupLayoutDescriptor blurBindGroupLayoutDesc{ - .entryCount = blurBindGroupLayoutEntries.size(), - .entries = blurBindGroupLayoutEntries.data(), - }; - m_blurBindGroupLayout = webgpu::g_device.CreateBindGroupLayout(&blurBindGroupLayoutDesc); - - constexpr std::array dropShadowBindGroupLayoutEntries{ - wgpu::BindGroupLayoutEntry{ - .binding = 0, - .visibility = wgpu::ShaderStage::Fragment, - .buffer = - { - .type = wgpu::BufferBindingType::Uniform, - .hasDynamicOffset = true, - }, - }, - }; - const wgpu::BindGroupLayoutDescriptor dropShadowBindGroupLayoutDesc{ - .entryCount = dropShadowBindGroupLayoutEntries.size(), - .entries = dropShadowBindGroupLayoutEntries.data(), - }; - m_dropShadowBindGroupLayout = webgpu::g_device.CreateBindGroupLayout(&dropShadowBindGroupLayoutDesc); - - constexpr std::array shaderBindGroupLayoutEntries{ - wgpu::BindGroupLayoutEntry{ - .binding = 0, - .visibility = wgpu::ShaderStage::Fragment, - .buffer = - { - .type = wgpu::BufferBindingType::Uniform, - .hasDynamicOffset = true, - }, - }, - }; - const wgpu::BindGroupLayoutDescriptor shaderBindGroupLayoutDesc{ - .entryCount = shaderBindGroupLayoutEntries.size(), - .entries = shaderBindGroupLayoutEntries.data(), - }; - m_shaderBindGroupLayout = webgpu::g_device.CreateBindGroupLayout(&shaderBindGroupLayoutDesc); - - const std::array layouts{m_commonBindGroupLayout, m_imageBindGroupLayout}; - const wgpu::PipelineLayoutDescriptor layoutDesc{ - .bindGroupLayoutCount = layouts.size(), - .bindGroupLayouts = layouts.data(), - }; - m_pipelineLayout = webgpu::g_device.CreatePipelineLayout(&layoutDesc); - - const std::array blurLayouts{m_commonBindGroupLayout, m_imageBindGroupLayout, m_blurBindGroupLayout}; - const wgpu::PipelineLayoutDescriptor blurLayoutDesc{ - .bindGroupLayoutCount = blurLayouts.size(), - .bindGroupLayouts = blurLayouts.data(), - }; - m_blurPipelineLayout = webgpu::g_device.CreatePipelineLayout(&blurLayoutDesc); - - const std::array dropShadowLayouts{m_commonBindGroupLayout, m_imageBindGroupLayout, m_dropShadowBindGroupLayout}; - const wgpu::PipelineLayoutDescriptor dropShadowLayoutDesc{ - .bindGroupLayoutCount = dropShadowLayouts.size(), - .bindGroupLayouts = dropShadowLayouts.data(), - }; - m_dropShadowPipelineLayout = webgpu::g_device.CreatePipelineLayout(&dropShadowLayoutDesc); - - const std::array colorMatrixLayouts{m_commonBindGroupLayout, m_imageBindGroupLayout, m_shaderBindGroupLayout}; - const wgpu::PipelineLayoutDescriptor colorMatrixLayoutDesc{ - .bindGroupLayoutCount = colorMatrixLayouts.size(), - .bindGroupLayouts = colorMatrixLayouts.data(), - }; - m_colorMatrixPipelineLayout = webgpu::g_device.CreatePipelineLayout(&colorMatrixLayoutDesc); - - const std::array maskImageLayouts{m_commonBindGroupLayout, m_imageBindGroupLayout, m_imageBindGroupLayout}; - const wgpu::PipelineLayoutDescriptor maskImageLayoutDesc{ - .bindGroupLayoutCount = maskImageLayouts.size(), - .bindGroupLayouts = maskImageLayouts.data(), - }; - m_maskImagePipelineLayout = webgpu::g_device.CreatePipelineLayout(&maskImageLayoutDesc); - - const std::array shaderLayouts{m_commonBindGroupLayout, m_shaderBindGroupLayout}; - const wgpu::PipelineLayoutDescriptor shaderLayoutDesc{ - .bindGroupLayoutCount = shaderLayouts.size(), - .bindGroupLayouts = shaderLayouts.data(), - }; - m_shaderPipelineLayout = webgpu::g_device.CreatePipelineLayout(&shaderLayoutDesc); - - const auto vertexShader = compile_shader(vertexSource, "RmlUi Vertex Shader"); - const auto fragmentShader = compile_shader(fragmentSource, "RmlUi Fragment Shader"); - const auto gradientFragmentShader = compile_shader(gradientFragmentSource, "RmlUi Gradient Fragment Shader"); - const auto fullscreenVertexShader = compile_shader(fullscreenVertexSource, "RmlUi Fullscreen Vertex Shader"); - const auto blurVertexShader = compile_shader(blurVertexSource, "RmlUi Blur Vertex Shader"); - const auto blitFragmentShader = compile_shader(blitFragmentSource, "RmlUi Blit Fragment Shader"); - const auto opaqueBlitFragmentShader = compile_shader(opaqueBlitFragmentSource, "RmlUi Opaque Blit Fragment Shader"); - const auto colorMatrixFragmentShader = - compile_shader(colorMatrixFragmentSource, "RmlUi Color Matrix Fragment Shader"); - const auto maskImageFragmentShader = compile_shader(maskImageFragmentSource, "RmlUi Mask Image Fragment Shader"); - const auto blurFragmentShader = compile_shader(blurFragmentSource, "RmlUi Blur Fragment Shader"); - const auto regionBlitFragmentShader = compile_shader(regionBlitFragmentSource, "RmlUi Region Blit Fragment Shader"); - const auto dropShadowFragmentShader = compile_shader(dropShadowFragmentSource, "RmlUi Drop Shadow Fragment Shader"); - constexpr std::array vertexAttributes{ - wgpu::VertexAttribute{ - .format = wgpu::VertexFormat::Float32x2, - .offset = offsetof(Rml::Vertex, position), - .shaderLocation = 0, - }, - wgpu::VertexAttribute{ - .format = wgpu::VertexFormat::Float32x2, - .offset = offsetof(Rml::Vertex, tex_coord), - .shaderLocation = 1, - }, - wgpu::VertexAttribute{ - .format = wgpu::VertexFormat::Unorm8x4, - .offset = offsetof(Rml::Vertex, colour), - .shaderLocation = 2, - }, - }; - const std::array vertexBufferLayouts{ - wgpu::VertexBufferLayout{ - .stepMode = wgpu::VertexStepMode::Vertex, - .arrayStride = sizeof(Rml::Vertex), - .attributeCount = vertexAttributes.size(), - .attributes = vertexAttributes.data(), - }, - }; - constexpr wgpu::BlendState premultipliedBlendState{ - .color = - { - .operation = wgpu::BlendOperation::Add, - .srcFactor = wgpu::BlendFactor::One, - .dstFactor = wgpu::BlendFactor::OneMinusSrcAlpha, - }, - .alpha = - { - .operation = wgpu::BlendOperation::Add, - .srcFactor = wgpu::BlendFactor::One, - .dstFactor = wgpu::BlendFactor::OneMinusSrcAlpha, - }, - }; - constexpr wgpu::BlendState opacityBlendState{ - .color = - { - .operation = wgpu::BlendOperation::Add, - .srcFactor = wgpu::BlendFactor::Constant, - .dstFactor = wgpu::BlendFactor::Zero, - }, - .alpha = - { - .operation = wgpu::BlendOperation::Add, - .srcFactor = wgpu::BlendFactor::Constant, - .dstFactor = wgpu::BlendFactor::Zero, - }, - }; - - const auto create_pipeline = [&](const char* label, wgpu::CompareFunction compareFn, - wgpu::StencilOperation stencilPass, wgpu::ColorWriteMask colorWriteMask) { - const wgpu::ColorTargetState colorState{ - .format = m_renderTargetFormat, - .blend = &premultipliedBlendState, - .writeMask = colorWriteMask, - }; - const wgpu::FragmentState fragmentState{ - .module = fragmentShader.module, - .entryPoint = fragmentShader.entryPoint, - .targetCount = 1, - .targets = &colorState, - }; - const auto stencilFace = stencil_face(compareFn, stencilPass); - const wgpu::DepthStencilState depthStencilState{ - .format = ClipMaskStencilFormat, - .stencilFront = stencilFace, - .stencilBack = stencilFace, - .stencilReadMask = 0xFF, - .stencilWriteMask = 0xFF, - }; - const wgpu::RenderPipelineDescriptor pipelineDesc{ - .label = label, - .layout = m_pipelineLayout, - .vertex = - { - .module = vertexShader.module, - .entryPoint = vertexShader.entryPoint, - .bufferCount = vertexBufferLayouts.size(), - .buffers = vertexBufferLayouts.data(), - }, - .primitive = - { - .topology = wgpu::PrimitiveTopology::TriangleList, - .stripIndexFormat = wgpu::IndexFormat::Undefined, - .frontFace = wgpu::FrontFace::CW, - .cullMode = wgpu::CullMode::None, - }, - .depthStencil = &depthStencilState, - .multisample = - { - .count = LayerSampleCount, - }, - .fragment = &fragmentState, - }; - return webgpu::g_device.CreateRenderPipeline(&pipelineDesc); - }; - - m_pipelines[static_cast(PipelineType::Normal)] = create_pipeline( - "RmlUi Pipeline", wgpu::CompareFunction::Always, wgpu::StencilOperation::Keep, wgpu::ColorWriteMask::All); - m_pipelines[static_cast(PipelineType::Masked)] = create_pipeline( - "RmlUi Masked Pipeline", wgpu::CompareFunction::Equal, wgpu::StencilOperation::Keep, wgpu::ColorWriteMask::All); - m_pipelines[static_cast(PipelineType::ClipReplace)] = - create_pipeline("RmlUi Clip Replace Pipeline", wgpu::CompareFunction::Always, wgpu::StencilOperation::Replace, - wgpu::ColorWriteMask::None); - m_pipelines[static_cast(PipelineType::ClipIntersect)] = - create_pipeline("RmlUi Clip Intersect Pipeline", wgpu::CompareFunction::Equal, - wgpu::StencilOperation::IncrementClamp, wgpu::ColorWriteMask::None); - - const auto create_gradient_pipeline = [&](const char* label, wgpu::CompareFunction compareFn) { - const wgpu::ColorTargetState colorState{ - .format = m_renderTargetFormat, - .blend = &premultipliedBlendState, - .writeMask = wgpu::ColorWriteMask::All, - }; - const wgpu::FragmentState fragmentState{ - .module = gradientFragmentShader.module, - .entryPoint = gradientFragmentShader.entryPoint, - .targetCount = 1, - .targets = &colorState, - }; - const auto stencilFace = stencil_face(compareFn, wgpu::StencilOperation::Keep); - const wgpu::DepthStencilState depthStencilState{ - .format = ClipMaskStencilFormat, - .stencilFront = stencilFace, - .stencilBack = stencilFace, - .stencilReadMask = 0xFF, - .stencilWriteMask = 0xFF, - }; - const wgpu::RenderPipelineDescriptor pipelineDesc{ - .label = label, - .layout = m_shaderPipelineLayout, - .vertex = - { - .module = vertexShader.module, - .entryPoint = vertexShader.entryPoint, - .bufferCount = vertexBufferLayouts.size(), - .buffers = vertexBufferLayouts.data(), - }, - .primitive = - { - .topology = wgpu::PrimitiveTopology::TriangleList, - .stripIndexFormat = wgpu::IndexFormat::Undefined, - .frontFace = wgpu::FrontFace::CW, - .cullMode = wgpu::CullMode::None, - }, - .depthStencil = &depthStencilState, - .multisample = - { - .count = LayerSampleCount, - }, - .fragment = &fragmentState, - }; - return webgpu::g_device.CreateRenderPipeline(&pipelineDesc); - }; - - m_gradientPipelines[0] = create_gradient_pipeline("RmlUi Gradient Pipeline", wgpu::CompareFunction::Always); - m_gradientPipelines[1] = create_gradient_pipeline("RmlUi Masked Gradient Pipeline", wgpu::CompareFunction::Equal); - - const auto create_blit_pipeline = [&](const char* label, const wgpu::ComputeState& blitShader, - wgpu::CompareFunction compareFn, bool blend, uint32_t sampleCount, - bool useStencil) { - const wgpu::ColorTargetState colorState{ - .format = m_renderTargetFormat, - .blend = blend ? &premultipliedBlendState : nullptr, - .writeMask = wgpu::ColorWriteMask::All, - }; - const wgpu::FragmentState fragmentState{ - .module = blitShader.module, - .entryPoint = blitShader.entryPoint, - .targetCount = 1, - .targets = &colorState, - }; - const auto stencilFace = stencil_face(compareFn, wgpu::StencilOperation::Keep); - const wgpu::DepthStencilState depthStencilState{ - .format = ClipMaskStencilFormat, - .stencilFront = stencilFace, - .stencilBack = stencilFace, - .stencilReadMask = 0xFF, - .stencilWriteMask = 0xFF, - }; - const wgpu::RenderPipelineDescriptor pipelineDesc{ - .label = label, - .layout = m_pipelineLayout, - .vertex = - { - .module = fullscreenVertexShader.module, - .entryPoint = fullscreenVertexShader.entryPoint, - }, - .primitive = - { - .topology = wgpu::PrimitiveTopology::TriangleList, - }, - .depthStencil = useStencil ? &depthStencilState : nullptr, - .multisample = - { - .count = sampleCount, - }, - .fragment = &fragmentState, - }; - return webgpu::g_device.CreateRenderPipeline(&pipelineDesc); - }; - - m_blitPipelines[static_cast(BlitPipelineType::Blend)] = create_blit_pipeline( - "RmlUi Blit Blend Pipeline", blitFragmentShader, wgpu::CompareFunction::Always, true, 1, false); - m_blitPipelines[static_cast(BlitPipelineType::BlendMasked)] = create_blit_pipeline( - "RmlUi Blit Blend Masked Pipeline", blitFragmentShader, wgpu::CompareFunction::Equal, true, 1, false); - m_blitPipelines[static_cast(BlitPipelineType::Replace)] = create_blit_pipeline( - "RmlUi Blit Replace Pipeline", blitFragmentShader, wgpu::CompareFunction::Always, false, 1, false); - m_blitPipelines[static_cast(BlitPipelineType::ReplaceMasked)] = create_blit_pipeline( - "RmlUi Blit Replace Masked Pipeline", blitFragmentShader, wgpu::CompareFunction::Equal, false, 1, false); - m_layerBlitPipelines[static_cast(BlitPipelineType::Blend)] = - create_blit_pipeline("RmlUi Layer Blit Blend Pipeline", blitFragmentShader, wgpu::CompareFunction::Always, true, - LayerSampleCount, true); - m_layerBlitPipelines[static_cast(BlitPipelineType::BlendMasked)] = - create_blit_pipeline("RmlUi Layer Blit Blend Masked Pipeline", blitFragmentShader, wgpu::CompareFunction::Equal, - true, LayerSampleCount, true); - m_layerBlitPipelines[static_cast(BlitPipelineType::Replace)] = - create_blit_pipeline("RmlUi Layer Blit Replace Pipeline", blitFragmentShader, wgpu::CompareFunction::Always, - false, LayerSampleCount, true); - m_layerBlitPipelines[static_cast(BlitPipelineType::ReplaceMasked)] = - create_blit_pipeline("RmlUi Layer Blit Replace Masked Pipeline", blitFragmentShader, wgpu::CompareFunction::Equal, - false, LayerSampleCount, true); - m_opaqueBlitPipeline = create_blit_pipeline("RmlUi Opaque Blit Pipeline", opaqueBlitFragmentShader, - wgpu::CompareFunction::Always, false, 1, false); - m_layerOpaqueBlitPipeline = create_blit_pipeline("RmlUi Layer Opaque Blit Pipeline", opaqueBlitFragmentShader, - wgpu::CompareFunction::Always, false, LayerSampleCount, true); - - const wgpu::ColorTargetState opacityColorState{ - .format = m_renderTargetFormat, - .blend = &opacityBlendState, - .writeMask = wgpu::ColorWriteMask::All, - }; - const wgpu::FragmentState opacityFragmentState{ - .module = blitFragmentShader.module, - .entryPoint = blitFragmentShader.entryPoint, - .targetCount = 1, - .targets = &opacityColorState, - }; - const wgpu::RenderPipelineDescriptor opacityPipelineDesc{ - .label = "RmlUi Opacity Pipeline", - .layout = m_pipelineLayout, - .vertex = - { - .module = fullscreenVertexShader.module, - .entryPoint = fullscreenVertexShader.entryPoint, - }, - .primitive = - { - .topology = wgpu::PrimitiveTopology::TriangleList, - }, - .depthStencil = nullptr, - .multisample = - { - .count = 1, - }, - .fragment = &opacityFragmentState, - }; - m_opacityPipeline = webgpu::g_device.CreateRenderPipeline(&opacityPipelineDesc); - - const wgpu::ColorTargetState blurColorState{ - .format = m_renderTargetFormat, - .writeMask = wgpu::ColorWriteMask::All, - }; - const wgpu::FragmentState blurFragmentState{ - .module = blurFragmentShader.module, - .entryPoint = blurFragmentShader.entryPoint, - .targetCount = 1, - .targets = &blurColorState, - }; - const auto create_filter_pipeline = [&](const char* label, const wgpu::PipelineLayout& layout, - const wgpu::ComputeState& fragmentShader) { - const wgpu::FragmentState fragmentState{ - .module = fragmentShader.module, - .entryPoint = fragmentShader.entryPoint, - .targetCount = 1, - .targets = &blurColorState, - }; - const wgpu::RenderPipelineDescriptor pipelineDesc{ - .label = label, - .layout = layout, - .vertex = - { - .module = fullscreenVertexShader.module, - .entryPoint = fullscreenVertexShader.entryPoint, - }, - .primitive = - { - .topology = wgpu::PrimitiveTopology::TriangleList, - }, - .depthStencil = nullptr, - .multisample = - { - .count = 1, - }, - .fragment = &fragmentState, - }; - return webgpu::g_device.CreateRenderPipeline(&pipelineDesc); - }; - - m_colorMatrixPipeline = - create_filter_pipeline("RmlUi Color Matrix Pipeline", m_colorMatrixPipelineLayout, colorMatrixFragmentShader); - m_maskImagePipeline = - create_filter_pipeline("RmlUi Mask Image Pipeline", m_maskImagePipelineLayout, maskImageFragmentShader); - - const wgpu::RenderPipelineDescriptor blurPipelineDesc{ - .label = "RmlUi Blur Pipeline", - .layout = m_blurPipelineLayout, - .vertex = - { - .module = blurVertexShader.module, - .entryPoint = blurVertexShader.entryPoint, - }, - .primitive = - { - .topology = wgpu::PrimitiveTopology::TriangleList, - }, - .depthStencil = nullptr, - .multisample = - { - .count = 1, - }, - .fragment = &blurFragmentState, - }; - m_blurPipeline = webgpu::g_device.CreateRenderPipeline(&blurPipelineDesc); - - const wgpu::FragmentState regionBlitFragmentState{ - .module = regionBlitFragmentShader.module, - .entryPoint = regionBlitFragmentShader.entryPoint, - .targetCount = 1, - .targets = &blurColorState, - }; - const wgpu::RenderPipelineDescriptor regionBlitPipelineDesc{ - .label = "RmlUi Region Blit Pipeline", - .layout = m_blurPipelineLayout, - .vertex = - { - .module = fullscreenVertexShader.module, - .entryPoint = fullscreenVertexShader.entryPoint, - }, - .primitive = - { - .topology = wgpu::PrimitiveTopology::TriangleList, - }, - .depthStencil = nullptr, - .multisample = - { - .count = 1, - }, - .fragment = ®ionBlitFragmentState, - }; - m_regionBlitPipeline = webgpu::g_device.CreateRenderPipeline(®ionBlitPipelineDesc); - - const wgpu::ColorTargetState dropShadowColorState{ - .format = m_renderTargetFormat, - .writeMask = wgpu::ColorWriteMask::All, - }; - const wgpu::FragmentState dropShadowFragmentState{ - .module = dropShadowFragmentShader.module, - .entryPoint = dropShadowFragmentShader.entryPoint, - .targetCount = 1, - .targets = &dropShadowColorState, - }; - const wgpu::RenderPipelineDescriptor dropShadowPipelineDesc{ - .label = "RmlUi Drop Shadow Pipeline", - .layout = m_dropShadowPipelineLayout, - .vertex = - { - .module = fullscreenVertexShader.module, - .entryPoint = fullscreenVertexShader.entryPoint, - }, - .primitive = - { - .topology = wgpu::PrimitiveTopology::TriangleList, - }, - .depthStencil = nullptr, - .multisample = - { - .count = 1, - }, - .fragment = &dropShadowFragmentState, - }; - m_dropShadowPipeline = webgpu::g_device.CreateRenderPipeline(&dropShadowPipelineDesc); - - const wgpu::SamplerDescriptor samplerDesc{ - .addressModeU = wgpu::AddressMode::Repeat, - .addressModeV = wgpu::AddressMode::Repeat, - .addressModeW = wgpu::AddressMode::Repeat, - .magFilter = wgpu::FilterMode::Linear, - .minFilter = wgpu::FilterMode::Linear, - .mipmapFilter = wgpu::MipmapFilterMode::Linear, - .maxAnisotropy = 1, - }; - m_sampler = webgpu::g_device.CreateSampler(&samplerDesc); - - CreateUniformBuffer(); - const wgpu::BufferDescriptor blurUniformBufferDesc{ - .label = "RmlUi Blur Uniform Buffer", - .usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::Uniform, - .size = AURORA_ALIGN(UniformBufferSize, 16), - }; - m_blurUniformBuffer = webgpu::g_device.CreateBuffer(&blurUniformBufferDesc); - const wgpu::BufferDescriptor dropShadowUniformBufferDesc{ - .label = "RmlUi Drop Shadow Uniform Buffer", - .usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::Uniform, - .size = AURORA_ALIGN(UniformBufferSize, 16), - }; - m_dropShadowUniformBuffer = webgpu::g_device.CreateBuffer(&dropShadowUniformBufferDesc); - const wgpu::BufferDescriptor shaderUniformBufferDesc{ - .label = "RmlUi Shader Uniform Buffer", - .usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::Uniform, - .size = AURORA_ALIGN(UniformBufferSize, 16), - }; - m_shaderUniformBuffer = webgpu::g_device.CreateBuffer(&shaderUniformBufferDesc); - - const std::array commonBindGroupEntries{ - wgpu::BindGroupEntry{ - .binding = 0, - .buffer = m_uniformBuffer, - .offset = 0, - .size = AURORA_ALIGN(sizeof(UniformBlock), 16), - }, - wgpu::BindGroupEntry{ - .binding = 1, - .sampler = m_sampler, - }, - }; - const wgpu::BindGroupDescriptor commonBindGroupDescriptor{ - .layout = m_commonBindGroupLayout, - .entryCount = commonBindGroupEntries.size(), - .entries = commonBindGroupEntries.data(), - }; - m_commonBindGroup = webgpu::g_device.CreateBindGroup(&commonBindGroupDescriptor); - - const std::array blurBindGroupEntries{ - wgpu::BindGroupEntry{ - .binding = 0, - .buffer = m_blurUniformBuffer, - .offset = 0, - .size = AURORA_ALIGN(sizeof(BlurUniformBlock), 16), - }, - }; - const wgpu::BindGroupDescriptor blurBindGroupDescriptor{ - .layout = m_blurBindGroupLayout, - .entryCount = blurBindGroupEntries.size(), - .entries = blurBindGroupEntries.data(), - }; - m_blurBindGroup = webgpu::g_device.CreateBindGroup(&blurBindGroupDescriptor); - - const std::array dropShadowBindGroupEntries{ - wgpu::BindGroupEntry{ - .binding = 0, - .buffer = m_dropShadowUniformBuffer, - .offset = 0, - .size = AURORA_ALIGN(sizeof(DropShadowUniformBlock), 16), - }, - }; - const wgpu::BindGroupDescriptor dropShadowBindGroupDescriptor{ - .layout = m_dropShadowBindGroupLayout, - .entryCount = dropShadowBindGroupEntries.size(), - .entries = dropShadowBindGroupEntries.data(), - }; - m_dropShadowBindGroup = webgpu::g_device.CreateBindGroup(&dropShadowBindGroupDescriptor); - - const std::array shaderBindGroupEntries{ - wgpu::BindGroupEntry{ - .binding = 0, - .buffer = m_shaderUniformBuffer, - .offset = 0, - .size = AURORA_ALIGN(sizeof(GradientUniformBlock), 16), - }, - }; - const wgpu::BindGroupDescriptor shaderBindGroupDescriptor{ - .layout = m_shaderBindGroupLayout, - .entryCount = shaderBindGroupEntries.size(), - .entries = shaderBindGroupEntries.data(), - }; - m_shaderBindGroup = webgpu::g_device.CreateBindGroup(&shaderBindGroupDescriptor); - switch (m_renderTargetFormat) { case wgpu::TextureFormat::ASTC10x10UnormSrgb: case wgpu::TextureFormat::ASTC10x5UnormSrgb: @@ -2354,11 +1367,11 @@ void WebGPURenderInterface::CreateDeviceObjects() { CreateNullTexture(); } -void WebGPURenderInterface::SetupRenderState(const Rml::Vector2f& translation) { +gfx::Range WebGPURenderInterface::SetupRenderState(const Rml::Vector2f& translation) { const float L = 0.f; - const float R = static_cast(m_windowSize.x); + const float R = static_cast(std::max(m_windowSize.x, 1)); const float T = 0.f; - const float B = static_cast(m_windowSize.y); + const float B = static_cast(std::max(m_windowSize.y, 1)); const float near = -10000.f; const float far = 10000.f; @@ -2371,10 +1384,7 @@ void WebGPURenderInterface::SetupRenderState(const Rml::Vector2f& translation) { .translation = {translation.x, translation.y, 0.0f, 1.0f}, .Gamma = m_gamma, }; - webgpu::g_queue.WriteBuffer(m_uniformBuffer, m_uniformCurrentOffset, &ubo, sizeof(UniformBlock)); - - constexpr wgpu::Color BlendColor{0.f, 0.f, 0.f, 0.f}; - m_pass.SetBlendConstant(&BlendColor); + return gfx::push_uniform(ubo); } void WebGPURenderInterface::CreateNullTexture() { @@ -2408,20 +1418,7 @@ void WebGPURenderInterface::EnsureClipResetGeometry() { m_clipResetGeometrySize = m_windowSize; } -void WebGPURenderInterface::CreateUniformBuffer() { - constexpr wgpu::BufferDescriptor bufferDesc{ - .label = "RmlUi Uniform Buffer", - .usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::Uniform, - .size = AURORA_ALIGN(UniformBufferSize, 16), - }; - m_uniformBuffer = webgpu::g_device.CreateBuffer(&bufferDesc); -} - void WebGPURenderInterface::NewFrame() { - m_uniformCurrentOffset = 0; - m_blurUniformCurrentOffset = 0; - m_dropShadowUniformCurrentOffset = 0; - m_shaderUniformCurrentOffset = 0; m_clipMaskEnabled = false; m_stencilRef = 0; } diff --git a/lib/rmlui/WebGPURenderInterface.hpp b/lib/rmlui/WebGPURenderInterface.hpp index 896e200..9e9d4eb 100644 --- a/lib/rmlui/WebGPURenderInterface.hpp +++ b/lib/rmlui/WebGPURenderInterface.hpp @@ -61,6 +61,7 @@ struct TexCoordLimits { }; class WebGPURenderInterface : public Rml::RenderInterface { +public: static constexpr wgpu::TextureFormat ClipMaskStencilFormat = wgpu::TextureFormat::Stencil8; enum class PipelineType { @@ -79,6 +80,7 @@ class WebGPURenderInterface : public Rml::RenderInterface { Count, }; +private: enum class FilterType { Opacity, Blur, @@ -90,7 +92,6 @@ class WebGPURenderInterface : public Rml::RenderInterface { struct RenderTarget { wgpu::Texture texture; wgpu::TextureView view; - wgpu::BindGroup bindGroup; wgpu::Texture multisampleTexture; wgpu::TextureView multisampleView; wgpu::Extent3D size; @@ -105,33 +106,8 @@ class WebGPURenderInterface : public Rml::RenderInterface { Rml::Matrix4f colorMatrix; }; - wgpu::CommandEncoder m_encoder; - wgpu::RenderPassEncoder m_pass; wgpu::TextureView m_frameSeedView; - std::array(PipelineType::Count)> m_pipelines; - std::array(BlitPipelineType::Count)> m_blitPipelines; - std::array(BlitPipelineType::Count)> m_layerBlitPipelines; - wgpu::RenderPipeline m_opaqueBlitPipeline; - wgpu::RenderPipeline m_layerOpaqueBlitPipeline; - wgpu::RenderPipeline m_blurPipeline; - wgpu::RenderPipeline m_regionBlitPipeline; - wgpu::RenderPipeline m_dropShadowPipeline; - wgpu::RenderPipeline m_opacityPipeline; - wgpu::RenderPipeline m_colorMatrixPipeline; - wgpu::RenderPipeline m_maskImagePipeline; - std::array m_gradientPipelines; - wgpu::PipelineLayout m_pipelineLayout; - wgpu::PipelineLayout m_blurPipelineLayout; - wgpu::PipelineLayout m_dropShadowPipelineLayout; - wgpu::PipelineLayout m_colorMatrixPipelineLayout; - wgpu::PipelineLayout m_maskImagePipelineLayout; - wgpu::PipelineLayout m_shaderPipelineLayout; - wgpu::Buffer m_uniformBuffer; - wgpu::Buffer m_blurUniformBuffer; - wgpu::Buffer m_dropShadowUniformBuffer; - wgpu::Buffer m_shaderUniformBuffer; - wgpu::Sampler m_sampler; wgpu::TextureFormat m_renderTargetFormat = wgpu::TextureFormat::Undefined; wgpu::Texture m_clipMaskStencilTexture; wgpu::TextureView m_clipMaskStencilView; @@ -139,15 +115,6 @@ class WebGPURenderInterface : public Rml::RenderInterface { wgpu::Extent3D m_frameSize{}; gfx::Viewport m_viewport{}; - wgpu::BindGroupLayout m_commonBindGroupLayout; - wgpu::BindGroup m_commonBindGroup; - wgpu::BindGroupLayout m_imageBindGroupLayout; - wgpu::BindGroupLayout m_blurBindGroupLayout; - wgpu::BindGroup m_blurBindGroup; - wgpu::BindGroupLayout m_dropShadowBindGroupLayout; - wgpu::BindGroup m_dropShadowBindGroup; - wgpu::BindGroupLayout m_shaderBindGroupLayout; - wgpu::BindGroup m_shaderBindGroup; Rml::TextureHandle m_nullTexture = 0; Rml::CompiledGeometryHandle m_clipResetGeometry = 0; Rml::Vector2i m_clipResetGeometrySize{}; @@ -163,23 +130,18 @@ class WebGPURenderInterface : public Rml::RenderInterface { Rml::Rectanglei m_scissorRegion{}; float m_gamma = 0.0f; - uint32_t m_uniformCurrentOffset = 0; - uint32_t m_blurUniformCurrentOffset = 0; - uint32_t m_dropShadowUniformCurrentOffset = 0; - uint32_t m_shaderUniformCurrentOffset = 0; bool m_enableScissorRegion = false; bool m_clipMaskEnabled = false; + bool m_frameActive = false; + bool m_passActive = false; bool m_frameRenderingStarted = false; uint32_t m_stencilRef = 0; - void CreateUniformBuffer(); - - void SetupRenderState(const Rml::Vector2f& translation); + gfx::Range SetupRenderState(const Rml::Vector2f& translation); void EnsureRenderTarget(RenderTarget& target, const char* label, const wgpu::Extent3D& size, bool multisampled = false); void EnsureFrameTargets(const wgpu::Extent3D& size); - wgpu::BindGroup CreateImageBindGroup(const wgpu::TextureView& view) const; Rml::Rectanglei GetActiveScissorRegion() const; TexCoordLimits GetPostprocessTexCoordLimits() const; TexCoordLimits GetPostprocessTexCoordLimits(Rml::Rectanglei region) const; @@ -190,21 +152,22 @@ class WebGPURenderInterface : public Rml::RenderInterface { void EnsureFrameRenderingStarted(); void EnsureActiveLayerPass(const char* label); void EndActivePass(); - void ApplyViewport(); - void ApplyFullFrameScissor(); - void ApplyScissorRegion(Rml::Rectanglei region); + void ApplyViewport() const; + void ApplyFullFrameScissor() const; + void ApplyScissorRegion(Rml::Rectanglei region) const; void CreateNullTexture(); void EnsureClipResetGeometry(); void ApplyScissorRegion(); void DrawGeometry(Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation, Rml::TextureHandle texture, - const wgpu::RenderPipeline& pipeline); - void DrawFullscreenTexture(const wgpu::BindGroup& bindGroup, const wgpu::RenderPipeline& pipeline, - const wgpu::BindGroup* extraBindGroup = nullptr, uint32_t extraDynamicOffset = 0, - bool extraBindGroupHasDynamicOffset = true); - void CompositeToTarget(const wgpu::BindGroup& bindGroup, const wgpu::TextureView& view, wgpu::LoadOp loadOp, - const wgpu::RenderPipeline& pipeline, const char* label, - const wgpu::BindGroup* extraBindGroup = nullptr, uint32_t extraDynamicOffset = 0, - bool extraBindGroupHasDynamicOffset = true); + gfx::PipelineRef pipeline); + void DrawFullscreenTexture(gfx::BindGroupRef bindGroup, gfx::PipelineRef pipeline, + gfx::BindGroupRef extraBindGroup = 0, gfx::Range extraUniformRange = {}, + bool extraBindGroupHasDynamicOffset = true, std::array blendConstant = {}, + bool hasBlendConstant = false); + void CompositeToTarget(gfx::BindGroupRef bindGroup, const wgpu::TextureView& view, wgpu::LoadOp loadOp, + gfx::PipelineRef pipeline, const char* label, gfx::BindGroupRef extraBindGroup = 0, + gfx::Range extraUniformRange = {}, bool extraBindGroupHasDynamicOffset = true, + std::array blendConstant = {}, bool hasBlendConstant = false); void RenderBlur(float sigma, const RenderTarget& sourceDestination, const RenderTarget& temp); void RenderFilters(Rml::Span filters); @@ -236,8 +199,7 @@ public: Rml::TextureHandle texture) override; void ReleaseShader(Rml::CompiledShaderHandle shader) override; - void BeginFrame(const wgpu::CommandEncoder& encoder, const webgpu::TextureWithSampler& target, - const webgpu::TextureWithSampler& seed_target); + void BeginFrame(const webgpu::TextureWithSampler& target, const webgpu::TextureWithSampler& seed_target); bool EndFrame(); void SetWindowSize(const Rml::Vector2i& window_size) { m_windowSize = window_size; } void SetRenderTargetFormat(wgpu::TextureFormat render_target_format) { m_renderTargetFormat = render_target_format; } diff --git a/lib/rmlui/pipeline.cpp b/lib/rmlui/pipeline.cpp new file mode 100644 index 0000000..e6dfab7 --- /dev/null +++ b/lib/rmlui/pipeline.cpp @@ -0,0 +1,917 @@ +#include "pipeline.hpp" + +#include "../internal.hpp" +#include "../webgpu/gpu.hpp" + +#include +#include +#include + +#include +#include + +namespace aurora::rmlui { +namespace { +using namespace std::string_view_literals; + +wgpu::BindGroupLayout g_commonBindGroupLayout; +wgpu::BindGroupLayout g_imageBindGroupLayout; +wgpu::BindGroupLayout g_uniformBindGroupLayout; +wgpu::Sampler g_sampler; + +constexpr uint32_t DynamicGroup1 = 1u << 1u; +constexpr uint32_t DynamicGroup2 = 1u << 2u; + +constexpr uint64_t CommonUniformBindingSize = AURORA_ALIGN(sizeof(UniformBlock), 16); +constexpr uint64_t ExtraUniformBindingSize = + AURORA_ALIGN(std::max({sizeof(BlurUniformBlock), sizeof(DropShadowUniformBlock), sizeof(ColorMatrixUniformBlock), + sizeof(GradientUniformBlock), sizeof(SeedResampleUniformBlock)}), + 16); + +constexpr std::string_view vertexSource = R"( +struct VertexInput { + @location(0) position: vec2, + @location(1) uv: vec2, + @location(2) color: vec4, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) color: vec4, + @location(1) uv: vec2, +}; + +struct Uniforms { + mvp: mat4x4, + translation: vec4, + gamma: f32, +}; + +@group(0) @binding(0) var uniforms: Uniforms; + +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + var translatedPos = uniforms.translation.xy + in.position; + + out.position = uniforms.mvp * vec4(translatedPos, 0.0, 1.0); + out.color = in.color; + out.uv = in.uv; + return out; +} +)"sv; + +constexpr std::string_view fragmentSource = R"( +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) color: vec4, + @location(1) uv: vec2, +}; + +struct Uniforms { + mvp: mat4x4, + translation: vec4, + gamma: f32, +}; + +@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(1) var s: sampler; +@group(1) @binding(0) var t: texture_2d; + +@fragment +fn main(in: VertexOutput) -> @location(0) vec4 { + let color = in.color * textureSample(t, s, in.uv); + if (uniforms.gamma == 1.0) { + return color; + } + let corrected_color = pow(color.rgb, vec3(uniforms.gamma)); + return vec4(corrected_color, color.a); +} +)"sv; + +constexpr std::string_view gradientFragmentSource = R"( +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) color: vec4, + @location(1) uv: vec2, +}; + +struct Uniforms { + mvp: mat4x4, + translation: vec4, + gamma: f32, +}; + +struct GradientUniforms { + function: i32, + num_stops: i32, + p: vec2, + v: vec2, + padding: vec2, + stop_colors: array, 16>, + stop_positions: array, 4>, +}; + +@group(0) @binding(0) var uniforms: Uniforms; +@group(1) @binding(0) var gradient: GradientUniforms; + +const LINEAR: i32 = 0; +const RADIAL: i32 = 1; +const CONIC: i32 = 2; +const REPEATING_LINEAR: i32 = 3; +const REPEATING_RADIAL: i32 = 4; +const REPEATING_CONIC: i32 = 5; +const PI: f32 = 3.14159265; + +fn bayer_dither(position: vec4) -> f32 { + let bayer = array( + 0u, 32u, 8u, 40u, 2u, 34u, 10u, 42u, + 48u, 16u, 56u, 24u, 50u, 18u, 58u, 26u, + 12u, 44u, 4u, 36u, 14u, 46u, 6u, 38u, + 60u, 28u, 52u, 20u, 62u, 30u, 54u, 22u, + 3u, 35u, 11u, 43u, 1u, 33u, 9u, 41u, + 51u, 19u, 59u, 27u, 49u, 17u, 57u, 25u, + 15u, 47u, 7u, 39u, 13u, 45u, 5u, 37u, + 63u, 31u, 55u, 23u, 61u, 29u, 53u, 21u + ); + let x = u32(position.x) % 8u; + let y = u32(position.y) % 8u; + return (f32(bayer[x + y * 8u]) / 64.0 - 0.5) / 255.0; +} + +fn stop_position(index: i32) -> f32 { + let stop_index = u32(index); + let group_index = stop_index / 4u; + let component_index = stop_index % 4u; + return gradient.stop_positions[group_index][component_index]; +} + +fn stop_color_mix(t: f32) -> vec4 { + var color = gradient.stop_colors[0]; + + for (var i = 1; i < 16; i = i + 1) { + if (i < gradient.num_stops) { + color = mix(color, gradient.stop_colors[u32(i)], smoothstep(stop_position(i - 1), stop_position(i), t)); + } + } + + return color; +} + +@fragment +fn main(in: VertexOutput) -> @location(0) vec4 { + var t = 0.0; + + if (gradient.function == LINEAR || gradient.function == REPEATING_LINEAR) { + let dist_square = dot(gradient.v, gradient.v); + let v = in.uv - gradient.p; + t = dot(gradient.v, v) / dist_square; + } else if (gradient.function == RADIAL || gradient.function == REPEATING_RADIAL) { + let v = in.uv - gradient.p; + t = length(gradient.v * v); + } else if (gradient.function == CONIC || gradient.function == REPEATING_CONIC) { + let v = in.uv - gradient.p; + let rotated = vec2( + gradient.v.x * v.x + gradient.v.y * v.y, + -gradient.v.y * v.x + gradient.v.x * v.y + ); + t = 0.5 + atan2(-rotated.x, rotated.y) / (2.0 * PI); + } + + if (gradient.function == REPEATING_LINEAR || + gradient.function == REPEATING_RADIAL || + gradient.function == REPEATING_CONIC) { + let t0 = stop_position(0); + let t1 = stop_position(gradient.num_stops - 1); + let span = t1 - t0; + t = t0 + (t - t0) - span * floor((t - t0) / span); + } + + let color = in.color * stop_color_mix(t); + if (uniforms.gamma == 1.0) { + return color; + } + let corrected_color = pow(color.rgb, vec3(uniforms.gamma)); + let dithered_color = clamp(corrected_color + vec3(bayer_dither(in.position)), vec3(0.0), vec3(1.0)); + return vec4(dithered_color, color.a); +} +)"sv; + +constexpr std::string_view fullscreenVertexSource = R"( +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) uv: vec2, +}; + +var pos: array, 3> = array, 3>( + vec2(-1.0, 1.0), + vec2(-1.0, -3.0), + vec2(3.0, 1.0), +); +var uvs: array, 3> = array, 3>( + vec2(0.0, 0.0), + vec2(0.0, 2.0), + vec2(2.0, 0.0), +); + +@vertex +fn main(@builtin(vertex_index) vtxIdx: u32) -> VertexOutput { + var out: VertexOutput; + out.position = vec4(pos[vtxIdx], 0.0, 1.0); + out.uv = uvs[vtxIdx]; + return out; +} +)"sv; + +constexpr std::string_view blurVertexSource = R"( +struct BlurUniforms { + texel_offset: vec2, + radius: f32, + padding: f32, + tex_coord_min: vec2, + tex_coord_max: vec2, + weights: vec4, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) uv0: vec2, + @location(1) uv1: vec2, + @location(2) uv2: vec2, + @location(3) uv3: vec2, + @location(4) uv4: vec2, + @location(5) uv5: vec2, + @location(6) uv6: vec2, +}; + +@group(2) @binding(0) var blur: BlurUniforms; + +const BLUR_NUM_WEIGHTS: i32 = 4; + +var pos: array, 3> = array, 3>( + vec2(-1.0, 1.0), + vec2(-1.0, -3.0), + vec2(3.0, 1.0), +); +var uvs: array, 3> = array, 3>( + vec2(0.0, 0.0), + vec2(0.0, 2.0), + vec2(2.0, 0.0), +); + +fn blur_uv(uv: vec2, index: i32) -> vec2 { + return uv - f32(index - BLUR_NUM_WEIGHTS + 1) * blur.texel_offset; +} + +@vertex +fn main(@builtin(vertex_index) vtxIdx: u32) -> VertexOutput { + let uv = uvs[vtxIdx]; + var out: VertexOutput; + out.position = vec4(pos[vtxIdx], 0.0, 1.0); + out.uv0 = blur_uv(uv, 0); + out.uv1 = blur_uv(uv, 1); + out.uv2 = blur_uv(uv, 2); + out.uv3 = blur_uv(uv, 3); + out.uv4 = blur_uv(uv, 4); + out.uv5 = blur_uv(uv, 5); + out.uv6 = blur_uv(uv, 6); + return out; +} +)"sv; + +constexpr std::string_view blitFragmentSource = R"( +@group(0) @binding(1) var s: sampler; +@group(1) @binding(0) var t: texture_2d; + +@fragment +fn main(@location(0) uv: vec2) -> @location(0) vec4 { + return textureSample(t, s, uv); +} +)"sv; + +constexpr std::string_view opaqueBlitFragmentSource = R"( +@group(0) @binding(1) var s: sampler; +@group(1) @binding(0) var t: texture_2d; + +@fragment +fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let color = textureSample(t, s, uv); + return vec4(color.rgb, 1.0); +} +)"sv; + +constexpr std::string_view seedResampleFragmentSource = R"( +struct SeedUniforms { + sampler_mode: u32, + frame_width: f32, + frame_height: f32, + pad: u32, +}; + +@group(0) @binding(1) var s: sampler; +@group(1) @binding(0) var t: texture_2d; +@group(2) @binding(0) var uniforms: SeedUniforms; + +fn sample_by_pixel(pixel: vec2) -> vec4 { + let source_dims = textureDimensions(t); + let max_coord = vec2(source_dims) - vec2(1, 1); + let coord = clamp(pixel, vec2(0, 0), max_coord); + return textureLoad(t, coord, 0); +} + +fn sample_area(frag_position: vec4) -> vec4 { + let source_size = vec2(textureDimensions(t)); + let target_size = max(vec2(uniforms.frame_width, uniforms.frame_height), vec2(1.0, 1.0)); + + let source_min = clamp((frag_position.xy - vec2(0.5, 0.5)) / target_size, + vec2(0.0, 0.0), vec2(1.0, 1.0)) * source_size; + let source_max = clamp((frag_position.xy + vec2(0.5, 0.5)) / target_size, + vec2(0.0, 0.0), vec2(1.0, 1.0)) * source_size; + + let first_pixel = vec2(floor(source_min)); + let last_pixel = vec2(ceil(source_max)); + let max_iterations: i32 = 16; + + var avg_color = vec4(0.0, 0.0, 0.0, 0.0); + var total_weight = 0.0; + + for (var iy: i32 = 0; iy < max_iterations; iy = iy + 1) { + let source_y = first_pixel.y + iy; + if (source_y < last_pixel.y) { + let y0 = f32(source_y); + let weight_y = max(min(source_max.y, y0 + 1.0) - max(source_min.y, y0), 0.0); + + for (var ix: i32 = 0; ix < max_iterations; ix = ix + 1) { + let source_x = first_pixel.x + ix; + if (source_x < last_pixel.x) { + let x0 = f32(source_x); + let weight_x = max(min(source_max.x, x0 + 1.0) - max(source_min.x, x0), 0.0); + let weight = weight_x * weight_y; + avg_color += weight * sample_by_pixel(vec2(source_x, source_y)); + total_weight += weight; + } + } + } + } + + return avg_color / max(total_weight, 0.000001); +} + +@fragment +fn main(@builtin(position) position: vec4, @location(0) uv: vec2) -> @location(0) vec4 { + var color = textureSample(t, s, uv); + if (uniforms.sampler_mode == 1u) { + color = sample_area(position); + } + return vec4(color.rgb, 1.0); +} +)"sv; + +constexpr std::string_view colorMatrixFragmentSource = R"( +struct ColorMatrixUniforms { + matrix: mat4x4, +}; + +@group(0) @binding(1) var s: sampler; +@group(1) @binding(0) var t: texture_2d; +@group(2) @binding(0) var color_matrix: ColorMatrixUniforms; + +@fragment +fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let tex_color = textureSample(t, s, uv); + let transformed_color = (color_matrix.matrix * tex_color).rgb; + return vec4(transformed_color, tex_color.a); +} +)"sv; + +constexpr std::string_view maskImageFragmentSource = R"( +@group(0) @binding(1) var s: sampler; +@group(1) @binding(0) var t: texture_2d; +@group(2) @binding(0) var mask_t: texture_2d; + +@fragment +fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let tex_color = textureSample(t, s, uv); + let mask_alpha = textureSample(mask_t, s, uv).a; + return tex_color * mask_alpha; +} +)"sv; + +constexpr std::string_view blurFragmentSource = R"( +struct BlurUniforms { + texel_offset: vec2, + radius: f32, + padding: f32, + tex_coord_min: vec2, + tex_coord_max: vec2, + weights: vec4, +}; + +@group(0) @binding(1) var s: sampler; +@group(1) @binding(0) var t: texture_2d; +@group(2) @binding(0) var blur: BlurUniforms; + +fn get_weight(index: i32) -> f32 { + return blur.weights[u32(abs(index))]; +} + +fn sample_blur(sample_uv: vec2, offset_index: i32) -> vec4 { + let in_region = step(blur.tex_coord_min, sample_uv) * step(sample_uv, blur.tex_coord_max); + return textureSample(t, s, sample_uv) * get_weight(offset_index) * in_region.x * in_region.y; +} + +@fragment +fn main(@location(0) uv0: vec2, @location(1) uv1: vec2, @location(2) uv2: vec2, + @location(3) uv3: vec2, @location(4) uv4: vec2, @location(5) uv5: vec2, + @location(6) uv6: vec2) -> @location(0) vec4 { + var color = sample_blur(uv0, -3); + color += sample_blur(uv1, -2); + color += sample_blur(uv2, -1); + color += sample_blur(uv3, 0); + color += sample_blur(uv4, 1); + color += sample_blur(uv5, 2); + color += sample_blur(uv6, 3); + return color; +} +)"sv; + +constexpr std::string_view regionBlitFragmentSource = R"( +struct BlurUniforms { + texel_offset: vec2, + radius: f32, + padding: f32, + tex_coord_min: vec2, + tex_coord_max: vec2, + weights: vec4, +}; + +@group(0) @binding(1) var s: sampler; +@group(1) @binding(0) var t: texture_2d; +@group(2) @binding(0) var blur: BlurUniforms; + +@fragment +fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let sample_uv = mix(blur.tex_coord_min, blur.tex_coord_max, uv); + return textureSample(t, s, sample_uv); +} +)"sv; + +constexpr std::string_view dropShadowFragmentSource = R"( +struct DropShadowUniforms { + color: vec4, + uv_offset: vec2, + tex_coord_min: vec2, + tex_coord_max: vec2, +}; + +@group(0) @binding(1) var s: sampler; +@group(1) @binding(0) var t: texture_2d; +@group(2) @binding(0) var shadow: DropShadowUniforms; + +@fragment +fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let sample_uv = uv - shadow.uv_offset; + let in_region = step(shadow.tex_coord_min, sample_uv) * step(sample_uv, shadow.tex_coord_max); + let alpha = textureSample(t, s, sample_uv).a * in_region.x * in_region.y; + return shadow.color * alpha; +} +)"sv; + +wgpu::ComputeState compile_shader(std::string_view wgslSource, std::string_view label) { + const wgpu::ShaderSourceWGSL source{ + wgpu::ShaderSourceWGSL::Init{ + .nextInChain = nullptr, + .code = wgslSource, + }, + }; + const wgpu::ShaderModuleDescriptor desc{ + .nextInChain = &source, + .label = label, + }; + return { + .module = webgpu::g_device.CreateShaderModule(&desc), + .entryPoint = "main", + }; +} + +wgpu::BlendState blend_state(BlendMode mode) { + switch (mode) { + case BlendMode::Premultiplied: + return { + .color = + { + .operation = wgpu::BlendOperation::Add, + .srcFactor = wgpu::BlendFactor::One, + .dstFactor = wgpu::BlendFactor::OneMinusSrcAlpha, + }, + .alpha = + { + .operation = wgpu::BlendOperation::Add, + .srcFactor = wgpu::BlendFactor::One, + .dstFactor = wgpu::BlendFactor::OneMinusSrcAlpha, + }, + }; + case BlendMode::Opacity: + return { + .color = + { + .operation = wgpu::BlendOperation::Add, + .srcFactor = wgpu::BlendFactor::Constant, + .dstFactor = wgpu::BlendFactor::Zero, + }, + .alpha = + { + .operation = wgpu::BlendOperation::Add, + .srcFactor = wgpu::BlendFactor::Constant, + .dstFactor = wgpu::BlendFactor::Zero, + }, + }; + case BlendMode::None: + default: + return {}; + } +} + +const wgpu::PipelineLayout create_pipeline_layout(PipelineKind kind) { + std::array layouts{}; + uint32_t layoutCount = 0; + layouts[layoutCount++] = g_commonBindGroupLayout; + + switch (kind) { + case PipelineKind::Gradient: + layouts[layoutCount++] = g_uniformBindGroupLayout; + break; + case PipelineKind::MaskImage: + layouts[layoutCount++] = g_imageBindGroupLayout; + layouts[layoutCount++] = g_imageBindGroupLayout; + break; + case PipelineKind::Blur: + case PipelineKind::RegionBlit: + case PipelineKind::DropShadow: + case PipelineKind::ColorMatrix: + case PipelineKind::SeedResample: + layouts[layoutCount++] = g_imageBindGroupLayout; + layouts[layoutCount++] = g_uniformBindGroupLayout; + break; + case PipelineKind::Geometry: + case PipelineKind::Blit: + case PipelineKind::OpaqueBlit: + case PipelineKind::Opacity: + default: + layouts[layoutCount++] = g_imageBindGroupLayout; + break; + } + + const wgpu::PipelineLayoutDescriptor layoutDesc{ + .bindGroupLayoutCount = layoutCount, + .bindGroupLayouts = layouts.data(), + }; + return webgpu::g_device.CreatePipelineLayout(&layoutDesc); +} + +const std::string_view fragment_source(PipelineKind kind) { + switch (kind) { + case PipelineKind::Geometry: + return fragmentSource; + case PipelineKind::Gradient: + return gradientFragmentSource; + case PipelineKind::OpaqueBlit: + return opaqueBlitFragmentSource; + case PipelineKind::SeedResample: + return seedResampleFragmentSource; + case PipelineKind::Blur: + return blurFragmentSource; + case PipelineKind::RegionBlit: + return regionBlitFragmentSource; + case PipelineKind::DropShadow: + return dropShadowFragmentSource; + case PipelineKind::ColorMatrix: + return colorMatrixFragmentSource; + case PipelineKind::MaskImage: + return maskImageFragmentSource; + case PipelineKind::Blit: + case PipelineKind::Opacity: + default: + return blitFragmentSource; + } +} + +const std::string_view vertex_source(VertexLayoutKind kind) { + switch (kind) { + case VertexLayoutKind::Geometry: + return vertexSource; + case VertexLayoutKind::BlurFullscreen: + return blurVertexSource; + case VertexLayoutKind::Fullscreen: + default: + return fullscreenVertexSource; + } +} + +} // namespace + +void initialize_pipeline() { + constexpr std::array commonEntries{ + wgpu::BindGroupLayoutEntry{ + .binding = 0, + .visibility = wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment, + .buffer = + { + .type = wgpu::BufferBindingType::Uniform, + .hasDynamicOffset = true, + }, + }, + wgpu::BindGroupLayoutEntry{ + .binding = 1, + .visibility = wgpu::ShaderStage::Fragment, + .sampler = + { + .type = wgpu::SamplerBindingType::Filtering, + }, + }, + }; + const wgpu::BindGroupLayoutDescriptor commonDesc{ + .entryCount = commonEntries.size(), + .entries = commonEntries.data(), + }; + g_commonBindGroupLayout = webgpu::g_device.CreateBindGroupLayout(&commonDesc); + + constexpr std::array imageEntries{ + wgpu::BindGroupLayoutEntry{ + .binding = 0, + .visibility = wgpu::ShaderStage::Fragment, + .texture = + { + .sampleType = wgpu::TextureSampleType::Float, + .viewDimension = wgpu::TextureViewDimension::e2D, + }, + }, + }; + const wgpu::BindGroupLayoutDescriptor imageDesc{ + .entryCount = imageEntries.size(), + .entries = imageEntries.data(), + }; + g_imageBindGroupLayout = webgpu::g_device.CreateBindGroupLayout(&imageDesc); + + constexpr std::array uniformEntries{ + wgpu::BindGroupLayoutEntry{ + .binding = 0, + .visibility = wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment, + .buffer = + { + .type = wgpu::BufferBindingType::Uniform, + .hasDynamicOffset = true, + }, + }, + }; + const wgpu::BindGroupLayoutDescriptor uniformDesc{ + .entryCount = uniformEntries.size(), + .entries = uniformEntries.data(), + }; + g_uniformBindGroupLayout = webgpu::g_device.CreateBindGroupLayout(&uniformDesc); + + constexpr wgpu::SamplerDescriptor samplerDesc{ + .addressModeU = wgpu::AddressMode::Repeat, + .addressModeV = wgpu::AddressMode::Repeat, + .addressModeW = wgpu::AddressMode::Repeat, + .magFilter = wgpu::FilterMode::Linear, + .minFilter = wgpu::FilterMode::Linear, + .mipmapFilter = wgpu::MipmapFilterMode::Linear, + .maxAnisotropy = 1, + }; + g_sampler = webgpu::g_device.CreateSampler(&samplerDesc); +} + +void shutdown_pipeline() { + g_commonBindGroupLayout = {}; + g_imageBindGroupLayout = {}; + g_uniformBindGroupLayout = {}; + g_sampler = {}; +} + +gfx::BindGroupRef texture_bind_group_ref(const wgpu::TextureView& view) { + const std::array entries{ + wgpu::BindGroupEntry{ + .binding = 0, + .textureView = view, + }, + }; + const wgpu::BindGroupDescriptor desc{ + .layout = g_imageBindGroupLayout, + .entryCount = entries.size(), + .entries = entries.data(), + }; + return gfx::bind_group_ref(desc); +} + +gfx::BindGroupRef common_bind_group_ref() { + const std::array entries{ + wgpu::BindGroupEntry{ + .binding = 0, + .buffer = gfx::g_uniformBuffer, + .offset = 0, + .size = CommonUniformBindingSize, + }, + wgpu::BindGroupEntry{ + .binding = 1, + .sampler = g_sampler, + }, + }; + const wgpu::BindGroupDescriptor desc{ + .layout = g_commonBindGroupLayout, + .entryCount = entries.size(), + .entries = entries.data(), + }; + return gfx::bind_group_ref(desc); +} + +gfx::BindGroupRef uniform_bind_group_ref() { + const std::array entries{ + wgpu::BindGroupEntry{ + .binding = 0, + .buffer = gfx::g_uniformBuffer, + .offset = 0, + .size = ExtraUniformBindingSize, + }, + }; + const wgpu::BindGroupDescriptor desc{ + .layout = g_uniformBindGroupLayout, + .entryCount = entries.size(), + .entries = entries.data(), + }; + return gfx::bind_group_ref(desc); +} + +wgpu::RenderPipeline create_pipeline(const PipelineConfig& config) { + ZoneScoped; + const auto kind = static_cast(config.kind); + const auto vertexLayoutKind = static_cast(config.vertexLayout); + const auto colorFormat = static_cast(config.colorFormat); + const auto stencilFormat = static_cast(config.stencilFormat); + const auto stencilMode = static_cast(config.stencilMode); + const auto blendMode = static_cast(config.blendMode); + + const auto vertexShader = compile_shader(vertex_source(vertexLayoutKind), "RmlUi Vertex Shader"); + const auto fragmentShader = compile_shader(fragment_source(kind), "RmlUi Fragment Shader"); + + constexpr std::array vertexAttributes{ + wgpu::VertexAttribute{ + .format = wgpu::VertexFormat::Float32x2, + .offset = offsetof(Rml::Vertex, position), + .shaderLocation = 0, + }, + wgpu::VertexAttribute{ + .format = wgpu::VertexFormat::Float32x2, + .offset = offsetof(Rml::Vertex, tex_coord), + .shaderLocation = 1, + }, + wgpu::VertexAttribute{ + .format = wgpu::VertexFormat::Unorm8x4, + .offset = offsetof(Rml::Vertex, colour), + .shaderLocation = 2, + }, + }; + const std::array vertexBufferLayouts{ + wgpu::VertexBufferLayout{ + .stepMode = wgpu::VertexStepMode::Vertex, + .arrayStride = sizeof(Rml::Vertex), + .attributeCount = vertexAttributes.size(), + .attributes = vertexAttributes.data(), + }, + }; + + const auto blend = blend_state(blendMode); + const wgpu::ColorTargetState colorState{ + .format = colorFormat, + .blend = blendMode == BlendMode::None ? nullptr : &blend, + .writeMask = static_cast(config.colorWriteMask), + }; + const wgpu::FragmentState fragmentState{ + .module = fragmentShader.module, + .entryPoint = fragmentShader.entryPoint, + .targetCount = 1, + .targets = &colorState, + }; + + wgpu::DepthStencilState depthStencilState{}; + const wgpu::DepthStencilState* depthStencil = nullptr; + if (stencilMode != StencilMode::None) { + wgpu::CompareFunction compare = wgpu::CompareFunction::Always; + wgpu::StencilOperation passOp = wgpu::StencilOperation::Keep; + switch (stencilMode) { + case StencilMode::EqualKeep: + compare = wgpu::CompareFunction::Equal; + break; + case StencilMode::ClipReplace: + passOp = wgpu::StencilOperation::Replace; + break; + case StencilMode::ClipIntersect: + compare = wgpu::CompareFunction::Equal; + passOp = wgpu::StencilOperation::IncrementClamp; + break; + case StencilMode::AlwaysKeep: + case StencilMode::None: + default: + break; + } + const wgpu::StencilFaceState face{ + .compare = compare, + .failOp = wgpu::StencilOperation::Keep, + .depthFailOp = wgpu::StencilOperation::Keep, + .passOp = passOp, + }; + depthStencilState = { + .format = stencilFormat, + .stencilFront = face, + .stencilBack = face, + .stencilReadMask = 0xFF, + .stencilWriteMask = 0xFF, + }; + depthStencil = &depthStencilState; + } + + const bool hasVertexBuffer = vertexLayoutKind == VertexLayoutKind::Geometry; + const auto pipelineLayout = create_pipeline_layout(kind); + const auto label = fmt::format("RmlUi Pipeline {}", config.kind); + const wgpu::RenderPipelineDescriptor pipelineDesc{ + .label = label.c_str(), + .layout = pipelineLayout, + .vertex = + { + .module = vertexShader.module, + .entryPoint = vertexShader.entryPoint, + .bufferCount = hasVertexBuffer ? vertexBufferLayouts.size() : 0, + .buffers = hasVertexBuffer ? vertexBufferLayouts.data() : nullptr, + }, + .primitive = + { + .topology = wgpu::PrimitiveTopology::TriangleList, + .stripIndexFormat = wgpu::IndexFormat::Undefined, + .frontFace = wgpu::FrontFace::CW, + .cullMode = wgpu::CullMode::None, + }, + .depthStencil = depthStencil, + .multisample = + { + .count = config.sampleCount, + }, + .fragment = &fragmentState, + }; + return webgpu::g_device.CreateRenderPipeline(&pipelineDesc); +} + +void render(const DrawData& data, const wgpu::RenderPassEncoder& pass) { + if (!gfx::bind_pipeline(data.pipeline, pass)) { + return; + } + + const auto commonBindGroup = gfx::find_bind_group(common_bind_group_ref()); + const std::array commonOffsets{data.uniformRange.offset}; + pass.SetBindGroup(0, commonBindGroup, commonOffsets.size(), commonOffsets.data()); + + if (data.bindGroup1 != 0) { + const auto bindGroup = gfx::find_bind_group(data.bindGroup1); + if ((data.dynamicBindGroupMask & DynamicGroup1) != 0) { + const std::array offsets{data.bindGroup1DynamicOffset}; + pass.SetBindGroup(1, bindGroup, offsets.size(), offsets.data()); + } else { + pass.SetBindGroup(1, bindGroup); + } + } + + if (data.bindGroup2 != 0) { + const auto bindGroup = gfx::find_bind_group(data.bindGroup2); + if ((data.dynamicBindGroupMask & DynamicGroup2) != 0) { + const std::array offsets{data.bindGroup2DynamicOffset}; + pass.SetBindGroup(2, bindGroup, offsets.size(), offsets.data()); + } else { + pass.SetBindGroup(2, bindGroup); + } + } + + if (data.hasBlendConstant != 0) { + const wgpu::Color color{data.blendConstant[0], data.blendConstant[1], data.blendConstant[2], data.blendConstant[3]}; + pass.SetBlendConstant(&color); + } + pass.SetStencilReference(data.stencilRef); + + if (static_cast(data.drawKind) == DrawKind::Geometry) { + pass.SetVertexBuffer(0, gfx::g_vertexBuffer, data.vertexRange.offset, data.vertexRange.size); + pass.SetIndexBuffer(gfx::g_indexBuffer, wgpu::IndexFormat::Uint32, data.indexRange.offset, data.indexRange.size); + pass.DrawIndexed(data.indexCount); + } else { + pass.Draw(data.vertexCount); + } +} + +uint32_t sampler_mode() noexcept { + switch (webgpu::get_resampler()) { + case SAMPLER_AREA: + return 1; + case SAMPLER_BILINEAR: + default: + return 0; + } +} + +} // namespace aurora::rmlui diff --git a/lib/rmlui/pipeline.hpp b/lib/rmlui/pipeline.hpp new file mode 100644 index 0000000..7ec451d --- /dev/null +++ b/lib/rmlui/pipeline.hpp @@ -0,0 +1,105 @@ +#pragma once + +#include "../gfx/common.hpp" +#include "WebGPURenderInterface.hpp" + +#include +#include + +#include + +namespace aurora::rmlui { + +constexpr uint32_t RmlPipelineConfigVersion = 1; + +enum class PipelineKind : uint32_t { + Geometry, + Gradient, + Blit, + OpaqueBlit, + SeedResample, + Opacity, + Blur, + RegionBlit, + DropShadow, + ColorMatrix, + MaskImage, +}; + +enum class VertexLayoutKind : uint32_t { + Geometry, + Fullscreen, + BlurFullscreen, +}; + +enum class StencilMode : uint32_t { + None, + AlwaysKeep, + EqualKeep, + ClipReplace, + ClipIntersect, +}; + +enum class BlendMode : uint32_t { + None, + Premultiplied, + Opacity, +}; + +enum class DrawKind : uint32_t { + Geometry, + Fullscreen, +}; + +struct PipelineConfig { + uint32_t version = RmlPipelineConfigVersion; + uint32_t kind = static_cast(PipelineKind::Geometry); + uint32_t colorFormat = static_cast(wgpu::TextureFormat::Undefined); + uint32_t sampleCount = 1; + uint32_t vertexLayout = static_cast(VertexLayoutKind::Geometry); + uint32_t stencilFormat = static_cast(wgpu::TextureFormat::Undefined); + uint32_t stencilMode = static_cast(StencilMode::None); + uint32_t blendMode = static_cast(BlendMode::Premultiplied); + uint32_t colorWriteMask = static_cast(wgpu::ColorWriteMask::All); +}; +static_assert(std::has_unique_object_representations_v); + +struct SeedResampleUniformBlock { + uint32_t samplerMode = 0; + float frameWidth = 0.f; + float frameHeight = 0.f; + uint32_t _pad = 0; +}; + +struct DrawData { + gfx::PipelineRef pipeline = 0; + gfx::Range vertexRange; + gfx::Range indexRange; + gfx::Range uniformRange; + gfx::BindGroupRef bindGroup1 = 0; + gfx::BindGroupRef bindGroup2 = 0; + uint32_t bindGroup1DynamicOffset = 0; + uint32_t bindGroup2DynamicOffset = 0; + uint32_t dynamicBindGroupMask = 0; + uint32_t drawKind = static_cast(DrawKind::Geometry); + uint32_t vertexCount = 0; + uint32_t indexCount = 0; + uint32_t stencilRef = 0; + std::array blendConstant{}; + uint32_t hasBlendConstant = 0; +}; +static_assert(std::is_trivially_copyable_v); + +void initialize_pipeline(); +void shutdown_pipeline(); + +gfx::BindGroupRef texture_bind_group_ref(const wgpu::TextureView& view); +gfx::BindGroupRef common_bind_group_ref(); +gfx::BindGroupRef uniform_bind_group_ref(); + +wgpu::RenderPipeline create_pipeline(const PipelineConfig& config); +void render(const DrawData& data, const wgpu::RenderPassEncoder& pass); + +uint32_t sampler_mode() noexcept; + +} // namespace aurora::rmlui diff --git a/lib/webgpu/gpu.cpp b/lib/webgpu/gpu.cpp index 9b75f06..3bd54f5 100644 --- a/lib/webgpu/gpu.cpp +++ b/lib/webgpu/gpu.cpp @@ -14,6 +14,7 @@ #include #include "../gfx/common.hpp" +#include "../gfx/render_worker.hpp" #include "../internal.hpp" #include "../window.hpp" @@ -594,6 +595,7 @@ const TextureWithSampler& resample_present_source(const wgpu::CommandEncoder& en .frameWidth = static_cast(width), .frameHeight = static_cast(height), }; + ASSERT(gfx::render_worker::is_worker_thread(), "Present resample queue write must run on the render worker"); g_queue.WriteBuffer(g_ResampleUniformBuffer, 0, &uniform, sizeof(uniform)); const std::array bindGroupEntries{ @@ -948,6 +950,7 @@ bool initialize(AuroraBackend auroraBackend, bool allowCpu) { } void shutdown() { + gfx::gpu_synchronize(); g_CopyBindGroupLayout = {}; g_CopyPipeline = {}; g_CopyBindGroup = {}; @@ -968,6 +971,7 @@ void shutdown() { } void release_surface() noexcept { + gfx::gpu_synchronize(); if (g_surface) { g_surface.Unconfigure(); } @@ -975,6 +979,7 @@ void release_surface() noexcept { } bool refresh_surface(bool recreate) { + gfx::gpu_synchronize(); if (!g_instance || !g_device) { return false; } @@ -1003,6 +1008,7 @@ bool refresh_surface(bool recreate) { } void resize_swapchain(uint32_t width, uint32_t height, uint32_t native_width, uint32_t native_height, bool force) { + gfx::gpu_synchronize(); if (!g_surface || !g_device || width == 0 || height == 0 || native_height == 0 || native_width == 0) { return; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9b04337..43b2d61 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -63,6 +63,22 @@ if (AURORA_ENABLE_GX) ) gtest_discover_tests(gx_fifo_tests) + + add_executable(render_worker_tests + render_worker_test.cpp + ../lib/gfx/render_worker.cpp + ) + target_include_directories(render_worker_tests PRIVATE + ../include + ../lib + ) + target_compile_definitions(render_worker_tests PRIVATE AURORA TARGET_PC) + target_link_libraries(render_worker_tests PRIVATE + gtest + gtest_main + TracyClient + ) + gtest_discover_tests(render_worker_tests) endif () # AURORA_ENABLE_GX # DVD API tests diff --git a/tests/gx_test_stubs.cpp b/tests/gx_test_stubs.cpp index 0fb506c..36880ad 100644 --- a/tests/gx_test_stubs.cpp +++ b/tests/gx_test_stubs.cpp @@ -60,11 +60,6 @@ Vec2 configured_fb_size() noexcept { return {640, 480}; } void configure(const GXRenderModeObj*) noexcept {} } // namespace aurora::vi -// --- Texture uploads --- -namespace aurora::gfx { -std::vector g_textureUploads; -} // namespace aurora::gfx - // --- get_texture --- namespace aurora::gx { const gfx::TextureBind& get_texture(GXTexMapID id) noexcept { return g_gxState.textures[id]; } @@ -179,7 +174,10 @@ TextureHandle new_render_texture(uint32_t width, uint32_t height, u32 gxFormat, return {}; } TextureHandle new_conv_texture(uint32_t width, uint32_t height, u32 gxFormat, const char* label) noexcept { return {}; } -void write_texture(const TextureRef& ref, ArrayRef data) noexcept {} +void write_texture(TextureRef& ref, ArrayRef data) noexcept {} +void queue_texture_upload(TextureUpload upload) {} +void queue_texture_upload_data(const uint8_t* data, size_t length, uint32_t bytesPerRow, uint32_t rowsPerImage, + wgpu::TexelCopyTextureInfo tex, wgpu::Extent3D size) {} void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool clearAlpha, bool clearDepth, Vec4 clearColorValue, float clearDepthValue, GXTexFmt resolveFormat) {} void queue_palette_conv(tex_palette_conv::ConvRequest req) {} diff --git a/tests/render_worker_test.cpp b/tests/render_worker_test.cpp new file mode 100644 index 0000000..74e83ed --- /dev/null +++ b/tests/render_worker_test.cpp @@ -0,0 +1,107 @@ +#include "../lib/gfx/render_worker.hpp" + +#include +#include +#include +#include +#include + +#include + +namespace { +using namespace std::chrono_literals; +using aurora::gfx::render_worker::BoundedQueue; +using aurora::gfx::render_worker::FrameSlotPool; +using aurora::gfx::render_worker::ItemType; +using aurora::gfx::render_worker::QueueItem; + +class RenderWorkerTest : public testing::Test { +protected: + void TearDown() override { aurora::gfx::render_worker::shutdown(); } +}; + +TEST(RenderWorkerQueue, PreservesOrdering) { + BoundedQueue queue{4}; + ASSERT_TRUE(queue.push(QueueItem{.type = ItemType::BeginFrame, .frameId = 1})); + ASSERT_TRUE(queue.push(QueueItem{.type = ItemType::EncodePass, .frameId = 1, .passIndex = 7})); + ASSERT_TRUE(queue.push(QueueItem{.type = ItemType::EndFrame, .frameId = 1})); + + bool closed = false; + auto first = queue.pop_for(0ms, closed); + auto second = queue.pop_for(0ms, closed); + auto third = queue.pop_for(0ms, closed); + + ASSERT_TRUE(first.has_value()); + ASSERT_TRUE(second.has_value()); + ASSERT_TRUE(third.has_value()); + EXPECT_EQ(first->type, ItemType::BeginFrame); + EXPECT_EQ(second->type, ItemType::EncodePass); + EXPECT_EQ(second->passIndex, 7u); + EXPECT_EQ(third->type, ItemType::EndFrame); + EXPECT_FALSE(closed); +} + +TEST(RenderWorkerQueue, PushBlocksWhenFull) { + BoundedQueue queue{1}; + ASSERT_TRUE(queue.push(QueueItem{.type = ItemType::BeginFrame})); + + std::atomic_bool pushed = false; + auto future = std::async(std::launch::async, [&] { + const bool result = queue.push(QueueItem{.type = ItemType::EncodePass}); + pushed.store(result, std::memory_order_release); + }); + + std::this_thread::sleep_for(20ms); + EXPECT_FALSE(pushed.load(std::memory_order_acquire)); + + bool closed = false; + ASSERT_TRUE(queue.pop_for(0ms, closed).has_value()); + future.wait(); + EXPECT_TRUE(pushed.load(std::memory_order_acquire)); +} + +TEST_F(RenderWorkerTest, SyncWaitsForPriorWork) { + std::vector order; + aurora::gfx::render_worker::initialize(); + aurora::gfx::render_worker::enqueue_begin_frame(1, [&] { order.push_back(1); }); + aurora::gfx::render_worker::enqueue_encode_pass(1, 0, [&] { order.push_back(2); }); + aurora::gfx::render_worker::synchronize(); + + ASSERT_EQ(order.size(), 2u); + EXPECT_EQ(order[0], 1); + EXPECT_EQ(order[1], 2); + EXPECT_TRUE(aurora::gfx::render_worker::is_idle()); +} + +TEST_F(RenderWorkerTest, ShutdownCompletesQueuedWork) { + std::atomic_int count = 0; + aurora::gfx::render_worker::initialize(); + aurora::gfx::render_worker::enqueue_begin_frame(1, [&] { count.fetch_add(1, std::memory_order_acq_rel); }); + aurora::gfx::render_worker::shutdown(); + + EXPECT_EQ(count.load(std::memory_order_acquire), 1); + EXPECT_TRUE(aurora::gfx::render_worker::is_idle()); +} + +TEST(RenderWorkerFrameSlots, RecyclesReleasedSlots) { + FrameSlotPool slots{2}; + const size_t first = slots.acquire(); + const size_t second = slots.acquire(); + EXPECT_NE(first, second); + EXPECT_EQ(slots.free_count(), 0u); + + std::atomic_bool acquired = false; + auto future = std::async(std::launch::async, [&] { + const size_t recycled = slots.acquire(); + EXPECT_EQ(recycled, first); + acquired.store(true, std::memory_order_release); + }); + + std::this_thread::sleep_for(20ms); + EXPECT_FALSE(acquired.load(std::memory_order_acquire)); + slots.release(first); + future.wait(); + slots.release(second); + EXPECT_EQ(slots.free_count(), 1u); +} +} // namespace