feat(agc): fill the overlay's second page from the driver's counters

Page 2 now samples its sources once a second, in NotePresent, while it
shows; nothing of it runs while the overlay is hidden or on page 1.

- Busy share per queue worker, the top 5 phases in ms per frame and queue
  0's waits (memory, lock, GPU) come from the trace's per-thread totals.
  Showing page 2 turns recording on with buffering off, so a timed scope
  only adds to its thread's totals; leaving it turns recording off again.
  Toggle switches it under a mutex, and ANYPS5_OVERLAY=2 switches it on at
  start. An APS5_TRACE_* run keeps its own recording untouched, and page 2
  then shows these lines only while that trace records. A driver thread
  names itself when it starts (BindThread without APS5_TRACE_* stores the
  name only) and binds on its first timed scope while page 2 shows;
  unnamed threads are not bound.
- Render passes per frame: the draw path counts a begun pass in the trace
  totals while page 2's totals are on (PerformanceTrace::Count).
- Flips/s and the draw cache's hit rate come from the driver through a
  source it registers at start (the draw cache's lock is only tried); the
  pipeline store's hit rate from PipelineStoreTotals; the compile backlog
  from the shader preparation and library prebuild pools (tried locks).
- Dropped frames per second are the presents that found no free swapchain
  image; repeated ones are presents of the display buffer shown just before.
  The device passes the buffer's address, its present mode and the drops.
- Commit charge against the commit limit: GetPerformanceInfo on Windows,
  Committed_AS and CommitLimit from /proc/meminfo on Linux; page faults per
  second from PageFaultCount or getrusage.
- Host imports: their count and bytes against the budget, on devices that
  import host memory.
- CPU copies: address-based builds count the bytes they copy in a
  per-thread counter (a relaxed load and store, folded once a second) with
  the largest build of each second against the address-copy cap.
- Unimplemented-function calls: libc counts NotImplemented_nid_no_patch
  calls; refused shaders are the APS5_SKIP_REFUSED_DRAWS set's size.
- Title ID (when the title has a param.json) and present mode.

The tests cover the trace deltas, hit rates, present-mode names, the
/proc/meminfo parser and the OS reads, the per-thread copy counter's fold,
the counted unimplemented call, the trace switch with page 2 and lazy
binding without buffering, and on lavapipe the sampler end to end with a
fake driver source, its reset when page 2 is left, and the cost of a
present hidden, on page 1 and on page 2.
This commit is contained in:
Yunus Shaikh authored and Adrià Franch committed 2026-10-26 20:42:29 +01:00
1 parent b6d3ae0dd6
commit b1889783be
21 files changed
+837 -19

No files matched your search

@@ -2,7 +2,9 @@
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DIAGNOSTICS_HPP
#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp"
#include <cstddef>
#include <cstdint>
#include <optional>
#include <stdexcept>
#include <string>
@@ -21,6 +23,7 @@ public:
bool SkipRefusedDraws();
void ReportRefusedDraw(const RefusedDraw& refused);
std::optional<std::size_t> RefusedShaderCount();
void require(bool condition, const char* reason);
@@ -45,6 +45,7 @@
namespace AgcDriver {
class PerformanceTimer;
struct OverlayDriverCounters;
}
namespace AgcDriver::DriverDetail {
@@ -99,6 +100,7 @@ public:
static void tagVariantRuns(DispatchVariant& variant, std::span<const ShaderRecompiler::MemoryRegion> regions, const ShaderRecompiler::SrtUserDataUse& use, bool eligible, std::uint32_t hiddenWords, std::span<const std::shared_ptr<const ShaderRecompiler::SrtGuestWords>> supplied = {});
static std::string dumpRequest(std::uint64_t address, const ShaderRecompiler::RecompileRequest& request, std::string_view suffix = {});
DrawEntryCounters DrawCacheCounters();
OverlayDriverCounters OverlayCounters();
EntryCounters DispatchCacheCounters();
struct DispatchCacheFootprint {
std::size_t entries = 0;
@@ -227,6 +227,7 @@ void SubmitLibraryPrebuild(std::function<void()> task);
void AwaitLibraryPrebuilds();
void CloseRegistrationPreparation();
unsigned RegistrationPreparationThreads();
std::optional<std::size_t> RegistrationPreparationsPending();
void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr<const ShaderSnapshot>& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target, bool deferUnprepared = false);
@@ -4,6 +4,7 @@
#define VK_NO_PROTOTYPES
#include <vulkan/vulkan.h>
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp"
#include <array>
#include <atomic>
#include <chrono>
@@ -13,6 +14,7 @@
#include <map>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <tuple>
@@ -205,6 +207,31 @@ struct OverlayDetailSample {
std::string presentMode;
};
struct OverlayHits {
std::uint64_t lookups = 0;
std::uint64_t hits = 0;
};
struct OverlayDriverCounters {
std::uint64_t flips = 0;
std::optional<OverlayHits> drawCache;
};
struct OverlayPresentInfo {
std::uint64_t displayAddress = 0;
VkPresentModeKHR presentMode = VK_PRESENT_MODE_MAX_ENUM_KHR;
std::uint64_t acquireDrops = 0;
};
struct OverlayTraceFigures {
std::vector<OverlayThreadBusy> busy;
std::vector<OverlayPhase> phases;
std::optional<OverlayQueueWaits> queueWaits;
std::optional<double> renderPassesPerFrame;
};
inline constexpr std::size_t OverlayTopPhases = 5;
struct OverlayLayout {
OverlayRect bounds;
std::vector<OverlayRect> rects;
@@ -221,9 +248,15 @@ OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, boo
OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver, const OverlayDetails& details);
std::string OverlayText(std::string_view text);
OverlayContent ComposeOverlayDetails(const OverlayDetailSample& sample);
OverlayTraceFigures OverlayTraceDeltas(std::span<const TraceThreadTotals> before, std::span<const TraceThreadTotals> now, OverlayStatistics::Clock::duration elapsed, std::uint64_t presents, bool passes);
std::optional<double> OverlayHitPercent(const OverlayHits& before, const OverlayHits& now);
std::string_view OverlayPresentModeName(VkPresentModeKHR mode);
bool OverlaySampleDue(std::optional<OverlayStatistics::Clock::time_point> sampledAt, OverlayStatistics::Clock::time_point now);
std::optional<OverlayMemory> ReadVramBudget(const Graphics::Context& context);
std::optional<std::uint64_t> ReadProcessPrivateBytes();
std::optional<OverlayMemory> ParseCommitCharge(std::string_view meminfo);
std::optional<OverlayMemory> ReadCommitCharge();
std::optional<std::uint64_t> ReadPageFaults();
std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content);
OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, OverlayCorner corner);
std::array<std::array<float, 4>, OverlayColorCount> OverlayPalette(VkFormat format, VkColorSpaceKHR colorSpace);
@@ -239,8 +272,9 @@ public:
static std::uint32_t Page();
static void Toggle();
static OverlayCorner Corner();
static void SetDriverCounters(OverlayDriverCounters (*source)());
void NotePresent(OverlayStatistics::Clock::time_point at, VkExtent2D display);
void NotePresent(OverlayStatistics::Clock::time_point at, VkExtent2D display, const OverlayPresentInfo& info = {});
const OverlayStatistics& Statistics() const { return statistics; }
const OverlayDetails& Details() const { return details; }
const OverlayDetailSample& DetailSample() const { return detailSample; }
@@ -260,7 +294,29 @@ private:
VkPipeline pipeline(VkFormat format, VkRenderPass compatible);
void releaseTransient(Slot& slot) noexcept;
void release() noexcept;
struct DetailCounters {
std::optional<OverlayDriverCounters> driver;
OverlayHits pipelines;
std::uint64_t copiedBytes = 0;
std::optional<std::uint64_t> pageFaults;
std::uint64_t unimplemented = 0;
std::uint64_t acquireDrops = 0;
};
struct DetailState {
std::optional<OverlayStatistics::Clock::time_point> sampledAt;
DetailCounters base;
std::uint64_t repeats = 0;
std::uint64_t lastDisplayAddress = 0;
std::optional<OverlayStatistics::Clock::time_point> traceSampledAt;
std::vector<TraceThreadTotals> traceBase;
std::uint64_t tracePresents = 0;
};
void restartIfToggled();
void noteDetailPresent(OverlayStatistics::Clock::time_point at, const OverlayPresentInfo& info);
void sampleDetails(OverlayStatistics::Clock::time_point at, const OverlayPresentInfo& info);
void sampleTrace(OverlayStatistics::Clock::time_point at);
Graphics::Context context;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
@@ -272,6 +328,7 @@ private:
OverlayStatistics statistics;
OverlayDetails details;
OverlayDetailSample detailSample;
DetailState detail;
std::optional<OverlayStatistics::Clock::time_point> sampledAt;
std::uint64_t toggles = 0;
std::uint32_t page = 0;
@@ -32,6 +32,7 @@ class TraceThread;
extern "C" bool AgcDriverTraceRecording_nid_postfix();
extern "C" AgcDriver::TraceThread* AgcDriverTraceThread_nid_postfix();
extern "C" void AgcDriverTraceCount_nid_postfix(const char* scope);
namespace AgcDriver {
@@ -769,8 +770,15 @@ private:
}
};
struct TraceThreadTotals {
std::string name;
std::vector<TraceThread::Total> totals;
};
class PerformanceTrace {
public:
static constexpr const char* RenderPassScope = "Graphics.RenderPass";
static bool Configured() {
static const bool configured = std::getenv("APS5_TRACE_FRAMES") != nullptr || std::getenv("APS5_TRACE_SECONDS") != nullptr || std::getenv("APS5_TRACE_SUMMARY_EVERY") != nullptr || std::getenv("APS5_TRACE_BENCH") != nullptr;
return configured;
@@ -797,11 +805,21 @@ public:
return AgcDriverTraceThread_nid_postfix();
}
static void Count(const char* scope) {
if (Recording()) AgcDriverTraceCount_nid_postfix(scope);
}
static void SetOverlayTotals(bool on);
static bool OverlayTotals();
static std::optional<std::vector<TraceThreadTotals>> ThreadTotals();
static void StopOutput();
private:
friend bool ::AgcDriverTraceRecording_nid_postfix();
friend TraceThread* ::AgcDriverTraceThread_nid_postfix();
friend void ::AgcDriverTraceCount_nid_postfix(const char* scope);
static TraceThread* bindNamed();
static void OpenInterval(std::chrono::steady_clock::time_point now, std::uint64_t present);
static void FlushInterval(std::chrono::steady_clock::time_point now, std::uint32_t width, std::uint32_t height, const VulkanDevice* device);
@@ -810,6 +828,8 @@ private:
inline static std::atomic<bool> buffering{false};
inline static std::atomic<std::uint64_t> presentCount{0};
static thread_local TraceThread* local;
static thread_local const char* localName;
static std::atomic<TraceThread* (*)()> binder;
inline static std::mutex gpuMutex;
inline static std::vector<TraceGpuRange> gpuRanges;
inline static std::uint64_t gpuLost = 0;
@@ -88,18 +88,38 @@ bool SkipRefusedDraws() {
return skip;
}
namespace {
struct RefusedShaders {
std::mutex mutex;
std::set<std::uint64_t> reported;
};
RefusedShaders& refusedShaders() {
static RefusedShaders shaders;
return shaders;
}
}
void ReportRefusedDraw(const RefusedDraw& refused) {
static std::mutex reportedMutex;
static std::set<std::uint64_t> reported;
auto& shaders = refusedShaders();
{
std::lock_guard lock(reportedMutex);
if (!reported.insert(refused.address).second) return;
std::lock_guard lock(shaders.mutex);
if (!shaders.reported.insert(refused.address).second) return;
}
const std::string_view what = refused.what();
const auto line = what.substr(0, what.find('\n'));
std::fprintf(stderr, "[gpu] APS5_SKIP_REFUSED_DRAWS: skipping the draws that use shader 0x%llx: %.*s\n", static_cast<unsigned long long>(refused.address), static_cast<int>(line.size()), line.data());
}
std::optional<std::size_t> RefusedShaderCount() {
if (!SkipRefusedDraws()) return std::nullopt;
auto& shaders = refusedShaders();
std::lock_guard lock(shaders.mutex);
return shaders.reported.size();
}
void require(bool condition, const char* reason) {
if (!condition) {
throw std::runtime_error(std::string("AGC driver: ") + reason);
@@ -1,5 +1,6 @@
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp"
#include <cstdlib>
namespace AgcDriver::DriverDetail {
@@ -46,6 +47,13 @@ DrawEntryCounters Driver::DrawCacheCounters() {
return drawEntryCounters;
}
OverlayDriverCounters Driver::OverlayCounters() {
OverlayDriverCounters counters{flipsCounted.load(std::memory_order_relaxed), std::nullopt};
std::unique_lock cacheLock(drawCacheMutex, std::try_to_lock);
if (cacheLock.owns_lock()) counters.drawCache = OverlayHits{drawEntryCounters.lookups, drawEntryCounters.hits};
return counters;
}
std::string Driver::profileDrawCacheLines(const DrawEntryCounters& counters) {
std::string text;
const auto append = [&](const char* format, auto... values) {
@@ -6,6 +6,7 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp"
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
@@ -230,6 +231,7 @@ Driver::Driver() {
gpuMapImportThreads.fetch_add(1, std::memory_order_relaxed);
host.join();
});
PerformanceOverlay::SetDriverCounters([] { return Driver::Get().OverlayCounters(); });
}
void Driver::WaitIdle() {
@@ -146,7 +146,7 @@ void Driver::run(std::uint32_t id) noexcept {
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
auto& costs = submissionCosts(id);
try {
if (PerformanceTrace::Configured()) PerformanceTrace::BindThread(role);
PerformanceTrace::BindThread(role);
for (;;) {
submission = Submission{};
struct Running {
@@ -1498,6 +1498,17 @@ public:
return liveWorkers.load(std::memory_order_acquire);
}
static std::optional<std::size_t> Pending() {
std::size_t pending = 0;
for (auto* pool : {Existing(), ExistingPrebuild()}) {
if (pool == nullptr) continue;
std::unique_lock lock(pool->mutex, std::try_to_lock);
if (!lock.owns_lock()) return std::nullopt;
pending += pool->tasks.size() + pool->running;
}
return pending;
}
static void CloseAll() {
std::array<RegistrationPreparePool*, 2> pools{};
{
@@ -1637,7 +1648,7 @@ private:
static void* Main(void* argument) {
auto& pool = *static_cast<RegistrationPreparePool*>(argument);
if (PerformanceTrace::Configured()) PerformanceTrace::BindThread("shader prepare", TraceThread::DefaultCapacity / 8);
PerformanceTrace::BindThread("shader prepare", TraceThread::DefaultCapacity / 8);
for (;;) {
Task task;
{
@@ -1750,6 +1761,10 @@ unsigned RegistrationPreparationThreads() {
return RegistrationPreparePool::LiveWorkers();
}
std::optional<std::size_t> RegistrationPreparationsPending() {
return RegistrationPreparePool::Pending();
}
std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target, bool deferUnprepared) {
ShaderPreparationTransaction transaction;
require(decoded.programs.size() == decoded.roles.size(), "graphics ABI program roles are incomplete");
@@ -1,19 +1,27 @@
#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
#include "prx/libSceAgcDriver/Execution/include/Presentation.hpp"
#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
#include "prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlayFrag_spv.h"
#include "prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlayVert_spv.h"
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libc/include/HostThreadLocal.hpp"
#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp"
#include <algorithm>
#include <atomic>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <charconv>
#include <initializer_list>
#include <map>
#include <mutex>
#include <stdexcept>
#include <string_view>
@@ -24,6 +32,8 @@
#endif
#include <windows.h>
#include <psapi.h>
#else
#include <sys/resource.h>
#endif
namespace AgcDriver {
@@ -41,6 +51,7 @@ std::atomic<std::uint32_t> currentPage{0};
std::atomic<std::uint64_t> gpuTotal{0};
std::atomic<std::uint64_t> driverTotal{0};
std::atomic<std::uint64_t> pipelineTotal{0};
std::atomic<OverlayDriverCounters (*)()> driverCounters{nullptr};
struct PacketCounterRegistry {
std::mutex mutex;
@@ -69,11 +80,28 @@ const OverlaySettings& settings() {
static const OverlaySettings parsed = [] {
const auto read = ParseOverlaySettings(std::getenv("ANYPS5_OVERLAY"), std::getenv("ANYPS5_OVERLAY_POS"));
currentPage.store(read.page, std::memory_order_relaxed);
if (read.page == 2) PerformanceTrace::SetOverlayTotals(true);
return read;
}();
return parsed;
}
std::string busyName(std::string_view thread) {
auto queue = thread.substr(TraceFormat::WorkerPrefix.size());
if (queue.starts_with("0x")) queue.remove_prefix(2);
return "Q" + std::string(queue);
}
std::optional<std::uint64_t> meminfoKilobytes(std::string_view line, std::string_view name) {
if (!line.starts_with(name)) return std::nullopt;
auto rest = line.substr(name.size());
while (!rest.empty() && rest.front() == ' ') rest.remove_prefix(1);
std::uint64_t value = 0;
const auto [last, error] = std::from_chars(rest.data(), rest.data() + rest.size(), value);
if (error != std::errc{} || std::string_view(last, static_cast<std::size_t>(rest.data() + rest.size() - last)) != " kB") return std::nullopt;
return value * 1024u;
}
std::int32_t panelWidth(std::int32_t columns, std::int32_t scale) {
return (columns * OverlayMetrics::Advance - 1 + 2 * OverlayMetrics::Padding) * scale;
}
@@ -505,6 +533,68 @@ OverlayContent ComposeOverlayDetails(const OverlayDetailSample& sample) {
return content;
}
OverlayTraceFigures OverlayTraceDeltas(std::span<const TraceThreadTotals> before, std::span<const TraceThreadTotals> now, OverlayStatistics::Clock::duration elapsed, std::uint64_t presents, bool passes) {
using Duration = OverlayStatistics::Clock::duration;
const auto milliseconds = [](Duration length) { return std::chrono::duration<double, std::milli>(length).count(); };
std::map<std::string, Duration> busy;
std::map<std::string, Duration> phases;
std::optional<OverlayQueueWaits> waits;
std::uint64_t renderPasses = 0;
for (std::size_t index = 0; index < now.size(); ++index) {
const auto& thread = now[index];
const auto totals = TraceFormat::Since(thread.totals, index < before.size() ? std::span<const TraceThread::Total>(before[index].totals) : std::span<const TraceThread::Total>());
const bool worker = std::string_view(thread.name).starts_with(TraceFormat::WorkerPrefix);
const bool firstQueue = thread.name == std::string(TraceFormat::WorkerPrefix) + "0x0";
if (firstQueue && !waits) waits = OverlayQueueWaits{};
for (const auto& total : totals) {
const std::string_view scope = total.scope;
const std::string_view stage = total.stage != nullptr ? total.stage : "";
if (scope == PerformanceTrace::RenderPassScope) {
renderPasses += total.count;
continue;
}
if (worker && total.stage == nullptr && std::find(TraceFormat::WorkerBusyScopes.begin(), TraceFormat::WorkerBusyScopes.end(), scope) != TraceFormat::WorkerBusyScopes.end()) busy[thread.name] += total.length;
if (scope != TraceFormat::WorkerWaitScope) phases[TraceFormat::Name(total.scope, total.stage)] += total.length;
if (!firstQueue) continue;
if (scope == "Driver.Packet" && (stage == "memory_wait" || stage == "rendering_wait")) waits->memoryMs += milliseconds(total.length);
else if (scope == "GpuMutex.wait") waits->lockMs += milliseconds(total.length);
else if ((total.stage == nullptr && (scope == "Recorder.Sync" || scope == "Recorder.WaitSerial")) || (scope == "Graphics.Wait" && stage == "fence_wait")) waits->gpuMs += milliseconds(total.length);
}
if (worker && !busy.contains(thread.name)) busy[thread.name] = Duration{};
}
OverlayTraceFigures figures;
if (elapsed > Duration{}) {
for (const auto& [name, length] : busy) figures.busy.push_back({busyName(name), 100.0 * static_cast<double>(length.count()) / static_cast<double>(elapsed.count())});
std::stable_sort(figures.busy.begin(), figures.busy.end(), [](const OverlayThreadBusy& left, const OverlayThreadBusy& right) { return left.name.size() < right.name.size(); });
}
if (presents == 0) return figures;
const double perFrame = 1.0 / static_cast<double>(presents);
std::vector<std::pair<std::string, Duration>> top(phases.begin(), phases.end());
std::stable_sort(top.begin(), top.end(), [](const auto& left, const auto& right) { return left.second > right.second; });
for (std::size_t index = 0; index < top.size() && index < OverlayTopPhases; ++index) figures.phases.push_back({top[index].first, milliseconds(top[index].second) * perFrame});
if (waits) figures.queueWaits = OverlayQueueWaits{waits->memoryMs * perFrame, waits->lockMs * perFrame, waits->gpuMs * perFrame};
if (passes) figures.renderPassesPerFrame = static_cast<double>(renderPasses) * perFrame;
return figures;
}
std::optional<double> OverlayHitPercent(const OverlayHits& before, const OverlayHits& now) {
const bool reset = now.lookups < before.lookups || now.hits < before.hits;
const auto lookups = reset ? now.lookups : now.lookups - before.lookups;
const auto hits = reset ? now.hits : now.hits - before.hits;
if (lookups == 0 || hits > lookups) return std::nullopt;
return 100.0 * static_cast<double>(hits) / static_cast<double>(lookups);
}
std::string_view OverlayPresentModeName(VkPresentModeKHR mode) {
switch (mode) {
case VK_PRESENT_MODE_FIFO_KHR: return "fifo";
case VK_PRESENT_MODE_FIFO_LATEST_READY_KHR: return "fifo_latest_ready";
case VK_PRESENT_MODE_MAILBOX_KHR: return "mailbox";
case VK_PRESENT_MODE_IMMEDIATE_KHR: return "immediate";
default: return {};
}
}
bool OverlaySampleDue(std::optional<OverlayStatistics::Clock::time_point> sampledAt, OverlayStatistics::Clock::time_point now) {
return !sampledAt || now - *sampledAt >= std::chrono::seconds(1);
}
@@ -543,6 +633,51 @@ std::optional<std::uint64_t> ReadProcessPrivateBytes() {
#endif
}
std::optional<OverlayMemory> ParseCommitCharge(std::string_view meminfo) {
std::optional<std::uint64_t> committed;
std::optional<std::uint64_t> limit;
while (!meminfo.empty()) {
const auto end = meminfo.find('\n');
const auto line = meminfo.substr(0, end);
meminfo = end == std::string_view::npos ? std::string_view{} : meminfo.substr(end + 1);
if (const auto value = meminfoKilobytes(line, "Committed_AS:")) committed = value;
else if (const auto value = meminfoKilobytes(line, "CommitLimit:")) limit = value;
}
if (!committed || !limit) return std::nullopt;
return OverlayMemory{*committed, *limit};
}
std::optional<OverlayMemory> ReadCommitCharge() {
#ifdef _WIN32
PERFORMANCE_INFORMATION information{};
information.cb = sizeof(information);
if (K32GetPerformanceInfo(&information, sizeof(information)) == 0) return std::nullopt;
const auto page = static_cast<std::uint64_t>(information.PageSize);
return OverlayMemory{static_cast<std::uint64_t>(information.CommitTotal) * page, static_cast<std::uint64_t>(information.CommitLimit) * page};
#else
std::FILE* meminfo = std::fopen("/proc/meminfo", "r");
if (meminfo == nullptr) return std::nullopt;
std::string text;
char block[1024];
for (std::size_t read = 0; (read = std::fread(block, 1, sizeof(block), meminfo)) != 0;) text.append(block, read);
std::fclose(meminfo);
return ParseCommitCharge(text);
#endif
}
std::optional<std::uint64_t> ReadPageFaults() {
#ifdef _WIN32
PROCESS_MEMORY_COUNTERS counters{};
counters.cb = sizeof(counters);
if (K32GetProcessMemoryInfo(GetCurrentProcess(), &counters, sizeof(counters)) == 0) return std::nullopt;
return static_cast<std::uint64_t>(counters.PageFaultCount);
#else
rusage usage{};
if (getrusage(RUSAGE_SELF, &usage) != 0) return std::nullopt;
return static_cast<std::uint64_t>(usage.ru_minflt) + static_cast<std::uint64_t>(usage.ru_majflt);
#endif
}
std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content) {
auto scale = std::max<std::int32_t>(1, static_cast<std::int32_t>((extent.height + 240) / 480));
while (scale > 1 && !fits(extent, content, scale)) --scale;
@@ -699,22 +834,24 @@ void PerformanceOverlay::Toggle() {
static std::mutex toggleMutex;
std::lock_guard lock(toggleMutex);
const auto presses = toggleCount.load(std::memory_order_relaxed) + 1;
currentPage.store(OverlayPageAfter(settings().page, presses), std::memory_order_relaxed);
const auto shown = OverlayPageAfter(settings().page, presses);
currentPage.store(shown, std::memory_order_relaxed);
toggleCount.store(presses, std::memory_order_relaxed);
PerformanceTrace::SetOverlayTotals(shown == 2);
}
void PerformanceOverlay::SetDriverCounters(OverlayDriverCounters (*source)()) {
driverCounters.store(source, std::memory_order_release);
}
OverlayCorner PerformanceOverlay::Corner() {
return settings().corner;
}
void PerformanceOverlay::NotePresent(OverlayStatistics::Clock::time_point at, VkExtent2D display) {
void PerformanceOverlay::NotePresent(OverlayStatistics::Clock::time_point at, VkExtent2D display, const OverlayPresentInfo& info) {
restartIfToggled();
if (page == 2) {
statistics.AddPresent(at, OverlayCounters{0, 0, 0, 0, PerformanceCounters::Pipelines()});
detailSample.fps = statistics.Fps();
detailSample.frameMs = statistics.LastFrameMs();
detailSample.presentsPerSecond = statistics.Fps();
detailSample.pipelinesPerSecond = static_cast<double>(statistics.PipelinesInWindow());
noteDetailPresent(at, info);
return;
}
if (OverlaySampleDue(sampledAt, at)) {
@@ -734,10 +871,95 @@ void PerformanceOverlay::restartIfToggled() {
statistics.Reset();
details = {};
detailSample = {};
detail = {};
sampledAt.reset();
toggles = current;
}
void PerformanceOverlay::noteDetailPresent(OverlayStatistics::Clock::time_point at, const OverlayPresentInfo& info) {
PerformanceTrace::BindThread("presenter");
statistics.AddPresent(at, OverlayCounters{0, 0, 0, 0, PerformanceCounters::Pipelines()});
if (info.displayAddress != 0 && info.displayAddress == detail.lastDisplayAddress) ++detail.repeats;
detail.lastDisplayAddress = info.displayAddress;
++detail.tracePresents;
detailSample.fps = statistics.Fps();
detailSample.frameMs = statistics.LastFrameMs();
detailSample.presentsPerSecond = statistics.Fps();
detailSample.pipelinesPerSecond = static_cast<double>(statistics.PipelinesInWindow());
if (OverlaySampleDue(detail.sampledAt, at)) sampleDetails(at, info);
}
void PerformanceOverlay::sampleDetails(OverlayStatistics::Clock::time_point at, const OverlayPresentInfo& info) {
DetailCounters now;
if (const auto source = driverCounters.load(std::memory_order_acquire)) now.driver = source();
const auto pipelines = Graphics::PipelineStoreTotals();
now.pipelines = OverlayHits{pipelines.hits + pipelines.misses, pipelines.hits};
const auto copies = Graphics::AddressCopyCounters::Fold();
now.copiedBytes = copies.bytes;
now.pageFaults = ReadPageFaults();
now.unimplemented = NotImplementedCalls_nid_no_patch();
now.acquireDrops = info.acquireDrops;
const auto& before = detail.base;
if (detail.sampledAt) {
const double seconds = std::chrono::duration<double>(at - *detail.sampledAt).count();
const auto perSecond = [&](std::uint64_t count) { return static_cast<double>(count) / seconds; };
if (now.driver && before.driver) {
detailSample.flipsPerSecond = perSecond(now.driver->flips - before.driver->flips);
if (now.driver->drawCache && before.driver->drawCache) detailSample.drawCacheHitPercent = OverlayHitPercent(*before.driver->drawCache, *now.driver->drawCache);
}
detailSample.pipelineCacheHitPercent = OverlayHitPercent(before.pipelines, now.pipelines);
detailSample.cpuCopyMegabytesPerSecond = perSecond(now.copiedBytes - before.copiedBytes) / (1024.0 * 1024.0);
if (const auto limit = Graphics::AddressCopyCounters::Limit(); limit != 0) detailSample.addressCopyCapPercent = 100.0 * static_cast<double>(copies.largest) / static_cast<double>(limit);
if (now.pageFaults && before.pageFaults) detailSample.pageFaultsPerSecond = perSecond(*now.pageFaults - *before.pageFaults);
detailSample.unimplementedPerSecond = perSecond(now.unimplemented - before.unimplemented);
detailSample.droppedFrames = static_cast<std::uint64_t>(std::llround(perSecond(now.acquireDrops - before.acquireDrops)));
detailSample.repeatedPresents = static_cast<std::uint64_t>(std::llround(perSecond(detail.repeats)));
} else if (HasAppParamJson_nid_no_patch()) {
const auto id = GetAppTitleId_nid_postfix();
detailSample.title.assign(id.value, strnlen(id.value, sizeof(id.value)));
}
if (now.driver && before.driver && !now.driver->drawCache) now.driver->drawCache = before.driver->drawCache;
detailSample.unimplementedTotal = now.unimplemented;
if (const auto pending = DriverDetail::RegistrationPreparationsPending()) detailSample.pipelinesCompiling = *pending;
if (const auto refused = DriverDetail::RefusedShaderCount()) detailSample.refusedShaders = *refused;
else detailSample.refusedShaders.reset();
detailSample.commit = ReadCommitCharge();
if (context.hostImportAlignment != 0) {
const auto imports = Graphics::HostImportCounters();
detailSample.hostImports = imports.live;
detailSample.hostImportBytes = OverlayMemory{imports.liveBytes, imports.liveBytes + Graphics::HostImportBudgetRemaining(context)};
}
detailSample.presentMode = std::string(OverlayPresentModeName(info.presentMode));
sampleTrace(at);
detail.base = std::move(now);
detail.repeats = 0;
detail.sampledAt = at;
}
void PerformanceOverlay::sampleTrace(OverlayStatistics::Clock::time_point at) {
if (!PerformanceTrace::Recording()) {
detail.traceSampledAt.reset();
detail.traceBase.clear();
detailSample.busy.clear();
detailSample.phases.clear();
detailSample.queueWaits.reset();
detailSample.renderPassesPerFrame.reset();
return;
}
auto threads = PerformanceTrace::ThreadTotals();
if (!threads) return;
if (detail.traceSampledAt) {
auto figures = OverlayTraceDeltas(detail.traceBase, *threads, at - *detail.traceSampledAt, detail.tracePresents, PerformanceTrace::OverlayTotals());
detailSample.busy = std::move(figures.busy);
detailSample.phases = std::move(figures.phases);
detailSample.queueWaits = figures.queueWaits;
detailSample.renderPassesPerFrame = figures.renderPassesPerFrame;
}
detail.traceBase = std::move(*threads);
detail.traceSampledAt = at;
detail.tracePresents = 0;
}
VkRenderPass PerformanceOverlay::renderPass(VkFormat format, VkImageLayout initialLayout, VkImageLayout finalLayout) {
const auto key = std::tuple{format, initialLayout, finalLayout};
if (const auto found = renderPasses.find(key); found != renderPasses.end()) return found->second;
@@ -214,7 +214,11 @@ double seconds(Clock::duration elapsed) {
}
void PerformanceTrace::BindThread(const char* name, std::size_t capacity) {
if (!Configured() || local != nullptr) return;
if (local != nullptr) return;
if (!Configured()) {
localName = name;
return;
}
const bool windowed = std::getenv("APS5_TRACE_FRAMES") != nullptr || std::getenv("APS5_TRACE_SECONDS") != nullptr;
if (windowed && capacity == TraceThread::DefaultCapacity) capacity = TraceThread::ParseCapacity(std::getenv("APS5_TRACE_EVENTS"));
auto bound = std::make_unique<TraceThread>(name, presentCount, windowed ? capacity : 0, buffering);
@@ -224,6 +228,39 @@ void PerformanceTrace::BindThread(const char* name, std::size_t capacity) {
state.threads.push_back(std::move(bound));
}
TraceThread* PerformanceTrace::bindNamed() {
if (localName == nullptr) return nullptr;
auto bound = std::make_unique<TraceThread>(localName, presentCount, 0, buffering);
auto& state = session();
std::lock_guard lock(state.mutex);
local = bound.get();
state.threads.push_back(std::move(bound));
return local;
}
void PerformanceTrace::SetOverlayTotals(bool on) {
if (Configured()) return;
binder.store(on ? &bindNamed : nullptr, std::memory_order_release);
recording.store(on, std::memory_order_relaxed);
}
bool PerformanceTrace::OverlayTotals() {
return binder.load(std::memory_order_relaxed) != nullptr;
}
std::optional<std::vector<TraceThreadTotals>> PerformanceTrace::ThreadTotals() {
auto& state = session();
std::unique_lock lock(state.mutex, std::try_to_lock);
if (!lock.owns_lock()) return std::nullopt;
std::vector<TraceThreadTotals> threads;
threads.reserve(state.threads.size());
for (const auto& thread : state.threads) {
auto snapshot = thread->Collect();
threads.push_back({std::move(snapshot.name), std::move(snapshot.totals)});
}
return threads;
}
void PerformanceTrace::OpenInterval(Clock::time_point now, std::uint64_t present) {
auto& state = session();
state.interval->Restart(now - state.first);
@@ -4,6 +4,8 @@ namespace AgcDriver {
std::atomic<bool> PerformanceTrace::recording{false};
thread_local TraceThread* PerformanceTrace::local = nullptr;
thread_local const char* PerformanceTrace::localName = nullptr;
std::atomic<TraceThread* (*)()> PerformanceTrace::binder{nullptr};
}
@@ -12,5 +14,12 @@ extern "C" bool AgcDriverTraceRecording_nid_postfix() {
}
extern "C" AgcDriver::TraceThread* AgcDriverTraceThread_nid_postfix() {
return AgcDriver::PerformanceTrace::local;
if (auto* thread = AgcDriver::PerformanceTrace::local) return thread;
const auto bind = AgcDriver::PerformanceTrace::binder.load(std::memory_order_acquire);
return bind != nullptr ? bind() : nullptr;
}
extern "C" void AgcDriverTraceCount_nid_postfix(const char* scope) {
if (AgcDriver::PerformanceTrace::binder.load(std::memory_order_relaxed) == nullptr) return;
if (auto* thread = AgcDriverTraceThread_nid_postfix()) thread->Add(scope, nullptr, {}, {});
}
@@ -345,6 +345,7 @@ struct VulkanDevice::State {
std::vector<VkImage> images;
VkFence acquireFence = VK_NULL_HANDLE;
VkPresentModeKHR presentMode = VK_PRESENT_MODE_FIFO_KHR;
std::uint64_t presentedAddress = 0;
struct RetiredSwapchain {
VkSwapchainKHR swapchain = VK_NULL_HANDLE;
std::vector<VkSemaphore> rendered;
@@ -3418,6 +3419,7 @@ FrameDumps& Dumps() {
}
bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer, const DisplayBuffer* plane) {
state->presentedAddress = buffer.address;
if (buffer.dccAddress != 0 && buffer.tilingMode == 0) {
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
const auto before = profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0;
@@ -3781,7 +3783,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu
}
if (overlay) {
if (!state->overlay) state->overlay = std::make_unique<PerformanceOverlay>(graphicsContext(), state->presentSlots.size());
state->overlay->NotePresent(std::chrono::steady_clock::now(), clearOnly ? VkExtent2D{} : VkExtent2D{width, height});
state->overlay->NotePresent(std::chrono::steady_clock::now(), clearOnly ? VkExtent2D{} : VkExtent2D{width, height}, OverlayPresentInfo{std::exchange(state->presentedAddress, 0), state->presentMode, presentCounters.acquireDrops});
state->overlay->Record(commands, static_cast<std::size_t>(&slot - state->presentSlots.data()), barrier.image, state->surfaceFormat, state->surfaceColorSpace, state->extent, drawn || plane != nullptr ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
} else if (!drawn && plane == nullptr) {
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
@@ -6,6 +6,7 @@
#include "prx/libc/include/GuestAllocations.hpp"
#include <algorithm>
#include <array>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <limits>
@@ -272,6 +273,24 @@ struct AddressCopy {
};
std::string AddressCopyOverflow(std::vector<AddressCopy> copies, std::uint64_t limit);
class AddressCopyCounters {
public:
struct Totals {
std::uint64_t bytes = 0;
std::uint64_t largest = 0;
};
static AddressCopyCounters& ThisThread();
static Totals Fold();
static std::uint64_t Limit();
void Count(std::uint64_t copied);
private:
alignas(64) std::atomic<std::uint64_t> bytes{0};
std::atomic<std::uint64_t> largest{0};
};
class GuestBufferMemory {
public:
explicit GuestBufferMemory(const Context& context);
@@ -2253,7 +2253,10 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
outcome.passContinued = continued;
outcome.passBegun = !continued;
const auto commands = start.commands;
if (!continued) recorder->PrepareSampleSlot();
if (!continued) {
recorder->PrepareSampleSlot();
PerformanceTrace::Count(PerformanceTrace::RenderPassScope);
}
if (capture) captureInputs(context, *recorder, commands, resources, drawBindings, record.targets.empty() || record.targets.front() == nullptr ? 0 : record.targets.front()->Descriptor().baseAddress);
// The draw's [gputime] class range: from its first barrier to the pass's trailing barrier (a
// continued draw lies inside its pass's range).
@@ -34,6 +34,7 @@
#include <functional>
#include <bit>
#include <condition_variable>
#include <deque>
#include <iterator>
#include <list>
#include <map>
@@ -1633,6 +1634,41 @@ std::uint64_t physicalMemoryBytes() {
std::atomic<std::uint64_t> budgetOverride{0};
std::atomic<std::uint64_t> addressCopyLimitOverride{0};
std::uint64_t addressCopyLimit() {
static const std::uint64_t configured = [] {
const char* value = std::getenv("APS5_ADDRESS_COPY_MAX_MIB");
return (value ? std::strtoull(value, nullptr, 10) : 64ull) << 20u;
}();
const auto limit = addressCopyLimitOverride.load(std::memory_order_relaxed);
return limit != 0 ? limit : configured;
}
constexpr unsigned AddressCopyEpochShift = 48;
constexpr std::uint64_t AddressCopyEpochMask = 0xffff;
constexpr std::uint64_t AddressCopyLargestMask = (std::uint64_t{1} << AddressCopyEpochShift) - 1;
std::atomic<std::uint64_t> addressCopyEpoch{0};
struct AddressCopyRegistry {
std::mutex mutex;
std::deque<AddressCopyCounters> counters;
std::vector<AddressCopyCounters*> unused;
};
AddressCopyRegistry& addressCopyRegistry() {
static auto* registry = new AddressCopyRegistry;
return *registry;
}
struct AddressCopyLease {
AddressCopyCounters* counters = nullptr;
~AddressCopyLease() {
if (counters == nullptr) return;
auto& registry = addressCopyRegistry();
std::lock_guard lock(registry.mutex);
registry.unused.push_back(counters);
}
};
// Pinned imports count against the driver's system memory heap, which imports, type-3 mirrors, buffers and textures
// placed in host memory share; past it ordinary host allocations fail. The budget is APS5_HOST_IMPORT_MIB, else
// HostImportBudget.
@@ -3847,6 +3883,47 @@ void SetAddressCopyLimit(std::uint64_t bytes) {
addressCopyLimitOverride.store(bytes, std::memory_order_relaxed);
}
AddressCopyCounters& AddressCopyCounters::ThisThread() {
auto& lease = HostThreadLocal<AddressCopyLease, AddressCopyLease>();
if (lease.counters == nullptr) {
auto& registry = addressCopyRegistry();
std::lock_guard lock(registry.mutex);
if (registry.unused.empty()) {
lease.counters = &registry.counters.emplace_back();
} else {
lease.counters = registry.unused.back();
registry.unused.pop_back();
}
}
return *lease.counters;
}
void AddressCopyCounters::Count(std::uint64_t copied) {
bytes.store(bytes.load(std::memory_order_relaxed) + copied, std::memory_order_relaxed);
const auto epoch = addressCopyEpoch.load(std::memory_order_relaxed) & AddressCopyEpochMask;
const auto kept = largest.load(std::memory_order_relaxed);
const auto clamped = std::min(copied, AddressCopyLargestMask);
if ((kept >> AddressCopyEpochShift) != epoch || (kept & AddressCopyLargestMask) < clamped) largest.store(epoch << AddressCopyEpochShift | clamped, std::memory_order_relaxed);
}
AddressCopyCounters::Totals AddressCopyCounters::Fold() {
auto& registry = addressCopyRegistry();
std::lock_guard lock(registry.mutex);
const auto epoch = addressCopyEpoch.load(std::memory_order_relaxed);
Totals totals;
for (const auto& counters : registry.counters) {
totals.bytes += counters.bytes.load(std::memory_order_relaxed);
const auto kept = counters.largest.load(std::memory_order_relaxed);
if ((kept >> AddressCopyEpochShift) == (epoch & AddressCopyEpochMask)) totals.largest = std::max(totals.largest, kept & AddressCopyLargestMask);
}
addressCopyEpoch.store(epoch + 1, std::memory_order_relaxed);
return totals;
}
std::uint64_t AddressCopyCounters::Limit() {
return addressCopyLimit();
}
void SetHostImportDeferredSettles(bool enabled) {
#ifdef _WIN32
deferredSettles.store(enabled, std::memory_order_relaxed);
@@ -4330,6 +4407,11 @@ void GuestBufferMemory::AcquireRegistered() {
}
releaseImportedMirrors(context, imported);
regionsSorted = sortedLookup;
if (!copies.empty()) {
std::uint64_t copied = 0;
for (const auto& copy : copies) copied += copy.committed;
AddressCopyCounters::ThisThread().Count(copied);
}
if (const auto overflow = AddressCopyOverflow(std::move(copies), cap); !overflow.empty()) {
std::fprintf(stderr, "FATAL: %s\n", overflow.c_str());
std::fflush(stderr);
@@ -2,7 +2,9 @@
#include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp"
#include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp"
#include "prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
#include "prx/libc/include/General.hpp"
#include <SDL_loadso.h>
#include <algorithm>
#include <array>
@@ -18,6 +20,7 @@
#include <span>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
namespace {
@@ -626,6 +629,113 @@ void pageCycleTest(const Device& device) {
std::printf("F10 cycle: off, page 1 (%dx%d), page 2 (%dx%d), off; each change of page resets the statistics\n", pageOne.width, pageOne.height, pageTwo.width, pageTwo.height);
}
AgcDriver::OverlayDriverCounters fakeDriverCounters() {
static std::uint64_t calls = 0;
++calls;
return AgcDriver::OverlayDriverCounters{calls * 66, AgcDriver::OverlayHits{calls * 1000, calls * 900}};
}
void pageTwoSamplerTest(const Device& device) {
using AgcDriver::PerformanceTrace;
using namespace std::chrono_literals;
const auto& context = device.GetContext();
const VkExtent2D extent{1280, 720};
Require(PerformanceOverlay::Page() == 0 && !PerformanceTrace::Recording(), "the trace records before page 2 is shown");
PerformanceOverlay overlay(context, 1);
PerformanceOverlay::SetDriverCounters(&fakeDriverCounters);
PerformanceOverlay::Toggle();
Require(!PerformanceTrace::Recording(), "page 1 turns the trace on");
PerformanceOverlay::Toggle();
Require(PerformanceOverlay::Page() == 2 && PerformanceTrace::Recording() && PerformanceTrace::OverlayTotals(), "page 2 does not turn the trace totals on");
const auto start = AgcDriver::OverlayStatistics::Clock::now();
const auto present = [&](std::chrono::milliseconds at, std::uint64_t address, std::uint64_t drops) { overlay.NotePresent(start + at, VkExtent2D{640, 360}, AgcDriver::OverlayPresentInfo{address, VK_PRESENT_MODE_FIFO_KHR, drops}); };
present(0ms, 0x1000, 7);
const auto& sample = overlay.DetailSample();
Require(sample.commit.has_value() && sample.unimplementedTotal.has_value() && sample.presentMode == "fifo" && sample.pipelinesCompiling == std::optional<std::uint64_t>{0}, "page 2's first sample does not read the commit charge, the unimplemented calls, the compile backlog and the present mode");
Require(!sample.flipsPerSecond && !sample.pageFaultsPerSecond && !sample.cpuCopyMegabytesPerSecond && sample.busy.empty() && sample.phases.empty(), "page 2's first sample shows rates before a second has passed");
Require(sample.hostImports.has_value() == (context.hostImportAlignment != 0), "the host import line does not follow the device's host import support");
std::thread([] {
PerformanceTrace::BindThread("queue worker 0x0");
if (auto* trace = PerformanceTrace::Thread()) {
const auto begin = std::chrono::steady_clock::now();
trace->Add("Driver.Packet", nullptr, begin, begin + 110ms);
}
for (int pass = 0; pass < 6; ++pass) PerformanceTrace::Count(PerformanceTrace::RenderPassScope);
AgcDriver::Graphics::AddressCopyCounters::ThisThread().Count(std::uint64_t{11} << 20u);
}).join();
try {
NotImplemented_nid_no_patch("page 2 sampler test");
} catch (const std::runtime_error&) {
}
present(16ms, 0x1000, 7);
present(33ms, 0x2000, 8);
present(1100ms, 0x2000, 9);
const auto near = [](const std::optional<double>& value, double expected) { return value.has_value() && std::abs(*value - expected) < 1e-6; };
Require(near(sample.flipsPerSecond, 60) && near(sample.drawCacheHitPercent, 90), "66 flips and 900 of 1000 draw-cache lookups over 1.1 s are not 60 flips/s and 90 %");
Require(sample.droppedFrames == std::optional<std::uint64_t>{2} && sample.repeatedPresents == std::optional<std::uint64_t>{2}, "2 dropped and 2 repeated presents over 1.1 s are not shown as 2/s each");
Require(sample.pageFaultsPerSecond.has_value() && near(sample.unimplementedPerSecond, 1 / 1.1), "the page faults or the unimplemented calls per second are missing");
Require(near(sample.cpuCopyMegabytesPerSecond, 10), "11 MiB of address copies over 1.1 s are not 10 MB/s");
if (std::getenv("APS5_ADDRESS_COPY_MAX_MIB") == nullptr) Require(near(sample.addressCopyCapPercent, 100.0 * 11 / 64), "an 11 MiB build is not 17.2 % of the 64 MiB cap");
Require(near(sample.renderPassesPerFrame, 2), "6 render passes over 3 frames are not 2 per frame");
const auto queue = std::find_if(sample.busy.begin(), sample.busy.end(), [](const AgcDriver::OverlayThreadBusy& busy) { return busy.name == "Q0"; });
Require(queue != sample.busy.end() && std::abs(queue->percent - 10) < 1e-6 && sample.queueWaits.has_value(), "110 ms of packets over 1.1 s do not make queue 0 10 % busy");
Require(!sample.phases.empty() && sample.phases.front().name == "Driver.Packet" && std::abs(sample.phases.front().msPerFrame - 110.0 / 3) < 1e-6, "the top phase is not 110 ms of packets over 3 frames");
Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT);
CommandBatch batch(context);
const auto commands = batch.Handle();
transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT);
const auto bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
batch.SubmitAndWait();
overlay.Release(0);
const auto content = AgcDriver::ComposeOverlayDetails(sample);
const auto expected = AgcDriver::LayOutOverlay(content, extent, PerformanceOverlay::Corner()).bounds;
Require(bounds.width == expected.width && bounds.height == expected.height, "page 2 is not drawn from its sample");
std::printf("page 2 from its sampler (fake driver counters, a 'queue worker 0x0' thread, this process's OS counters):\n");
for (const auto& line : content.below) std::printf(" |%-46s|\n", line.c_str());
PerformanceOverlay::Toggle();
Require(PerformanceOverlay::Page() == 0 && !PerformanceTrace::Recording() && !PerformanceTrace::OverlayTotals(), "hiding the overlay leaves the trace totals on");
present(1200ms, 0x3000, 9);
Require(overlay.ShownPage() == 0 && overlay.DetailSample().fps == 0 && overlay.DetailSample().busy.empty() && !overlay.DetailSample().commit, "hiding the overlay kept page 2's sample");
PerformanceOverlay::SetDriverCounters(nullptr);
}
void presentCostReport(const Device& device) {
using AgcDriver::PerformanceCounters;
using AgcDriver::PerformanceTrace;
using Clock = AgcDriver::OverlayStatistics::Clock;
const auto& context = device.GetContext();
const VkExtent2D extent{1920, 1080};
Require(PerformanceOverlay::Page() == 0, "the cost report does not start hidden");
PerformanceOverlay overlay(context, 1);
const auto start = Clock::now();
const auto perCall = [](int calls, auto&& body) {
const auto began = std::chrono::steady_clock::now();
for (int call = 0; call < calls; ++call) body(call);
return std::chrono::duration<double, std::nano>(std::chrono::steady_clock::now() - began).count() / calls;
};
volatile bool shown = false;
const auto hiddenNs = perCall(10'000'000, [&](int) { shown = PerformanceOverlay::Visible(); });
const auto passNs = perCall(10'000'000, [](int) { PerformanceTrace::Count(PerformanceTrace::RenderPassScope); });
auto& copies = AgcDriver::Graphics::AddressCopyCounters::ThisThread();
const auto copyNs = perCall(10'000'000, [&](int) { copies.Count(4096); });
PerformanceOverlay::Toggle();
const auto pageOneNs = perCall(20'000, [&](int frame) {
overlay.NotePresent(start + std::chrono::microseconds(16667) * frame, extent);
static_cast<void>(AgcDriver::LayOutOverlay(AgcDriver::ComposeOverlay(overlay.Statistics(), PerformanceCounters::GpuEnabled(), PerformanceCounters::DriverEnabled(), overlay.Details()), extent, PerformanceOverlay::Corner()));
});
PerformanceOverlay::Toggle();
const auto later = start + std::chrono::hours(1);
const auto pageTwoNs = perCall(20'000, [&](int frame) {
overlay.NotePresent(later + std::chrono::microseconds(16667) * frame, extent, AgcDriver::OverlayPresentInfo{0x1000u + static_cast<std::uint64_t>(frame % 2), VK_PRESENT_MODE_FIFO_KHR, 0});
static_cast<void>(AgcDriver::LayOutOverlay(AgcDriver::ComposeOverlayDetails(overlay.DetailSample()), extent, PerformanceOverlay::Corner()));
});
const auto sampled = later + std::chrono::hours(1);
const auto sampleNs = perCall(200, [&](int second) { overlay.NotePresent(sampled + std::chrono::seconds(second), extent, AgcDriver::OverlayPresentInfo{0x1000, VK_PRESENT_MODE_FIFO_KHR, 0}); });
PerformanceOverlay::Toggle();
Require(!PerformanceOverlay::Visible() && !PerformanceTrace::Recording(), "the cost report does not end hidden");
std::printf("present cost on this host: hidden %.1f ns per present (Visible), %.1f ns per render pass begun (the trace check), %.1f ns per address-copying build (the per-thread counter); page 1 %.2f us per present (NotePresent, compose, layout); page 2 %.2f us per present at 60 fps (NotePresent with its once-a-second sample, compose, layout), %.1f us per present that samples\n", hiddenNs, passNs, copyNs, pageOneNs / 1000, pageTwoNs / 1000, sampleNs / 1000);
}
void notePresentTest(const Device& device) {
const auto& context = device.GetContext();
const VkExtent2D extent{1280, 720};
@@ -826,6 +936,8 @@ int main(int argc, char** argv) {
}
notePresentTest(*device);
pageCycleTest(*device);
pageTwoSamplerTest(*device);
presentCostReport(*device);
disabledIsUnchangedTest(context);
std::cout << "performance overlay tests passed\n";
return 0;
@@ -1,4 +1,8 @@
#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
#include "prx/libc/include/General.hpp"
#include <algorithm>
#include <chrono>
#include <cmath>
@@ -658,6 +662,187 @@ void sampleTests() {
require(touched[Bytes - 1] == 0x5a, "the touched block was not written");
}
constexpr const char* PacketScope = "Driver.Packet";
constexpr const char* WorkerWaitScope = "Driver.WorkerWait";
constexpr const char* LockScope = "GpuMutex.wait";
constexpr const char* SyncScope = "Recorder.Sync";
constexpr const char* SerialScope = "Recorder.WaitSerial";
constexpr const char* WaitScope = "Graphics.Wait";
constexpr const char* FinishScope = "Driver.DrawFinish";
constexpr const char* MemoryStage = "memory_wait";
constexpr const char* RenderingStage = "rendering_wait";
constexpr const char* DrawSite = "draw";
constexpr const char* PresentSite = "present";
constexpr const char* FenceStage = "fence_wait";
AgcDriver::TraceThread::Total total(const char* scope, const char* stage, std::chrono::milliseconds length, std::uint64_t count) {
return AgcDriver::TraceThread::Total{scope, stage, std::chrono::duration_cast<Clock::duration>(length), count};
}
void traceDeltaTests() {
using AgcDriver::TraceThreadTotals;
const std::vector<TraceThreadTotals> before{
{"queue worker 0x0", {total(PacketScope, nullptr, 100ms, 10), total(PacketScope, MemoryStage, 5ms, 1), total(WorkerWaitScope, nullptr, 50ms, 2)}},
{"queue worker 0x1", {total(PacketScope, nullptr, 10ms, 1)}},
};
const std::vector<TraceThreadTotals> now{
{"queue worker 0x0", {total(PacketScope, nullptr, 500ms, 60), total(PacketScope, MemoryStage, 35ms, 3), total(PacketScope, RenderingStage, 10ms, 1), total(LockScope, DrawSite, 20ms, 4), total(LockScope, PresentSite, 5ms, 1), total(SyncScope, nullptr, 6ms, 2), total(SerialScope, nullptr, 4ms, 1), total(WaitScope, FenceStage, 5ms, 1), total(WaitScope, nullptr, 9ms, 1), total(WorkerWaitScope, nullptr, 450ms, 40)}},
{"queue worker 0x1", {total(PacketScope, nullptr, 110ms, 2)}},
{"queue worker 0xa", {total(PacketScope, nullptr, 50ms, 1)}},
{"draw committer", {total(AgcDriver::PerformanceTrace::RenderPassScope, nullptr, 0ms, 120), total(FinishScope, nullptr, 80ms, 40), total(SyncScope, nullptr, 7ms, 1)}},
};
const auto figures = AgcDriver::OverlayTraceDeltas(before, now, 1s, 40, true);
require(figures.busy.size() == 3 && figures.busy[0].name == "Q0" && figures.busy[1].name == "Q1" && figures.busy[2].name == "Qa", "the busy queues are not Q0, Q1 and Qa in that order");
require(within(figures.busy[0].percent, 40) && within(figures.busy[1].percent, 10) && within(figures.busy[2].percent, 5), "the busy shares over a second are not 40, 10 and 5 %");
const std::vector<std::pair<std::string, double>> phases{{"Driver.Packet", 13.75}, {"Driver.DrawFinish", 2}, {"Driver.Packet.memory_wait", 0.75}, {"GpuMutex.wait.draw", 0.5}, {"Recorder.Sync", 0.325}};
require(figures.phases.size() == phases.size(), "page 2 does not keep the top 5 phases");
for (std::size_t index = 0; index < phases.size(); ++index) require(figures.phases[index].name == phases[index].first && within(figures.phases[index].msPerFrame, phases[index].second), "phase " + std::to_string(index) + " is " + figures.phases[index].name + " at " + std::to_string(figures.phases[index].msPerFrame) + " ms per frame, not " + phases[index].first);
require(figures.queueWaits && within(figures.queueWaits->memoryMs, 1.0) && within(figures.queueWaits->lockMs, 0.625) && within(figures.queueWaits->gpuMs, 0.375), "queue 0's waits per frame are not 1 ms on memory, 0.625 ms on the lock and 0.375 ms on the GPU");
require(figures.renderPassesPerFrame && within(*figures.renderPassesPerFrame, 3), "120 render passes over 40 frames are not 3 per frame");
require(!AgcDriver::OverlayTraceDeltas(before, now, 1s, 40, false).renderPassesPerFrame, "render passes are shown while they are not counted");
const auto noFrames = AgcDriver::OverlayTraceDeltas(before, now, 1s, 0, true);
require(noFrames.busy.size() == 3 && noFrames.phases.empty() && !noFrames.queueWaits && !noFrames.renderPassesPerFrame, "per-frame figures are shown for a second without frames");
require(AgcDriver::OverlayTraceDeltas(before, now, Clock::duration{}, 40, true).busy.empty(), "busy shares are shown for no elapsed time");
const auto noQueue = AgcDriver::OverlayTraceDeltas({}, std::vector<TraceThreadTotals>{now[3]}, 1s, 40, true);
require(noQueue.busy.empty() && !noQueue.queueWaits && noQueue.renderPassesPerFrame, "a trace without queue workers shows busy shares or queue waits");
AgcDriver::OverlayDetailSample sample;
sample.busy = figures.busy;
sample.phases = figures.phases;
sample.queueWaits = figures.queueWaits;
sample.renderPassesPerFrame = figures.renderPassesPerFrame;
requireDrawable(AgcDriver::ComposeOverlayDetails(sample), "page 2 from trace totals");
}
void hitRateTests() {
require(within(AgcDriver::OverlayHitPercent({100, 80}, {300, 230}).value_or(-1), 75), "150 hits of 200 lookups are not 75 %");
require(within(AgcDriver::OverlayHitPercent({1000, 900}, {200, 150}).value_or(-1), 75), "counters that restarted (the draw cache's 10 s report) are not read from zero");
require(!AgcDriver::OverlayHitPercent({5, 5}, {5, 5}), "a hit rate is shown without lookups");
require(!AgcDriver::OverlayHitPercent({0, 0}, {4, 5}), "more hits than lookups give a hit rate");
}
void presentModeTests() {
require(AgcDriver::OverlayPresentModeName(VK_PRESENT_MODE_FIFO_KHR) == "fifo" && AgcDriver::OverlayPresentModeName(VK_PRESENT_MODE_FIFO_LATEST_READY_KHR) == "fifo_latest_ready" && AgcDriver::OverlayPresentModeName(VK_PRESENT_MODE_MAILBOX_KHR) == "mailbox" && AgcDriver::OverlayPresentModeName(VK_PRESENT_MODE_IMMEDIATE_KHR) == "immediate", "the present modes are not named as ANYPS5_PRESENT_MODE names them");
require(AgcDriver::OverlayPresentModeName(VK_PRESENT_MODE_FIFO_RELAXED_KHR).empty() && AgcDriver::OverlayPresentModeName(VK_PRESENT_MODE_MAX_ENUM_KHR).empty(), "a present mode the driver never selects has a name");
AgcDriver::OverlayDetailSample sample;
sample.title = "PPSA01234";
sample.presentMode = AgcDriver::OverlayPresentModeName(VK_PRESENT_MODE_FIFO_LATEST_READY_KHR);
const auto content = AgcDriver::ComposeOverlayDetails(sample);
require(content.below.back() == "TITLE: PPSA01234 PRESENT: FIFO_LATEST_READY", "the longest present mode is drawn as '" + content.below.back() + "'");
}
void operatingSystemTests() {
const auto parsed = AgcDriver::ParseCommitCharge("MemTotal: 16318412 kB\nCommitLimit: 8159204 kB\nCommitted_AS: 12345678 kB\nVmallocTotal: 34359738367 kB\n");
require(parsed && parsed->used == 12345678ull * 1024 && parsed->budget == 8159204ull * 1024, "Committed_AS and CommitLimit are not read from /proc/meminfo's text");
require(!AgcDriver::ParseCommitCharge("Committed_AS: 12345678 kB\n"), "a commit charge is shown without its limit");
require(!AgcDriver::ParseCommitCharge("CommitLimit: 8159204 kB\nCommitted_AS: 12 MB\n"), "a value in another unit is read as kB");
require(!AgcDriver::ParseCommitCharge(""), "an empty /proc/meminfo gives a commit charge");
const auto commit = AgcDriver::ReadCommitCharge();
require(commit && commit->used != 0 && commit->budget != 0, "the system's commit charge and limit cannot be read");
static std::uint8_t* volatile touched = nullptr;
const auto before = AgcDriver::ReadPageFaults();
require(before.has_value(), "the process's page faults cannot be read");
constexpr std::size_t Bytes = std::size_t{16} << 20u;
const auto block = std::make_unique<std::uint8_t[]>(Bytes);
touched = block.get();
std::memset(touched, 0x3c, Bytes);
const auto after = AgcDriver::ReadPageFaults();
require(after && *after > *before, "touching 16 MiB of new memory did not raise the page faults from " + std::to_string(*before));
require(touched[Bytes - 1] == 0x3c, "the touched block was not written");
}
void addressCopyFoldTests() {
using AgcDriver::Graphics::AddressCopyCounters;
const auto start = AddressCopyCounters::Fold();
auto& counters = AddressCopyCounters::ThisThread();
require(&counters == &AddressCopyCounters::ThisThread(), "one thread got two address copy counters");
counters.Count(std::uint64_t{3} << 20u);
counters.Count(std::uint64_t{1} << 20u);
bool separate = false;
std::thread worker([&] {
auto& own = AddressCopyCounters::ThisThread();
separate = &own != &counters;
own.Count(std::uint64_t{5} << 20u);
});
worker.join();
require(separate, "two builders share address copy counters");
const auto folded = AddressCopyCounters::Fold();
require(folded.bytes - start.bytes == std::uint64_t{9} << 20u && folded.largest == std::uint64_t{5} << 20u, "two builders' 9 MiB of copies with a largest build of 5 MiB fold to " + std::to_string(folded.bytes - start.bytes) + " bytes, largest " + std::to_string(folded.largest));
const auto idle = AddressCopyCounters::Fold();
require(idle.bytes == folded.bytes && idle.largest == 0, "a second without copies keeps the previous second's largest build");
std::thread next([] { AddressCopyCounters::ThisThread().Count(std::uint64_t{2} << 20u); });
next.join();
const auto reused = AddressCopyCounters::Fold();
require(reused.bytes == idle.bytes + (std::uint64_t{2} << 20u) && reused.largest == std::uint64_t{2} << 20u, "the copies of a builder that ended were lost, or a new builder restarted them");
if (std::getenv("APS5_ADDRESS_COPY_MAX_MIB") == nullptr) require(AddressCopyCounters::Limit() == std::uint64_t{64} << 20u, "the address copy cap is not 64 MiB by default");
AgcDriver::Graphics::SetAddressCopyLimit(std::uint64_t{8} << 20u);
require(AddressCopyCounters::Limit() == std::uint64_t{8} << 20u, "the cap in use is not the one set");
AgcDriver::Graphics::SetAddressCopyLimit(0);
}
void driverSourceTests() {
const auto before = NotImplementedCalls_nid_no_patch();
try {
NotImplemented_nid_no_patch("performance overlay test");
} catch (const std::runtime_error&) {
}
require(NotImplementedCalls_nid_no_patch() == before + 1, "an unimplemented-function call is not counted");
if (std::getenv("APS5_SKIP_REFUSED_DRAWS") == nullptr) require(!AgcDriver::DriverDetail::RefusedShaderCount(), "refused shaders are counted while a refused shader still stops the title");
require(AgcDriver::DriverDetail::RegistrationPreparationsPending() == std::optional<std::size_t>{0}, "a process that prepared no shaders has a compile backlog");
}
void traceSwitchTests() {
using AgcDriver::PerformanceOverlay;
using AgcDriver::PerformanceTrace;
require(!PerformanceTrace::Configured(), "the overlay statistics test runs with APS5_TRACE_* set");
require(PerformanceOverlay::Page() == 0 && !PerformanceTrace::Recording() && !PerformanceTrace::OverlayTotals(), "the trace records before page 2 is shown");
std::string failure;
std::thread([&] {
if (PerformanceTrace::Thread() != nullptr) failure = "a thread is bound to the trace while nothing records";
}).join();
require(failure.empty(), failure);
PerformanceOverlay::Toggle();
require(PerformanceOverlay::Page() == 1 && !PerformanceTrace::Recording() && !PerformanceTrace::OverlayTotals(), "page 1 turns the trace on");
PerformanceOverlay::Toggle();
require(PerformanceOverlay::Page() == 2 && PerformanceTrace::Recording() && PerformanceTrace::OverlayTotals(), "page 2 does not turn the trace totals on");
std::thread([&] {
PerformanceTrace::BindThread("queue worker 0x5");
auto* thread = PerformanceTrace::Thread();
if (thread == nullptr || thread != PerformanceTrace::Thread()) {
failure = "a named worker is not bound once while page 2 shows";
return;
}
const auto begin = Clock::now();
thread->Add(PacketScope, nullptr, begin, begin + 3ms);
PerformanceTrace::Count(AgcDriver::PerformanceTrace::RenderPassScope);
PerformanceTrace::Count(AgcDriver::PerformanceTrace::RenderPassScope);
const auto snapshot = thread->Collect();
if (!snapshot.slices.empty() || snapshot.dropped) failure = "page 2's trace totals buffer slices";
}).join();
require(failure.empty(), failure);
std::thread([&] {
if (PerformanceTrace::Thread() != nullptr) failure = "a thread that never named itself is bound while page 2 shows";
}).join();
require(failure.empty(), failure);
const auto threads = PerformanceTrace::ThreadTotals();
require(threads.has_value(), "the trace totals cannot be read");
const auto worker = std::find_if(threads->begin(), threads->end(), [](const AgcDriver::TraceThreadTotals& thread) { return thread.name == "queue worker 0x5"; });
require(worker != threads->end(), "the named worker's totals are missing");
std::uint64_t passes = 0;
Clock::duration packets{};
for (const auto& entry : worker->totals) {
if (std::string_view(entry.scope) == AgcDriver::PerformanceTrace::RenderPassScope) passes += entry.count;
if (std::string_view(entry.scope) == PacketScope && entry.stage == nullptr) packets += entry.length;
}
require(passes == 2 && packets == 3ms, "the worker's totals hold " + std::to_string(passes) + " render passes and " + std::to_string(std::chrono::duration<double, std::milli>(packets).count()) + " ms of packets, not 2 and 3 ms");
PerformanceOverlay::Toggle();
require(PerformanceOverlay::Page() == 0 && !PerformanceTrace::Recording() && !PerformanceTrace::OverlayTotals(), "leaving page 2 leaves the trace totals on");
std::thread([&] {
PerformanceTrace::Count(AgcDriver::PerformanceTrace::RenderPassScope);
if (PerformanceTrace::Thread() != nullptr) failure = "a thread is bound to the trace after page 2 was left";
}).join();
require(failure.empty(), failure);
}
void costReport() {
auto& counters = AgcDriver::PacketCounters::ThisThread();
constexpr int Increments = 20'000'000;
@@ -707,8 +892,15 @@ int main() {
pageTwoComposeTests();
pageTwoLayoutTests();
sampleTests();
traceDeltaTests();
hitRateTests();
presentModeTests();
operatingSystemTests();
addressCopyFoldTests();
driverSourceTests();
traceSwitchTests();
costReport();
std::cout << "performance overlay statistics, layout, palette, packet, pipeline, memory and resolution tests passed\n";
std::cout << "performance overlay statistics, layout, palette, packet, pipeline, memory, resolution, page 2 source and trace switch tests passed\n";
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
+1
View File
@@ -13,6 +13,7 @@
#include "general/ExportMacros.hpp"
extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* funcName);
extern "C" std::uint64_t NotImplementedCalls_nid_no_patch();
extern "C" void CxaFinalize_nid_no_patch(void* dsoHandle);
extern "C" bool StopAtPrefixLetter_nid_no_patch(const char* str, char** endptr, int base);
extern "C" bool RejectIntegerBase_nid_no_patch(const char* str, char** endptr, int base, bool setError);
+11
View File
@@ -996,6 +996,17 @@ extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor) {
}
#endif
namespace {
std::atomic<std::uint64_t> notImplementedCalls{0};
}
extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* funcName) {
notImplementedCalls.fetch_add(1, std::memory_order_relaxed);
throw std::runtime_error(std::string(funcName) + " not implemented");
}
extern "C" std::uint64_t NotImplementedCalls_nid_no_patch() {
return notImplementedCalls.load(std::memory_order_relaxed);
}