From b979ff69133b536bc5a8b820214ea6e84c578875 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 7 Jul 2026 13:13:53 -0600 Subject: [PATCH] Public aurora::gfx API (custom draws & compute) --- include/aurora/dvd.h | 7 +- include/aurora/gfx.hpp | 148 +++++++ lib/dolphin/gx/GXFrameBuffer.cpp | 4 +- lib/gfx/common.cpp | 612 ++++++++++++++++++++++++++-- lib/gfx/common.hpp | 15 +- lib/gfx/tex_copy_conv.cpp | 161 ++++++++ lib/gfx/tex_copy_conv.hpp | 4 + lib/rmlui/WebGPURenderInterface.cpp | 2 - 8 files changed, 911 insertions(+), 42 deletions(-) create mode 100644 include/aurora/gfx.hpp diff --git a/include/aurora/dvd.h b/include/aurora/dvd.h index e6ae862..a352797 100644 --- a/include/aurora/dvd.h +++ b/include/aurora/dvd.h @@ -101,8 +101,11 @@ void aurora_dvd_overlay_callbacks(const AuroraOverlayCallbacks* callbacks); /** * \brief Specify a set of overlay files to be used by the DVD layer. * - * Calling this function immediately applies the new files and rebuilds the FST. This is not thread safe. - * It is best you only call this once on startup, before the game's code has started. + * Calling this function immediately applies the new files and rebuilds the FST, replacing any + * previously specified set. It may be called again at runtime; FST reads and overlay opens are + * serialized against the rebuild, and previously assigned EntryNums are stable per path. Files + * removed by a re-registration fail to open from then on (or revert to the underlying DVD file); + * already-open handles are unaffected. Only call from one thread at a time. * * This function must be called *after* aurora_dvd_overlay_callbacks. * diff --git a/include/aurora/gfx.hpp b/include/aurora/gfx.hpp new file mode 100644 index 0000000..0a136ab --- /dev/null +++ b/include/aurora/gfx.hpp @@ -0,0 +1,148 @@ +#pragma once + +#include +#include + +#include + +namespace aurora::gfx { + +inline constexpr size_t InlineDrawPayloadSize = 128; + +/// Generational handle: 0 is never valid, and IDs are not reused after unregister_draw_type. +using DrawTypeId = uint64_t; +inline constexpr DrawTypeId InvalidDrawType = 0; + +struct Range { + uint32_t offset = 0; + uint32_t size = 0; + + bool operator==(const Range& rhs) const { return offset == rhs.offset && size == rhs.size; } + bool operator!=(const Range& rhs) const { return !(*this == rhs); } +}; + +struct DrawContext { + wgpu::Device device; + wgpu::Queue queue; + wgpu::Buffer vertexBuffer; + wgpu::Buffer indexBuffer; + wgpu::Buffer uniformBuffer; + wgpu::Buffer storageBuffer; + wgpu::TextureFormat colorFormat; + wgpu::TextureFormat depthFormat; + uint32_t sampleCount = 1; + uint32_t targetWidth = 0; + uint32_t targetHeight = 0; +}; + +/// Invoked on the render worker thread while replaying the pass the draw was +/// recorded into. The encoder's pipeline/bind-group/viewport/scissor state is +/// restored after the callback returns. Handles in the context are borrowed and +/// valid only for the duration of the call. sampleCount/target dimensions are +/// those of the containing pass (offscreen passes are always single-sample). +using DrawCallback = void (*)(const DrawContext& ctx, const wgpu::RenderPassEncoder& pass, + const void* payload, size_t payloadSize, void* userdata); + +struct DrawTypeDescriptor { + const char* label = nullptr; + DrawCallback draw = nullptr; + void* userdata = nullptr; +}; + +wgpu::Device device() noexcept; +wgpu::Queue queue() noexcept; +wgpu::TextureFormat color_format() noexcept; +wgpu::TextureFormat depth_format() noexcept; +uint32_t sample_count() noexcept; +bool uses_reversed_z() noexcept; + +DrawTypeId register_draw_type(const DrawTypeDescriptor& desc); +void unregister_draw_type(DrawTypeId type) noexcept; +/// Records an inline custom draw into the currently open render pass at the +/// current position in the command stream. Payload (<= InlineDrawPayloadSize) +/// is copied. Returns false (with a warning) outside an active render pass. +bool push_custom_draw(DrawTypeId type, const void* payload, size_t payloadSize); + +/// Generational handle: 0 is never valid, and IDs are not reused after unregister_encoder_task_type. +using EncoderTaskId = uint64_t; +inline constexpr EncoderTaskId InvalidEncoderTask = 0; + +struct EncoderTaskContext { + wgpu::Device device; + wgpu::Queue queue; + wgpu::Buffer vertexBuffer; + wgpu::Buffer indexBuffer; + wgpu::Buffer uniformBuffer; + wgpu::Buffer storageBuffer; +}; + +/// Invoked on the render worker thread with the frame's command encoder, +/// positioned between two render passes. The callback may begin/end compute +/// passes and record copies on the encoder; it must leave no pass open when it +/// returns and must not Finish the encoder. Handles in the context are borrowed +/// and valid only for the duration of the call. Data appended to the streaming +/// buffers before the task was pushed is GPU-visible inside it. +using EncoderTaskCallback = void (*)(const EncoderTaskContext& ctx, const wgpu::CommandEncoder& cmd, + const void* payload, size_t payloadSize, void* userdata); + +struct EncoderTaskDescriptor { + const char* label = nullptr; + EncoderTaskCallback callback = nullptr; + void* userdata = nullptr; +}; + +EncoderTaskId register_encoder_task_type(const EncoderTaskDescriptor& desc); +void unregister_encoder_task_type(EncoderTaskId type) noexcept; +/// Seals the current EFB pass, records an encoder task to execute on the frame +/// encoder at this point, and resumes rendering on a pass that loads the +/// existing contents. Payload semantics match push_custom_draw. Returns false +/// (with a warning) outside an active render pass or while an offscreen pass +/// is open. +bool push_encoder_task(EncoderTaskId type, const void* payload, size_t payloadSize); + +/// Append transient data to the shared per-frame streaming buffers. Returned +/// ranges are valid for the current frame only. Returns an empty Range (with a +/// warning) outside an active recording frame. +Range push_verts(const uint8_t* data, size_t length, size_t alignment); +Range push_indices(const uint8_t* data, size_t length, size_t alignment); +Range push_uniform(const uint8_t* data, size_t length); +Range push_storage(const uint8_t* data, size_t length); + +struct ResolveDesc { + bool color = true; + bool depth = false; +}; + +struct ResolvedTargets { + wgpu::TextureView color; // single-sample snapshot; null if not requested + wgpu::TextureView depth; // single-sample R32Float depth snapshot; null if not requested + wgpu::TextureFormat colorFormat = wgpu::TextureFormat::Undefined; + uint32_t width = 0; + uint32_t height = 0; +}; + +/// Snapshots the current pass targets into pooled textures (valid for the +/// current frame), then: on the EFB, continues rendering on a fresh EFB pass +/// (GXCopyTex semantics); in an offscreen pass created by create_pass, ends it +/// and restores the suspended EFB pass (GXRestoreFrameBuffer semantics). +/// Requesting neither color nor depth is a plain pass break (or offscreen +/// close, discarding its output). Depth is left null when unsupported by the +/// device. Returns false (with a warning) outside an active render pass. +bool resolve_pass(const ResolveDesc& desc, ResolvedTargets& out); + +/// Opens an offscreen render pass (GXCreateFrameBuffer semantics): cleared +/// single-sample color+depth at (width, height) with full-target +/// viewport/scissor. Subsequent draws target it until resolve_pass restores the +/// EFB. Nesting is unsupported: returns false (with a warning) outside an +/// active render pass or while any offscreen pass is already open. +bool create_pass(uint32_t width, uint32_t height); + +/// True while an offscreen pass (create_pass or GXCreateFrameBuffer) is open. +bool is_offscreen() noexcept; + +/// Blocks until the render worker has drained its queue. After this returns, +/// no draw callback is executing or queued to execute; used before unloading +/// code that registered draw types. Callable from the game thread only. +void synchronize(); + +} // namespace aurora::gfx diff --git a/lib/dolphin/gx/GXFrameBuffer.cpp b/lib/dolphin/gx/GXFrameBuffer.cpp index ad36cc4..536ebf1 100644 --- a/lib/dolphin/gx/GXFrameBuffer.cpp +++ b/lib/dolphin/gx/GXFrameBuffer.cpp @@ -78,8 +78,8 @@ void copy_tex(const void* dest, GXBool clear) noexcept { const auto clearColor = clear && g_gxState.colorUpdate; const auto clearAlpha = clear && g_gxState.alphaUpdate; const auto clearDepth = clear && g_gxState.depthUpdate; - gfx::resolve_pass(handle.handle, rect, clearColor, clearAlpha, clearDepth, g_gxState.clearColor, clear_depth_value(), - texCopyFmt); + gfx::resolve_pass_into(handle.handle, rect, clearColor, clearAlpha, clearDepth, g_gxState.clearColor, + clear_depth_value(), texCopyFmt); ++handle.revision; g_gxState.copyTextures[dest] = handle; } diff --git a/lib/gfx/common.cpp b/lib/gfx/common.cpp index 73ed55d..7862182 100644 --- a/lib/gfx/common.cpp +++ b/lib/gfx/common.cpp @@ -16,7 +16,9 @@ #include "texture_replacement.hpp" #include "texture.hpp" #include "../window.hpp" +#include "../gx/fifo.hpp" +#include #include #include #include @@ -66,6 +68,12 @@ struct StagingHighWater { size_t textureUploadCount = 0; }; +struct CustomDrawCommand { + DrawTypeId type; + uint32_t payloadSize; + std::array payload; +}; + struct ShaderDrawCommand { ShaderType type; union { @@ -80,6 +88,7 @@ enum class CommandType { SetViewport, SetScissor, Draw, + CustomDraw, DebugMarker, }; struct Command { @@ -91,6 +100,7 @@ struct Command { Viewport setViewport; ClipRect setScissor; ShaderDrawCommand draw; + CustomDrawCommand customDraw; size_t debugMarkerIndex; } data; }; @@ -122,14 +132,65 @@ struct CachedBindGroup { uint32_t lastUsedFrame = 0; }; +struct RuntimeDrawType { + std::string label; + DrawCallback draw = nullptr; + void* userdata = nullptr; + uint32_t generation = 1; +}; + +struct RuntimeEncoderTaskType { + std::string label; + EncoderTaskCallback callback = nullptr; + void* userdata = nullptr; + uint32_t generation = 1; +}; + +constexpr uint32_t draw_type_index(DrawTypeId id) { return static_cast(id & 0xFFFFFFFFu); } +constexpr uint32_t draw_type_generation(DrawTypeId id) { return static_cast(id >> 32); } +constexpr DrawTypeId make_draw_type_id(uint32_t index, uint32_t generation) { + return static_cast(generation) << 32 | index; +} + constexpr uint32_t BindGroupCacheRetainFrames = 32; constexpr uint32_t BindGroupCacheSweepPeriod = 16; } // namespace static absl::flat_hash_map g_cachedBindGroups; static absl::flat_hash_map g_cachedSamplers; +static std::vector g_runtimeDrawTypes; +static std::vector g_freeDrawTypeSlots; +static std::vector g_runtimeEncoderTaskTypes; +static std::vector g_freeEncoderTaskTypeSlots; static std::mutex g_bindGroupCacheMutex; static std::mutex g_samplerCacheMutex; +static std::mutex g_runtimeDrawTypeMutex; + +// Requires g_runtimeDrawTypeMutex held. +static const RuntimeDrawType* find_runtime_draw_type(DrawTypeId id) { + const auto idx = draw_type_index(id); + if (id == InvalidDrawType || idx >= g_runtimeDrawTypes.size()) { + return nullptr; + } + const auto& slot = g_runtimeDrawTypes[idx]; + if (slot.generation != draw_type_generation(id) || slot.draw == nullptr) { + return nullptr; + } + return &slot; +} + +// Requires g_runtimeDrawTypeMutex held. +static const RuntimeEncoderTaskType* find_runtime_encoder_task_type(EncoderTaskId id) { + const auto idx = draw_type_index(id); + if (id == InvalidEncoderTask || idx >= g_runtimeEncoderTaskTypes.size()) { + return nullptr; + } + const auto& slot = g_runtimeEncoderTaskTypes[idx]; + if (slot.generation != draw_type_generation(id) || slot.callback == nullptr) { + return nullptr; + } + return &slot; +} wgpu::Buffer g_vertexBuffer; wgpu::Buffer g_uniformBuffer; @@ -171,6 +232,9 @@ struct RenderPass { GXTexFmt resolveFormat = GX_TF_RGBA8; ClipRect resolveRect; Range resolveUniformRange; + // Full-target snapshots for the public resolve_pass API + wgpu::Texture snapshotColorDst; + wgpu::TextureView snapshotDepthDst; Vec4 clearColorValue{0.f, 0.f, 0.f, 0.f}; float clearDepthValue = gx::UseReversedZ ? 0.f : 1.f; wgpu::LoadOp colorLoadOp = wgpu::LoadOp::Undefined; @@ -185,11 +249,16 @@ struct RenderPass { bool clearDepth = true; bool hasDepth = true; bool hasStencil = false; - bool offscreen = false; - bool observable = false; + bool hasDraws = false; + bool discardable = false; bool captureDepthSnapshot = false; bool sealed = false; std::vector paletteConvs; + + // Something copies this pass's output after it ends: a GX resolve or resolve_pass snapshots. + bool has_consumer() const { return resolveTarget || snapshotColorDst || snapshotDepthDst; } + // The pass mutates its attachments: draws or pending clears. + bool has_content() const { return hasDraws || clearColor || clearDepth; } }; struct TextureCopy { @@ -198,9 +267,16 @@ struct TextureCopy { wgpu::Extent3D size; }; +struct EncoderTask { + EncoderTaskId type = InvalidEncoderTask; + std::array payload{}; + uint32_t payloadSize = 0; +}; + enum class FrameOpType : uint8_t { RenderPass, TextureCopy, + EncoderTask, }; struct FrameOp { @@ -208,6 +284,7 @@ struct FrameOp { uint32_t index = 0; RenderPass* renderPass = nullptr; TextureCopy* textureCopy = nullptr; + EncoderTask* encoderTask = nullptr; StagingHighWater highWater; std::vector textureUploads; }; @@ -216,6 +293,7 @@ using RenderPassList = std::deque; struct FramePacket { RenderPassList renderPasses; std::deque textureCopies; + std::deque encoderTasks; std::deque ops; std::deque textureUploads; ByteBuffer verts; @@ -344,8 +422,8 @@ static void pace_frame_start() { TracyPlot("aurora: frameStartPaceWaitMs", initialWaitMs); while (nowNs < targetStartNs) { const int64_t remainingNs = targetStartNs - nowNs; - const auto sleepDuration = remainingNs > 1'000'000 ? std::chrono::milliseconds{1} - : std::chrono::nanoseconds{remainingNs}; + const auto sleepDuration = + remainingNs > 1'000'000 ? std::chrono::milliseconds{1} : std::chrono::nanoseconds{remainingNs}; wait_for_gpu_progress(sleepDuration); nowNs = timestamp_ns(PresentClock::now()); } @@ -409,6 +487,70 @@ struct OffscreenCacheEntry { }; static absl::flat_hash_map g_offscreenCache; +// Pooled destinations for the public resolve_pass API. Entries are recycled +// per frame slot: a slot is only re-acquired after the render worker has +// submitted its previous frame, and queue serialization orders the new frame's +// copies after the old frame's reads. +struct PassSnapshotEntry { + webgpu::TextureWithSampler color; + webgpu::TextureWithSampler depth; // R32Float raw depth +}; +struct PassSnapshotPool { + std::vector entries; + size_t used = 0; +}; +static std::array g_passSnapshotPools; + +static PassSnapshotEntry& acquire_pass_snapshot(uint32_t width, uint32_t height, bool wantColor, bool wantDepth) { + auto& pool = g_passSnapshotPools[g_recordingFrameSlot]; + if (pool.used == pool.entries.size()) { + pool.entries.emplace_back(); + } + auto& entry = pool.entries[pool.used++]; + const wgpu::Extent3D size{width, height, 1}; + if (wantColor && (!entry.color.texture || entry.color.size.width != width || entry.color.size.height != height || + entry.color.format != webgpu::g_graphicsConfig.surfaceConfiguration.format)) { + const auto format = webgpu::g_graphicsConfig.surfaceConfiguration.format; + const wgpu::TextureDescriptor desc{ + .label = "Pass Snapshot Color", + .usage = wgpu::TextureUsage::CopyDst | wgpu::TextureUsage::TextureBinding, + .dimension = wgpu::TextureDimension::e2D, + .size = size, + .format = format, + .mipLevelCount = 1, + .sampleCount = 1, + }; + auto texture = g_device.CreateTexture(&desc); + auto view = texture.CreateView(); + entry.color = webgpu::TextureWithSampler{ + .texture = std::move(texture), + .view = std::move(view), + .size = size, + .format = format, + }; + } + if (wantDepth && (!entry.depth.texture || entry.depth.size.width != width || entry.depth.size.height != height)) { + const wgpu::TextureDescriptor desc{ + .label = "Pass Snapshot Depth", + .usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding, + .dimension = wgpu::TextureDimension::e2D, + .size = size, + .format = wgpu::TextureFormat::R32Float, + .mipLevelCount = 1, + .sampleCount = 1, + }; + auto texture = g_device.CreateTexture(&desc); + auto view = texture.CreateView(); + entry.depth = webgpu::TextureWithSampler{ + .texture = std::move(texture), + .view = std::move(view), + .size = size, + .format = wgpu::TextureFormat::R32Float, + }; + } + return entry; +} + static FramePacket& current_frame_packet() { CHECK(g_recordingFrame != nullptr, "No active frame packet"); return *g_recordingFrame; @@ -436,6 +578,8 @@ static FrameOp capture_frame_op(FramePacket& frame, FrameOpType type, uint32_t i .textureCopy = type == FrameOpType::TextureCopy && index < frame.textureCopies.size() ? &frame.textureCopies[index] : nullptr, + .encoderTask = + type == FrameOpType::EncoderTask && index < frame.encoderTasks.size() ? &frame.encoderTasks[index] : nullptr, .highWater = current_high_water(frame), }; op.textureUploads.reserve(op.highWater.textureUploadCount); @@ -459,6 +603,10 @@ static void seal_pass(FramePacket& frame, uint32_t passIndex) { 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 render_custom_draw(const CustomDrawCommand& draw, const wgpu::RenderPassEncoder& pass, + const RenderPass& passInfo); +static void execute_encoder_task(wgpu::CommandEncoder& cmd, const EncoderTask& task); +static void resume_efb_pass_loading(const RenderPass& prevPass); static void expire_cached_bind_groups(); static void push_command(CommandType type, const Command::Data& data); @@ -468,7 +616,7 @@ static void enqueue_op(FramePacket& frame, size_t frameSlot, uint32_t opIndex) { } 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) { + if (op.renderPass == nullptr && op.textureCopy == nullptr && op.encoderTask == nullptr) { return; } auto& packet = g_framePackets[frameSlot]; @@ -582,7 +730,6 @@ void begin_color_pass(const ColorPassDescriptor& desc) { .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)); @@ -604,7 +751,7 @@ void end_color_pass() { g_currentRenderPass = UINT32_MAX; } -static inline void push_command(CommandType type, const Command::Data& data) { +static void push_command(CommandType type, const Command::Data& data) { if (g_currentRenderPass == UINT32_MAX) UNLIKELY { Log.warn("Dropping command {}", magic_enum::enum_name(type)); @@ -613,6 +760,9 @@ static inline void push_command(CommandType type, const Command::Data& data) { 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); + if (type == CommandType::Draw || type == CommandType::CustomDraw) { + renderPass.hasDraws = true; + } renderPass.commands.push_back({ .type = type, #ifdef AURORA_GFX_DEBUG_GROUPS @@ -672,8 +822,8 @@ PipelineRef pipeline_ref(const clear::PipelineConfig& config) { return find_pipeline(ShaderType::Clear, config, [=] { return create_pipeline(config); }); } -void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool clearAlpha, bool clearDepth, - Vec4 clearColorValue, float clearDepthValue, GXTexFmt resolveFormat) { +void resolve_pass_into(TextureHandle texture, ClipRect rect, bool clearColor, bool clearAlpha, bool clearDepth, + Vec4 clearColorValue, float clearDepthValue, GXTexFmt resolveFormat) { // Resolve current render pass auto& prevPass = current_render_passes()[g_currentRenderPass]; prevPass.resolveTarget = std::move(texture); @@ -755,6 +905,93 @@ void clear_caches() noexcept { g_cachedBindGroups.clear(); } +wgpu::Device device() noexcept { return g_device; } + +wgpu::Queue queue() noexcept { return g_queue; } + +wgpu::TextureFormat color_format() noexcept { return webgpu::g_graphicsConfig.surfaceConfiguration.format; } + +wgpu::TextureFormat depth_format() noexcept { return webgpu::g_graphicsConfig.depthFormat; } + +uint32_t sample_count() noexcept { return webgpu::g_graphicsConfig.msaaSamples; } + +bool uses_reversed_z() noexcept { return gx::UseReversedZ; } + +DrawTypeId register_draw_type(const DrawTypeDescriptor& desc) { + if (desc.draw == nullptr) { + Log.warn("register_draw_type: draw callback is null"); + return InvalidDrawType; + } + + std::lock_guard lock{g_runtimeDrawTypeMutex}; + uint32_t idx; + if (!g_freeDrawTypeSlots.empty()) { + idx = g_freeDrawTypeSlots.back(); + g_freeDrawTypeSlots.pop_back(); + } else { + idx = static_cast(g_runtimeDrawTypes.size()); + g_runtimeDrawTypes.emplace_back(); + } + auto& slot = g_runtimeDrawTypes[idx]; + slot.label = desc.label != nullptr ? desc.label : ""; + slot.draw = desc.draw; + slot.userdata = desc.userdata; + return make_draw_type_id(idx, slot.generation); +} + +void unregister_draw_type(DrawTypeId type) noexcept { + std::lock_guard lock{g_runtimeDrawTypeMutex}; + if (find_runtime_draw_type(type) == nullptr) { + return; + } + const auto idx = draw_type_index(type); + auto& slot = g_runtimeDrawTypes[idx]; + slot.label.clear(); + slot.draw = nullptr; + slot.userdata = nullptr; + ++slot.generation; + g_freeDrawTypeSlots.push_back(idx); +} + +bool push_custom_draw(DrawTypeId type, const void* payload, size_t payloadSize) { + if (type == InvalidDrawType) { + Log.warn("push_custom_draw: invalid draw type"); + return false; + } + if (payloadSize > InlineDrawPayloadSize) { + Log.warn("push_custom_draw: payload size {} exceeds inline payload size {}", payloadSize, InlineDrawPayloadSize); + return false; + } + if (payloadSize > 0 && payload == nullptr) { + Log.warn("push_custom_draw: non-zero payload size with null payload"); + return false; + } + { + std::lock_guard lock{g_runtimeDrawTypeMutex}; + if (find_runtime_draw_type(type) == nullptr) { + Log.warn("push_custom_draw: unregistered draw type {:#x}", type); + return false; + } + } + + gx::fifo::drain(); + + if (g_recordingFrame == nullptr || g_currentRenderPass == UINT32_MAX) { + Log.warn("push_custom_draw: called outside an active render pass"); + return false; + } + + CustomDrawCommand draw{}; + draw.type = type; + draw.payloadSize = static_cast(payloadSize); + if (payloadSize > 0) { + std::memcpy(draw.payload.data(), payload, payloadSize); + } + push_command(CommandType::CustomDraw, Command::Data{.customDraw = draw}); + ++g_drawCallCount; + return true; +} + static OffscreenCacheEntry get_offscreen_textures(uint32_t width, uint32_t height) { OffscreenCacheKey key{width, height}; if (const auto it = g_offscreenCache.find(key); it != g_offscreenCache.end()) { @@ -783,7 +1020,7 @@ static OffscreenCacheEntry get_offscreen_textures(uint32_t width, uint32_t heigh const auto depthFormat = webgpu::g_graphicsConfig.depthFormat; const wgpu::TextureDescriptor depthDesc{ .label = "Offscreen Depth", - .usage = wgpu::TextureUsage::RenderAttachment, + .usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding, .dimension = wgpu::TextureDimension::e2D, .size = size, .format = depthFormat, @@ -846,7 +1083,6 @@ void begin_offscreen(uint32_t width, uint32_t height) { .clearDepth = true, .hasDepth = true, .hasStencil = false, - .offscreen = true, }; current_render_passes().emplace_back(std::move(newPass)); ++g_currentRenderPass; @@ -863,6 +1099,10 @@ void end_offscreen() { ZoneScoped; CHECK(g_inOffscreen, "end_offscreen called without begin_offscreen"); + // Mark current render pass as discardable if there is no consumer + auto& offscreenPass = current_render_passes()[g_currentRenderPass]; + offscreenPass.discardable = !offscreenPass.has_consumer(); + enqueue_pass(current_frame_packet(), g_recordingFrameSlot, g_currentRenderPass); g_inOffscreen = false; @@ -888,6 +1128,189 @@ void end_offscreen() { push_command(CommandType::SetScissor, Command::Data{.setScissor = g_cachedScissor}); } +bool create_pass(uint32_t width, uint32_t height) { + if (width == 0 || height == 0) { + Log.warn("create_pass: invalid size {}x{}", width, height); + return false; + } + + gx::fifo::drain(); + + if (g_recordingFrame == nullptr || g_currentRenderPass == UINT32_MAX) { + Log.warn("create_pass: called outside an active render pass"); + return false; + } + if (g_inOffscreen) { + Log.warn("create_pass: an offscreen pass is already active (nesting is unsupported)"); + return false; + } + + begin_offscreen(width, height); + return true; +} + +bool resolve_pass(const ResolveDesc& desc, ResolvedTargets& out) { + out = {}; + gx::fifo::drain(); + + if (g_recordingFrame == nullptr || g_currentRenderPass == UINT32_MAX) { + Log.warn("resolve_pass: called outside an active render pass"); + return false; + } + + bool wantDepth = desc.depth; + if (wantDepth && !tex_copy_conv::snapshot_depth_supported()) { + Log.warn("resolve_pass: depth snapshots are unsupported on this device"); + wantDepth = false; + } + + auto& prevPass = current_render_passes()[g_currentRenderPass]; + const uint32_t width = prevPass.targetSize.width; + const uint32_t height = prevPass.targetSize.height; + // Requesting no snapshots is a plain pass break (or offscreen close, discarding its output). + if (desc.color || wantDepth) { + auto& entry = acquire_pass_snapshot(width, height, desc.color, wantDepth); + if (desc.color) { + prevPass.snapshotColorDst = entry.color.texture; + out.color = entry.color.view; + out.colorFormat = entry.color.format; + } + if (wantDepth) { + prevPass.snapshotDepthDst = entry.depth.view; + out.depth = entry.depth.view; + } + } + out.width = width; + out.height = height; + + if (g_inOffscreen) { + // Seal the offscreen pass and resume the EFB. + end_offscreen(); + return true; + } + + // Seal the current EFB pass and continue on a new one that loads the existing contents. + // EFB writes persist into the loading continuation, so content keeps the pass alive even + // without a consumer. + prevPass.discardable = !prevPass.has_consumer() && !prevPass.has_content(); + enqueue_pass(current_frame_packet(), g_recordingFrameSlot, g_currentRenderPass); + resume_efb_pass_loading(prevPass); + return true; +} + +static void resume_efb_pass_loading(const RenderPass& prevPass) { + RenderPass newPass{ + .label = pass_label("EFB"), + .colorView = prevPass.colorView, + .resolveView = prevPass.resolveView, + .depthStencilView = prevPass.depthStencilView, + .copySourceTexture = prevPass.copySourceTexture, + .copySourceView = prevPass.copySourceView, + .copySourceDepthView = prevPass.copySourceDepthView, + .targetSize = prevPass.targetSize, + .msaaSamples = prevPass.msaaSamples, + .clearColor = false, + .clearDepth = false, + .hasDepth = prevPass.hasDepth, + .hasStencil = prevPass.hasStencil, + }; + newPass.commands.reserve(2048); + current_render_passes().emplace_back(std::move(newPass)); + ++g_currentRenderPass; + push_command(CommandType::SetViewport, Command::Data{.setViewport = g_cachedViewport}); + push_command(CommandType::SetScissor, Command::Data{.setScissor = g_cachedScissor}); +} + +EncoderTaskId register_encoder_task_type(const EncoderTaskDescriptor& desc) { + if (desc.callback == nullptr) { + Log.warn("register_encoder_task_type: callback is null"); + return InvalidEncoderTask; + } + + std::lock_guard lock{g_runtimeDrawTypeMutex}; + uint32_t idx; + if (!g_freeEncoderTaskTypeSlots.empty()) { + idx = g_freeEncoderTaskTypeSlots.back(); + g_freeEncoderTaskTypeSlots.pop_back(); + } else { + idx = static_cast(g_runtimeEncoderTaskTypes.size()); + g_runtimeEncoderTaskTypes.emplace_back(); + } + auto& slot = g_runtimeEncoderTaskTypes[idx]; + slot.label = desc.label != nullptr ? desc.label : ""; + slot.callback = desc.callback; + slot.userdata = desc.userdata; + return make_draw_type_id(idx, slot.generation); +} + +void unregister_encoder_task_type(EncoderTaskId type) noexcept { + std::lock_guard lock{g_runtimeDrawTypeMutex}; + if (find_runtime_encoder_task_type(type) == nullptr) { + return; + } + const auto idx = draw_type_index(type); + auto& slot = g_runtimeEncoderTaskTypes[idx]; + slot.label.clear(); + slot.callback = nullptr; + slot.userdata = nullptr; + ++slot.generation; + g_freeEncoderTaskTypeSlots.push_back(idx); +} + +bool push_encoder_task(EncoderTaskId type, const void* payload, size_t payloadSize) { + if (type == InvalidEncoderTask) { + Log.warn("push_encoder_task: invalid encoder task type"); + return false; + } + if (payloadSize > InlineDrawPayloadSize) { + Log.warn("push_encoder_task: payload size {} exceeds inline payload size {}", payloadSize, InlineDrawPayloadSize); + return false; + } + if (payloadSize > 0 && payload == nullptr) { + Log.warn("push_encoder_task: non-zero payload size with null payload"); + return false; + } + { + std::lock_guard lock{g_runtimeDrawTypeMutex}; + if (find_runtime_encoder_task_type(type) == nullptr) { + Log.warn("push_encoder_task: unregistered encoder task type {:#x}", type); + return false; + } + } + + gx::fifo::drain(); + + if (g_recordingFrame == nullptr || g_currentRenderPass == UINT32_MAX) { + Log.warn("push_encoder_task: called outside an active render pass"); + return false; + } + if (g_inOffscreen) { + Log.warn("push_encoder_task: unsupported while an offscreen pass is active"); + return false; + } + + // Seal the current EFB pass, record the task between it and a continuation + // pass that loads the existing contents. EFB writes persist into the continuation, so + // content keeps the sealed pass alive even without a consumer. + auto& frame = current_frame_packet(); + auto& prevPass = current_render_passes()[g_currentRenderPass]; + prevPass.discardable = !prevPass.has_consumer() && !prevPass.has_content(); + enqueue_pass(frame, g_recordingFrameSlot, g_currentRenderPass); + + const auto taskIndex = static_cast(frame.encoderTasks.size()); + auto& task = frame.encoderTasks.emplace_back(EncoderTask{.type = type}); + task.payloadSize = static_cast(payloadSize); + if (payloadSize > 0) { + std::memcpy(task.payload.data(), payload, payloadSize); + } + const auto opIndex = static_cast(frame.ops.size()); + frame.ops.emplace_back(capture_frame_op(frame, FrameOpType::EncoderTask, taskIndex)); + enqueue_op(frame, g_recordingFrameSlot, opIndex); + + resume_efb_pass_loading(prevPass); + return true; +} + template <> void push_draw_command(gx::DrawData data) { push_draw_command(ShaderDrawCommand{.type = ShaderType::GX, .gx = data}); @@ -1087,6 +1510,14 @@ void shutdown() { std::lock_guard lock{g_samplerCacheMutex}; g_cachedSamplers.clear(); } + { + std::lock_guard lock{g_runtimeDrawTypeMutex}; + g_runtimeDrawTypes.clear(); + g_freeDrawTypeSlots.clear(); + } + for (auto& pool : g_passSnapshotPools) { + pool = {}; + } g_vertexBuffer = {}; g_uniformBuffer = {}; g_indexBuffer = {}; @@ -1173,6 +1604,7 @@ bool begin_frame() { frame.stagingBuffer = *stagingSlot; g_recordingFrame = &frame; g_recordingFrameSlot = frameSlot; + g_passSnapshotPools[frameSlot].used = 0; size_t bufferOffset = 0; const auto& stagingBuf = g_stagingBuffers[*stagingSlot]; @@ -1227,7 +1659,6 @@ void finish() { auto& frame = current_frame_packet(); frame.uniforms.append_zeroes(gx::MaxUniformSize); auto& pass = frame.renderPasses[g_currentRenderPass]; - pass.observable = true; pass.captureDepthSnapshot = true; enqueue_pass(frame, g_recordingFrameSlot, g_currentRenderPass); g_currentRenderPass = UINT32_MAX; @@ -1396,6 +1827,11 @@ static void encode_op(wgpu::CommandEncoder& cmd, FramePacket& frame, const Frame cmd.CopyTextureToTexture(&op.textureCopy->src, &op.textureCopy->dst, &op.textureCopy->size); } break; + case FrameOpType::EncoderTask: + if (op.encoderTask != nullptr) { + execute_encoder_task(cmd, *op.encoderTask); + } + break; } } @@ -1408,8 +1844,9 @@ static void render(wgpu::CommandEncoder& cmd, FramePacket& frame, RenderPass& pa 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. + if (passInfo.discardable) { + // This pass has no effect and can be safely discarded (e.g. an empty EFB segment between two back-to-back pass + // breaks, or an unresolved offscreen pass). return; } @@ -1506,20 +1943,40 @@ static void render(wgpu::CommandEncoder& cmd, FramePacket& frame, RenderPass& pa cmd.CopyTextureToTexture(&src, &dst, &size); } } + + if (passInfo.snapshotColorDst) { + const webgpu::gpu_prof::Zone zone{cmd, "Pass snapshot"}; + const wgpu::TexelCopyTextureInfo src{ + .texture = passInfo.copySourceTexture, + }; + const wgpu::TexelCopyTextureInfo dst{ + .texture = passInfo.snapshotColorDst, + }; + const wgpu::Extent3D size{ + .width = passInfo.targetSize.width, + .height = passInfo.targetSize.height, + .depthOrArrayLayers = 1, + }; + cmd.CopyTextureToTexture(&src, &dst, &size); + } + if (passInfo.snapshotDepthDst) { + tex_copy_conv::snapshot_depth(cmd, passInfo.copySourceDepthView, passInfo.msaaSamples, passInfo.snapshotDepthDst); + } } void after_submit() noexcept { depth_peek::after_submit(); } void gpu_synchronize() { render_worker::synchronize(); } +void synchronize() { render_worker::synchronize(); } + void after_present() noexcept { const auto now = PresentClock::now(); const int64_t nowNs = timestamp_ns(now); const int64_t previousPresentNs = g_lastPresentNs.exchange(nowNs, std::memory_order_acq_rel); if (previousPresentNs != 0) { update_ema(g_presentPeriodNs, nowNs - previousPresentNs); - const double presentPeriodMs = - static_cast(g_presentPeriodNs.load(std::memory_order_acquire)) / 1'000'000.0; + const double presentPeriodMs = static_cast(g_presentPeriodNs.load(std::memory_order_acquire)) / 1'000'000.0; TracyPlot("aurora: presentPeriodMs", presentPeriodMs); } std::lock_guard lock{g_presentStatsMutex}; @@ -1542,12 +1999,86 @@ float calculate_fps() noexcept { return static_cast(g_presentTimes.size() - 1) / elapsed; } +static void apply_viewport(const wgpu::RenderPassEncoder& pass, const Viewport& vp) { + const float minDepth = gx::UseReversedZ ? 1.f - vp.zfar : vp.znear; + const float maxDepth = gx::UseReversedZ ? 1.f - vp.znear : vp.zfar; + pass.SetViewport(vp.left, vp.top, vp.width, vp.height, minDepth, maxDepth); +} + +static void apply_scissor(const wgpu::RenderPassEncoder& pass, const ClipRect& sc, const wgpu::Extent3D& size) { + 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); + const auto h = std::clamp(static_cast(sc.height), 0u, size.height - y); + pass.SetScissorRect(x, y, w, h); +} + +static DrawContext make_draw_context(const RenderPass& passInfo) { + return { + .device = g_device, + .queue = g_queue, + .vertexBuffer = g_vertexBuffer, + .indexBuffer = g_indexBuffer, + .uniformBuffer = g_uniformBuffer, + .storageBuffer = g_storageBuffer, + .colorFormat = webgpu::g_graphicsConfig.surfaceConfiguration.format, + .depthFormat = webgpu::g_graphicsConfig.depthFormat, + .sampleCount = passInfo.msaaSamples, + .targetWidth = passInfo.targetSize.width, + .targetHeight = passInfo.targetSize.height, + }; +} + +static void render_custom_draw(const CustomDrawCommand& draw, const wgpu::RenderPassEncoder& pass, + const RenderPass& passInfo) { + RuntimeDrawType drawType; + { + std::lock_guard lock{g_runtimeDrawTypeMutex}; + const auto* slot = find_runtime_draw_type(draw.type); + if (slot == nullptr) { + // Unregistered between record and replay; the command is a no-op. + return; + } + drawType = *slot; + } + + const auto context = make_draw_context(passInfo); + drawType.draw(context, pass, draw.payload.data(), draw.payloadSize, drawType.userdata); +} + +static void execute_encoder_task(wgpu::CommandEncoder& cmd, const EncoderTask& task) { + RuntimeEncoderTaskType taskType; + { + std::lock_guard lock{g_runtimeDrawTypeMutex}; + const auto* slot = find_runtime_encoder_task_type(task.type); + if (slot == nullptr) { + // Unregistered between record and encode; the task is a no-op. + return; + } + taskType = *slot; + } + + const EncoderTaskContext context{ + .device = g_device, + .queue = g_queue, + .vertexBuffer = g_vertexBuffer, + .indexBuffer = g_indexBuffer, + .uniformBuffer = g_uniformBuffer, + .storageBuffer = g_storageBuffer, + }; + taskType.callback(context, cmd, task.payload.data(), task.payloadSize, taskType.userdata); +} + 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; #endif + Viewport currentViewport{}; + ClipRect currentScissor{}; + bool hasViewport = false; + bool hasScissor = false; // Bind static bind group for the whole pass pass.SetBindGroup(0, g_staticBindGroup); @@ -1575,18 +2106,15 @@ static void render_pass(const wgpu::RenderPassEncoder& pass, FramePacket& frame, switch (cmd.type) { case CommandType::SetViewport: { const auto& vp = cmd.data.setViewport; - const float minDepth = gx::UseReversedZ ? 1.f - vp.zfar : vp.znear; - const float maxDepth = gx::UseReversedZ ? 1.f - vp.znear : vp.zfar; - pass.SetViewport(vp.left, vp.top, vp.width, vp.height, minDepth, maxDepth); + apply_viewport(pass, vp); + currentViewport = vp; + hasViewport = true; } break; case CommandType::SetScissor: { const auto& sc = cmd.data.setScissor; - 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); - const auto h = std::clamp(static_cast(sc.height), 0u, size.height - y); - pass.SetScissorRect(x, y, w, h); + apply_scissor(pass, sc, passInfo.targetSize); + currentScissor = sc; + hasScissor = true; } break; case CommandType::Draw: { const auto& draw = cmd.data.draw; @@ -1604,6 +2132,18 @@ static void render_pass(const wgpu::RenderPassEncoder& pass, FramePacket& frame, #endif } } break; + case CommandType::CustomDraw: { + render_custom_draw(cmd.data.customDraw, pass, passInfo); + g_currentPipeline = UINTPTR_MAX; + pass.SetBindGroup(0, g_staticBindGroup); + pass.SetBindGroup(2, gx::g_emptyTextureBindGroup); + if (hasViewport) { + apply_viewport(pass, currentViewport); + } + if (hasScissor) { + apply_scissor(pass, currentScissor, passInfo.targetSize); + } + } break; case CommandType::DebugMarker: { #if defined(AURORA_GFX_DEBUG_GROUPS) pass.InsertDebugMarker(wgpu::StringView(g_debugMarkers[cmd.data.debugMarkerIndex])); @@ -1667,23 +2207,45 @@ static Range map(ByteBuffer& target, size_t length, size_t alignment) { return {static_cast(begin), static_cast(length)}; } +// For our public API, warn instead of fatal-ing when called outside an active recording frame. +static bool check_recording(const char* name) { + if (g_recordingFrame == nullptr) + UNLIKELY { + Log.warn("{}: called outside an active frame", name); + return false; + } + return true; +} + Range push_verts(const uint8_t* data, size_t length, size_t alignment) { ZoneScoped; + if (!check_recording("push_verts")) { + return {}; + } return push(current_frame_packet().verts, data, length, alignment); } Range push_indices(const uint8_t* data, size_t length, size_t alignment) { ZoneScoped; + if (!check_recording("push_indices")) { + return {}; + } return push(current_frame_packet().indices, data, length, alignment); } Range push_uniform(const uint8_t* data, size_t length) { ZoneScoped; + if (!check_recording("push_uniform")) { + return {}; + } return push(current_frame_packet().uniforms, data, length, g_cachedLimits.minUniformBufferOffsetAlignment); } Range push_storage(const uint8_t* data, size_t length) { ZoneScoped; + if (!check_recording("push_storage")) { + return {}; + } return push(current_frame_packet().storage, data, length, g_cachedLimits.minStorageBufferOffsetAlignment); } diff --git a/lib/gfx/common.hpp b/lib/gfx/common.hpp index 15557fc..033e963 100644 --- a/lib/gfx/common.hpp +++ b/lib/gfx/common.hpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -173,7 +174,7 @@ private: namespace aurora::gfx { inline constexpr bool UseTextureBuffer = true; inline constexpr uint64_t UniformBufferSize = 25165824; // 24mb -inline constexpr uint64_t VertexBufferSize = 3145728; // 3mb +inline constexpr uint64_t VertexBufferSize = 5242880; // 5mb inline constexpr uint64_t IndexBufferSize = 1048576; // 1mb inline constexpr uint64_t StorageBufferSize = 8388608; // 8mb inline constexpr uint64_t TextureUploadSize = 25165824; // 24mb @@ -194,13 +195,6 @@ using BindGroupRef = HashType; using PipelineRef = HashType; using SamplerRef = HashType; using ShaderRef = HashType; -struct Range { - uint32_t offset = 0; - uint32_t size = 0; - - bool operator==(const Range& rhs) const { return memcmp(this, &rhs, sizeof(*this)) == 0; } - bool operator!=(const Range& rhs) const { return !(*this == rhs); } -}; struct ClipRect { int32_t x; @@ -236,8 +230,8 @@ void after_submit() noexcept; void gpu_synchronize(); void after_present() noexcept; float calculate_fps() noexcept; -void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool clearAlpha, bool clearDepth, - Vec4 clearColorValue, float clearDepthValue, GXTexFmt resolveFormat = GX_TF_RGBA8); +void resolve_pass_into(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; @@ -257,7 +251,6 @@ struct ColorPassDescriptor { 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); diff --git a/lib/gfx/tex_copy_conv.cpp b/lib/gfx/tex_copy_conv.cpp index ca1a87a..c6038f3 100644 --- a/lib/gfx/tex_copy_conv.cpp +++ b/lib/gfx/tex_copy_conv.cpp @@ -244,6 +244,45 @@ static constexpr std::string_view FragZ16 = R"( } )"sv; +// Depth -> R32Float (no scaling) +static constexpr std::string_view DepthSnapshotShader = R"( +@group(0) @binding(0) var src: texture_depth_2d; + +var positions: array = array( + vec2f(-1.0, 1.0), + vec2f(-1.0, -3.0), + vec2f(3.0, 1.0), +); + +@vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f { + return vec4f(positions[vi], 0.0, 1.0); +} + +@fragment fn fs_main(@builtin(position) pos: vec4f) -> @location(0) vec4f { + let depth = textureLoad(src, vec2i(pos.xy), 0); + return vec4f(depth, 0.0, 0.0, 1.0); +} +)"sv; + +static constexpr std::string_view DepthSnapshotShaderMS = R"( +@group(0) @binding(0) var src: texture_depth_multisampled_2d; + +var positions: array = array( + vec2f(-1.0, 1.0), + vec2f(-1.0, -3.0), + vec2f(3.0, 1.0), +); + +@vertex fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f { + return vec4f(positions[vi], 0.0, 1.0); +} + +@fragment fn fs_main(@builtin(position) pos: vec4f) -> @location(0) vec4f { + let depth = textureLoad(src, vec2i(pos.xy), 0); + return vec4f(depth, 0.0, 0.0, 1.0); +} +)"sv; + struct ConvPipeline { GXTexFmt fmt; std::string_view fragShader; @@ -278,6 +317,75 @@ static wgpu::Sampler g_nearestSampler; static wgpu::Sampler g_linearSampler; static absl::flat_hash_map g_pipelines; static wgpu::RenderPipeline g_blitPipeline; +static wgpu::BindGroupLayout g_depthSnapshotBindGroupLayout; +static wgpu::BindGroupLayout g_depthSnapshotBindGroupLayoutMS; +static wgpu::RenderPipeline g_depthSnapshotPipeline; +static wgpu::RenderPipeline g_depthSnapshotPipelineMS; + +static wgpu::BindGroupLayout create_depth_snapshot_layout(bool multisampled, const char* label) { + const std::array entries{ + wgpu::BindGroupLayoutEntry{ + .binding = 0, + .visibility = wgpu::ShaderStage::Fragment, + .texture = + wgpu::TextureBindingLayout{ + .sampleType = wgpu::TextureSampleType::Depth, + .viewDimension = wgpu::TextureViewDimension::e2D, + .multisampled = multisampled, + }, + }, + }; + const wgpu::BindGroupLayoutDescriptor descriptor{ + .label = label, + .entryCount = entries.size(), + .entries = entries.data(), + }; + return g_device.CreateBindGroupLayout(&descriptor); +} + +static wgpu::RenderPipeline create_depth_snapshot_pipeline(const std::string_view shaderSource, + const wgpu::BindGroupLayout& bindGroupLayout, + const char* label) { + const std::string source{shaderSource}; + const wgpu::ShaderSourceWGSL wgslSource{wgpu::ShaderSourceWGSL::Init{ + .code = source.c_str(), + }}; + const wgpu::ShaderModuleDescriptor moduleDescriptor{ + .nextInChain = &wgslSource, + .label = label, + }; + const auto module = g_device.CreateShaderModule(&moduleDescriptor); + + const std::array colorTargets{wgpu::ColorTargetState{ + .format = wgpu::TextureFormat::R32Float, + }}; + const wgpu::FragmentState fragmentState{ + .module = module, + .entryPoint = "fs_main", + .targetCount = colorTargets.size(), + .targets = colorTargets.data(), + }; + const wgpu::PipelineLayoutDescriptor layoutDescriptor{ + .bindGroupLayoutCount = 1, + .bindGroupLayouts = &bindGroupLayout, + }; + const auto pipelineLayout = g_device.CreatePipelineLayout(&layoutDescriptor); + const wgpu::RenderPipelineDescriptor pipelineDescriptor{ + .label = label, + .layout = pipelineLayout, + .vertex = + wgpu::VertexState{ + .module = module, + .entryPoint = "vs_main", + }, + .primitive = + wgpu::PrimitiveState{ + .topology = wgpu::PrimitiveTopology::TriangleList, + }, + .fragment = &fragmentState, + }; + return g_device.CreateRenderPipeline(&pipelineDescriptor); +} static wgpu::RenderPipeline create_pipeline(const ConvPipeline& conv, const std::string_view shaderPreamble, const wgpu::BindGroupLayout& bindGroupLayout) { @@ -408,6 +516,12 @@ void initialize() { Log.fatal("Output format mismatch for {}", conv.fmt); } } + g_depthSnapshotBindGroupLayout = create_depth_snapshot_layout(false, "Depth Snapshot Bind Group Layout"); + g_depthSnapshotBindGroupLayoutMS = create_depth_snapshot_layout(true, "Depth Snapshot MS Bind Group Layout"); + g_depthSnapshotPipeline = + create_depth_snapshot_pipeline(DepthSnapshotShader, g_depthSnapshotBindGroupLayout, "Depth Snapshot"); + g_depthSnapshotPipelineMS = + create_depth_snapshot_pipeline(DepthSnapshotShaderMS, g_depthSnapshotBindGroupLayoutMS, "Depth Snapshot MS"); } static constexpr wgpu::SamplerDescriptor nearestSamplerDescriptor{ @@ -432,6 +546,10 @@ void shutdown() { g_depthBindGroupLayout = {}; g_nearestSampler = {}; g_linearSampler = {}; + g_depthSnapshotBindGroupLayout = {}; + g_depthSnapshotBindGroupLayoutMS = {}; + g_depthSnapshotPipeline = {}; + g_depthSnapshotPipelineMS = {}; } static void execute(const wgpu::CommandEncoder& cmd, const ConvRequest& req, const wgpu::RenderPipeline& pipeline) { @@ -519,4 +637,47 @@ void run(const wgpu::CommandEncoder& cmd, const ConvRequest& req) { void blit(const wgpu::CommandEncoder& cmd, const ConvRequest& req) { execute(cmd, req, g_blitPipeline); } +bool snapshot_depth_supported() noexcept { return static_cast(g_depthSnapshotPipeline); } + +void snapshot_depth(const wgpu::CommandEncoder& cmd, const wgpu::TextureView& srcDepth, uint32_t msaaSamples, + const wgpu::TextureView& dst) { + const bool multisampled = msaaSamples > 1; + const auto& pipeline = multisampled ? g_depthSnapshotPipelineMS : g_depthSnapshotPipeline; + if (!pipeline) { + return; + } + const std::array bindGroupEntries{ + wgpu::BindGroupEntry{ + .binding = 0, + .textureView = srcDepth, + }, + }; + const wgpu::BindGroupDescriptor bindGroupDescriptor{ + .layout = multisampled ? g_depthSnapshotBindGroupLayoutMS : g_depthSnapshotBindGroupLayout, + .entryCount = bindGroupEntries.size(), + .entries = bindGroupEntries.data(), + }; + const auto bindGroup = g_device.CreateBindGroup(&bindGroupDescriptor); + + const std::array colorAttachments{ + wgpu::RenderPassColorAttachment{ + .view = dst, + .loadOp = wgpu::LoadOp::Clear, + .storeOp = wgpu::StoreOp::Store, + .clearValue = {0.0, 0.0, 0.0, 0.0}, + }, + }; + const wgpu::RenderPassDescriptor renderPassDescriptor{ + .label = "Depth Snapshot Pass", + .colorAttachmentCount = colorAttachments.size(), + .colorAttachments = colorAttachments.data(), + .timestampWrites = webgpu::gpu_prof::pass_writes("Depth snapshot"), + }; + const auto pass = cmd.BeginRenderPass(&renderPassDescriptor); + pass.SetPipeline(pipeline); + pass.SetBindGroup(0, bindGroup); + pass.Draw(3); + pass.End(); +} + } // namespace aurora::gfx::tex_copy_conv diff --git a/lib/gfx/tex_copy_conv.hpp b/lib/gfx/tex_copy_conv.hpp index 31aa804..8fb9f7b 100644 --- a/lib/gfx/tex_copy_conv.hpp +++ b/lib/gfx/tex_copy_conv.hpp @@ -26,4 +26,8 @@ void shutdown(); void run(const wgpu::CommandEncoder& cmd, const ConvRequest& req); void blit(const wgpu::CommandEncoder& cmd, const ConvRequest& req); +bool snapshot_depth_supported() noexcept; +void snapshot_depth(const wgpu::CommandEncoder& cmd, const wgpu::TextureView& srcDepth, uint32_t msaaSamples, + const wgpu::TextureView& dst); + } // namespace aurora::gfx::tex_copy_conv diff --git a/lib/rmlui/WebGPURenderInterface.cpp b/lib/rmlui/WebGPURenderInterface.cpp index acac5fe..f61a838 100644 --- a/lib/rmlui/WebGPURenderInterface.cpp +++ b/lib/rmlui/WebGPURenderInterface.cpp @@ -697,7 +697,6 @@ void WebGPURenderInterface::BeginRenderTargetPass(const wgpu::TextureView& view, .stencilLoadOp = clearStencil ? wgpu::LoadOp::Clear : wgpu::LoadOp::Undefined, .stencilStoreOp = clearStencil ? wgpu::StoreOp::Store : wgpu::StoreOp::Undefined, .stencilClearValue = 0, - .observable = true, }); m_passActive = true; ApplyViewport(); @@ -725,7 +724,6 @@ void WebGPURenderInterface::BeginLayerPass(Rml::LayerHandle layer, wgpu::LoadOp .stencilLoadOp = clearStencil ? wgpu::LoadOp::Clear : wgpu::LoadOp::Load, .stencilStoreOp = wgpu::StoreOp::Store, .stencilClearValue = 0, - .observable = true, }); m_passActive = true; ApplyViewport();