diff options
| author | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-10-28 23:07:49 -0400 |
|---|---|---|
| committer | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-10-28 23:07:49 -0400 |
| commit | 571f9b58eb8a9c102a91a5d26cd32ebae4330232 (patch) | |
| tree | 989c5032c268044452d7ad7c324b147c964227e9 /src/video_core | |
| parent | 808039e5806dc841a30ef7d887462635860ae291 (diff) | |
nx2: GPU-thread zone timers and an in-process sampling profiler
EDEN_NX2_GPU_PROFILE=1 times the GPU thread's stages (command parsing,
macros, draw setup by step, compute, texture uploads) and logs each
one's share of every second.
EDEN_NX2_SAMPLE=<thread names> (Windows) pauses the named threads about
600 times a second for EDEN_NX2_SAMPLE_SECONDS, walks their stacks and
writes per-function counts plus folded stacks (EDEN_NX2_SAMPLE_OUT).
Diffstat (limited to 'src/video_core')
| -rw-r--r-- | src/video_core/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | src/video_core/gpu_thread.cpp | 15 | ||||
| -rw-r--r-- | src/video_core/nx2_profile.h | 149 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp | 109 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/vk_rasterizer.cpp | 30 | ||||
| -rw-r--r-- | src/video_core/texture_cache/texture_cache.h | 2 |
6 files changed, 260 insertions, 46 deletions
diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index d17deacacd..513a644166 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -103,6 +103,7 @@ add_library(video_core STATIC native_qmd.h native_query_trace.h nx2.h + nx2_profile.h present.h pte_kind.h query_cache/bank_base.h diff --git a/src/video_core/gpu_thread.cpp b/src/video_core/gpu_thread.cpp index 793e8e9eeb..cf4e159806 100644 --- a/src/video_core/gpu_thread.cpp +++ b/src/video_core/gpu_thread.cpp @@ -17,6 +17,7 @@ #include "video_core/dma_pusher.h" #include "video_core/gpu.h" #include "video_core/gpu_thread.h" +#include "video_core/nx2_profile.h" #include "video_core/host1x/host1x.h" #include "video_core/renderer_base.h" @@ -38,29 +39,41 @@ void ThreadManager::StartThread(VideoCore::RendererBase& renderer, Core::Fronten auto current_context = context.Acquire(); CommandDataContainer next; + namespace Profile = VideoCommon::Nx2Profile; while (!stop_token.stop_requested()) { + Profile::Report(); if (scheduler.HasPendingChannels()) { // Suspended channels wait on semaphores the CPU releases: poll them meanwhile. - scheduler.ResumePending(system.GPU()); + { + Profile::Scope scope{Profile::Zone::ResumePending}; + scheduler.ResumePending(system.GPU()); + } if (!state.queue.TryPop(next)) { + Profile::Scope scope{Profile::Zone::PendingSleep}; std::this_thread::sleep_for(std::chrono::milliseconds{1}); continue; } } else { + Profile::Scope scope{Profile::Zone::Idle}; state.queue.PopWait(next, stop_token); } if (stop_token.stop_requested()) { break; } if (auto* submit_list = std::get_if<SubmitListCommand>(&next.data)) { + Profile::Scope scope{Profile::Zone::Push}; scheduler.Push(system.GPU(), submit_list->channel, std::move(submit_list->entries)); } else if (std::holds_alternative<GPUTickCommand>(next.data)) { + Profile::Scope scope{Profile::Zone::Tick}; system.GPU().TickWork(); } else if (auto* run = std::get_if<RunCommand>(&next.data)) { + Profile::Scope scope{Profile::Zone::Run}; run->func(); } else if (const auto* flush = std::get_if<FlushRegionCommand>(&next.data)) { + Profile::Scope scope{Profile::Zone::FlushInvalidate}; renderer.ReadRasterizer()->FlushRegion(flush->addr, flush->size); } else if (const auto* invalidate = std::get_if<InvalidateRegionCommand>(&next.data)) { + Profile::Scope scope{Profile::Zone::FlushInvalidate}; renderer.ReadRasterizer()->OnCacheInvalidation(invalidate->addr, invalidate->size); } else { ASSERT(false); diff --git a/src/video_core/nx2_profile.h b/src/video_core/nx2_profile.h new file mode 100644 index 0000000000..051403c982 --- /dev/null +++ b/src/video_core/nx2_profile.h @@ -0,0 +1,149 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <array> +#include <chrono> +#include <cstdlib> +#include <string> + +#include "common/common_types.h" +#include "common/logging.h" + +// EDEN_NX2_GPU_PROFILE: time where the GPU thread spends its time. Each zone counts its own +// (exclusive) time, nested zones excluded; every 10 s the thread logs each zone's share. +namespace VideoCommon::Nx2Profile { + +enum class Zone : u32 { + Other, ///< GPU thread time outside every zone below (register writes, command parsing) + Idle, + PendingSleep, + ResumePending, + Push, + Tick, + Run, + FlushInvalidate, + Macro, + Draw, + Clear, + Compute, + FlushWork, + PipelineLookup, + Configure, + SyncDescriptors, + ReadHandles, + FillImageViews, + TextureBuffers, + UpdateBuffers, + DescriptorAcquire, + BindStageBuffers, + PushDescriptors, + RenderTargets, + ConfigureDraw, + DynamicState, + DrawRecord, + TextureUpload, + Count, +}; + +inline constexpr std::array<const char*, static_cast<size_t>(Zone::Count)> zone_names{ + "other", "idle", "pending sleep", "resume pending", "push", + "tick", "run", "flush/inval", "macro", "draw", + "clear", "compute", "flush work", "pipeline lookup", "configure", + "sync descriptors", "read handles", "fill image views", "texture buffers", "update buffers", + "descriptor acquire", "bind stage buffers", "push descriptors", "render targets", + "configure draw", "dynamic state", "draw record", "texture upload", +}; + +inline bool Enabled() { + static const bool enabled = std::getenv("EDEN_NX2_GPU_PROFILE") != nullptr; + return enabled; +} + +inline u64 Now() { + return static_cast<u64>(std::chrono::duration_cast<std::chrono::nanoseconds>( + std::chrono::steady_clock::now().time_since_epoch()) + .count()); +} + +struct ThreadData { + std::array<u64, static_cast<size_t>(Zone::Count)> exclusive_ns{}; + std::array<u64, static_cast<size_t>(Zone::Count)> count{}; + u64 child_ns{}; ///< Time of the zones nested in the current one + u64 window_start{}; +}; + +inline thread_local ThreadData t_data; + +class Scope { +public: + explicit Scope(Zone zone_) : zone{zone_}, enabled{Enabled()} { + if (!enabled) { + return; + } + parent_child_ns = t_data.child_ns; + t_data.child_ns = 0; + start = Now(); + } + + ~Scope() { + if (!enabled) { + return; + } + const u64 elapsed = Now() - start; + auto& data = t_data; + const auto index = static_cast<size_t>(zone); + data.exclusive_ns[index] += elapsed - data.child_ns; + ++data.count[index]; + data.child_ns = parent_child_ns + elapsed; + } + + Scope(const Scope&) = delete; + Scope& operator=(const Scope&) = delete; + +private: + Zone zone; + bool enabled; + u64 start{}; + u64 parent_child_ns{}; +}; + +/// Called by the GPU thread between commands: logs and resets the totals every 10 s. +inline void Report() { + if (!Enabled()) { + return; + } + auto& data = t_data; + const u64 now = Now(); + if (data.window_start == 0) { + data.window_start = now; + data.child_ns = 0; + return; + } + const u64 window = now - data.window_start; + if (window < 10'000'000'000ULL) { + return; + } + // Time in no zone at all is the wall time minus the zones' totals. + u64 zoned{}; + for (size_t i = 1; i < data.exclusive_ns.size(); ++i) { + zoned += data.exclusive_ns[i]; + } + data.exclusive_ns[0] = window > zoned ? window - zoned : 0; + std::string line; + for (size_t i = 0; i < data.exclusive_ns.size(); ++i) { + if (data.exclusive_ns[i] == 0 && data.count[i] == 0) { + continue; + } + // Per second of wall time: milliseconds spent and calls made. + const double ms = static_cast<double>(data.exclusive_ns[i]) * 1000.0 / window; + const double calls = static_cast<double>(data.count[i]) * 1e9 / window; + line += fmt::format(" | {} {:.1f} ms ({:.0f}/s)", zone_names[i], ms, calls); + } + LOG_INFO(HW_GPU, "GPU thread per second:{}", line); + data = {}; + data.window_start = now; +} + +} // namespace VideoCommon::Nx2Profile diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp index 172ee4dcaa..f737ad9f70 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp @@ -29,6 +29,7 @@ #include "video_core/vulkan_common/vulkan_device.h" #include "video_core/gpu_logging/gpu_logging.h" #include "common/settings.h" +#include "video_core/nx2_profile.h" #if defined(_MSC_VER) && defined(NDEBUG) #define LAMBDA_FORCEINLINE [[msvc::forceinline]] @@ -355,7 +356,11 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) { views.reserve(num_image_elements); samplers.reserve(num_textures); - texture_cache.SynchronizeDescriptors(false); + namespace Profile = VideoCommon::Nx2Profile; + { + Profile::Scope scope{Profile::Zone::SyncDescriptors}; + texture_cache.SynchronizeDescriptors(false); + } buffer_cache.SetUniformBuffersState(enabled_uniform_buffer_masks, &uniform_buffer_sizes); @@ -432,24 +437,31 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) { return true; }}; - if constexpr (Spec::enabled_stages[0]) { - config_stage(0); - } - if constexpr (Spec::enabled_stages[1]) { - config_stage(1); - } - if constexpr (Spec::enabled_stages[2]) { - config_stage(2); - } - if constexpr (Spec::enabled_stages[3]) { - config_stage(3); - } - if constexpr (Spec::enabled_stages[4]) { - config_stage(4); + { + Profile::Scope scope{Profile::Zone::ReadHandles}; + if constexpr (Spec::enabled_stages[0]) { + config_stage(0); + } + if constexpr (Spec::enabled_stages[1]) { + config_stage(1); + } + if constexpr (Spec::enabled_stages[2]) { + config_stage(2); + } + if constexpr (Spec::enabled_stages[3]) { + config_stage(3); + } + if constexpr (Spec::enabled_stages[4]) { + config_stage(4); + } } ASSERT(views.size() == num_image_elements); ASSERT(samplers.size() == num_textures); - texture_cache.FillImageViews(std::span(views.data(), views.size()), false, Spec::has_images); + { + Profile::Scope scope{Profile::Zone::FillImageViews}; + texture_cache.FillImageViews(std::span(views.data(), views.size()), false, + Spec::has_images); + } VideoCommon::ImageViewInOut* texture_buffer_it{views.data()}; const auto bind_stage_info{[&](size_t stage) LAMBDA_FORCEINLINE { @@ -493,39 +505,54 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) { texture_buffer_it += Shader::NumDescriptors(info.image_descriptors); } }}; - if constexpr (Spec::enabled_stages[0]) { - bind_stage_info(0); - } - if constexpr (Spec::enabled_stages[1]) { - bind_stage_info(1); - } - if constexpr (Spec::enabled_stages[2]) { - bind_stage_info(2); - } - if constexpr (Spec::enabled_stages[3]) { - bind_stage_info(3); - } - if constexpr (Spec::enabled_stages[4]) { - bind_stage_info(4); + { + Profile::Scope scope{Profile::Zone::TextureBuffers}; + if constexpr (Spec::enabled_stages[0]) { + bind_stage_info(0); + } + if constexpr (Spec::enabled_stages[1]) { + bind_stage_info(1); + } + if constexpr (Spec::enabled_stages[2]) { + bind_stage_info(2); + } + if constexpr (Spec::enabled_stages[3]) { + bind_stage_info(3); + } + if constexpr (Spec::enabled_stages[4]) { + bind_stage_info(4); + } } if (regs.transform_feedback_enabled != 0) { scheduler.RequestOutsideRenderPassOperationContext(); } - buffer_cache.UpdateGraphicsBuffers(is_indexed); - buffer_cache.BindHostGeometryBuffers(is_indexed); + { + Profile::Scope scope{Profile::Zone::UpdateBuffers}; + buffer_cache.UpdateGraphicsBuffers(is_indexed); + buffer_cache.BindHostGeometryBuffers(is_indexed); + } - guest_descriptor_queue.Acquire(scheduler, num_descriptor_entries, uses_descriptor_buffer); + { + Profile::Scope scope{Profile::Zone::DescriptorAcquire}; + guest_descriptor_queue.Acquire(scheduler, num_descriptor_entries, uses_descriptor_buffer); + } RescalingPushConstant rescaling; RenderAreaPushConstant render_area; const VideoCommon::SamplerId* samplers_it{samplers.data()}; const VideoCommon::ImageViewInOut* views_it{views.data()}; const auto prepare_stage{[&](size_t stage) LAMBDA_FORCEINLINE { - buffer_cache.BindHostStageBuffers(stage); - PushImageDescriptors(texture_cache, guest_descriptor_queue, stage_infos[stage], rescaling, - samplers_it, views_it); + { + Profile::Scope scope{Profile::Zone::BindStageBuffers}; + buffer_cache.BindHostStageBuffers(stage); + } + { + Profile::Scope scope{Profile::Zone::PushDescriptors}; + PushImageDescriptors(texture_cache, guest_descriptor_queue, stage_infos[stage], + rescaling, samplers_it, views_it); + } const auto& info{stage_infos[stage]}; if (info.uses_render_area) { render_area.uses_render_area = true; @@ -555,12 +582,16 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) { buffer_cache.runtime.PostCopyBarrier(); buffer_cache.any_buffer_uploaded = false; } - texture_cache.UpdateRenderTargets(false); - texture_cache.CheckFeedbackLoop(std::span<const VideoCommon::ImageViewInOut>{views.data(), - views.size()}); + { + Profile::Scope scope{Profile::Zone::RenderTargets}; + texture_cache.UpdateRenderTargets(false); + texture_cache.CheckFeedbackLoop( + std::span<const VideoCommon::ImageViewInOut>{views.data(), views.size()}); + } if (IsBuilt() && !pipeline) { return false; } + Profile::Scope scope{Profile::Zone::ConfigureDraw}; return ConfigureDraw(rescaling, render_area); } diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 609ac12fb4..decfb0d136 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -49,6 +49,7 @@ #include "video_core/texture_cache/texture_cache_base.h" #include "video_core/vulkan_common/vulkan_device.h" #include "video_core/vulkan_common/vulkan_wrapper.h" +#include "video_core/nx2_profile.h" namespace Vulkan { @@ -253,8 +254,12 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) { LOG_DEBUG(HW_GPU, "Native graphics draw failed; refusing submission completion"); } }; - FlushWork(); - gpu_memory->FlushCaching(); + namespace Profile = VideoCommon::Nx2Profile; + { + Profile::Scope scope{Profile::Zone::FlushWork}; + FlushWork(); + gpu_memory->FlushCaching(); + } const auto report_failure = [&](std::string_view reason) { static std::set<std::tuple<u64, u64, std::string_view>> seen; @@ -266,7 +271,10 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) { regs.rt[0].Address(), regs.rt[0].width, regs.rt[0].height); } }; - GraphicsPipeline* const pipeline{pipeline_cache.CurrentGraphicsPipeline()}; + GraphicsPipeline* const pipeline{[&] { + Profile::Scope scope{Profile::Zone::PipelineLookup}; + return pipeline_cache.CurrentGraphicsPipeline(); + }()}; if (!pipeline) { report_failure("no pipeline"); return; @@ -274,17 +282,23 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) { std::scoped_lock lock{buffer_cache.mutex, texture_cache.mutex}; // update engine as channel may be different. pipeline->SetEngine(maxwell3d, gpu_memory); - if (!pipeline->Configure(is_indexed)) { + if (Profile::Scope scope{Profile::Zone::Configure}; !pipeline->Configure(is_indexed)) { report_failure("configure failed"); return; } - UpdateDynamicStates(); + { + Profile::Scope scope{Profile::Zone::DynamicState}; + UpdateDynamicStates(); + } query_cache.NotifySegment(true); HandleTransformFeedback(); query_cache.CounterEnable(VideoCommon::QueryType::ZPassPixelCount64, maxwell3d->regs.zpass_pixel_count_enable); - draw_func(); + { + Profile::Scope scope{Profile::Zone::DrawRecord}; + draw_func(); + } submitted = true; static u32 native_capture_count{}; if (maxwell3d->GetNativeMacroEngine().native_turing && @@ -412,6 +426,7 @@ void TraceWindowTarget(const Tegra::Engines::Maxwell3D& maxwell3d, Tegra::Memory } // Anonymous namespace void RasterizerVulkan::Draw(bool is_indexed, u32 instance_count) { + VideoCommon::Nx2Profile::Scope profile_scope{VideoCommon::Nx2Profile::Zone::Draw}; TraceWindowTarget(*maxwell3d, *gpu_memory, is_indexed ? "draw-indexed" : "draw", is_indexed ? maxwell3d->draw_manager.draw_state.index_buffer.count : maxwell3d->draw_manager.draw_state.vertex_buffer.count); @@ -554,6 +569,7 @@ void RasterizerVulkan::Draw(bool is_indexed, u32 instance_count) { } void RasterizerVulkan::DrawIndirect() { + VideoCommon::Nx2Profile::Scope profile_scope{VideoCommon::Nx2Profile::Zone::Draw}; const auto& params = maxwell3d->draw_manager.indirect_state; buffer_cache.SetDrawIndirect(¶ms); PrepareDraw(params.is_indexed, [this, ¶ms] { @@ -659,6 +675,7 @@ void RasterizerVulkan::DrawTexture() { } void RasterizerVulkan::Clear(u32 layer_count) { + VideoCommon::Nx2Profile::Scope profile_scope{VideoCommon::Nx2Profile::Zone::Clear}; if (Tegra::Engines::g_nx2_trace_draws_after_upscale > 0) { const auto& regs = maxwell3d->regs; LOG_INFO(HW_GPU, "after upscale: clear rt0 {:#x} {}x{} fmt {:#x} surface {:#x} colour {} {} {} {}", @@ -852,6 +869,7 @@ void RasterizerVulkan::Clear(u32 layer_count) { } void RasterizerVulkan::DispatchCompute() { + VideoCommon::Nx2Profile::Scope profile_scope{VideoCommon::Nx2Profile::Zone::Compute}; FlushWork(); gpu_memory->FlushCaching(); diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index 75df31dc27..ad1804877a 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -28,6 +28,7 @@ #include "video_core/texture_cache/samples_helper.h" #include "video_core/texture_cache/texture_cache_base.h" #include "video_core/texture_cache/util.h" +#include "video_core/nx2_profile.h" #include "video_core/textures/decoders.h" namespace VideoCommon { @@ -1251,6 +1252,7 @@ void TextureCache<P>::RefreshContents(Image& image, ImageId image_id) { // Only upload modified images return; } + VideoCommon::Nx2Profile::Scope profile_scope{VideoCommon::Nx2Profile::Zone::TextureUpload}; image.flags &= ~ImageFlagBits::CpuModified; |
