From ad55eedb3121e540b44fbabf8e9f68bfa55ba182 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 13 Jun 2026 10:39:24 -0600 Subject: [PATCH] GPU profiling & Dawn trace integration with Tracy --- cmake/aurora_core.cmake | 8 +- lib/aurora.cpp | 5 + lib/dawn/TracyPlatform.cpp | 50 ++++ lib/dawn/TracyPlatform.hpp | 9 + lib/gfx/common.cpp | 39 ++- lib/gfx/depth_peek.cpp | 2 + lib/gfx/tex_copy_conv.cpp | 2 + lib/gfx/tex_palette_conv.cpp | 2 + lib/gx/pipeline.cpp | 5 +- lib/gx/shader.cpp | 2 +- lib/webgpu/gpu.cpp | 39 ++- lib/webgpu/gpu.hpp | 1 + lib/webgpu/gpu_prof.cpp | 444 +++++++++++++++++++++++++++++++++++ lib/webgpu/gpu_prof.hpp | 29 +++ 14 files changed, 621 insertions(+), 16 deletions(-) create mode 100644 lib/dawn/TracyPlatform.cpp create mode 100644 lib/dawn/TracyPlatform.hpp create mode 100644 lib/webgpu/gpu_prof.cpp create mode 100644 lib/webgpu/gpu_prof.hpp diff --git a/cmake/aurora_core.cmake b/cmake/aurora_core.cmake index 774c52a..b6ec177 100644 --- a/cmake/aurora_core.cmake +++ b/cmake/aurora_core.cmake @@ -57,7 +57,13 @@ endif () if (AURORA_ENABLE_GX) target_compile_definitions(aurora_core PUBLIC AURORA_ENABLE_GX WEBGPU_DAWN) - target_sources(aurora_core PRIVATE lib/webgpu/gpu.cpp lib/webgpu/gpu_cache.cpp lib/dawn/BackendBinding.cpp) + target_sources(aurora_core PRIVATE + lib/webgpu/gpu.cpp + lib/webgpu/gpu_cache.cpp + lib/webgpu/gpu_prof.cpp + lib/dawn/BackendBinding.cpp + lib/dawn/TracyPlatform.cpp + ) target_link_libraries(aurora_core PRIVATE dawn::webgpu_dawn) if (DAWN_ENABLE_VULKAN) target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_VULKAN) diff --git a/lib/aurora.cpp b/lib/aurora.cpp index dc5ae5b..ea19b95 100644 --- a/lib/aurora.cpp +++ b/lib/aurora.cpp @@ -5,6 +5,7 @@ #include "gx/fifo.hpp" #include "imgui.hpp" #include "webgpu/gpu.hpp" +#include "webgpu/gpu_prof.hpp" #include #endif @@ -283,6 +284,7 @@ void end_frame() noexcept { .label = "EFB copy render pass", .colorAttachmentCount = attachments.size(), .colorAttachments = attachments.data(), + .timestampWrites = webgpu::gpu_prof::pass_writes("Present blit"), }; const auto pass = encoder.BeginRenderPass(&renderPassDescriptor); // Copy EFB -> XFB (swapchain) @@ -305,6 +307,7 @@ void end_frame() noexcept { .label = "ImGui render pass", .colorAttachmentCount = attachments.size(), .colorAttachments = attachments.data(), + .timestampWrites = webgpu::gpu_prof::pass_writes("ImGui"), }; const auto pass = encoder.BeginRenderPass(&renderPassDescriptor); pass.SetViewport(0.f, 0.f, static_cast(webgpu::g_graphicsConfig.surfaceConfiguration.width), @@ -315,12 +318,14 @@ void end_frame() noexcept { } else { Log.info("Skipping present; window not presentable"); } + webgpu::gpu_prof::frame_end(encoder); const wgpu::CommandBufferDescriptor cmdBufDescriptor{.label = "Redraw command buffer"}; const auto buffer = encoder.Finish(&cmdBufDescriptor); { ZoneScopedN("Queue Submit"); g_queue.Submit(1, &buffer); } + webgpu::gpu_prof::after_submit(); if (canPresent && g_surface) { ZoneScopedN("Present"); auto presentStatus = g_surface.Present(); diff --git a/lib/dawn/TracyPlatform.cpp b/lib/dawn/TracyPlatform.cpp new file mode 100644 index 0000000..922c0e9 --- /dev/null +++ b/lib/dawn/TracyPlatform.cpp @@ -0,0 +1,50 @@ +#include "TracyPlatform.hpp" + +#if defined(TRACY_ENABLE) && __has_include() + +#include +#include +#include + +#include +#include + +namespace aurora::webgpu { +namespace { +class TracyDawnPlatform final : public dawn::platform::Platform { +public: + const unsigned char* GetTraceCategoryEnabledFlag(dawn::platform::TraceCategory) override { + static const unsigned char enabled = 1; + return &enabled; + } + double MonotonicallyIncreasingTime() override { + return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); + } + uint64_t AddTraceEvent(char phase, const unsigned char*, const char* name, uint64_t, double, int, const char**, + const unsigned char*, const uint64_t*, unsigned char) override { + thread_local std::vector zoneStack; + if (phase == 'B') { + const uint64_t srcloc = ___tracy_alloc_srcloc_name(0, "dawn", 4, "", 0, name, std::strlen(name), 0); + zoneStack.push_back(___tracy_emit_zone_begin_alloc(srcloc, 1)); + } else if (phase == 'E') { + if (!zoneStack.empty()) { + ___tracy_emit_zone_end(zoneStack.back()); + zoneStack.pop_back(); + } + } + return 0; + } +}; +TracyDawnPlatform g_tracyDawnPlatform; +} // namespace + +dawn::platform::Platform* tracy_dawn_platform() { return &g_tracyDawnPlatform; } +} // namespace aurora::webgpu + +#else + +namespace aurora::webgpu { +dawn::platform::Platform* tracy_dawn_platform() { return nullptr; } +} // namespace aurora::webgpu + +#endif diff --git a/lib/dawn/TracyPlatform.hpp b/lib/dawn/TracyPlatform.hpp new file mode 100644 index 0000000..1279e65 --- /dev/null +++ b/lib/dawn/TracyPlatform.hpp @@ -0,0 +1,9 @@ +#pragma once + +namespace dawn::platform { +class Platform; +} // namespace dawn::platform + +namespace aurora::webgpu { +dawn::platform::Platform* tracy_dawn_platform(); +} // namespace aurora::webgpu diff --git a/lib/gfx/common.cpp b/lib/gfx/common.cpp index eff0c65..f53a8ae 100644 --- a/lib/gfx/common.cpp +++ b/lib/gfx/common.cpp @@ -4,6 +4,7 @@ #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" @@ -42,6 +43,15 @@ 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; @@ -582,6 +592,7 @@ void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool cl // 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, @@ -719,6 +730,7 @@ void begin_offscreen(uint32_t width, uint32_t height) { // 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, @@ -761,6 +773,7 @@ void end_offscreen() { g_suspendedEfbPass.reset(); } else { auto& pass = current_render_passes().emplace_back(); + pass.label = pass_label("EFB"); pass.clearColor = false; pass.clearDepth = false; } @@ -1070,6 +1083,7 @@ bool begin_frame() { 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(); @@ -1085,6 +1099,7 @@ bool begin_frame() { .label = "Redraw encoder", }; g_framePackets[frameSlot].encoder = g_device.CreateCommandEncoder(&encoderDescriptor); + webgpu::gpu_prof::frame_begin(g_framePackets[frameSlot].encoder); }); return true; } @@ -1204,7 +1219,23 @@ static void copy_staging_buffer_range(wgpu::CommandEncoder& cmd, const FramePack 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, @@ -1241,6 +1272,7 @@ static void encode_op(wgpu::CommandEncoder& cmd, FramePacket& frame, const Frame 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; @@ -1295,12 +1327,14 @@ static void render(wgpu::CommandEncoder& cmd, FramePacket& frame, RenderPass& pa }; depthStencilAttachmentPtr = &depthStencilAttachment; } - const auto label = fmt::format("Render pass {}", passIndex); + const auto label = + passInfo.label.empty() ? fmt::format("Render pass {}", passIndex) : fmt::format("{} {}", passInfo.label, passIndex); const wgpu::RenderPassDescriptor renderPassDescriptor{ - .label = passInfo.label.empty() ? label.c_str() : passInfo.label.c_str(), + .label = label.c_str(), .colorAttachmentCount = attachments.size(), .colorAttachments = attachments.data(), .depthStencilAttachment = depthStencilAttachmentPtr, + .timestampWrites = webgpu::gpu_prof::pass_writes(label), }; auto pass = cmd.BeginRenderPass(&renderPassDescriptor); @@ -1332,6 +1366,7 @@ static void render(wgpu::CommandEncoder& cmd, FramePacket& frame, RenderPass& pa } 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 = diff --git a/lib/gfx/depth_peek.cpp b/lib/gfx/depth_peek.cpp index 6e7f888..8ba493c 100644 --- a/lib/gfx/depth_peek.cpp +++ b/lib/gfx/depth_peek.cpp @@ -4,6 +4,7 @@ #include "../gx/gx.hpp" #include "../gfx/render_worker.hpp" #include "../webgpu/gpu.hpp" +#include "../webgpu/gpu_prof.hpp" #include #include @@ -399,6 +400,7 @@ void encode_frame_snapshot(const wgpu::CommandEncoder& cmd, const wgpu::TextureV const wgpu::ComputePassDescriptor passDescriptor{ .label = "Depth Peek Compute Pass", + .timestampWrites = webgpu::gpu_prof::pass_writes("Depth peek"), }; const auto pass = cmd.BeginComputePass(&passDescriptor); pass.SetPipeline(g_pipeline); diff --git a/lib/gfx/tex_copy_conv.cpp b/lib/gfx/tex_copy_conv.cpp index 50f4c43..56187c9 100644 --- a/lib/gfx/tex_copy_conv.cpp +++ b/lib/gfx/tex_copy_conv.cpp @@ -3,6 +3,7 @@ #include "../internal.hpp" #include "../gx/gx.hpp" #include "../webgpu/gpu.hpp" +#include "../webgpu/gpu_prof.hpp" #include "texture.hpp" #include "../gx/gx_fmt.hpp" @@ -489,6 +490,7 @@ static void execute(const wgpu::CommandEncoder& cmd, const ConvRequest& req, con .label = "TexCopyConv Pass", .colorAttachmentCount = colorAttachments.size(), .colorAttachments = colorAttachments.data(), + .timestampWrites = webgpu::gpu_prof::pass_writes("EFB copy convert"), }; const auto pass = cmd.BeginRenderPass(&renderPassDescriptor); pass.SetPipeline(pipeline); diff --git a/lib/gfx/tex_palette_conv.cpp b/lib/gfx/tex_palette_conv.cpp index 842cca3..392dcc8 100644 --- a/lib/gfx/tex_palette_conv.cpp +++ b/lib/gfx/tex_palette_conv.cpp @@ -2,6 +2,7 @@ #include "../internal.hpp" #include "../webgpu/gpu.hpp" +#include "../webgpu/gpu_prof.hpp" #include "texture.hpp" #include @@ -249,6 +250,7 @@ void run(const wgpu::CommandEncoder& cmd, const ConvRequest& req) { .label = "TexPaletteConv Pass", .colorAttachmentCount = colorAttachments.size(), .colorAttachments = colorAttachments.data(), + .timestampWrites = webgpu::gpu_prof::pass_writes("Palette convert"), }; const auto pass = cmd.BeginRenderPass(&renderPassDescriptor); pass.SetPipeline(pipeline); diff --git a/lib/gx/pipeline.cpp b/lib/gx/pipeline.cpp index a5088fd..74f3d8a 100644 --- a/lib/gx/pipeline.cpp +++ b/lib/gx/pipeline.cpp @@ -11,7 +11,10 @@ static Module Log("aurora::gx"); wgpu::RenderPipeline create_pipeline(const PipelineConfig& config) { ZoneScoped; const auto shader = build_shader(config.shaderConfig); - return build_pipeline(config, {}, shader, "GX Pipeline"); + const auto label = fmt::format("GX Pipeline {:x} shader {:x}", + xxh3_hash(config, static_cast(gfx::ShaderType::GX)), + xxh3_hash(config.shaderConfig)); + return build_pipeline(config, {}, shader, label.c_str()); } void render(const DrawData& data, const wgpu::RenderPassEncoder& pass) { diff --git a/lib/gx/shader.cpp b/lib/gx/shader.cpp index 3671ed5..2b83f52 100644 --- a/lib/gx/shader.cpp +++ b/lib/gx/shader.cpp @@ -1928,7 +1928,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4f {{{6}{5} uniBufAttrs, texBindings, vtxOutAttrs, vtxInAttrs, vtxXfrAttrs, fragmentFn, fragmentFnPre, vtxXfrAttrsPre, uniformPre); if (EnableDebugPrints) { - Log.info("Generated shader: {}", shaderSource); + Log.info("Generated shader (hash {:x}): {}", hash, shaderSource); } return shaderSource; diff --git a/lib/webgpu/gpu.cpp b/lib/webgpu/gpu.cpp index a969465..f9269a4 100644 --- a/lib/webgpu/gpu.cpp +++ b/lib/webgpu/gpu.cpp @@ -17,9 +17,11 @@ #include "../gfx/render_worker.hpp" #include "../internal.hpp" #include "../window.hpp" +#include "gpu_prof.hpp" #ifdef WEBGPU_DAWN #include "../dawn/BackendBinding.hpp" +#include "../dawn/TracyPlatform.hpp" #include #endif @@ -54,7 +56,7 @@ static TextureWithSampler g_resampledFrameBuffer; static wgpu::Adapter g_adapter; wgpu::Instance g_instance; -static wgpu::AdapterInfo g_adapterInfo; +wgpu::AdapterInfo g_adapterInfo; static wgpu::SurfaceCapabilities g_surfaceCapabilities; bool g_bcTexturesSupported = false; bool g_astcTexturesSupported = false; @@ -632,6 +634,7 @@ const TextureWithSampler& resample_present_source(const wgpu::CommandEncoder& en .label = "Present resample render pass", .colorAttachmentCount = attachments.size(), .colorAttachments = attachments.data(), + .timestampWrites = gpu_prof::pass_writes("Present resample"), }; const auto pass = encoder.BeginRenderPass(&renderPassDescriptor); pass.SetPipeline(g_ResamplePipeline); @@ -701,6 +704,9 @@ bool initialize(AuroraBackend auroraBackend, bool allowCpu) { #ifdef WEBGPU_DAWN dawn::native::DawnInstanceDescriptor dawnInstanceDescriptor; dawnInstanceDescriptor.backendValidationLevel = dawn::native::BackendValidationLevel::Disabled; +#ifdef TRACY_ENABLE + dawnInstanceDescriptor.platform = tracy_dawn_platform(); +#endif instanceDescriptor.nextInChain = &dawnInstanceDescriptor; #endif g_instance = wgpu::CreateInstance(&instanceDescriptor); @@ -824,6 +830,11 @@ bool initialize(AuroraBackend auroraBackend, bool allowCpu) { } requiredFeatures.push_back(feature); } +#ifdef TRACY_ENABLE + if (feature == wgpu::FeatureName::TimestampQuery) { + requiredFeatures.push_back(feature); + } +#endif } #ifdef WEBGPU_DAWN wgpu::DawnCacheDeviceDescriptor cacheDescriptor({ @@ -834,28 +845,32 @@ bool initialize(AuroraBackend auroraBackend, bool allowCpu) { }); constexpr std::array enableToggles{ - /* clang-format off */ #if _WIN32 - "use_dxc", + "use_dxc", #ifndef NDEBUG - "emit_hlsl_debug_symbols", + "emit_hlsl_debug_symbols", #endif #endif #ifdef NDEBUG - "skip_validation", - "disable_robustness", + "skip_validation", + "disable_robustness", #endif #ifndef ANDROID - "use_user_defined_labels_in_backend", + "use_user_defined_labels_in_backend", #endif - "disable_symbol_renaming", - "enable_immediate_error_handling", - /* clang-format on */ + "allow_unsafe_apis", + "disable_symbol_renaming", + "enable_immediate_error_handling", }; - wgpu::DawnTogglesDescriptor togglesDescriptor({ + constexpr std::array disableToggles{ + "timestamp_quantization", + }; + wgpu::DawnTogglesDescriptor togglesDescriptor(wgpu::DawnTogglesDescriptor::Init{ .nextInChain = &cacheDescriptor, .enabledToggleCount = enableToggles.size(), .enabledToggles = enableToggles.data(), + .disabledToggleCount = disableToggles.size(), + .disabledToggles = disableToggles.data(), }); #endif wgpu::DeviceDescriptor deviceDescriptor({ @@ -952,6 +967,7 @@ bool initialize(AuroraBackend auroraBackend, bool allowCpu) { }; create_copy_pipeline(); create_resample_pipeline(); + gpu_prof::initialize(); { window::SurfaceLock surfaceLock; resize_swapchain(size.fb_width, size.fb_height, size.native_fb_width, size.native_fb_height, true); @@ -962,6 +978,7 @@ bool initialize(AuroraBackend auroraBackend, bool allowCpu) { void shutdown() { g_shuttingDown = true; gfx::gpu_synchronize(); + gpu_prof::shutdown(); g_CopyBindGroupLayout = {}; g_CopyPipeline = {}; g_CopyBindGroup = {}; diff --git a/lib/webgpu/gpu.hpp b/lib/webgpu/gpu.hpp index ed05584..22cf1fd 100644 --- a/lib/webgpu/gpu.hpp +++ b/lib/webgpu/gpu.hpp @@ -50,6 +50,7 @@ extern TextureWithSampler g_depthBuffer; extern wgpu::RenderPipeline g_CopyPipeline; extern wgpu::BindGroup g_CopyBindGroup; extern wgpu::Instance g_instance; +extern wgpu::AdapterInfo g_adapterInfo; extern bool g_bcTexturesSupported; extern bool g_astcTexturesSupported; extern bool g_textureComponentSwizzleSupported; diff --git a/lib/webgpu/gpu_prof.cpp b/lib/webgpu/gpu_prof.cpp new file mode 100644 index 0000000..d60011f --- /dev/null +++ b/lib/webgpu/gpu_prof.cpp @@ -0,0 +1,444 @@ +#include "gpu_prof.hpp" + +#include "../internal.hpp" +#include "gpu.hpp" + +#include + +#ifdef TRACY_ENABLE + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace aurora::webgpu::gpu_prof { +namespace { +Module Log("aurora::webgpu::gpu_prof"); + +// Each zone consumes a begin/end timestamp pair. +// The last pair is reserved for the frame zone. +constexpr uint32_t MaxZones = 127; +constexpr uint32_t QueryCount = MaxZones * 2 + 2; +constexpr uint32_t FrameBeginQuery = MaxZones * 2; +constexpr uint32_t FrameEndQuery = MaxZones * 2 + 1; +constexpr uint64_t ReadbackSize = QueryCount * sizeof(uint64_t); +constexpr size_t RingDepth = 4; +constexpr uint8_t ContextId = 0; + +enum class EventKind : uint8_t { + ZoneBegin, + PassBegin, + End, +}; + +struct Event { + const char* name; // static lifetime; nullptr for End + uint32_t query; + EventKind kind; +}; + +enum class SlotState : uint8_t { + Free, + Recording, + InFlight, + Mapped, + Failed, +}; + +struct Slot { + wgpu::Buffer readback; + std::vector events; + uint32_t passCount = 0; + int64_t submitNs = 0; + std::atomic state{SlotState::Free}; +}; + +bool g_enabled = false; +bool g_timestampsEnabled = false; +wgpu::QuerySet g_querySet; +wgpu::Buffer g_resolveBuffer; +std::array g_slots; +size_t g_recordSlot = 0; +size_t g_emitSlot = 0; +bool g_frameActive = false; +bool g_framePending = false; +uint32_t g_zoneCount = 0; + +bool g_contextEmitted = false; +uint16_t g_queryId = 0; +uint64_t g_lastEmittedTs = 0; +uint64_t g_lastFrameEnd = 0; + +int64_t now_ns() { + return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()) + .count(); +} + +// Tracy keeps references to zone names; intern them for static lifetime. +const char* intern_name(std::string_view name) { + static absl::flat_hash_map names; + const auto it = names.find(name); + if (it != names.end()) { + return it->second; + } + char* stable = new char[name.size() + 1]; + std::memcpy(stable, name.data(), name.size()); + stable[name.size()] = '\0'; + names.emplace(name, stable); + return stable; +} + +// tracy::GpuContextType not exposed through TracyC.h +uint8_t tracy_context_type(wgpu::BackendType backend) { + switch (backend) { + case wgpu::BackendType::OpenGL: + case wgpu::BackendType::OpenGLES: + return 1; // OpenGl + case wgpu::BackendType::Vulkan: + return 2; // Vulkan + case wgpu::BackendType::D3D12: + return 4; // Direct3D12 + case wgpu::BackendType::D3D11: + return 5; // Direct3D11 + case wgpu::BackendType::Metal: + return 6; // Metal + default: + return 7; // Custom + } +} + +void emit_context(const Slot& slot, uint64_t frameBegin) { + // Tracy has no notion of WebGPU's opaque timestamp epoch, so the context + // anchor pairs a GPU timestamp with "now" at emission. Shift the timestamp + // by the time elapsed since this frame was submitted, so the frame zone + // lands at the submit point on the timeline instead of trailing it by the + // readback latency. Residual error is the GPU's submit-to-execute delay. + const int64_t anchor = int64_t(frameBegin) + (now_ns() - slot.submitNs); + ___tracy_emit_gpu_new_context_serial({ + .gpuTime = anchor, + .period = 1.0f, + .context = ContextId, + .flags = 0, + .type = tracy_context_type(g_backendType), + }); + const auto& info = adapter_info(); + const std::string name = fmt::format("{} ({})", std::string_view{info.device}, magic_enum::enum_name(g_backendType)); + ___tracy_emit_gpu_context_name_serial({ + .context = ContextId, + .name = name.c_str(), + .len = uint16_t(std::min(name.size(), UINT16_MAX)), + }); +} + +void emit_zone_begin(const char* name, uint64_t gpuNs) { + const uint64_t srcloc = ___tracy_alloc_srcloc_name(0, "aurora", 6, "gpu_prof", 8, name, std::strlen(name), 0); + const uint16_t queryId = g_queryId++; + ___tracy_emit_gpu_zone_begin_alloc_serial({.srcloc = srcloc, .queryId = queryId, .context = ContextId}); + ___tracy_emit_gpu_time_serial({.gpuTime = int64_t(gpuNs), .queryId = queryId, .context = ContextId}); +} + +void emit_zone_end(uint64_t gpuNs) { + const uint16_t queryId = g_queryId++; + ___tracy_emit_gpu_zone_end_serial({.queryId = queryId, .context = ContextId}); + ___tracy_emit_gpu_time_serial({.gpuTime = int64_t(gpuNs), .queryId = queryId, .context = ContextId}); +} + +void emit_frame(Slot& slot) { + const auto* ts = static_cast(slot.readback.GetConstMappedRange(0, ReadbackSize)); + if (ts == nullptr) { + return; + } + + // Frame bounds; fall back to the recorded zones when encoder-level + // timestamps are unavailable. + uint64_t frameBegin = g_timestampsEnabled ? ts[FrameBeginQuery] : 0; + uint64_t frameEnd = g_timestampsEnabled ? ts[FrameEndQuery] : 0; + if (frameBegin == 0 || frameEnd == 0) { + for (const auto& event : slot.events) { + const uint64_t t = ts[event.query]; + if (t == 0) { + continue; + } + if (frameBegin == 0 || t < frameBegin) { + frameBegin = t; + } + if (t > frameEnd) { + frameEnd = t; + } + } + } + if (frameBegin == 0 || frameEnd <= frameBegin) { + return; + } + + if (!g_contextEmitted) { + const uint64_t lastFrameEnd = std::exchange(g_lastFrameEnd, frameEnd); + if (!TracyIsConnected) { + return; + } + // Timestamp warm-up: some backends report a bogus epoch for the first + // frames (Dawn re-correlates its Metal CPU/GPU timestamp mapping after + // startup). Only anchor the context once two consecutive frames are + // mutually consistent, discarding frames until then. + constexpr uint64_t SaneFrameGapNs = UINT64_C(5'000'000'000); + if (lastFrameEnd == 0 || frameBegin < lastFrameEnd || frameBegin - lastFrameEnd >= SaneFrameGapNs) { + return; + } + emit_context(slot, frameBegin); + g_contextEmitted = true; + } + + // Tracy requires GPU zones within a context to be properly nested in + // time, and treats large backward jumps as timer wraparound. The recorded + // events mirror encode order, which matches GPU execution order. Clamp + // to keep the stream monotonic. + uint64_t prev = std::max(g_lastEmittedTs, frameBegin); + const uint64_t endBound = std::max(frameEnd, prev); + const auto clamped = [&prev, endBound](uint64_t t) { + prev = std::min(std::max(t, prev), endBound); + return prev; + }; + + emit_zone_begin(intern_name("Frame"), clamped(frameBegin)); + uint32_t depth = 0; + uint64_t topLevelEnd = prev; + uint64_t idleNs = 0; + // Zones whose timestamps were never written resolve to 0 (e.g. Metal + // cannot sample encoder-level timestamps); drop those to keep the stream + // balanced. + std::bitset dropped; + for (const auto& event : slot.events) { + if (event.kind == EventKind::End) { + if (dropped[event.query / 2]) { + continue; + } + const uint64_t t = clamped(ts[event.query]); + emit_zone_end(t); + if (--depth == 0) { + topLevelEnd = t; + } + } else { + if (ts[event.query] == 0 && ts[event.query + 1] == 0) { + dropped[event.query / 2] = true; + continue; + } + const uint64_t t = clamped(ts[event.query]); + if (depth++ == 0 && t > topLevelEnd) { + idleNs += t - topLevelEnd; + } + emit_zone_begin(event.name, t); + } + } + const uint64_t end = clamped(frameEnd); + if (end > topLevelEnd) { + idleNs += end - topLevelEnd; + } + emit_zone_end(end); + g_lastEmittedTs = prev; + + TracyPlot("aurora: gpuFrameMs", double(frameEnd - frameBegin) * 1e-6); + TracyPlot("aurora: gpuIdleMs", double(idleNs) * 1e-6); + TracyPlot("aurora: gpuPasses", int64_t(slot.passCount)); +} + +Slot& record_slot() { return g_slots[g_recordSlot]; } + +uint32_t alloc_zone() { + if (!g_frameActive || g_zoneCount >= MaxZones) { + return UINT32_MAX; + } + return g_zoneCount++; +} +} // namespace + +void initialize() { + g_enabled = g_device.HasFeature(wgpu::FeatureName::TimestampQuery); + if (!g_enabled) { + Log.info("Timestamp queries unsupported; GPU profiling disabled"); + return; + } + g_timestampsEnabled = true; // TODO: check if allow_unsafe_apis enabled? + const wgpu::QuerySetDescriptor querySetDescriptor{ + .label = "GPU profiler timestamps", + .type = wgpu::QueryType::Timestamp, + .count = QueryCount, + }; + g_querySet = g_device.CreateQuerySet(&querySetDescriptor); + const wgpu::BufferDescriptor resolveDescriptor{ + .label = "GPU profiler resolve", + .usage = wgpu::BufferUsage::QueryResolve | wgpu::BufferUsage::CopySrc, + .size = ReadbackSize, + }; + g_resolveBuffer = g_device.CreateBuffer(&resolveDescriptor); + const wgpu::BufferDescriptor readbackDescriptor{ + .label = "GPU profiler readback", + .usage = wgpu::BufferUsage::MapRead | wgpu::BufferUsage::CopyDst, + .size = ReadbackSize, + }; + for (auto& slot : g_slots) { + slot.readback = g_device.CreateBuffer(&readbackDescriptor); + slot.events.reserve(MaxZones * 2); + slot.state = SlotState::Free; + } + g_recordSlot = 0; + g_emitSlot = 0; + g_framePending = false; + TracyPlotConfig("aurora: gpuFrameMs", tracy::PlotFormatType::Number, false, true, 0); + TracyPlotConfig("aurora: gpuIdleMs", tracy::PlotFormatType::Number, false, true, 0); + TracyPlotConfig("aurora: gpuPasses", tracy::PlotFormatType::Number, true, true, 0); + Log.info("GPU profiling enabled ({} zones max)", MaxZones); +} + +void shutdown() { + g_querySet = {}; + g_resolveBuffer = {}; + for (auto& slot : g_slots) { + slot.readback = {}; + slot.events.clear(); + slot.passCount = 0; + slot.state = SlotState::Free; + } + g_enabled = false; + g_timestampsEnabled = false; + g_frameActive = false; + g_framePending = false; +} + +void frame_begin(const wgpu::CommandEncoder& encoder) { + if (!g_enabled) { + return; + } + auto& slot = record_slot(); + if (slot.state != SlotState::Free) { + g_frameActive = false; + return; + } + slot.state = SlotState::Recording; + slot.events.clear(); + slot.passCount = 0; + g_zoneCount = 0; + g_frameActive = true; + if (g_timestampsEnabled) { + encoder.WriteTimestamp(g_querySet, FrameBeginQuery); + } +} + +void frame_end(const wgpu::CommandEncoder& encoder) { + if (!g_enabled || !g_frameActive) { + return; + } + g_frameActive = false; + auto& slot = record_slot(); + if (slot.events.empty() && !g_timestampsEnabled) { + slot.state = SlotState::Free; + return; + } + if (g_timestampsEnabled) { + encoder.WriteTimestamp(g_querySet, FrameEndQuery); + } + encoder.ResolveQuerySet(g_querySet, 0, QueryCount, g_resolveBuffer, 0); + encoder.CopyBufferToBuffer(g_resolveBuffer, 0, slot.readback, 0, ReadbackSize); + g_framePending = true; +} + +void after_submit() { + if (!g_enabled) { + return; + } + if (g_framePending) { + g_framePending = false; + auto& slot = record_slot(); + slot.submitNs = now_ns(); + slot.state = SlotState::InFlight; + slot.readback.MapAsync(wgpu::MapMode::Read, 0, ReadbackSize, wgpu::CallbackMode::AllowProcessEvents, + [&slot](wgpu::MapAsyncStatus status, wgpu::StringView) { + slot.state = + status == wgpu::MapAsyncStatus::Success ? SlotState::Mapped : SlotState::Failed; + }); + g_recordSlot = (g_recordSlot + 1) % RingDepth; + } + g_instance.ProcessEvents(); + while (true) { + auto& slot = g_slots[g_emitSlot]; + const auto state = slot.state.load(std::memory_order_acquire); + if (state == SlotState::Mapped) { + emit_frame(slot); + slot.readback.Unmap(); + } else if (state != SlotState::Failed) { + break; + } + slot.state.store(SlotState::Free, std::memory_order_release); + g_emitSlot = (g_emitSlot + 1) % RingDepth; + } +} + +const wgpu::PassTimestampWrites* pass_writes(std::string_view name) { + const uint32_t index = alloc_zone(); + if (index == UINT32_MAX) { + return nullptr; + } + auto& slot = record_slot(); + slot.events.push_back({intern_name(name), index * 2, EventKind::PassBegin}); + slot.events.push_back({nullptr, index * 2 + 1, EventKind::End}); + ++slot.passCount; + + static std::array writes; + static size_t writesIndex = 0; + auto& out = writes[writesIndex++ % writes.size()]; + out = { + .querySet = g_querySet, + .beginningOfPassWriteIndex = index * 2, + .endOfPassWriteIndex = index * 2 + 1, + }; + return &out; +} + +Zone::Zone(const wgpu::CommandEncoder& encoder, std::string_view name) { + if (!g_timestampsEnabled) { + return; + } + const uint32_t index = alloc_zone(); + if (index == UINT32_MAX) { + return; + } + record_slot().events.push_back({intern_name(name), index * 2, EventKind::ZoneBegin}); + encoder.WriteTimestamp(g_querySet, index * 2); + m_encoder = &encoder; + m_endQuery = index * 2 + 1; +} + +Zone::~Zone() { + if (m_encoder == nullptr) { + return; + } + record_slot().events.push_back({nullptr, m_endQuery, EventKind::End}); + m_encoder->WriteTimestamp(g_querySet, m_endQuery); +} + +} // namespace aurora::webgpu::gpu_prof + +#else + +namespace aurora::webgpu::gpu_prof { +void initialize() {} +void shutdown() {} +void frame_begin(const wgpu::CommandEncoder&) {} +void frame_end(const wgpu::CommandEncoder&) {} +void after_submit() {} +const wgpu::PassTimestampWrites* pass_writes(std::string_view) { return nullptr; } +Zone::Zone(const wgpu::CommandEncoder&, std::string_view) {} +Zone::~Zone() = default; +} // namespace aurora::webgpu::gpu_prof + +#endif diff --git a/lib/webgpu/gpu_prof.hpp b/lib/webgpu/gpu_prof.hpp new file mode 100644 index 0000000..7861201 --- /dev/null +++ b/lib/webgpu/gpu_prof.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include + +namespace aurora::webgpu::gpu_prof { + +void initialize(); +void shutdown(); + +void frame_begin(const wgpu::CommandEncoder& encoder); +void frame_end(const wgpu::CommandEncoder& encoder); +void after_submit(); +const wgpu::PassTimestampWrites* pass_writes(std::string_view name); + +class Zone { +public: + Zone(const wgpu::CommandEncoder& encoder, std::string_view name); + ~Zone(); + Zone(const Zone&) = delete; + Zone& operator=(const Zone&) = delete; + +private: + const wgpu::CommandEncoder* m_encoder = nullptr; + uint32_t m_endQuery = 0; +}; + +} // namespace aurora::webgpu::gpu_prof