From 842d6fabdf32623009227773518a2af47da1a2b2 Mon Sep 17 00:00:00 2001 From: dougchansan Date: Sun, 6 Sep 2026 18:47:24 -1000 Subject: [PATCH] Let an embedder observe guest execution, GX, MMIO and shaders The core and the GX paths already know where time goes; none of it was reachable from outside. Three headers of optional pointers an embedder installs, and probes at the points that already funnel. Granularity is the design: the guest PC is published on a 4096-dispatch cadence, guest time is measured once per timing slice, and the FIFO scope wraps a batch rather than each opcode. All pointers are null unless installed, so an unobserved build pays one predictable branch. Rebased from main onto moderngekko-vendor: main currently fails to configure for a ModernGekko consumer at Externals/tinygltf, with or without these changes. --- Source/Core/Core/HW/MMIO.h | 5 ++ Source/Core/Core/HW/MmioObserver.h | 28 ++++++++ .../StaticRecomp/StaticRecompCore_Run.cpp | 32 +++++++++ .../StaticRecomp/StaticRecompObserver.h | 39 +++++++++++ Source/Core/VideoCommon/OpcodeDecoding.cpp | 8 ++- Source/Core/VideoCommon/ShaderCache.cpp | 13 ++++ Source/Core/VideoCommon/TextureCacheBase.cpp | 3 + .../VideoCommon/TextureDecoder_Common.cpp | 4 ++ .../Core/VideoCommon/VertexLoaderManager.cpp | 9 +++ Source/Core/VideoCommon/VideoZoneObserver.h | 70 +++++++++++++++++++ 10 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 Source/Core/Core/HW/MmioObserver.h create mode 100644 Source/Core/Core/PowerPC/StaticRecomp/StaticRecompObserver.h create mode 100644 Source/Core/VideoCommon/VideoZoneObserver.h diff --git a/Source/Core/Core/HW/MMIO.h b/Source/Core/Core/HW/MMIO.h index cee439ab43..24eb7de530 100644 --- a/Source/Core/Core/HW/MMIO.h +++ b/Source/Core/Core/HW/MMIO.h @@ -14,6 +14,7 @@ #include "Common/CommonTypes.h" #include "Core/HW/GPFifo.h" #include "Core/HW/MMIOHandlers.h" +#include "Core/HW/MmioObserver.h" namespace Core { @@ -136,12 +137,16 @@ class Mapping template Unit Read(Core::System& system, u32 addr) { + if (const auto* obs = GetMmioObservers(); obs && obs->reads) + obs->reads->fetch_add(1, std::memory_order_relaxed); return GetHandlerForRead(addr).Read(system, addr); } template void Write(Core::System& system, u32 addr, Unit val) { + if (const auto* obs = GetMmioObservers(); obs && obs->writes) + obs->writes->fetch_add(1, std::memory_order_relaxed); GetHandlerForWrite(addr).Write(system, addr, val); } diff --git a/Source/Core/Core/HW/MmioObserver.h b/Source/Core/Core/HW/MmioObserver.h new file mode 100644 index 0000000000..bee2d2b996 --- /dev/null +++ b/Source/Core/Core/HW/MmioObserver.h @@ -0,0 +1,28 @@ +// RecompCore: optional MMIO tallies for an embedder. +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "Common/CommonTypes.h" + +// MMIO is far hotter than the GX paths, so these stay null unless an embedder +// asks for them; an unobserved build pays one predictable branch per access. +struct MmioObservers +{ + std::atomic* reads = nullptr; + std::atomic* writes = nullptr; +}; + +inline const MmioObservers* g_mmio_observers = nullptr; + +inline void SetMmioObservers(const MmioObservers* observers) +{ + g_mmio_observers = observers; +} + +inline const MmioObservers* GetMmioObservers() +{ + return g_mmio_observers; +} diff --git a/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore_Run.cpp b/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore_Run.cpp index d91ec2dc19..fe818708c2 100644 --- a/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore_Run.cpp +++ b/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore_Run.cpp @@ -2,6 +2,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "Core/PowerPC/StaticRecomp/StaticRecompCore.h" +#include "Core/PowerPC/StaticRecomp/StaticRecompObserver.h" + +#include #include "Core/System.h" #include "Core/PowerPC/PowerPC.h" #include "Core/PowerPC/Interpreter/Interpreter.h" @@ -117,12 +120,18 @@ void StaticRecompCore::Run() return; } + const StaticRecompObservers* const observers = GetStaticRecompObservers(); + std::atomic* const observed_pc = observers ? observers->guest_pc : nullptr; + const bool time_guest = observers != nullptr && observers->guest_cpu_ns != nullptr; + while (*state_ptr == CPU::State::Running) { core_timing.Advance(); const std::string current_game_id = SConfig::GetInstance().GetGameID(); m_module_active = m_module && (current_game_id.empty() || current_game_id == m_module->game_id); + const auto slice_start = time_guest ? std::chrono::steady_clock::now() + : std::chrono::steady_clock::time_point{}; do { // MSR.FP needs no gate here: generated FPU instructions raise the @@ -155,6 +164,9 @@ void StaticRecompCore::Run() if (m_has_rel_modules) ResolveNativeAddress(runtime_dispatch_address, &linked_dispatch_address, nullptr); m_guest.pc = linked_dispatch_address; + // Same cadence as the histogram above, but independent of its flag. + if (observed_pc && (m_native_dispatches & 4095u) == 0) + observed_pc->store(m_guest.pc, std::memory_order_relaxed); m_module->dispatch(&m_guest, linked_dispatch_address); if (m_has_rel_modules) m_guest.pc = TranslateRelAddress(m_guest.pc); @@ -263,6 +275,26 @@ void StaticRecompCore::Run() } } } while (ppc.downcount > 0 && *state_ptr == CPU::State::Running); + + if (observers) + { + // Plain counters the core already keeps; republished per slice. + if (observers->dispatches) + observers->dispatches->store(m_native_dispatches, std::memory_order_relaxed); + if (observers->interpreter_fallbacks) + observers->interpreter_fallbacks->store(m_fallback_steps, std::memory_order_relaxed); + if (observers->exceptions) + observers->exceptions->store(m_native_exceptions, std::memory_order_relaxed); + } + if (time_guest) + { + // One clock pair per timing slice rather than per dispatch. + const auto elapsed = std::chrono::steady_clock::now() - slice_start; + observers->guest_cpu_ns->fetch_add( + static_cast( + std::chrono::duration_cast(elapsed).count()), + std::memory_order_relaxed); + } } } diff --git a/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompObserver.h b/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompObserver.h new file mode 100644 index 0000000000..3f61d07a36 --- /dev/null +++ b/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompObserver.h @@ -0,0 +1,39 @@ +// RecompCore: optional profiling observation points for an embedder. +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "Common/CommonTypes.h" + +// The core already samples its own guest PC and knows how long it spends +// executing guest code, but had no way to hand either to an embedder: the +// dispatch histogram in StaticRecompCore was only ever printed to stderr at +// shutdown. An embedder installs these pointers to observe both live. +// +// Both are null unless installed, so an unobserved core pays one predictable +// null check per timing slice and per sampled dispatch -- never per dispatch. +struct StaticRecompObservers +{ + // Live guest PC, published at the core's existing dispatch sample cadence. + std::atomic* guest_pc = nullptr; + // Cumulative nanoseconds spent executing guest code, summed per timing slice. + std::atomic* guest_cpu_ns = nullptr; + // Cumulative core tallies, republished per timing slice. + std::atomic* dispatches = nullptr; + std::atomic* interpreter_fallbacks = nullptr; + std::atomic* exceptions = nullptr; +}; + +inline const StaticRecompObservers* g_static_recomp_observers = nullptr; + +inline void SetStaticRecompObservers(const StaticRecompObservers* observers) +{ + g_static_recomp_observers = observers; +} + +inline const StaticRecompObservers* GetStaticRecompObservers() +{ + return g_static_recomp_observers; +} diff --git a/Source/Core/VideoCommon/OpcodeDecoding.cpp b/Source/Core/VideoCommon/OpcodeDecoding.cpp index fc6f896cd7..bec815b561 100644 --- a/Source/Core/VideoCommon/OpcodeDecoding.cpp +++ b/Source/Core/VideoCommon/OpcodeDecoding.cpp @@ -13,6 +13,7 @@ // vertices. #include "VideoCommon/OpcodeDecoding.h" +#include "VideoCommon/VideoZoneObserver.h" #include "Common/Assert.h" #include "Common/Logging/Log.h" @@ -261,7 +262,12 @@ u8* RunFifo(DataReader src, u32* cycles) { using CallbackT = RunCallback; auto callback = CallbackT{}; - u32 size = Run(src.GetPointer(), static_cast(src.size()), callback); + // One scope per FIFO batch, not per opcode. + u32 size; + { + const VideoZoneScope zone(VIDEO_ZONE_SINK(command_processor_ns)); + size = Run(src.GetPointer(), static_cast(src.size()), callback); + } if (cycles != nullptr) *cycles = callback.m_cycles; diff --git a/Source/Core/VideoCommon/ShaderCache.cpp b/Source/Core/VideoCommon/ShaderCache.cpp index 565e4f42f8..4d8789ab9c 100644 --- a/Source/Core/VideoCommon/ShaderCache.cpp +++ b/Source/Core/VideoCommon/ShaderCache.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "VideoCommon/ShaderCache.h" +#include "VideoCommon/VideoZoneObserver.h" #include @@ -438,6 +439,9 @@ void ShaderCache::CompileMissingPipelines() std::unique_ptr ShaderCache::CompileVertexShader(const VertexShaderUid& uid) const { + const VideoZoneScope zone(VIDEO_ZONE_SINK(shader_generation_ns)); + if (const auto* obs = GetVideoZoneObservers(); obs && obs->shader_compilations) + obs->shader_compilations->fetch_add(1, std::memory_order_relaxed); const ShaderCode source_code = GenerateVertexShaderCode(m_api_type, m_host_config, uid.GetUidData(), {}); return g_gfx->CreateShaderFromSource(ShaderStage::Vertex, source_code.GetBuffer()); @@ -446,6 +450,9 @@ std::unique_ptr ShaderCache::CompileVertexShader(const VertexSha std::unique_ptr ShaderCache::CompileVertexUberShader(const UberShader::VertexShaderUid& uid) const { + const VideoZoneScope zone(VIDEO_ZONE_SINK(shader_generation_ns)); + if (const auto* obs = GetVideoZoneObservers(); obs && obs->shader_compilations) + obs->shader_compilations->fetch_add(1, std::memory_order_relaxed); const ShaderCode source_code = UberShader::GenVertexShader(m_api_type, m_host_config, uid.GetUidData()); return g_gfx->CreateShaderFromSource(ShaderStage::Vertex, source_code.GetBuffer(), nullptr, @@ -454,6 +461,9 @@ ShaderCache::CompileVertexUberShader(const UberShader::VertexShaderUid& uid) con std::unique_ptr ShaderCache::CompilePixelShader(const PixelShaderUid& uid) const { + const VideoZoneScope zone(VIDEO_ZONE_SINK(shader_generation_ns)); + if (const auto* obs = GetVideoZoneObservers(); obs && obs->shader_compilations) + obs->shader_compilations->fetch_add(1, std::memory_order_relaxed); const ShaderCode source_code = GeneratePixelShaderCode(m_api_type, m_host_config, uid.GetUidData(), {}); return g_gfx->CreateShaderFromSource(ShaderStage::Pixel, source_code.GetBuffer()); @@ -462,6 +472,9 @@ std::unique_ptr ShaderCache::CompilePixelShader(const PixelShade std::unique_ptr ShaderCache::CompilePixelUberShader(const UberShader::PixelShaderUid& uid) const { + const VideoZoneScope zone(VIDEO_ZONE_SINK(shader_generation_ns)); + if (const auto* obs = GetVideoZoneObservers(); obs && obs->shader_compilations) + obs->shader_compilations->fetch_add(1, std::memory_order_relaxed); const ShaderCode source_code = UberShader::GenPixelShader(m_api_type, m_host_config, uid.GetUidData()); return g_gfx->CreateShaderFromSource(ShaderStage::Pixel, source_code.GetBuffer(), nullptr, diff --git a/Source/Core/VideoCommon/TextureCacheBase.cpp b/Source/Core/VideoCommon/TextureCacheBase.cpp index c5d6096391..4d94e7305a 100644 --- a/Source/Core/VideoCommon/TextureCacheBase.cpp +++ b/Source/Core/VideoCommon/TextureCacheBase.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "VideoCommon/TextureCacheBase.h" +#include "VideoCommon/VideoZoneObserver.h" #include #include @@ -2132,6 +2133,8 @@ void TextureCacheBase::CopyRenderTargetToTexture( float gamma, bool clamp_top, bool clamp_bottom, const CopyFilterCoefficients::Values& filter_coefficients) { + if (const auto* obs = GetVideoZoneObservers(); obs && obs->efb_copies) + obs->efb_copies->fetch_add(1, std::memory_order_relaxed); // Emulation methods: // // - EFB to RAM: diff --git a/Source/Core/VideoCommon/TextureDecoder_Common.cpp b/Source/Core/VideoCommon/TextureDecoder_Common.cpp index 3506514a54..615bffb63c 100644 --- a/Source/Core/VideoCommon/TextureDecoder_Common.cpp +++ b/Source/Core/VideoCommon/TextureDecoder_Common.cpp @@ -13,6 +13,7 @@ #include "VideoCommon/LookUpTables.h" #include "VideoCommon/TextureDecoder.h" +#include "VideoCommon/VideoZoneObserver.h" #include "VideoCommon/TextureDecoder_Util.h" #include "VideoCommon/sfont.inc" @@ -300,6 +301,9 @@ static void TexDecoder_DrawOverlay(u8* dst, int width, int height, TextureFormat void TexDecoder_Decode(u8* dst, const u8* src, int width, int height, TextureFormat texformat, const u8* tlut, TLUTFormat tlutfmt) { + const VideoZoneScope zone(VIDEO_ZONE_SINK(texture_decode_ns)); + if (const auto* obs = GetVideoZoneObservers(); obs && obs->texture_decodes) + obs->texture_decodes->fetch_add(1, std::memory_order_relaxed); _TexDecoder_DecodeImpl((u32*)dst, src, width, height, texformat, tlut, tlutfmt); if (TexFmt_Overlay_Enable) diff --git a/Source/Core/VideoCommon/VertexLoaderManager.cpp b/Source/Core/VideoCommon/VertexLoaderManager.cpp index e3d3b9f33b..d33fb1df81 100644 --- a/Source/Core/VideoCommon/VertexLoaderManager.cpp +++ b/Source/Core/VideoCommon/VertexLoaderManager.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "VideoCommon/VertexLoaderManager.h" +#include "VideoCommon/VideoZoneObserver.h" #include #include @@ -398,6 +399,14 @@ static bool CanSplit(OpcodeDecoder::Primitive primitive) template int RunVertices(int vtx_attr_group, OpcodeDecoder::Primitive primitive, int count, const u8* src) { + const VideoZoneScope zone(VIDEO_ZONE_SINK(vertex_loader_ns)); + if (const auto* obs = GetVideoZoneObservers()) + { + if (obs->draw_calls) + obs->draw_calls->fetch_add(1, std::memory_order_relaxed); + if (obs->vertices_loaded) + obs->vertices_loaded->fetch_add(static_cast(count), std::memory_order_relaxed); + } if (count == 0) [[unlikely]] return 0; ASSERT(count > 0); diff --git a/Source/Core/VideoCommon/VideoZoneObserver.h b/Source/Core/VideoCommon/VideoZoneObserver.h new file mode 100644 index 0000000000..7fb1c453c8 --- /dev/null +++ b/Source/Core/VideoCommon/VideoZoneObserver.h @@ -0,0 +1,70 @@ +// RecompCore: optional GX timing observation points for an embedder. +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "Common/CommonTypes.h" + +// Cumulative nanoseconds per GX subsystem. Null unless an embedder installs +// them, so an unobserved build pays one predictable branch per scope. +struct VideoZoneObservers +{ + std::atomic* command_processor_ns = nullptr; + std::atomic* vertex_loader_ns = nullptr; + std::atomic* texture_decode_ns = nullptr; + // Cumulative tallies for the same paths. + std::atomic* draw_calls = nullptr; + std::atomic* vertices_loaded = nullptr; + std::atomic* texture_decodes = nullptr; + std::atomic* shader_generation_ns = nullptr; + std::atomic* shader_compilations = nullptr; + std::atomic* efb_copies = nullptr; +}; + +inline const VideoZoneObservers* g_video_zone_observers = nullptr; + +inline void SetVideoZoneObservers(const VideoZoneObservers* observers) +{ + g_video_zone_observers = observers; +} + +inline const VideoZoneObservers* GetVideoZoneObservers() +{ + return g_video_zone_observers; +} + +// Adds its lifetime to one accumulator. A null sink measures nothing. +class VideoZoneScope final +{ +public: + explicit VideoZoneScope(std::atomic* sink) + : m_sink(sink), m_start(sink ? std::chrono::steady_clock::now() + : std::chrono::steady_clock::time_point{}) + { + } + + ~VideoZoneScope() + { + if (m_sink == nullptr) + return; + const auto elapsed = std::chrono::steady_clock::now() - m_start; + m_sink->fetch_add( + static_cast( + std::chrono::duration_cast(elapsed).count()), + std::memory_order_relaxed); + } + + VideoZoneScope(const VideoZoneScope&) = delete; + VideoZoneScope& operator=(const VideoZoneScope&) = delete; + +private: + std::atomic* m_sink; + std::chrono::steady_clock::time_point m_start; +}; + +// Picks one accumulator out of the installed set, or null when unobserved. +#define VIDEO_ZONE_SINK(field) \ + (GetVideoZoneObservers() ? GetVideoZoneObservers()->field : nullptr)