From 28ca53dc891dc96a6cd8b3a81e61c6bcbf14eecc Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+yunusshaikh7@users.noreply.github.com> Date: Mon, 12 Oct 2026 08:08:31 -0700 Subject: [PATCH] feat(agc): report performance timer scopes to Tracy with APS5_ENABLE_TRACY APS5_ENABLE_TRACY=ON builds the Tracy 0.14.1 client from the new 3rdparty/tracy submodule as a shared library next to the patched libraries. Every PerformanceTimer scope of libSceAgcDriver and libSceVideoOut becomes a Tracy zone, each presented flip ends a frame, and the queue workers and the presenter name their threads. The client listens only on localhost and records only while a profiler is connected; sampling, system tracing, call stacks, code transfer and its crash handler are off. Without the option nothing of Tracy is compiled. On MinGW the client is built without thread_local destructors and with TRACY_DELAYED_INIT, because emulated TLS frees thread_local objects before their destructors run; a thread's event producer then stays alive until the process exits instead of being destroyed on freed memory. --- .gitmodules | 3 + 3rdparty/tracy | 1 + CMakeLists.txt | 4 + cmake/Tracy.cmake | 21 +++++ cmake/TracyThreadExit.cpp | 3 + core/libs/CMakeLists.txt | 18 +++++ core/libs/prx/libSceAgcDriver/CMakeLists.txt | 3 + .../Execution/include/PerformanceTimer.hpp | 3 + .../Execution/include/TracyZone.hpp | 81 +++++++++++++++++++ .../Execution/src/Driver/Presentation.cpp | 2 + .../src/Driver/Queues/WorkerAffinity.cpp | 2 + docs/dev/BUILD.md | 7 ++ 12 files changed, 148 insertions(+) create mode 160000 3rdparty/tracy create mode 100644 cmake/Tracy.cmake create mode 100644 cmake/TracyThreadExit.cpp create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/TracyZone.hpp diff --git a/.gitmodules b/.gitmodules index fc2e48631..e9d45bf28 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,6 +28,9 @@ [submodule "3rdparty/stb"] path = 3rdparty/stb url = https://github.com/nothings/stb.git +[submodule "3rdparty/tracy"] + path = 3rdparty/tracy + url = https://github.com/wolfpld/tracy.git [submodule "3rdparty/libjpeg-turbo"] path = 3rdparty/libjpeg-turbo url = https://github.com/libjpeg-turbo/libjpeg-turbo.git diff --git a/3rdparty/tracy b/3rdparty/tracy new file mode 160000 index 000000000..30997d5ca --- /dev/null +++ b/3rdparty/tracy @@ -0,0 +1 @@ +Subproject commit 30997d5ca6bb632cc10807a1da8a6d3de0aeeb3c diff --git a/CMakeLists.txt b/CMakeLists.txt index a551fe53a..ad24fb3c1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,7 @@ endif() option(ANYPS5_ENABLE_SPIRV_TOOLS "Enable SPIRV-Tools validation and optimization" OFF) option(ANYPS5_ENABLE_ASAN "Build Linux targets with AddressSanitizer and use an ASan-compatible guest arena range" OFF) option(APS5_ENABLE_TIMING_LOG "Write frame timings asynchronously to frame-timing.log" OFF) +option(APS5_ENABLE_TRACY "Build the Tracy profiler client and report performance timer scopes as Tracy zones" OFF) option(ANYPS5_USE_SYSTEM_DEPS "Resolve SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools from the system instead of the bundled submodules" OFF) add_compile_definitions(APS5_ENABLE_TIMING_LOG=$) include(cmake/SystemDeps.cmake) @@ -165,5 +166,8 @@ endif() add_subdirectory(core/shader/recompiler) add_subdirectory(core/Decoder/Jpeg) add_subdirectory(core/Decoder/Png) +if(APS5_ENABLE_TRACY) + include(cmake/Tracy.cmake) +endif() add_subdirectory(core/libs) add_subdirectory(core/relinker) diff --git a/cmake/Tracy.cmake b/cmake/Tracy.cmake new file mode 100644 index 000000000..d108607a0 --- /dev/null +++ b/cmake/Tracy.cmake @@ -0,0 +1,21 @@ +set(TRACY_ENABLE ON CACHE BOOL "" FORCE) +set(TRACY_STATIC OFF CACHE BOOL "" FORCE) +set(TRACY_ON_DEMAND ON CACHE BOOL "" FORCE) +set(TRACY_ONLY_LOCALHOST ON CACHE BOOL "" FORCE) +set(TRACY_NO_CRASH_HANDLER ON CACHE BOOL "" FORCE) +set(TRACY_NO_CODE_TRANSFER ON CACHE BOOL "" FORCE) +set(TRACY_NO_SAMPLING ON CACHE BOOL "" FORCE) +set(TRACY_NO_SYSTEM_TRACING ON CACHE BOOL "" FORCE) +set(TRACY_NO_CALLSTACK ON CACHE BOOL "" FORCE) +set(TRACY_NO_FRAME_IMAGE ON CACHE BOOL "" FORCE) +set(TRACY_NO_VERIFY ON CACHE BOOL "" FORCE) +set(CMAKE_DISABLE_FIND_PACKAGE_rocprofiler-sdk ON) +add_subdirectory(${CMAKE_SOURCE_DIR}/3rdparty/tracy ${CMAKE_BINARY_DIR}/3rdparty/tracy EXCLUDE_FROM_ALL) + +if(MINGW) + target_sources(TracyClient PRIVATE ${CMAKE_CURRENT_LIST_DIR}/TracyThreadExit.cpp) + target_compile_definitions(TracyClient PRIVATE TRACY_DELAYED_INIT) + get_target_property(tracyInterfaceDefinitions TracyClient INTERFACE_COMPILE_DEFINITIONS) + list(FILTER tracyInterfaceDefinitions EXCLUDE REGEX "^(WINVER|_WIN32_WINNT)=") + set_target_properties(TracyClient PROPERTIES INTERFACE_COMPILE_DEFINITIONS "${tracyInterfaceDefinitions}") +endif() diff --git a/cmake/TracyThreadExit.cpp b/cmake/TracyThreadExit.cpp new file mode 100644 index 000000000..704dce3e4 --- /dev/null +++ b/cmake/TracyThreadExit.cpp @@ -0,0 +1,3 @@ +extern "C" int __cxa_thread_atexit(void (*)(void*), void*, void*) { + return 0; +} diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index d094f39cf..f1a1ab9a2 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -149,6 +149,24 @@ foreach(LIB_DIR ${LIBS_DIRS}) endif() endforeach() +if(APS5_ENABLE_TRACY) + set_target_properties(TracyClient PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${UNPATCHED_DIR} + LIBRARY_OUTPUT_DIRECTORY ${UNPATCHED_DIR} + ARCHIVE_OUTPUT_DIRECTORY ${IMPLIB_DIR} + ) + set(TRACY_CLIENT_COPIED ${CMAKE_CURRENT_BINARY_DIR}/TracyClient.copied) + add_custom_command( + OUTPUT ${TRACY_CLIENT_COPIED} + COMMAND ${CMAKE_COMMAND} -E make_directory ${PATCHED_DIR} + COMMAND ${CMAKE_COMMAND} -E copy $ ${PATCHED_DIR} + COMMAND ${CMAKE_COMMAND} -E touch ${TRACY_CLIENT_COPIED} + DEPENDS TracyClient + COMMENT "Copying the Tracy client next to the patched libraries" + ) + list(APPEND PATCHED_OUTPUTS ${TRACY_CLIENT_COPIED}) +endif() + add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index e7428c6d5..4cfd84ce1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -265,6 +265,9 @@ target_link_libraries(libSceAgcDriver PRIVATE jpeg_turbo ${CMAKE_DL_LIBS} ) +if(APS5_ENABLE_TRACY) + target_link_libraries(libSceAgcDriver PUBLIC TracyClient) +endif() set_target_properties(libSceAgcDriver PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(libSceAgcDriver) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp index c2cf77331..28ccc5c2d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PERFORMANCETIMER_HPP #include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" +#include "prx/libSceAgcDriver/Execution/include/TracyZone.hpp" #include #include #include @@ -275,6 +276,7 @@ public: } void Finish() { + zone.End(); if (frame == nullptr && trace == nullptr) return; const auto end = Clock::now(); if (trace != nullptr) trace->Add(scope, nullptr, start, end); @@ -307,6 +309,7 @@ private: FrameTiming* frame; TraceThread* trace; const char* scope; + [[no_unique_address]] TracyZone zone{scope}; FrameTiming::Metric* total = nullptr; FrameTiming::Metric* tail = nullptr; Clock::time_point start{}; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/TracyZone.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/TracyZone.hpp new file mode 100644 index 000000000..f76321809 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/TracyZone.hpp @@ -0,0 +1,81 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_TRACYZONE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_TRACYZONE_HPP + +#ifdef TRACY_ENABLE +#include +#include +#include +#include +#include +#include +#include +#endif + +namespace AgcDriver { + +class TracyZone { +public: + explicit TracyZone([[maybe_unused]] const char* name) { +#ifdef TRACY_ENABLE + context = ___tracy_emit_zone_begin(location(name), 1); +#endif + } + + TracyZone(const TracyZone&) = delete; + TracyZone& operator=(const TracyZone&) = delete; + + ~TracyZone() { + End(); + } + + void End() { +#ifdef TRACY_ENABLE + if (context.active == 0) return; + ___tracy_emit_zone_end(context); + context.active = 0; +#endif + } + + static void MarkFrame() { +#ifdef TRACY_ENABLE + ___tracy_emit_frame_mark(nullptr); +#endif + } + + static void NameThread([[maybe_unused]] const char* name) { +#ifdef TRACY_ENABLE + ___tracy_set_thread_name(name); +#endif + } + +private: +#ifdef TRACY_ENABLE + static const ___tracy_source_location_data* location(const char* name) { + static constexpr std::size_t capacity = 256; + static std::array, capacity> names{}; + static std::array<___tracy_source_location_data, capacity> locations{}; + static std::mutex mutex; + const auto start = static_cast(reinterpret_cast(name) >> 4); + for (std::size_t probe = 0; probe < capacity; ++probe) { + const auto index = (start + probe) % capacity; + const auto* known = names[index].load(std::memory_order_acquire); + if (known == name) return &locations[index]; + if (known != nullptr) continue; + std::lock_guard lock(mutex); + known = names[index].load(std::memory_order_relaxed); + if (known == name) return &locations[index]; + if (known != nullptr) continue; + locations[index] = {name, name, "", 0, 0}; + names[index].store(name, std::memory_order_release); + return &locations[index]; + } + throw std::runtime_error("Tracy: too many zone names"); + } + + TracyCZoneCtx context; +#endif +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp index 8ea2d6e87..83f0f25ae 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Queues/WorkerAffinity.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include "prx/libSceAgcDriver/Execution/include/TracyZone.hpp" #include namespace AgcDriver::DriverDetail { @@ -127,6 +128,7 @@ void Driver::Present(const PresentationWindow& window, const DisplayBuffer* buff } gpuReady(context); timing.Mark("release_and_callback"); + TracyZone::MarkFrame(); if (profile) reportPresents(waitedMs, inFlight); CheckFailure(); } catch (const ProcessShutdown&) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp index d9d04c893..0bac6c5eb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Queues/WorkerAffinity.hpp" +#include "prx/libSceAgcDriver/Execution/include/TracyZone.hpp" #include "prx/libc/include/CpuTopology.hpp" #include @@ -20,6 +21,7 @@ std::uint64_t WorkerAffinityMask() { } void PinWorkerThread(const char* role) { + TracyZone::NameThread(role); const auto mask = WorkerAffinityMask(); if (mask == 0) return; static std::once_flag summary; diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 8825151d9..d886b8cee 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -62,6 +62,7 @@ Project switches accept `ON` or `OFF`: | `-DANYPS5_ENABLE_ASAN=ON` | `OFF` | Build with AddressSanitizer on Linux and move the guest arena out of ASan's shadow gap. | | `-DANYPS5_USE_SYSTEM_DEPS=ON` | `OFF` | Resolve SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools from the system instead of the submodules. | | `-DAPS5_ENABLE_TIMING_LOG=ON` | `OFF` | Compile frame timing logging. | +| `-DAPS5_ENABLE_TRACY=ON` | `OFF` | Build the [Tracy](#tracy) profiler client into the agc driver. | | `-DAPS5_AGC_CREATE_LOG=OFF` | `ON` | Disable successful `sceAgcCreateShader` logging. | | `-DAGC_BUILD_VISUAL_TEST=ON` | `OFF` | Build the standalone AGC SPIR-V visual test. | @@ -92,6 +93,12 @@ ctest --test-dir build/write-watch --output-on-failure With `CMAKE_OSX_ARCHITECTURES=x86_64` Apple silicon runs the tests through Rosetta, which is how relinked titles run; without it they run natively. The full `BUILD_TESTING` build on macOS also registers them. +## Tracy + +`-DAPS5_ENABLE_TRACY=ON` builds the [Tracy](https://github.com/wolfpld/tracy) 0.14.1 client from `3rdparty/tracy` as a shared library next to the patched `.prx` files (`libTracyClient.dll`, `libTracyClient.so.0.14.1` on Linux); copy it with them. Every `PerformanceTimer` scope of libSceAgcDriver and libSceVideoOut is a zone, every presented flip ends a frame, and the queue workers and the presenter are named threads. Connect the Tracy 0.14.1 profiler to `127.0.0.1`: the client listens only on localhost and records only while a profiler is connected. Sampling, system tracing, call stacks, code transfer and Tracy's crash handler are disabled. Without the flag nothing of Tracy is built. + +On Windows MinGW frees `thread_local` objects before their destructors run, so the Tracy client is built without `thread_local` destructors and never destroys its event queue: a thread that emitted a zone and ended keeps its event blocks (2 MiB each) until the process exits. + ## Compute pipeline replay Build `agc_compute_pipeline_replay` in the full build with `-DANYPS5_ENABLE_SPIRV_TOOLS=ON`; replay validates the captured module before calling Vulkan. Set `APS5_DUMP_COMPUTE_PIPELINES` to a dedicated empty directory before starting a title to save each uncached compute pipeline's final SPIR-V, descriptor layouts, push constant ranges, specialization values, subgroup size and creation flags before compilation. Files are named `compute-/capture.aps5cp`; identical records are deduplicated. Capture stops with an error at 128 records, 256 MiB total or 36 MiB per record (at most 8,388,608 SPIR-V words, or 32 MiB of shader code). Capture files contain shader code and device metadata, but no guest buffers or textures; review them before sharing.