GPU profiling & Dawn trace integration with Tracy

This commit is contained in:
Luke Street
2026-06-13 10:39:24 -06:00
parent 55b993e08e
commit ad55eedb31
14 changed files with 621 additions and 16 deletions
+7 -1
View File
@@ -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)
+5
View File
@@ -5,6 +5,7 @@
#include "gx/fifo.hpp"
#include "imgui.hpp"
#include "webgpu/gpu.hpp"
#include "webgpu/gpu_prof.hpp"
#include <webgpu/webgpu_cpp.h>
#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<float>(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();
+50
View File
@@ -0,0 +1,50 @@
#include "TracyPlatform.hpp"
#if defined(TRACY_ENABLE) && __has_include(<dawn/platform/DawnPlatform.h>)
#include <chrono>
#include <cstring>
#include <vector>
#include <dawn/platform/DawnPlatform.h>
#include <tracy/TracyC.h>
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<double>(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<TracyCZoneCtx> 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
+9
View File
@@ -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
+37 -2
View File
@@ -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<std::string> g_debugGroupStack;
std::vector<std::string> 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 =
+2
View File
@@ -4,6 +4,7 @@
#include "../gx/gx.hpp"
#include "../gfx/render_worker.hpp"
#include "../webgpu/gpu.hpp"
#include "../webgpu/gpu_prof.hpp"
#include <algorithm>
#include <array>
@@ -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);
+2
View File
@@ -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);
+2
View File
@@ -2,6 +2,7 @@
#include "../internal.hpp"
#include "../webgpu/gpu.hpp"
#include "../webgpu/gpu_prof.hpp"
#include "texture.hpp"
#include <vector>
@@ -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);
+4 -1
View File
@@ -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<HashType>(gfx::ShaderType::GX)),
xxh3_hash(config.shaderConfig));
return build_pipeline(config, {}, shader, label.c_str());
}
void render(const DrawData& data, const wgpu::RenderPassEncoder& pass) {
+1 -1
View File
@@ -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;
+28 -11
View File
@@ -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 <dawn/native/DawnNative.h>
#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 = {};
+1
View File
@@ -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;
+444
View File
@@ -0,0 +1,444 @@
#include "gpu_prof.hpp"
#include "../internal.hpp"
#include "gpu.hpp"
#include <tracy/Tracy.hpp>
#ifdef TRACY_ENABLE
#include <tracy/TracyC.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <bitset>
#include <chrono>
#include <cstring>
#include <string>
#include <utility>
#include <vector>
#include <absl/container/flat_hash_map.h>
#include <magic_enum.hpp>
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<Event> events;
uint32_t passCount = 0;
int64_t submitNs = 0;
std::atomic<SlotState> state{SlotState::Free};
};
bool g_enabled = false;
bool g_timestampsEnabled = false;
wgpu::QuerySet g_querySet;
wgpu::Buffer g_resolveBuffer;
std::array<Slot, RingDepth> 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::nanoseconds>(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<std::string, const char*> 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<size_t>(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<const uint64_t*>(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<MaxZones> 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<wgpu::PassTimestampWrites, 4> 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
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <string_view>
#include <webgpu/webgpu_cpp.h>
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