#include "common.hpp" #include "clear.hpp" #include "depth_peek.hpp" #include "../internal.hpp" #include "../webgpu/gpu.hpp" #include "../webgpu/gpu_prof.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" #include "texture.hpp" #include "../window.hpp" #include #include #include #include #include #include #include #include #include #include #include "tracy/Tracy.hpp" namespace aurora::gfx { static Module Log("aurora::gfx"); using webgpu::g_device; using webgpu::g_instance; using webgpu::g_queue; #ifdef AURORA_GFX_DEBUG_GROUPS std::vector g_debugGroupStack; std::vector g_debugMarkers; #endif static std::string pass_label(std::string_view kind) { #ifdef AURORA_GFX_DEBUG_GROUPS if (!g_debugGroupStack.empty()) { return fmt::format("{} ({})", kind, g_debugGroupStack.back()); } #endif return std::string{kind}; } 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 { SetViewport, SetScissor, Draw, DebugMarker, }; struct Command { CommandType type; #ifdef AURORA_GFX_DEBUG_GROUPS std::vector debugGroupStack; #endif union Data { Viewport setViewport; ClipRect setScissor; ShaderDrawCommand draw; size_t debugMarkerIndex; } data; }; } // namespace aurora::gfx namespace aurora { // For types that we can't ensure are safe to hash with has_unique_object_representations, // we create specialized methods to handle them. Note that these are highly dependent on // the structure definition, which could easily change with Dawn updates. template <> inline HashType xxh3_hash(const WGPUBindGroupDescriptor& input, HashType seed) { constexpr auto offset = offsetof(WGPUBindGroupDescriptor, layout); // skip nextInChain, label const auto hash = xxh3_hash_s(reinterpret_cast(&input) + offset, sizeof(WGPUBindGroupDescriptor) - offset - sizeof(void*) /* skip entries */, seed); return xxh3_hash_s(input.entries, sizeof(WGPUBindGroupEntry) * input.entryCount, hash); } template <> inline HashType xxh3_hash(const wgpu::SamplerDescriptor& input, HashType seed) { constexpr auto offset = offsetof(wgpu::SamplerDescriptor, addressModeU); // skip nextInChain, label return xxh3_hash_s(reinterpret_cast(&input) + offset, sizeof(wgpu::SamplerDescriptor) - offset - 2 /* skip padding */, seed); } } // namespace aurora namespace aurora::gfx { namespace { struct CachedBindGroup { wgpu::BindGroup bindGroup; uint32_t lastUsedFrame = 0; }; 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::mutex g_bindGroupCacheMutex; static std::mutex g_samplerCacheMutex; wgpu::Buffer g_vertexBuffer; wgpu::Buffer g_uniformBuffer; wgpu::Buffer g_indexBuffer; wgpu::Buffer g_storageBuffer; enum class BufferMapState { Unmapped, Mapping, Mapped, }; 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; wgpu::BindGroupLayout g_staticBindGroupLayout; wgpu::BindGroup g_staticBindGroup; wgpu::BindGroupLayout g_uniformBindGroupLayout; wgpu::BindGroup g_uniformBindGroup; // for imgui debug AuroraStats g_stats{}; uint32_t g_drawCallCount = 0; 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 depthStencilView; wgpu::Texture copySourceTexture; wgpu::TextureView copySourceView; wgpu::TextureView copySourceDepthView; wgpu::Extent3D targetSize; uint32_t msaaSamples = 1; TextureHandle resolveTarget; GXTexFmt resolveFormat = GX_TF_RGBA8; ClipRect resolveRect; 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; }; 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; 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.depthStencilView = webgpu::g_depthBuffer.view; pass.copySourceTexture = webgpu::g_graphicsConfig.msaaSamples > 1 ? webgpu::g_frameBufferResolved.texture : webgpu::g_frameBuffer.texture; pass.copySourceView = webgpu::g_graphicsConfig.msaaSamples > 1 ? webgpu::g_frameBufferResolved.view : webgpu::g_frameBuffer.view; 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 { uint32_t width; uint32_t height; bool operator==(const OffscreenCacheKey& rhs) const { return width == rhs.width && height == rhs.height; } template friend H AbslHashValue(H h, const OffscreenCacheKey& key) { return H::combine(std::move(h), key.width, key.height); } }; struct OffscreenCacheEntry { webgpu::TextureWithSampler color; webgpu::TextureWithSampler depth; }; static absl::flat_hash_map g_offscreenCache; 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) UNLIKELY { Log.warn("Dropping command {}", magic_enum::enum_name(type)); return; } 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, #endif .data = data, }); } template <> gx::DrawData* get_last_draw_command() { if (g_currentRenderPass >= current_render_passes().size()) { return nullptr; } auto& last = current_render_passes()[g_currentRenderPass].commands.back(); if (last.type != CommandType::Draw || last.data.draw.type != ShaderType::GX) { return nullptr; } return &last.data.draw.gx; } static void push_draw_command(ShaderDrawCommand data) { push_command(CommandType::Draw, Command::Data{.draw = data}); ++g_drawCallCount; } Vec2 get_render_target_size() noexcept { 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}; } void set_viewport(const Viewport& cmd) noexcept { if (cmd != g_cachedViewport) { push_command(CommandType::SetViewport, Command::Data{.setViewport = cmd}); g_cachedViewport = cmd; } } void set_scissor(const ClipRect& cmd) noexcept { if (cmd != g_cachedScissor) { push_command(CommandType::SetScissor, Command::Data{.setScissor = cmd}); g_cachedScissor = cmd; } } template <> void push_draw_command(clear::DrawData data) { push_draw_command(ShaderDrawCommand{.type = ShaderType::Clear, .clear = data}); } template <> 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) { // Resolve current render pass auto& prevPass = current_render_passes()[g_currentRenderPass]; prevPass.resolveTarget = std::move(texture); prevPass.resolveRect = rect; prevPass.resolveFormat = resolveFormat; // Push UV transform uniform for tex_copy_conv (crop region in UV space) const auto srcW = static_cast(prevPass.targetSize.width); const auto srcH = static_cast(prevPass.targetSize.height); const std::array uvTransform{ static_cast(rect.x) / srcW, static_cast(rect.y) / srcH, static_cast(rect.width) / srcW, 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{ .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 = msaaSamples, .clearColorValue = clearColorValue, .clearDepthValue = clearDepthValue, .clearColor = clearColor && clearAlpha, .clearDepth = clearDepth, .hasDepth = prevPass.hasDepth, .hasStencil = prevPass.hasStencil, }; newPass.commands.reserve(2048); current_render_passes().emplace_back(std::move(newPass)); ++g_currentRenderPass; if (!newPass.clearColor && (clearColor || clearAlpha)) { // If we're only clearing color _or_ alpha, perform a clear draw push_draw_command(clear::DrawData{ .pipeline = pipeline_ref(clear::PipelineConfig{ .msaaSamples = msaaSamples, .clearColor = clearColor, .clearAlpha = clearAlpha, .clearDepth = false, // Depth cleared via render attachment }), .color = wgpu::Color{ .r = clearColorValue.x(), .g = clearColorValue.y(), .b = clearColorValue.z(), .a = clearColorValue.w(), }, }); } push_command(CommandType::SetViewport, Command::Data{.setViewport = g_cachedViewport}); push_command(CommandType::SetScissor, Command::Data{.setScissor = g_cachedScissor}); } void queue_palette_conv(tex_palette_conv::ConvRequest 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 current_render_passes()[g_currentRenderPass].msaaSamples; } void clear_caches() noexcept { g_offscreenCache.clear(); std::lock_guard lock{g_bindGroupCacheMutex}; g_cachedBindGroups.clear(); } 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()) { return it->second; } const auto colorFormat = webgpu::g_graphicsConfig.surfaceConfiguration.format; const wgpu::Extent3D size{width, height, 1}; const wgpu::TextureDescriptor colorDesc{ .label = "Offscreen Color", .usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::CopyDst, .dimension = wgpu::TextureDimension::e2D, .size = size, .format = colorFormat, .mipLevelCount = 1, .sampleCount = 1, }; auto colorTexture = g_device.CreateTexture(&colorDesc); auto colorView = colorTexture.CreateView(); webgpu::TextureWithSampler color{ .texture = std::move(colorTexture), .view = std::move(colorView), .size = size, .format = colorFormat, }; const auto depthFormat = webgpu::g_graphicsConfig.depthFormat; const wgpu::TextureDescriptor depthDesc{ .label = "Offscreen Depth", .usage = wgpu::TextureUsage::RenderAttachment, .dimension = wgpu::TextureDimension::e2D, .size = size, .format = depthFormat, .mipLevelCount = 1, .sampleCount = 1, }; auto depthTexture = g_device.CreateTexture(&depthDesc); auto depthView = depthTexture.CreateView(); webgpu::TextureWithSampler depth{ .texture = std::move(depthTexture), .view = std::move(depthView), .size = size, .format = depthFormat, }; OffscreenCacheEntry entry{ .color = std::move(color), .depth = std::move(depth), }; auto [insertIt, _] = g_offscreenCache.emplace(key, std::move(entry)); return insertIt->second; } void begin_offscreen(uint32_t width, uint32_t height) { ZoneScoped; CHECK(g_currentRenderPass != UINT32_MAX, "begin_offscreen called outside of a frame"); // 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 = current_render_passes()[g_currentRenderPass]; if (!currentPass.resolveTarget) { g_suspendedEfbPass = std::move(currentPass); 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; } // Create offscreen textures auto offscreenEntry = get_offscreen_textures(width, height); g_offscreenColor = std::move(offscreenEntry.color); g_offscreenDepth = std::move(offscreenEntry.depth); // Start a new pass with offscreen targets RenderPass newPass{ .label = pass_label("Offscreen"), .colorView = g_offscreenColor.view, .depthStencilView = g_offscreenDepth.view, .copySourceTexture = g_offscreenColor.texture, .copySourceView = g_offscreenColor.view, .copySourceDepthView = g_offscreenDepth.view, .targetSize = {width, height, 1}, .msaaSamples = 1, .clearColorValue = {0.f, 0.f, 0.f, 0.f}, .clearDepthValue = gx::UseReversedZ ? 0.f : 1.f, .clearColor = true, .clearDepth = true, .hasDepth = true, .hasStencil = false, .offscreen = true, }; current_render_passes().emplace_back(std::move(newPass)); ++g_currentRenderPass; g_inOffscreen = true; g_cachedViewport = {0.f, 0.f, static_cast(width), static_cast(height), 0.f, 1.f}; g_cachedScissor = {0, 0, static_cast(width), static_cast(height)}; push_command(CommandType::SetViewport, Command::Data{.setViewport = g_cachedViewport}); push_command(CommandType::SetScissor, Command::Data{.setScissor = g_cachedScissor}); } 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) { current_render_passes().emplace_back(std::move(*g_suspendedEfbPass)); g_suspendedEfbPass.reset(); } else { auto& pass = current_render_passes().emplace_back(); pass.label = pass_label("EFB"); pass.clearColor = false; pass.clearDepth = false; } ++g_currentRenderPass; set_efb_targets(current_render_passes()[g_currentRenderPass]); g_cachedViewport = g_suspendedEfbViewport; g_cachedScissor = g_suspendedEfbScissor; push_command(CommandType::SetViewport, Command::Data{.setViewport = g_cachedViewport}); push_command(CommandType::SetScissor, Command::Data{.setScissor = g_cachedScissor}); } template <> 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(); texture_replacement::initialize(); // For uniform & storage buffer offset alignments g_device.GetLimits(&g_cachedLimits); const auto createBuffer = [](wgpu::Buffer& out, wgpu::BufferUsage usage, uint64_t size, const char* label) { if (size <= 0) { return; } const wgpu::BufferDescriptor descriptor{ .label = label, .usage = usage, .size = size, }; out = g_device.CreateBuffer(&descriptor); }; createBuffer(g_uniformBuffer, wgpu::BufferUsage::Uniform | wgpu::BufferUsage::CopyDst, UniformBufferSize, "Shared Uniform 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 (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()); } 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{ // Vertex data buffer wgpu::BindGroupLayoutEntry{ .binding = 0, .visibility = wgpu::ShaderStage::Vertex, .buffer = wgpu::BufferBindingLayout{ .type = wgpu::BufferBindingType::ReadOnlyStorage, }, }, // Storage data buffer wgpu::BindGroupLayoutEntry{ .binding = 1, .visibility = wgpu::ShaderStage::Vertex, .buffer = wgpu::BufferBindingLayout{ .type = wgpu::BufferBindingType::ReadOnlyStorage, }, }, }; const wgpu::BindGroupLayoutDescriptor layoutDesc{ .label = "Static bind group layout", .entryCount = layoutEntries.size(), .entries = layoutEntries.data(), }; g_staticBindGroupLayout = g_device.CreateBindGroupLayout(&layoutDesc); const std::array entries{ wgpu::BindGroupEntry{ .binding = 0, .buffer = g_vertexBuffer, }, wgpu::BindGroupEntry{ .binding = 1, .buffer = g_storageBuffer, }, }; const wgpu::BindGroupDescriptor bindGroupDescriptor{ .label = "Static bind group", .layout = g_staticBindGroupLayout, .entryCount = entries.size(), .entries = entries.data(), }; g_staticBindGroup = g_device.CreateBindGroup(&bindGroupDescriptor); } { constexpr std::array layoutEntries{ // Uniform buffer (dynamic offset) wgpu::BindGroupLayoutEntry{ .binding = 0, .visibility = wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment, .buffer = wgpu::BufferBindingLayout{ .type = wgpu::BufferBindingType::Uniform, .hasDynamicOffset = true, }, }, }; const wgpu::BindGroupLayoutDescriptor layoutDesc{ .label = "Uniform bind group layout", .entryCount = layoutEntries.size(), .entries = layoutEntries.data(), }; g_uniformBindGroupLayout = g_device.CreateBindGroupLayout(&layoutDesc); const std::array entries{ wgpu::BindGroupEntry{ .binding = 0, .buffer = g_uniformBuffer, .size = gx::MaxUniformSize, }, }; const wgpu::BindGroupDescriptor bindGroupDescriptor{ .label = "Uniform bind group", .layout = g_uniformBindGroupLayout, .entryCount = entries.size(), .entries = entries.data(), }; g_uniformBindGroup = g_device.CreateBindGroup(&bindGroupDescriptor); } 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 { 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({}); for (auto& packet : g_framePackets) { packet = {}; } g_recordingFrame = nullptr; g_currentRenderPass = UINT32_MAX; g_offscreenCache.clear(); g_offscreenColor = {}; g_offscreenDepth = {}; g_staticBindGroup = {}; g_staticBindGroupLayout = {}; g_uniformBindGroup = {}; g_uniformBindGroupLayout = {}; g_inOffscreen = false; g_frameIndex = UINT32_MAX; g_frameSlots.reset(); g_stagingSlots.reset(); for (auto& state : s_mappingStates) { state.store(BufferMapState::Unmapped, std::memory_order_release); } } 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}); } } } 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; 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[*stagingSlot]; const auto mapBuffer = [&](ByteBuffer& buf, uint64_t size) { if (size <= 0) { return; } buf = ByteBuffer{static_cast(stagingBuf.GetMappedRange(bufferOffset, size)), static_cast(size)}; bufferOffset += size; }; mapBuffer(frame.verts, VertexBufferSize); mapBuffer(frame.uniforms, UniformBufferSize); mapBuffer(frame.indices, IndexBufferSize); mapBuffer(frame.storage, StorageBufferSize); if constexpr (UseTextureBuffer) { mapBuffer(frame.textureUpload, TextureUploadSize); } g_drawCallCount = 0; g_mergedDrawCallCount = 0; g_suspendedEfbPass.reset(); current_render_passes().emplace_back(); auto& pass = current_render_passes()[0]; pass.label = pass_label("EFB"); 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); g_cachedScissor = gx::map_logical_scissor(gx::g_gxState.logicalScissor); 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); webgpu::gpu_prof::frame_begin(g_framePackets[frameSlot].encoder); }); return true; } 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"); 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; } ZoneScoped; for (auto it = g_cachedBindGroups.begin(); it != g_cachedBindGroups.end();) { if (g_frameIndex - it->second.lastUsedFrame > BindGroupCacheRetainFrames) { g_cachedBindGroups.erase(it++); } else { ++it; } } } 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; static constexpr uint32_t align_down_copy_offset(uint32_t value) noexcept { return value & ~3u; } 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; } static bool needs_staging_copy(const FramePacket& frame, const FrameOp& op) { const auto& highWater = op.highWater; if (highWater.verts > frame.copied.verts || highWater.uniforms > frame.copied.uniforms || highWater.indices > frame.copied.indices || highWater.storage > frame.copied.storage) { return true; } if constexpr (UseTextureBuffer) { return op.textureUploads.size() > frame.copied.textureUploadCount; } return false; } static void copy_staging_to_high_water(wgpu::CommandEncoder& cmd, FramePacket& frame, const FrameOp& op) { if (!needs_staging_copy(frame, op)) { return; } const webgpu::gpu_prof::Zone zone{cmd, "Staging copies"}; 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 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], }; cmd.CopyBufferToTexture(&buf, &item.tex, &item.size); } frame.copied.textureUpload = highWater.textureUpload; frame.copied.textureUploadCount = op.textureUploads.size(); } } 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); } break; case FrameOpType::TextureCopy: if (op.textureCopy != nullptr) { const webgpu::gpu_prof::Zone zone{cmd, "Texture copy"}; 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; } 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 = passInfo.label.empty() ? fmt::format("Render pass {}", passIndex) : fmt::format("{} {}", passInfo.label, passIndex); const wgpu::RenderPassDescriptor renderPassDescriptor{ .label = label.c_str(), .colorAttachmentCount = attachments.size(), .colorAttachments = attachments.data(), .depthStencilAttachment = depthStencilAttachmentPtr, .timestampWrites = webgpu::gpu_prof::pass_writes(label), }; 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 webgpu::gpu_prof::Zone zone{cmd, "EFB copy"}; 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); } } } void after_submit() noexcept { depth_peek::after_submit(); } 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; #endif // Bind static bind group for the whole pass pass.SetBindGroup(0, g_staticBindGroup); pass.SetBindGroup(2, gx::g_emptyTextureBindGroup); for (const auto& cmd : passInfo.commands) { #ifdef AURORA_GFX_DEBUG_GROUPS { size_t firstDiff = lastDebugGroupStack.size(); for (size_t i = 0; i < lastDebugGroupStack.size(); ++i) { if (i >= cmd.debugGroupStack.size() || cmd.debugGroupStack[i] != lastDebugGroupStack[i]) { firstDiff = i; break; } } for (size_t i = firstDiff; i < lastDebugGroupStack.size(); ++i) { pass.PopDebugGroup(); } for (size_t i = firstDiff; i < cmd.debugGroupStack.size(); ++i) { pass.PushDebugGroup(cmd.debugGroupStack[i].c_str()); } lastDebugGroupStack = cmd.debugGroupStack; } #endif 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); } 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); } break; case CommandType::Draw: { const auto& draw = cmd.data.draw; switch (draw.type) { case ShaderType::Clear: 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: { #if defined(AURORA_GFX_DEBUG_GROUPS) pass.InsertDebugMarker(wgpu::StringView(g_debugMarkers[cmd.data.debugMarkerIndex])); #endif } break; } } #ifdef AURORA_GFX_DEBUG_GROUPS for (size_t i = 0; i < lastDebugGroupStack.size(); ++i) { pass.PopDebugGroup(); } #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; } wgpu::RenderPipeline pipeline; if (!get_pipeline(ref, pipeline)) { return false; } pass.SetPipeline(pipeline); g_currentPipeline = ref; return true; } static Range push(ByteBuffer& target, const uint8_t* data, size_t length, size_t alignment) { if (alignment != 0) { const size_t begin = target.size(); const size_t alignedBegin = AURORA_ALIGN(begin, alignment); if (alignedBegin > begin) { target.append_zeroes(alignedBegin - begin); } } const auto begin = target.size(); if (length > 0) { target.append(data, length); } return {static_cast(begin), static_cast(length)}; } static Range map(ByteBuffer& target, size_t length, size_t alignment) { if (alignment != 0) { 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) { 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_uniform(const uint8_t* data, size_t length) { ZoneScoped; return push(current_frame_packet().uniforms, data, length, g_cachedLimits.minUniformBufferOffsetAlignment); } Range push_storage(const uint8_t* data, size_t length) { 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(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; dst += copyBytesPerRow; } return range; } std::pair map_verts(size_t length) { 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(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(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(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)); g_cachedBindGroups.emplace(id, CachedBindGroup{ .bindGroup = std::move(bg), .lastUsedFrame = g_frameIndex, }); } else { it->second.lastUsedFrame = g_frameIndex; } return 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) { 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; } return it->second; } uint32_t align_uniform(uint32_t value) { return AURORA_ALIGN(value, g_cachedLimits.minUniformBufferOffsetAlignment); } void insert_debug_marker(std::string label) { #if defined(AURORA_GFX_DEBUG_GROUPS) auto idx = g_debugMarkers.size(); g_debugMarkers.emplace_back(std::move(label)); push_command(CommandType::DebugMarker, {.debugMarkerIndex = idx}); #endif } } // namespace aurora::gfx void aurora::gfx::push_debug_group(std::string label) { #if defined(AURORA_GFX_DEBUG_GROUPS) g_debugGroupStack.push_back(std::move(label)); #endif } void push_debug_group(const char* label) { #ifdef AURORA_GFX_DEBUG_GROUPS aurora::gfx::g_debugGroupStack.emplace_back(label); #endif } void pop_debug_group() { #ifdef AURORA_GFX_DEBUG_GROUPS if (aurora::gfx::g_debugGroupStack.empty()) { aurora::gfx::Log.error("Debug group stack underflowed!"); return; } aurora::gfx::g_debugGroupStack.pop_back(); #endif } const AuroraStats* aurora_get_stats() { return &aurora::gfx::g_stats; }