#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 "../gx/fifo.hpp" #include #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 + 3; 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 CustomDrawCommand { DrawTypeId type; uint32_t payloadSize; std::array payload; }; 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, CustomDraw, DebugMarker, }; struct Command { CommandType type; #ifdef AURORA_GFX_DEBUG_GROUPS std::vector debugGroupStack; #endif union Data { Viewport setViewport; ClipRect setScissor; ShaderDrawCommand draw; CustomDrawCommand customDraw; 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; }; 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; 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; // 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; 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 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 { wgpu::TexelCopyTextureInfo src; wgpu::TexelCopyTextureInfo dst; 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 { FrameOpType type = FrameOpType::RenderPass; uint32_t index = 0; RenderPass* renderPass = nullptr; TextureCopy* textureCopy = nullptr; EncoderTask* encoderTask = nullptr; StagingHighWater highWater; std::vector textureUploads; }; using RenderPassList = std::deque; struct FramePacket { RenderPassList renderPasses; std::deque textureCopies; std::deque encoderTasks; 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{}; }; 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; using PresentClock = std::chrono::steady_clock; static constexpr auto PresentFpsWindow = std::chrono::seconds{1}; static std::mutex g_presentStatsMutex; static std::deque g_presentTimes; static std::atomic_bool g_processEventsQueued = false; static std::atomic_int64_t g_lastPresentNs = 0; static std::atomic_int64_t g_presentPeriodNs = 0; static std::atomic_int64_t g_cpuFrameTimeNs = 0; static PresentClock::time_point g_cpuFrameStart; static constexpr auto FrameStartSafetyMargin = std::chrono::milliseconds{2}; static constexpr auto MaxPacingSample = std::chrono::milliseconds{250}; static constexpr uint32_t PacingEmaWeight = 8; static int64_t timestamp_ns(PresentClock::time_point time) { return std::chrono::duration_cast(time.time_since_epoch()).count(); } static int64_t duration_ns(PresentClock::duration duration) { return std::chrono::duration_cast(duration).count(); } static void update_ema(std::atomic_int64_t& value, int64_t sample) { if (sample <= 0 || sample > duration_ns(MaxPacingSample)) { return; } int64_t current = value.load(std::memory_order_acquire); while (true) { const int64_t next = current == 0 ? sample : current + (sample - current) / static_cast(PacingEmaWeight); if (value.compare_exchange_weak(current, next, std::memory_order_acq_rel, std::memory_order_acquire)) { return; } } } static void prune_present_times(PresentClock::time_point now) { while (!g_presentTimes.empty() && g_presentTimes.front() + PresentFpsWindow < now) { g_presentTimes.pop_front(); } } static void process_events() { ZoneScopedN("ProcessEvents"); if (g_instance) { g_instance.ProcessEvents(); } } static void enqueue_process_events() { if (render_worker::is_worker_thread()) { process_events(); return; } bool expected = false; if (!g_processEventsQueued.compare_exchange_strong(expected, true, std::memory_order_acq_rel, std::memory_order_acquire)) { return; } render_worker::enqueue_work([] { process_events(); g_processEventsQueued.store(false, std::memory_order_release); }); } static void wait_for_gpu_progress(std::chrono::nanoseconds sleepDuration) { if (render_worker::is_idle()) { enqueue_process_events(); } std::this_thread::sleep_for(sleepDuration); } static void pace_frame_start() { ZoneScopedN("Frame start pacing"); if (g_frameSlots.free_count() == FrameSlotCount) { return; } const int64_t lastPresentNs = g_lastPresentNs.load(std::memory_order_acquire); const int64_t presentPeriodNs = g_presentPeriodNs.load(std::memory_order_acquire); const int64_t cpuFrameTimeNs = g_cpuFrameTimeNs.load(std::memory_order_acquire); if (lastPresentNs == 0 || presentPeriodNs == 0 || cpuFrameTimeNs == 0) { return; } const int64_t safetyMarginNs = duration_ns(FrameStartSafetyMargin); const int64_t targetStartNs = lastPresentNs + presentPeriodNs - cpuFrameTimeNs - safetyMarginNs; int64_t nowNs = timestamp_ns(PresentClock::now()); if (targetStartNs <= nowNs) { return; } const double initialWaitMs = static_cast(targetStartNs - nowNs) / 1'000'000.0; 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}; wait_for_gpu_progress(sleepDuration); nowNs = timestamp_ns(PresentClock::now()); } } 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; // 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; } 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, .encoderTask = type == FrameOpType::EncoderTask && index < frame.encoderTasks.size() ? &frame.encoderTasks[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 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); 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 && op.encoderTask == 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, 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, 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, }; 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 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); if (type == CommandType::Draw || type == CommandType::CustomDraw) { renderPass.hasDraws = true; } 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_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); 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(); } 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()) { 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 | wgpu::TextureUsage::TextureBinding, .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, }; 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"); // 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; 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}); } 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}); } #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; g_processEventsQueued.store(false, std::memory_order_release); g_lastPresentNs.store(0, std::memory_order_release); g_presentPeriodNs.store(0, std::memory_order_release); g_cpuFrameTimeNs.store(0, std::memory_order_release); g_cpuFrameStart = {}; { std::lock_guard lock{g_presentStatsMutex}; g_presentTimes.clear(); } render_worker::initialize(); // This appears to take a while and blocks the render thread for periods of time // 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(); g_processEventsQueued.store(false, std::memory_order_release); g_lastPresentNs.store(0, std::memory_order_release); g_presentPeriodNs.store(0, std::memory_order_release); g_cpuFrameTimeNs.store(0, std::memory_order_release); g_cpuFrameStart = {}; { std::lock_guard lock{g_presentStatsMutex}; g_presentTimes.clear(); } 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(); } { std::lock_guard lock{g_runtimeDrawTypeMutex}; g_runtimeDrawTypes.clear(); g_freeDrawTypeSlots.clear(); } for (auto& pool : g_passSnapshotPools) { pool = {}; } 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; } wait_for_gpu_progress(std::chrono::milliseconds{1}); } } static size_t acquire_frame_slot() { ZoneScopedN("Acquire frame slot"); const auto waitStart = PresentClock::now(); while (true) { if (const auto slot = g_frameSlots.try_acquire()) { const auto waitDuration = PresentClock::now() - waitStart; const double waitMs = std::chrono::duration{waitDuration}.count(); TracyPlot("aurora: frameSlotWaitMs", waitMs); return *slot; } wait_for_gpu_progress(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; } wait_for_gpu_progress(std::chrono::microseconds{100}); } } bool begin_frame() { ZoneScoped; // pace_frame_start(); 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; g_passSnapshotPools[frameSlot].used = 0; 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] { static constexpr wgpu::CommandEncoderDescriptor EncoderDescriptor{.label = "Redraw encoder"}; g_framePackets[frameSlot].encoder = g_device.CreateCommandEncoder(&EncoderDescriptor); webgpu::gpu_prof::frame_begin(g_framePackets[frameSlot].encoder); }); g_cpuFrameStart = PresentClock::now(); 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); auto& pass = frame.renderPasses[g_currentRenderPass]; 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"); if (g_cpuFrameStart.time_since_epoch().count() != 0) { const auto cpuFrameTime = PresentClock::now() - g_cpuFrameStart; update_ema(g_cpuFrameTimeNs, duration_ns(cpuFrameTime)); const double cpuFrameTimeMs = std::chrono::duration{cpuFrameTime}.count(); TracyPlot("aurora: cpuFrameTimeMs", cpuFrameTimeMs); } 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_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); } g_frameSlots.release(frameSlot); expire_cached_bind_groups(); map_staging_buffer(stagingSlot, true); process_events(); }); } 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; case FrameOpType::EncoderTask: if (op.encoderTask != nullptr) { execute_encoder_task(cmd, *op.encoderTask); } 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.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; } 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); } } 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; TracyPlot("aurora: presentPeriodMs", presentPeriodMs); } std::lock_guard lock{g_presentStatsMutex}; g_presentTimes.push_back(now); prune_present_times(now); } float calculate_fps() noexcept { const auto now = PresentClock::now(); std::lock_guard lock{g_presentStatsMutex}; prune_present_times(now); if (g_presentTimes.size() < 2) { return 0.f; } const auto elapsed = std::chrono::duration(g_presentTimes.back() - g_presentTimes.front()).count(); if (elapsed <= 0.f) { return 0.f; } 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); 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; apply_viewport(pass, vp); currentViewport = vp; hasViewport = true; } break; case CommandType::SetScissor: { const auto& sc = cmd.data.setScissor; apply_scissor(pass, sc, passInfo.targetSize); currentScissor = sc; hasScissor = true; } 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::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])); #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)}; } // 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); } Range push_texture_data(const uint8_t* data, 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; } 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; } float aurora_get_fps() { return aurora::gfx::calculate_fps(); }