diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp index 86505f5a7b..97a4339de3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp @@ -2,7 +2,9 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DIAGNOSTICS_HPP #include "prx/libSceAgcDriver/Execution/include/QueueState.hpp" +#include #include +#include #include #include @@ -21,6 +23,7 @@ public: bool SkipRefusedDraws(); void ReportRefusedDraw(const RefusedDraw& refused); +std::optional RefusedShaderCount(); void require(bool condition, const char* reason); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 8951aa78e1..7d89a2589f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -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 regions, const ShaderRecompiler::SrtUserDataUse& use, bool eligible, std::uint32_t hiddenWords, std::span> 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; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index b1907257c4..0c19b88668 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -227,6 +227,7 @@ void SubmitLibraryPrebuild(std::function task); void AwaitLibraryPrebuilds(); void CloseRegistrationPreparation(); unsigned RegistrationPreparationThreads(); +std::optional RegistrationPreparationsPending(); void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target, bool deferUnprepared = false); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp index dbba2002fe..8eca6e6190 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp @@ -4,6 +4,7 @@ #define VK_NO_PROTOTYPES #include #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" #include #include #include @@ -13,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -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 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 busy; + std::vector phases; + std::optional queueWaits; + std::optional renderPassesPerFrame; +}; + +inline constexpr std::size_t OverlayTopPhases = 5; + struct OverlayLayout { OverlayRect bounds; std::vector 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 before, std::span now, OverlayStatistics::Clock::duration elapsed, std::uint64_t presents, bool passes); +std::optional OverlayHitPercent(const OverlayHits& before, const OverlayHits& now); +std::string_view OverlayPresentModeName(VkPresentModeKHR mode); bool OverlaySampleDue(std::optional sampledAt, OverlayStatistics::Clock::time_point now); std::optional ReadVramBudget(const Graphics::Context& context); std::optional ReadProcessPrivateBytes(); +std::optional ParseCommitCharge(std::string_view meminfo); +std::optional ReadCommitCharge(); +std::optional ReadPageFaults(); std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content); OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, OverlayCorner corner); std::array, 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 driver; + OverlayHits pipelines; + std::uint64_t copiedBytes = 0; + std::optional pageFaults; + std::uint64_t unimplemented = 0; + std::uint64_t acquireDrops = 0; + }; + + struct DetailState { + std::optional sampledAt; + DetailCounters base; + std::uint64_t repeats = 0; + std::uint64_t lastDisplayAddress = 0; + std::optional traceSampledAt; + std::vector 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 sampledAt; std::uint64_t toggles = 0; std::uint32_t page = 0; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp index 49858aaa31..71c65e2bce 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp @@ -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 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> 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 buffering{false}; inline static std::atomic presentCount{0}; static thread_local TraceThread* local; + static thread_local const char* localName; + static std::atomic binder; inline static std::mutex gpuMutex; inline static std::vector gpuRanges; inline static std::uint64_t gpuLost = 0; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp index ca92adc259..85f7d2f0ca 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp @@ -88,18 +88,38 @@ bool SkipRefusedDraws() { return skip; } +namespace { + +struct RefusedShaders { + std::mutex mutex; + std::set reported; +}; + +RefusedShaders& refusedShaders() { + static RefusedShaders shaders; + return shaders; +} + +} + void ReportRefusedDraw(const RefusedDraw& refused) { - static std::mutex reportedMutex; - static std::set 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(refused.address), static_cast(line.size()), line.data()); } +std::optional 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); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp index b1f3dc2a02..dc0211bb25 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp @@ -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 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) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index ba19bb4a60..e12649fac4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -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() { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp index 546be83644..b6a5b5689c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp @@ -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 { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 312b71958b..7c8efbdf04 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1498,6 +1498,17 @@ public: return liveWorkers.load(std::memory_order_acquire); } + static std::optional 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 pools{}; { @@ -1637,7 +1648,7 @@ private: static void* Main(void* argument) { auto& pool = *static_cast(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 RegistrationPreparationsPending() { + return RegistrationPreparePool::Pending(); +} + std::vector 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"); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp index 1daa49877f..e0c805d9d4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp @@ -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 #include #include #include #include #include +#include #include +#include #include #include #include @@ -24,6 +32,8 @@ #endif #include #include +#else +#include #endif namespace AgcDriver { @@ -41,6 +51,7 @@ std::atomic currentPage{0}; std::atomic gpuTotal{0}; std::atomic driverTotal{0}; std::atomic pipelineTotal{0}; +std::atomic 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 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(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 before, std::span 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(length).count(); }; + std::map busy; + std::map phases; + std::optional 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(before[index].totals) : std::span()); + 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(length.count()) / static_cast(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(presents); + std::vector> 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(renderPasses) * perFrame; + return figures; +} + +std::optional 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(hits) / static_cast(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 sampledAt, OverlayStatistics::Clock::time_point now) { return !sampledAt || now - *sampledAt >= std::chrono::seconds(1); } @@ -543,6 +633,51 @@ std::optional ReadProcessPrivateBytes() { #endif } +std::optional ParseCommitCharge(std::string_view meminfo) { + std::optional committed; + std::optional 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 ReadCommitCharge() { +#ifdef _WIN32 + PERFORMANCE_INFORMATION information{}; + information.cb = sizeof(information); + if (K32GetPerformanceInfo(&information, sizeof(information)) == 0) return std::nullopt; + const auto page = static_cast(information.PageSize); + return OverlayMemory{static_cast(information.CommitTotal) * page, static_cast(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 ReadPageFaults() { +#ifdef _WIN32 + PROCESS_MEMORY_COUNTERS counters{}; + counters.cb = sizeof(counters); + if (K32GetProcessMemoryInfo(GetCurrentProcess(), &counters, sizeof(counters)) == 0) return std::nullopt; + return static_cast(counters.PageFaultCount); +#else + rusage usage{}; + if (getrusage(RUSAGE_SELF, &usage) != 0) return std::nullopt; + return static_cast(usage.ru_minflt) + static_cast(usage.ru_majflt); +#endif +} + std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content) { auto scale = std::max(1, static_cast((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(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(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(at - *detail.sampledAt).count(); + const auto perSecond = [&](std::uint64_t count) { return static_cast(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(copies.largest) / static_cast(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::llround(perSecond(now.acquireDrops - before.acquireDrops))); + detailSample.repeatedPresents = static_cast(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; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp index e9578a2e7c..1b1706181f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp @@ -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(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(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> PerformanceTrace::ThreadTotals() { + auto& state = session(); + std::unique_lock lock(state.mutex, std::try_to_lock); + if (!lock.owns_lock()) return std::nullopt; + std::vector 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); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTraceThread.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTraceThread.cpp index 9ce81e5294..edfd1fa98c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTraceThread.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTraceThread.cpp @@ -4,6 +4,8 @@ namespace AgcDriver { std::atomic PerformanceTrace::recording{false}; thread_local TraceThread* PerformanceTrace::local = nullptr; +thread_local const char* PerformanceTrace::localName = nullptr; +std::atomic 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, {}, {}); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 31b43695a5..f1f74c7206 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -345,6 +345,7 @@ struct VulkanDevice::State { std::vector 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 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(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(&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; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 5a12156180..4d29d617fb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -6,6 +6,7 @@ #include "prx/libc/include/GuestAllocations.hpp" #include #include +#include #include #include #include @@ -272,6 +273,24 @@ struct AddressCopy { }; std::string AddressCopyOverflow(std::vector 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 bytes{0}; + std::atomic largest{0}; +}; + class GuestBufferMemory { public: explicit GuestBufferMemory(const Context& context); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index aa8902722e..967d113df9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -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). diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 170aee9139..f262862165 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -1633,6 +1634,41 @@ std::uint64_t physicalMemoryBytes() { std::atomic budgetOverride{0}; std::atomic 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 addressCopyEpoch{0}; + +struct AddressCopyRegistry { + std::mutex mutex; + std::deque counters; + std::vector 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(); + if (lease.counters == nullptr) { + auto& registry = addressCopyRegistry(); + std::lock_guard lock(registry.mutex); + if (registry.unused.empty()) { + lease.counters = ®istry.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); diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp index acdd5ffbac..8a5ad4b746 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp @@ -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 #include #include @@ -18,6 +20,7 @@ #include #include #include +#include #include 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{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& 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{2} && sample.repeatedPresents == std::optional{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(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(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(frame % 2), VK_PRESENT_MODE_FIFO_KHR, 0}); + static_cast(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; diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp index 19c194c03b..316e631457 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp @@ -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 #include #include @@ -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(length), count}; +} + +void traceDeltaTests() { + using AgcDriver::TraceThreadTotals; + const std::vector 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 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> 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{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(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{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(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'; diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 7596a572a2..33499fd85e 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -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); diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index ecd6abc0c9..9c68872826 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -996,6 +996,17 @@ extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor) { } #endif +namespace { + +std::atomic 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); +}