Merge pull request #11365 from oneandonlydean/sa/per-draw-gpu-timing

feat(agc): opt-in per-draw GPU timing and dump-by-address diagnostics
This commit is contained in:
Dean authored and GitHub committed 2026-10-27 11:08:24 +13:00
commit 74acfad65b
8 files changed
+58 -4

No files matched your search

+2 -1
View File
@@ -1560,12 +1560,13 @@ configure_windows_unwind(agc_driver_recorder_tests)
if(BUILD_TESTING)
set_target_properties(agc_driver_recorder_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_recorder_device_selection COMMAND agc_driver_recorder_tests --device-selection-only)
add_test(NAME agc_gpu_timing_key COMMAND agc_driver_recorder_tests --gpu-timing-key-only)
add_test(NAME agc_gpu_timing_complete COMMAND agc_driver_recorder_tests --gpu-timing-complete-only)
add_test(NAME agc_gpu_timing_incomplete COMMAND agc_driver_recorder_tests --gpu-timing-incomplete-only)
set_tests_properties(agc_gpu_timing_complete agc_gpu_timing_incomplete PROPERTIES
ENVIRONMENT "APS5_PROFILE_GPU=1" TIMEOUT 15 SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(agc_recorder_device_selection agc_gpu_timing_complete agc_gpu_timing_incomplete PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(agc_recorder_device_selection agc_gpu_timing_key agc_gpu_timing_complete agc_gpu_timing_incomplete PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_command_memory_reuse COMMAND agc_driver_recorder_tests --command-reuse-only)
@@ -1210,6 +1210,15 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
return DrawVerdict::Rejected;
}
}
if (static const bool nameVariants = std::getenv("APS5_PROFILE_GPU_DRAWS") != nullptr; nameVariants && stages.size() == programs.size()) {
static std::mutex namedMutex;
static std::set<std::uint64_t> named;
std::lock_guard namedLock(namedMutex);
for (std::size_t i = 0; i < stages.size(); ++i) {
if (stages[i].program == nullptr || !named.insert(stages[i].program->variantId).second) continue;
std::fprintf(stderr, "[drawvariant] 0x%llx stage %d code 0x%llx\n", static_cast<unsigned long long>(stages[i].program->variantId), static_cast<int>(stages[i].stage), static_cast<unsigned long long>(programs[i].binary.codeAddress));
}
}
if (pipelined && !drawParameters.indirect && (tail.queue == nullptr || deferredLabels().labels.empty())) {
phaseTiming.Phase(DrawRowVectors);
auto writes = drawWriteRanges(graphics, stages);
@@ -133,7 +133,7 @@ std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::materializeDraw
if (dumpTarget != 0) {
const auto slot0 = (static_cast<std::uint64_t>(readRegister(queue.context, 0x390)) << 40u) | (static_cast<std::uint64_t>(readRegister(queue.context, 0x318)) << 8u);
if ((graphics.hasColorTarget && graphics.color.address == dumpTarget) || slot0 == dumpTarget) static_cast<void>(dumpRequest(program.binary.codeAddress, request));
if ((graphics.hasColorTarget && graphics.color.address == dumpTarget) || slot0 == dumpTarget || program.binary.codeAddress == dumpTarget) static_cast<void>(dumpRequest(program.binary.codeAddress, request));
}
if (dumpSlot1 != 0) {
const auto value = [&](std::uint32_t offset) -> std::uint64_t { const auto it = queue.context.find(offset); return it == queue.context.end() ? 0u : it->second; };
@@ -22,6 +22,7 @@
#include <tuple>
#include <unordered_set>
#include <utility>
#include <string>
#include <vector>
namespace AgcDriver::Graphics {
@@ -472,9 +473,13 @@ public:
// queries around each timed range; the totals per key are reported every 10 s. Begin returns the
// range index to pass to End, or NoTiming when timing is off or the batch's queries are used up.
static constexpr std::uint32_t NoTiming = 0xffffffffu;
static constexpr std::uint64_t DrawTimingFlag = std::uint64_t{1} << 63u;
static constexpr std::uint64_t DrawTimingKey(std::uint64_t variantId) { return variantId | DrawTimingFlag; }
static std::string TimingKeyLabel(std::uint64_t key);
static bool GpuTimingEnabled();
static bool BatchStampsEnabled();
std::uint32_t BeginGpuTiming(std::uint64_t key);
std::uint32_t BeginInPassGpuTiming(std::uint64_t key);
// `bytes`: what the range moved (a fill's, a copy's), summed per key on the [gputime] line.
void EndGpuTiming(std::uint32_t index, std::uint64_t bytes = 0);
// The key of the whole-batch range (first to last command; reported as "batch" on the [gputime]
@@ -2328,6 +2328,15 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
APS5_LOG_CHARS_OUT_DEBUG("Resources bound");
pushDrawConstants(*record.pipeline, commands, state, draw, shaders, resources, record.pushBytes, record.pushStages, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0, !inputs.restartTable.empty());
APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded");
static const bool timeEachDraw = std::getenv("APS5_PROFILE_GPU_DRAWS") != nullptr;
auto eachDrawTiming = Recorder::NoTiming;
if (timeEachDraw) {
std::uint64_t key = 0;
for (const auto& shader : shaders) {
if (shader.program != nullptr && (key == 0 || shader.stage == ShaderRecompiler::ShaderStage::Fragment)) key = shader.program->variantId;
}
eachDrawTiming = recorder->BeginInPassGpuTiming(Recorder::DrawTimingKey(key));
}
if (record.stencilReplay.empty()) recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset, trace, subdraw);
else {
recordStencilReplay(context, commands, state, draw, inputs, record.stencilReplay, shaders, resources, record.pushBytes, record.pushStages);
@@ -2341,6 +2350,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
record.pipeline->ContinueSecondChannel(commands);
recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset, trace, subdraw);
}
recorder->EndGpuTiming(eachDrawTiming);
if (meshArguments != nullptr) recorder->Keep(meshArguments);
if (args != nullptr) CountIndirectDraw(record.indirect->path, record.indirect->readMs, rewritten);
auto checkRecords = indirectRecordCheck(record.indirect);
@@ -2017,6 +2017,17 @@ std::uint32_t Recorder::BeginGpuTiming(std::uint64_t key) {
return beginTiming(key);
}
std::string Recorder::TimingKeyLabel(std::uint64_t key) {
char text[32];
std::snprintf(text, sizeof(text), (key & DrawTimingFlag) != 0 ? "draw 0x%llx" : "0x%llx", static_cast<unsigned long long>(key & ~DrawTimingFlag));
return text;
}
std::uint32_t Recorder::BeginInPassGpuTiming(std::uint64_t key) {
if (!GpuTimingEnabled() || open == nullptr || open->queries == VK_NULL_HANDLE) return NoTiming;
return beginTiming(key);
}
std::uint32_t Recorder::beginTiming(std::uint64_t key) {
const bool full = GpuTimingEnabled();
if (!full && !(key == BatchTimingKey && BatchStampsEnabled())) return NoTiming;
@@ -2688,7 +2699,7 @@ void Recorder::readGpuTiming(Batch& batch) {
std::sort(hot.begin(), hot.end(), [](const auto& a, const auto& b) { return a.second.ms > b.second.ms; });
// The first field stays the program sum: the measure scripts match on it.
AgcDriver::ProfilePrint_nid_no_patch("[gputime] %.0f ms of GPU time in %llu batches over 10 s (batch %.0f ms first to last command; classes %.0f ms, all ranges %.0f ms, untimed %.0f ms outside every range; %llu presents; %llu ranges dropped at the %u cap); by program:", timingProgramMs, static_cast<unsigned long long>(timingBatches), timingBatchMs, timingClassMs, timingUnionMs, timingBatchMs - timingUnionMs, static_cast<unsigned long long>(presents), static_cast<unsigned long long>(timingDropped.exchange(0, std::memory_order_relaxed)), MaxTimedRanges);
for (std::size_t i = 0; i < hot.size() && i < 12; ++i) AgcDriver::ProfilePrint_nid_no_patch(" 0x%llx x%llu %.0fms", static_cast<unsigned long long>(hot[i].first), static_cast<unsigned long long>(hot[i].second.count), hot[i].second.ms);
for (std::size_t i = 0; i < hot.size() && i < 12; ++i) AgcDriver::ProfilePrint_nid_no_patch(" %s x%llu %.0fms", Recorder::TimingKeyLabel(hot[i].first).c_str(), static_cast<unsigned long long>(hot[i].second.count), hot[i].second.ms);
AgcDriver::ProfilePrint_nid_no_patch("; by class:%s\n", classes.c_str());
timingByKey.clear();
timingProgramMs = timingClassMs = timingUnionMs = timingBatchMs = 0;
@@ -252,6 +252,18 @@ bool hintMappingHistoryTests() {
#endif
}
void gpuTimingKeyTests() {
constexpr std::uint64_t variant = 0x2;
const auto key = Recorder::DrawTimingKey(variant);
Require(key != variant && (key & Recorder::DrawTimingFlag) != 0, "a draw's timing key is not flagged apart from the other keys");
char named[64];
std::snprintf(named, sizeof(named), "0x%llx", static_cast<unsigned long long>(variant));
Require(Recorder::TimingKeyLabel(key) == std::string("draw ") + named, "a draw's timing label is not 'draw ' plus the id [drawvariant] prints");
Require(Recorder::TimingKeyLabel(variant) == named, "a key that is not a draw's is labelled with the draw prefix");
Require(Recorder::TimingKeyLabel(Recorder::DrawTimingKey(0)) == "draw 0x0", "a draw with no program is not labelled as a draw");
Require(Recorder::TimingKeyLabel(Recorder::DrawTimingKey(0x123456789abcdefull)) == "draw 0x123456789abcdef", "a wide variant id was cut in the label");
}
void deviceSelectionTests() {
const auto properties = [](const char* name, VkPhysicalDeviceType type) {
VkPhysicalDeviceProperties result{};
@@ -11492,6 +11504,11 @@ int main(int argc, char** argv) {
std::cout << "partial private write-watch restoration tests passed\n";
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--gpu-timing-key-only") {
gpuTimingKeyTests();
std::cout << "gpu timing key tests passed\n";
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--device-selection-only") {
deviceSelectionTests();
std::cout << "recorder device selection tests passed\n";
+2 -1
View File
@@ -136,7 +136,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti
| `APS5_DRAW_UPLOAD_MAX` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 4096 | largest draw snapshot in bytes copied into the per-recorder upload chunk; 0 turns the chunk off |
| `APS5_DUMP_COMPUTE_MIN_WORDS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | only captures compute pipelines whose SPIR-V has at least this many words |
| `APS5_DUMP_COMPUTE_PIPELINES` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | directory in which captured compute pipelines are written |
| `APS5_DUMP_DRAW_SHADERS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | 0 | hex color-target or slot-0 address whose draw's shaders are dumped |
| `APS5_DUMP_DRAW_SHADERS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | 0 | hex color-target or slot-0 address, or a program's code address, whose draw's shaders are dumped |
| `APS5_DUMP_DRAW_SLOT1` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | 0 | hex slot-1 address whose draw's shaders and register state are dumped |
| `APS5_DUMP_FRAMES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 0 | saves the first n presented frames as frame_<index>.bmp |
| `APS5_DUMP_FRAMES_EVERY` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 1 | dumps every n-th presented frame |
@@ -370,6 +370,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti
| `APS5_PROBE_DISPATCH` | string | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | probes one dispatch at a hex program address (optionally `:index`) with debug output |
| `APS5_PROFILE_DRAW` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | prints the per-phase driver and draw timing totals |
| `APS5_PROFILE_GPU` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records the GPU time of recorded work by key from batch timestamps |
| `APS5_PROFILE_GPU_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp`, `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | with APS5_PROFILE_GPU, records the GPU time of each recorded draw by its fragment program's variant id and prints a [drawvariant] line with the code address behind each variant id once (draws that do not use a recipe only; the [gputime] line labels each `draw 0x<variant>`, the id [drawvariant] prints, and the ranges share the batch's query cap) |
| `APS5_REAP_EACH_PACKET` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | reaps deferred labels after every packet |
| `APS5_RELEASE_ON_UNLOCK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | destroys deferred batches on the unlocking thread instead of a release thread |
| `APS5_RELEASE_QUEUE_MAX` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | 256 | maximum deferred batches handed to the release thread before a thread destroys its own |