From 17caf72bddd25a91500436e4df5a32a00a259d73 Mon Sep 17 00:00:00 2001 From: mchughalex <25671999+mchughalex@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:31:10 +0100 Subject: [PATCH] graphics: attribute native-pass GPU time to per-stage profile buckets --- include/rex/graphics/native_rhi.h | 25 +++++++++++ include/rex/perf/counter.h | 36 +++++++++++++++ src/graphics/d3d12/native_rhi_d3d12.cpp | 54 +++++++++++++++++++++++ src/graphics/vulkan/native_rhi_vulkan.cpp | 36 +++++++++++++++ 4 files changed, 151 insertions(+) diff --git a/include/rex/graphics/native_rhi.h b/include/rex/graphics/native_rhi.h index fad348d..6f7a0bb 100644 --- a/include/rex/graphics/native_rhi.h +++ b/include/rex/graphics/native_rhi.h @@ -398,6 +398,26 @@ struct Rect { int32_t bottom = 0; }; +// GPU-time attribution stages for ProfileRegion: the renderer marks the start +// of each of its passes and the command processor's timestamp-bucket profiler +// (d3d12_gpu_timestamp_buckets / vulkan_gpu_timestamp_buckets) reports the +// per-stage GPU spans. kTail marks the end of the last stage; on Vulkan the +// span from kTail to frame end is reported as its own bucket, on D3D12 it +// stays in the profiler's untimed remainder. +enum class ProfileStage : uint8_t { + kCommit, // content-store commits, decode uploads, eviction + kShadow, // dynamic caster shadow atlas + blur + kStaticSun, // static sun-shadow cascade renders + kMain, // main scene pass (opaque + transparent) + kResolve, // MSAA resolve + outline composite + kAmbientOcclusion, + kSsr, + kVolumetrics, + kBloom, // bloom pyramid + tonemap + k2d, // photo grab, blur chains, 2D overlay replay + kTail, // after the last stage: backend/present tail +}; + // --------------------------------------------------------------------------- // Command recording. Wraps the command processor's deferred command list for // the current frame; valid only inside the render callback, render thread @@ -469,6 +489,11 @@ class Cmd { ResourceState after) = 0; virtual void FlushBarriers() = 0; + // Marks the start of a renderer pass for GPU-time attribution (closing the + // previously marked stage). No-op while the backend's timestamp-bucket + // profiler is disabled; see ProfileStage for the reporting semantics. + virtual void ProfileRegion(ProfileStage stage) = 0; + protected: virtual ~Cmd() = default; }; diff --git a/include/rex/perf/counter.h b/include/rex/perf/counter.h index dfa59a9..17bad29 100644 --- a/include/rex/perf/counter.h +++ b/include/rex/perf/counter.h @@ -63,6 +63,20 @@ enum class DrawBucket : uint8_t { // The native guest-output renderer's whole GPU pass (scene + 2D replay + // resolve), recorded inside IssueSwap. Runs until frame end. kNativeScene, + // Native-pass stages (nrhi::Cmd::ProfileRegion): successive marks partition + // the native pass into its renderer-defined stages; when the renderer marks + // them, they replace kNativeScene in the frame's attribution. + kNativeCommit, // content-store commits, decode uploads, eviction + kNativeShadow, // dynamic caster shadow atlas + blur + kNativeSun, // static sun-shadow cascade renders + kNativeMain, // main scene pass (opaque + transparent + SSR G-buffer) + kNativeResolve, // MSAA resolve + outline composite + kNativeAo, // screen-space ambient occlusion + kNativeSsr, // screen-space reflection march + composite + kNativeVol, // volumetric sun shafts + haze + kNativeBloom, // bloom pyramid + tonemap + kNative2d, // photo grab, blur chains, 2D overlay replay + kNativeTail, // after the last stage: backend/present tail kCount, }; @@ -103,6 +117,28 @@ inline const char* DrawBucketName(DrawBucket bucket) { return "TexUpCopy"; case DrawBucket::kNativeScene: return "Native"; + case DrawBucket::kNativeCommit: + return "NatCommit"; + case DrawBucket::kNativeShadow: + return "NatShadow"; + case DrawBucket::kNativeSun: + return "NatSun"; + case DrawBucket::kNativeMain: + return "NatMain"; + case DrawBucket::kNativeResolve: + return "NatResolve"; + case DrawBucket::kNativeAo: + return "NatAO"; + case DrawBucket::kNativeSsr: + return "NatSSR"; + case DrawBucket::kNativeVol: + return "NatVol"; + case DrawBucket::kNativeBloom: + return "NatBloom"; + case DrawBucket::kNative2d: + return "Nat2D"; + case DrawBucket::kNativeTail: + return "NatTail"; default: return "Unknown"; } diff --git a/src/graphics/d3d12/native_rhi_d3d12.cpp b/src/graphics/d3d12/native_rhi_d3d12.cpp index 974148b..cc9175e 100644 --- a/src/graphics/d3d12/native_rhi_d3d12.cpp +++ b/src/graphics/d3d12/native_rhi_d3d12.cpp @@ -398,6 +398,7 @@ class NrCmdD3D12 : public nrhi::Cmd { void Barrier(nrhi::Texture* texture, ResourceState before, ResourceState after) override; void FlushBarriers() override; + void ProfileRegion(nrhi::ProfileStage stage) override; // Called at frame begin and on root-signature changes: the fresh deferred // command list / new root signature carries no bindings, so the latched @@ -407,9 +408,20 @@ class NrCmdD3D12 : public nrhi::Cmd { std::memset(last_table_counts_, 0, sizeof(last_table_counts_)); } + // Frame begin only (never on root-signature changes): each frame gets a + // fresh timestamp-query range, so an open profile region must not carry an + // end query across frames. + void ResetProfileRegion() { profile_region_query_ = UINT32_MAX; } + NrDeviceD3D12* device = nullptr; private: + // Start query of the open ProfileRegion span, UINT32_MAX when none. Unlike + // Vulkan's auto-closing regions, D3D12 buckets are explicit begin/end query + // pairs; an unended pair would read stale readback memory, so every opened + // region is closed by the next mark (kTail closes without opening). + uint32_t profile_region_query_ = UINT32_MAX; + void BindTextureTable(uint32_t param, NrTextureViewD3D12* const* views, uint32_t count); // Last tuple bound per root param: consecutive draws mostly rebind the @@ -936,6 +948,7 @@ class NrDeviceD3D12 : public nrhi::Device { uint32_t height, nrhi::Texture** guest_output_out) { guest_output_state_ = guest_output_internal_state; cmd_.ResetFrameState(); + cmd_.ResetProfileRegion(); const auto maint_t0 = std::chrono::steady_clock::now(); const size_t dissolved_count = dissolved_views_.size(); FlushDissolvedViews(); @@ -1417,6 +1430,47 @@ void NrCmdD3D12::Barrier(nrhi::Texture* texture, ResourceState before, ResourceS void NrCmdD3D12::FlushBarriers() { device->cp()->SubmitBarriers(); } +rex::perf::DrawBucket ProfileStageBucket(nrhi::ProfileStage stage) { + switch (stage) { + case nrhi::ProfileStage::kCommit: + return rex::perf::DrawBucket::kNativeCommit; + case nrhi::ProfileStage::kShadow: + return rex::perf::DrawBucket::kNativeShadow; + case nrhi::ProfileStage::kStaticSun: + return rex::perf::DrawBucket::kNativeSun; + case nrhi::ProfileStage::kMain: + return rex::perf::DrawBucket::kNativeMain; + case nrhi::ProfileStage::kResolve: + return rex::perf::DrawBucket::kNativeResolve; + case nrhi::ProfileStage::kAmbientOcclusion: + return rex::perf::DrawBucket::kNativeAo; + case nrhi::ProfileStage::kSsr: + return rex::perf::DrawBucket::kNativeSsr; + case nrhi::ProfileStage::kVolumetrics: + return rex::perf::DrawBucket::kNativeVol; + case nrhi::ProfileStage::kBloom: + return rex::perf::DrawBucket::kNativeBloom; + case nrhi::ProfileStage::k2d: + return rex::perf::DrawBucket::kNative2d; + case nrhi::ProfileStage::kTail: + return rex::perf::DrawBucket::kNativeTail; + } + return rex::perf::DrawBucket::kNativeScene; +} + +void NrCmdD3D12::ProfileRegion(nrhi::ProfileStage stage) { + if (profile_region_query_ != UINT32_MAX) { + device->cp()->EndGpuTimestampedDraw(profile_region_query_); + profile_region_query_ = UINT32_MAX; + } + if (stage == nrhi::ProfileStage::kTail) { + // No frame-end query to pair with on D3D12; the tail lands in the + // profiler's untimed remainder. + return; + } + profile_region_query_ = device->cp()->BeginGpuTimestampedDraw(ProfileStageBucket(stage)); +} + } // namespace nrhi::Device* CreateNativeRhiDevice(D3D12CommandProcessor* command_processor) { diff --git a/src/graphics/vulkan/native_rhi_vulkan.cpp b/src/graphics/vulkan/native_rhi_vulkan.cpp index 49f7e93..28fd5b0 100644 --- a/src/graphics/vulkan/native_rhi_vulkan.cpp +++ b/src/graphics/vulkan/native_rhi_vulkan.cpp @@ -440,6 +440,7 @@ class NrCmdVulkan : public nrhi::Cmd { uint32_t height) override; void Barrier(nrhi::Texture* texture, ResourceState before, ResourceState after) override; void FlushBarriers() override; + void ProfileRegion(nrhi::ProfileStage stage) override; // --- frame handling (driven by the device) --- void ResetFrameState(); @@ -2693,6 +2694,41 @@ void NrCmdVulkan::FlushBarriers() { device->cp()->SubmitBarriers(true); } +rex::perf::DrawBucket ProfileStageBucket(nrhi::ProfileStage stage) { + switch (stage) { + case nrhi::ProfileStage::kCommit: + return rex::perf::DrawBucket::kNativeCommit; + case nrhi::ProfileStage::kShadow: + return rex::perf::DrawBucket::kNativeShadow; + case nrhi::ProfileStage::kStaticSun: + return rex::perf::DrawBucket::kNativeSun; + case nrhi::ProfileStage::kMain: + return rex::perf::DrawBucket::kNativeMain; + case nrhi::ProfileStage::kResolve: + return rex::perf::DrawBucket::kNativeResolve; + case nrhi::ProfileStage::kAmbientOcclusion: + return rex::perf::DrawBucket::kNativeAo; + case nrhi::ProfileStage::kSsr: + return rex::perf::DrawBucket::kNativeSsr; + case nrhi::ProfileStage::kVolumetrics: + return rex::perf::DrawBucket::kNativeVol; + case nrhi::ProfileStage::kBloom: + return rex::perf::DrawBucket::kNativeBloom; + case nrhi::ProfileStage::k2d: + return rex::perf::DrawBucket::kNative2d; + case nrhi::ProfileStage::kTail: + return rex::perf::DrawBucket::kNativeTail; + } + return rex::perf::DrawBucket::kNativeScene; +} + +void NrCmdVulkan::ProfileRegion(nrhi::ProfileStage stage) { + // Region semantics: beginning a bucket closes the active one, and the CP + // closes the last bucket at frame end - so the kTail mark records the + // present/submission tail as its own span. + device->cp()->BeginGpuTimestampedRegion(ProfileStageBucket(stage)); +} + } // namespace // ---------------------------------------------------------------------------