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.
This commit is contained in:
Yunus Shaikh 2026-10-12 08:08:31 -07:00
parent 3e0e01f35f
commit 28ca53dc89
12 changed files with 148 additions and 0 deletions

3
.gitmodules vendored
View File

@ -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

1
3rdparty/tracy vendored Submodule

@ -0,0 +1 @@
Subproject commit 30997d5ca6bb632cc10807a1da8a6d3de0aeeb3c

View File

@ -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=$<BOOL:${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)

21
cmake/Tracy.cmake Normal file
View File

@ -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()

View File

@ -0,0 +1,3 @@
extern "C" int __cxa_thread_atexit(void (*)(void*), void*, void*) {
return 0;
}

View File

@ -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 $<TARGET_FILE:TracyClient> ${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)

View File

@ -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)

View File

@ -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 <algorithm>
#include <chrono>
#include <cstddef>
@ -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{};

View File

@ -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 <tracy/TracyC.h>
#include <array>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <stdexcept>
#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<std::atomic<const char*>, capacity> names{};
static std::array<___tracy_source_location_data, capacity> locations{};
static std::mutex mutex;
const auto start = static_cast<std::size_t>(reinterpret_cast<std::uintptr_t>(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

View File

@ -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 <cstdlib>
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&) {

View File

@ -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 <cstdlib>
@ -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;

View File

@ -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-<identity>/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.