mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-27 00:28:14 +00:00
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:
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 = ®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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user