mirror of
https://github.com/encounter/aurora.git
synced 2026-07-09 18:19:33 -07:00
GPU profiling & Dawn trace integration with Tracy
This commit is contained in:
@@ -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,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();
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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 =
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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
@@ -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
@@ -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
@@ -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 = {};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user