diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index b8a8865d6..75b3ff3cc 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -43,6 +43,10 @@ target_link_libraries(common ZArchive::zarchive nlohmann_json::nlohmann_json ) +if(APPLE) + # hostStats.cpp reads GPU utilization from the IOAccelerator registry entries. + target_link_libraries(common PRIVATE "-framework CoreFoundation" "-framework IOKit") +endif() get_property(inc_headers TARGET common PROPERTY INCLUDE_DIRECTORIES) set(check_headers ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/common/debugCounters.h b/src/common/debugCounters.h new file mode 100644 index 000000000..cf4e96b84 --- /dev/null +++ b/src/common/debugCounters.h @@ -0,0 +1,117 @@ +#ifndef KYTY_COMMON_DEBUG_COUNTERS_H_ +#define KYTY_COMMON_DEBUG_COUNTERS_H_ + +#include +#include +#include + +namespace Common::DebugCounters { + +static_assert(std::atomic::is_always_lock_free && std::atomic::is_always_lock_free); + +// Collect events only while the panel is visible. Relaxed, lock-free atomics keep updates +// cheap and safe in signal handlers. Rates come from comparing two snapshots. +enum class Counter { + // GPU work. + Draws, + DrawInstances, + IndirectDraws, + Dispatches, + QueueSubmits, + GpuWaits, // CPU blocked on a GPU timeline semaphore. + GpuWaitNs, + GpuFaults, // Guest writes to GPU-tracked memory. + ShadersCompiled, + ShaderCompileNs, + PipelinesCreated, + PipelineCreateNs, + // Caches. + TextureUploadBytes, + TextureDownloadBytes, + TextureEvictions, + BufferUploadBytes, + BufferDownloadBytes, + BufferEvictions, + // Presentation. + FlipsRequested, + FlipsRejected, // Guest flip queue was full. + LateVblanks, // The present thread fell behind the vblank rate. + SwapchainRecreations, + DroppedPresents, + // Emulator. + EmulatedInstructions, // Host CPU lacked an instruction the guest used. + UnresolvedImportCalls, + GuestFileReadBytes, + GuestFileWriteBytes, + AudioUnderruns, + Count +}; + +enum class Peak { ShaderCompileNs, PipelineCreateNs, Count }; + +namespace Detail { +inline std::atomic g_enabled {false}; +inline std::atomic g_counters[static_cast(Counter::Count)] {}; +inline std::atomic g_peaks[static_cast(Peak::Count)] {}; +} // namespace Detail + +[[nodiscard]] inline bool Enabled() noexcept { + return Detail::g_enabled.load(std::memory_order_relaxed); +} + +inline void SetEnabled(bool enabled) noexcept { + Detail::g_enabled.store(enabled, std::memory_order_relaxed); +} + +inline void Add(Counter counter, uint64_t value = 1) noexcept { + if (!Enabled()) { + return; + } + Detail::g_counters[static_cast(counter)].fetch_add(value, std::memory_order_relaxed); +} + +class ScopedTimer final { +public: + ScopedTimer(Counter count, Counter duration, Peak peak = Peak::Count) + : m_count(count), m_duration(duration), m_peak(peak), + m_start(Enabled() ? std::chrono::steady_clock::now() + : std::chrono::steady_clock::time_point {}) {} + ~ScopedTimer() { + if (m_start == std::chrono::steady_clock::time_point {} || !Enabled()) { + return; + } + const auto elapsed = + static_cast(std::chrono::duration_cast( + std::chrono::steady_clock::now() - m_start) + .count()); + Add(m_count); + Add(m_duration, elapsed); + if (m_peak != Peak::Count) { + auto& peak = Detail::g_peaks[static_cast(m_peak)]; + auto previous = peak.load(std::memory_order_relaxed); + while (previous < elapsed && + !peak.compare_exchange_weak(previous, elapsed, std::memory_order_relaxed)) { + } + } + } + ScopedTimer(const ScopedTimer&) = delete; + ScopedTimer& operator=(const ScopedTimer&) = delete; + +private: + Counter m_count; + Counter m_duration; + Peak m_peak; + std::chrono::steady_clock::time_point m_start; +}; + +[[nodiscard]] inline uint64_t Get(Counter counter) noexcept { + return Detail::g_counters[static_cast(counter)].load(std::memory_order_relaxed); +} + +[[nodiscard]] inline uint64_t Get(Peak peak) noexcept { + return Detail::g_peaks[static_cast(peak)].load(std::memory_order_relaxed); +} + +} // namespace Common::DebugCounters + +#endif /* KYTY_COMMON_DEBUG_COUNTERS_H_ */ diff --git a/src/common/emulatorConfig.cpp b/src/common/emulatorConfig.cpp index 98a90adb7..7705f5c61 100644 --- a/src/common/emulatorConfig.cpp +++ b/src/common/emulatorConfig.cpp @@ -1,6 +1,7 @@ #include "common/emulatorConfig.h" #include "common/assert.h" +#include "common/debugCounters.h" #include #include @@ -16,6 +17,7 @@ void Initialize() { } void Shutdown() { + Common::DebugCounters::SetEnabled(false); g_config.reset(); } @@ -27,6 +29,7 @@ void Load(const ConfigOptions& cfg) { EXIT_IF(cfg.keyboard_player > static_cast(MAX_PLAYERS)); *g_config = cfg; + Common::DebugCounters::SetEnabled(cfg.perf_overlay_enabled); } uint32_t GetScreenWidth() { diff --git a/src/common/emulatorConfig.h b/src/common/emulatorConfig.h index b46a6c133..4387dafc9 100644 --- a/src/common/emulatorConfig.h +++ b/src/common/emulatorConfig.h @@ -89,6 +89,7 @@ struct ConfigOptions { bool tessellation_enabled = false; bool trophy_enabled = true; uint32_t trophy_duration_seconds = 6; + bool perf_overlay_enabled = false; bool playgo_hack_enabled = false; bool skip_notice_screen = false; #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS diff --git a/src/common/hostStats.cpp b/src/common/hostStats.cpp new file mode 100644 index 000000000..12c9f1358 --- /dev/null +++ b/src/common/hostStats.cpp @@ -0,0 +1,580 @@ +#include "common/hostStats.h" + +#include "common/config.h" + +#include +#include +#include +#include + +#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include // IWYU pragma: keep +// clang-format off +#include +// clang-format on +#elif defined(__APPLE__) +#include +#include +#include +#include +#include +#include +#include +#include +#include +#else +#include +#include +#include +#include +#include +#endif + +namespace Common { + +namespace { + +// Cumulative counters; rates come from the difference between two samples. +struct Counters { + struct Thread { + uint64_t cpu_ns; + std::string name; + }; + std::optional process_cpu_ns; + std::optional system_busy; + std::optional system_total; + std::optional context_switches; + std::optional page_faults; + std::optional page_ins; + std::optional disk_read; + std::optional disk_write; + std::unordered_map threads; + uint64_t wall_ns = 0; +}; + +uint64_t WallNanoseconds() { + return static_cast(std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count()); +} + +#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS + +uint64_t FileTimeTo100Ns(const FILETIME& time) { + return (static_cast(time.dwHighDateTime) << 32u) | time.dwLowDateTime; +} + +struct PlatformState {}; + +void SamplePlatform(PlatformState& /*state*/, HostStats& stats, Counters& now) { + const auto process = GetCurrentProcess(); + FILETIME creation {}; + FILETIME exit {}; + FILETIME kernel {}; + FILETIME user {}; + if (GetProcessTimes(process, &creation, &exit, &kernel, &user) != 0) { + now.process_cpu_ns = (FileTimeTo100Ns(kernel) + FileTimeTo100Ns(user)) * 100u; + } + FILETIME idle {}; + if (GetSystemTimes(&idle, &kernel, &user) != 0) { + // Kernel time includes idle time. + now.system_total = FileTimeTo100Ns(kernel) + FileTimeTo100Ns(user); + now.system_busy = *now.system_total - FileTimeTo100Ns(idle); + } + + PROCESS_MEMORY_COUNTERS memory {}; + if (K32GetProcessMemoryInfo(process, &memory, sizeof(memory)) != 0) { + stats.process_memory = memory.WorkingSetSize; + stats.process_resident = memory.WorkingSetSize; + now.page_faults = memory.PageFaultCount; + } + IO_COUNTERS io {}; + if (GetProcessIoCounters(process, &io) != 0) { + now.disk_read = io.ReadTransferCount; + now.disk_write = io.WriteTransferCount; + } + + MEMORYSTATUSEX system {}; + system.dwLength = sizeof(system); + if (GlobalMemoryStatusEx(&system) != 0) { + stats.system_memory_total = system.ullTotalPhys; + stats.system_memory_used = system.ullTotalPhys - system.ullAvailPhys; + stats.system_free = system.ullAvailPhys; + stats.memory_pressure = system.dwMemoryLoad >= 90 ? MemoryPressure::Critical + : system.dwMemoryLoad >= 80 ? MemoryPressure::Warning + : MemoryPressure::Normal; + } +} + +#else + +void SampleRusage(Counters& now) { + rusage usage {}; + if (getrusage(RUSAGE_SELF, &usage) != 0) { + return; + } + const auto to_ns = [](const timeval& time) { + return static_cast(time.tv_sec) * 1000000000u + + static_cast(time.tv_usec) * 1000u; + }; + now.process_cpu_ns = to_ns(usage.ru_utime) + to_ns(usage.ru_stime); + now.context_switches = static_cast(usage.ru_nvcsw + usage.ru_nivcsw); + now.page_faults = static_cast(usage.ru_minflt + usage.ru_majflt); + now.page_ins = static_cast(usage.ru_majflt); +} + +#if defined(__APPLE__) + +template +std::optional SysctlValue(const char* name) { + T value {}; + size_t size = sizeof(value); + if (sysctlbyname(name, &value, &size, nullptr, 0) != 0 || size != sizeof(value)) { + return std::nullopt; + } + return value; +} + +struct PlatformState { + mach_port_t host = mach_host_self(); + int thermal_token = -1; + bool thermal_ready = false; + std::string gpu_model; + std::optional gpu_cores; + + ~PlatformState() { + if (thermal_ready) { + notify_cancel(thermal_token); + } + mach_port_deallocate(mach_task_self(), host); + } +}; + +std::optional DictionaryNumber(CFDictionaryRef dictionary, CFStringRef key) { + const auto* value = CFDictionaryGetValue(dictionary, key); + int64_t number = 0; + if (value == nullptr || CFGetTypeID(value) != CFNumberGetTypeID() || + !CFNumberGetValue(static_cast(value), kCFNumberSInt64Type, &number) || + number < 0) { + return std::nullopt; + } + return number; +} + +std::string RegistryString(io_object_t service, CFStringRef key) { + std::string result; + auto* value = IORegistryEntryCreateCFProperty(service, key, kCFAllocatorDefault, 0); + if (value == nullptr) { + return result; + } + if (CFGetTypeID(value) == CFStringGetTypeID()) { + char buffer[128] {}; + if (CFStringGetCString(static_cast(value), buffer, sizeof(buffer), + kCFStringEncodingUTF8)) { + result = buffer; + } + } else if (CFGetTypeID(value) == CFDataGetTypeID()) { + // Intel Macs store the model as NUL-terminated data. + const auto* data = static_cast(value); + const auto* bytes = reinterpret_cast(CFDataGetBytePtr(data)); + const auto length = static_cast(CFDataGetLength(data)); + result.assign(bytes, strnlen(bytes, length)); + } + CFRelease(value); + return result; +} + +// IOAccelerator exposes whole-device statistics; there is no per-process figure. +void SampleGpu(PlatformState& state, HostStats& stats) { + io_iterator_t iterator = 0; + if (IOServiceGetMatchingServices(MACH_PORT_NULL, IOServiceMatching("IOAccelerator"), + &iterator) != KERN_SUCCESS) { + return; + } + const auto take_max = [](std::optional& target, std::optional value) { + if (value) { + target = std::max(target.value_or(0.0), std::min(100.0, static_cast(*value))); + } + }; + const auto add = [](std::optional& target, std::optional value) { + if (value) { + target = target.value_or(0) + static_cast(*value); + } + }; + for (io_object_t service = IOIteratorNext(iterator); service != 0; + service = IOIteratorNext(iterator)) { + if (state.gpu_model.empty()) { + state.gpu_model = RegistryString(service, CFSTR("model")); + auto* cores = IORegistryEntryCreateCFProperty(service, CFSTR("gpu-core-count"), + kCFAllocatorDefault, 0); + int64_t count = 0; + if (cores != nullptr) { + if (CFGetTypeID(cores) == CFNumberGetTypeID() && + CFNumberGetValue(static_cast(cores), kCFNumberSInt64Type, + &count) && + count > 0) { + state.gpu_cores = static_cast(count); + } + CFRelease(cores); + } + } + auto* statistics = IORegistryEntryCreateCFProperty(service, CFSTR("PerformanceStatistics"), + kCFAllocatorDefault, 0); + if (statistics != nullptr) { + if (CFGetTypeID(statistics) == CFDictionaryGetTypeID()) { + const auto* values = static_cast(statistics); + take_max(stats.gpu_percent, + DictionaryNumber(values, CFSTR("Device Utilization %"))); + take_max(stats.gpu_renderer_percent, + DictionaryNumber(values, CFSTR("Renderer Utilization %"))); + take_max(stats.gpu_tiler_percent, + DictionaryNumber(values, CFSTR("Tiler Utilization %"))); + add(stats.gpu_memory_in_use, + DictionaryNumber(values, CFSTR("In use system memory"))); + add(stats.gpu_memory_allocated, + DictionaryNumber(values, CFSTR("Alloc system memory"))); + add(stats.gpu_recoveries, DictionaryNumber(values, CFSTR("recoveryCount"))); + } + CFRelease(statistics); + } + IOObjectRelease(service); + } + IOObjectRelease(iterator); + stats.gpu_model = state.gpu_model; + stats.gpu_cores = state.gpu_cores; +} + +void SampleThreads(HostStats& stats, Counters& now) { + thread_act_array_t threads = nullptr; + mach_msg_type_number_t count = 0; + const auto task = mach_task_self(); + if (task_threads(task, &threads, &count) != KERN_SUCCESS) { + return; + } + stats.thread_count = count; + for (mach_msg_type_number_t i = 0; i < count; i++) { + thread_identifier_info_data_t identifier {}; + mach_msg_type_number_t size = THREAD_IDENTIFIER_INFO_COUNT; + thread_basic_info_data_t basic {}; + mach_msg_type_number_t basic_size = THREAD_BASIC_INFO_COUNT; + if (thread_info(threads[i], THREAD_IDENTIFIER_INFO, + reinterpret_cast(&identifier), &size) == KERN_SUCCESS && + thread_info(threads[i], THREAD_BASIC_INFO, reinterpret_cast(&basic), + &basic_size) == KERN_SUCCESS) { + const auto to_ns = [](const time_value_t& time) { + return static_cast(time.seconds) * 1000000000u + + static_cast(time.microseconds) * 1000u; + }; + auto& thread = now.threads[identifier.thread_id]; + thread.cpu_ns = to_ns(basic.user_time) + to_ns(basic.system_time); + thread_extended_info_data_t extended {}; + mach_msg_type_number_t extended_size = THREAD_EXTENDED_INFO_COUNT; + if (thread_info(threads[i], THREAD_EXTENDED_INFO, + reinterpret_cast(&extended), + &extended_size) == KERN_SUCCESS) { + thread.name = std::string(extended.pth_name, + strnlen(extended.pth_name, sizeof(extended.pth_name))); + } + } + mach_port_deallocate(task, threads[i]); + } + vm_deallocate(task, reinterpret_cast(threads), count * sizeof(thread_act_t)); +} + +void SamplePlatform(PlatformState& state, HostStats& stats, Counters& now) { + SampleRusage(now); + + host_cpu_load_info_data_t load {}; + mach_msg_type_number_t load_count = HOST_CPU_LOAD_INFO_COUNT; + if (host_statistics(state.host, HOST_CPU_LOAD_INFO, reinterpret_cast(&load), + &load_count) == KERN_SUCCESS) { + const uint64_t busy = static_cast(load.cpu_ticks[CPU_STATE_USER]) + + load.cpu_ticks[CPU_STATE_SYSTEM] + load.cpu_ticks[CPU_STATE_NICE]; + now.system_busy = busy; + now.system_total = busy + load.cpu_ticks[CPU_STATE_IDLE]; + } + + // Physical footprint is what Activity Monitor shows in its Memory column. + task_vm_info_data_t vm_info {}; + mach_msg_type_number_t vm_info_count = TASK_VM_INFO_COUNT; + if (task_info(mach_task_self(), TASK_VM_INFO, reinterpret_cast(&vm_info), + &vm_info_count) == KERN_SUCCESS) { + stats.process_memory = vm_info.phys_footprint; + stats.process_resident = vm_info.resident_size; + stats.process_compressed = vm_info.compressed; + stats.process_file_backed = vm_info.external; + stats.process_virtual = vm_info.virtual_size; + } + rusage_info_v2 usage {}; + if (proc_pid_rusage(getpid(), RUSAGE_INFO_V2, reinterpret_cast(&usage)) == 0) { + now.disk_read = usage.ri_diskio_bytesread; + now.disk_write = usage.ri_diskio_byteswritten; + } + + stats.system_memory_total = SysctlValue("hw.memsize"); + vm_size_t page = 0; + vm_statistics64_data_t vm {}; + mach_msg_type_number_t count = HOST_VM_INFO64_COUNT; + if (host_page_size(state.host, &page) == KERN_SUCCESS && + host_statistics64(state.host, HOST_VM_INFO64, reinterpret_cast(&vm), + &count) == KERN_SUCCESS) { + // Activity Monitor's "Memory Used": app memory + wired + compressed. + const uint64_t app = vm.internal_page_count > vm.purgeable_count + ? vm.internal_page_count - vm.purgeable_count + : 0; + stats.system_memory_used = (app + vm.wire_count + vm.compressor_page_count) * page; + stats.system_wired = static_cast(vm.wire_count) * page; + stats.system_compressed = static_cast(vm.compressor_page_count) * page; + stats.system_cached = + (static_cast(vm.external_page_count) + vm.purgeable_count) * page; + stats.system_free = static_cast(vm.free_count) * page; + } + + if (const auto swap = SysctlValue("vm.swapusage")) { + stats.swap_used = swap->xsu_used; + stats.swap_total = swap->xsu_total; + } + + // 1 = normal, 2 = warning, 4 = critical, as reported to dispatch memory-pressure sources. + if (const auto level = SysctlValue("kern.memorystatus_vm_pressure_level")) { + stats.memory_pressure = *level >= 4 ? MemoryPressure::Critical + : *level >= 2 ? MemoryPressure::Warning + : MemoryPressure::Normal; + } + + // 0 nominal, 1 moderate, 2 heavy, 3 trapping, 4 sleeping. + if (!state.thermal_ready) { + state.thermal_ready = notify_register_check("com.apple.system.thermalpressurelevel", + &state.thermal_token) == NOTIFY_STATUS_OK; + } + uint64_t thermal = 0; + if (state.thermal_ready && + notify_get_state(state.thermal_token, &thermal) == NOTIFY_STATUS_OK) { + stats.thermal_pressure = thermal >= 3 ? ThermalPressure::Critical + : thermal == 2 ? ThermalPressure::Heavy + : thermal == 1 ? ThermalPressure::Moderate + : ThermalPressure::Nominal; + } + + SampleThreads(stats, now); + SampleGpu(state, stats); +} + +#else + +using ProcValues = std::unordered_map; + +// Reads ": [kB]" lines; kB values are returned in bytes. +ProcValues ReadProcValues(const char* path) { + ProcValues values; + std::ifstream file(path); + std::string line; + while (std::getline(file, line)) { + const auto colon = line.find(':'); + if (colon == std::string::npos) { + continue; + } + std::istringstream stream(line.substr(colon + 1)); + uint64_t number = 0; + std::string unit; + if (stream >> number) { + stream >> unit; + values[line.substr(0, colon)] = unit == "kB" ? number * 1024u : number; + } + } + return values; +} + +std::optional Find(const ProcValues& values, const char* key) { + const auto it = values.find(key); + return it != values.end() ? std::optional(it->second) : std::nullopt; +} + +struct PlatformState {}; + +void SampleThreads(HostStats& stats, Counters& now) { + static const double ns_per_tick = 1e9 / static_cast(std::max(sysconf(_SC_CLK_TCK), 1L)); + std::error_code error; + uint32_t count = 0; + for (std::filesystem::directory_iterator it("/proc/self/task", error), end; !error && it != end; + it.increment(error)) { + count++; + std::ifstream stat_file(it->path() / "stat"); + std::string stat; + uint64_t id = 0; + if (!(stat_file >> id)) { + continue; // The thread may have exited during enumeration. + } + std::getline(stat_file, stat); + const auto close = stat.rfind(')'); + if (close == std::string::npos) { + continue; + } + // Fields after the command name start at field 3 (state); utime and stime are 14 and 15. + std::istringstream fields(stat.substr(close + 2)); + std::string field; + for (int index = 3; index < 14 && fields >> field; index++) { + } + uint64_t user = 0; + uint64_t system = 0; + if (!(fields >> user >> system)) { + continue; + } + auto& thread = now.threads[id]; + thread.cpu_ns = static_cast(static_cast(user + system) * ns_per_tick); + std::ifstream name_file(it->path() / "comm"); + std::getline(name_file, thread.name); + } + if (!error) { + stats.thread_count = count; + } +} + +void SamplePlatform(PlatformState& /*state*/, HostStats& stats, Counters& now) { + SampleRusage(now); + + std::ifstream cpu_file("/proc/stat"); + std::string label; + if (cpu_file >> label && label == "cpu") { + uint64_t total = 0; + uint64_t idle = 0; + uint64_t value = 0; + for (int index = 0; index < 8 && cpu_file >> value; index++) { + total += value; + if (index == 3 || index == 4) { // idle, iowait + idle += value; + } + } + now.system_total = total; + now.system_busy = total - idle; + } + + const auto status = ReadProcValues("/proc/self/status"); + stats.process_memory = Find(status, "VmRSS"); + stats.process_resident = Find(status, "VmRSS"); + stats.process_file_backed = Find(status, "RssFile"); + stats.process_compressed = Find(status, "VmSwap"); + stats.process_virtual = Find(status, "VmSize"); + + const auto io = ReadProcValues("/proc/self/io"); + now.disk_read = Find(io, "read_bytes"); + now.disk_write = Find(io, "write_bytes"); + + const auto memory = ReadProcValues("/proc/meminfo"); + const auto total = Find(memory, "MemTotal"); + const auto available = Find(memory, "MemAvailable"); + if (total && available && *available <= *total) { + stats.system_memory_total = total; + stats.system_memory_used = *total - *available; + } + stats.system_free = Find(memory, "MemFree"); + if (const auto cached = Find(memory, "Cached")) { + stats.system_cached = *cached + Find(memory, "Buffers").value_or(0); + } + const auto swap_total = Find(memory, "SwapTotal"); + const auto swap_free = Find(memory, "SwapFree"); + if (swap_total && swap_free && *swap_free <= *swap_total) { + stats.swap_total = swap_total; + stats.swap_used = *swap_total - *swap_free; + } + + // Pressure stall information: share of the last 10 s some task waited on memory. + std::ifstream pressure("/proc/pressure/memory"); + std::string kind; + std::string average; + double stalled = 0.0; + if (pressure >> kind >> average && kind == "some" && average.rfind("avg10=", 0) == 0 && + std::istringstream(average.substr(6)) >> stalled) { + stats.memory_pressure = stalled >= 20.0 ? MemoryPressure::Critical + : stalled >= 5.0 ? MemoryPressure::Warning + : MemoryPressure::Normal; + } else if (total && available && *total != 0) { + const double free_share = static_cast(*available) / static_cast(*total); + stats.memory_pressure = free_share < 0.10 ? MemoryPressure::Critical + : free_share < 0.25 ? MemoryPressure::Warning + : MemoryPressure::Normal; + } + + SampleThreads(stats, now); +} + +#endif +#endif + +constexpr size_t BUSIEST_THREADS = 6; + +} // namespace + +struct HostStatsSampler::Impl { + PlatformState state; + Counters last; + uint32_t logical_cores = std::max(std::thread::hardware_concurrency(), 1u); + bool has_last = false; +}; + +HostStatsSampler::HostStatsSampler(): m_impl(std::make_unique()) {} + +HostStatsSampler::~HostStatsSampler() = default; + +HostStats HostStatsSampler::Sample() { + HostStats stats; + stats.logical_cores = m_impl->logical_cores; + + Counters now; + now.wall_ns = WallNanoseconds(); + SamplePlatform(m_impl->state, stats, now); + + const auto& last = m_impl->last; + const auto elapsed = static_cast(now.wall_ns - last.wall_ns) / 1e9; + if (m_impl->has_last && elapsed > 0.0) { + const auto rate = [&](std::optional current, + std::optional previous) -> std::optional { + if (!current || !previous || *current < *previous) { + return std::nullopt; + } + return static_cast(*current - *previous) / elapsed; + }; + if (const auto cpu = rate(now.process_cpu_ns, last.process_cpu_ns)) { + stats.process_cpu_percent = + std::clamp(*cpu / 1e9 * 100.0 / stats.logical_cores, 0.0, 100.0); + } + if (now.system_total && last.system_total && now.system_busy && last.system_busy && + *now.system_total > *last.system_total && *now.system_busy >= *last.system_busy) { + stats.system_cpu_percent = + std::clamp(static_cast(*now.system_busy - *last.system_busy) / + static_cast(*now.system_total - *last.system_total) * 100.0, + 0.0, 100.0); + } + stats.context_switches_per_s = rate(now.context_switches, last.context_switches); + stats.page_faults_per_s = rate(now.page_faults, last.page_faults); + stats.page_ins_per_s = rate(now.page_ins, last.page_ins); + stats.disk_read_per_s = rate(now.disk_read, last.disk_read); + stats.disk_write_per_s = rate(now.disk_write, last.disk_write); + + for (const auto& [id, thread]: now.threads) { + // A thread missing from the last sample started during this interval. + const auto previous = last.threads.find(id); + const uint64_t before = previous != last.threads.end() ? previous->second.cpu_ns : 0; + if (thread.cpu_ns < before) { + continue; + } + const double percent = + static_cast(thread.cpu_ns - before) / 1e9 / elapsed * 100.0; + stats.busiest_threads.push_back( + {thread.name.empty() ? "thread " + std::to_string(id) : thread.name, percent}); + } + std::sort(stats.busiest_threads.begin(), stats.busiest_threads.end(), + [](const auto& a, const auto& b) { return a.cpu_percent > b.cpu_percent; }); + if (stats.busiest_threads.size() > BUSIEST_THREADS) { + stats.busiest_threads.resize(BUSIEST_THREADS); + } + } + m_impl->last = std::move(now); + m_impl->has_last = true; + return stats; +} + +} // namespace Common diff --git a/src/common/hostStats.h b/src/common/hostStats.h new file mode 100644 index 000000000..24d2595a2 --- /dev/null +++ b/src/common/hostStats.h @@ -0,0 +1,86 @@ +#ifndef KYTY_COMMON_HOST_STATS_H_ +#define KYTY_COMMON_HOST_STATS_H_ + +#include +#include +#include +#include +#include + +namespace Common { + +enum class MemoryPressure { Unknown, Normal, Warning, Critical }; +enum class ThermalPressure { Unknown, Nominal, Moderate, Heavy, Critical }; + +struct ThreadUsage { + std::string name; + double cpu_percent = 0.0; // Of one core. +}; + +// Host-wide and process-wide resource usage. Missing values are std::nullopt; rates are per +// second since the previous sample. +struct HostStats { + uint32_t logical_cores = 0; + + // Processing. + std::optional process_cpu_percent; // Share of all logical cores, 0-100. + std::optional system_cpu_percent; // Whole machine, 0-100. + std::optional thread_count; + std::vector busiest_threads; // Busiest first. + std::optional context_switches_per_s; + std::optional page_faults_per_s; + std::optional page_ins_per_s; // Faults that read from disk. + ThermalPressure thermal_pressure = ThermalPressure::Unknown; + + // Emulator process memory. + std::optional process_memory; // Physical footprint / resident set. + std::optional process_resident; + std::optional process_compressed; // Compressed or swapped out. + std::optional process_file_backed; + std::optional process_virtual; // Reserved address space. + + // System memory. + std::optional system_memory_used; + std::optional system_memory_total; + std::optional system_wired; + std::optional system_compressed; + std::optional system_cached; // File cache, reclaimable. + std::optional system_free; + std::optional swap_used; + std::optional swap_total; + MemoryPressure memory_pressure = MemoryPressure::Unknown; + + // Disk I/O of the emulator process. + std::optional disk_read_per_s; + std::optional disk_write_per_s; + + // GPU, whole device (macOS IOAccelerator). + std::string gpu_model; + std::optional gpu_cores; + std::optional gpu_percent; + std::optional gpu_renderer_percent; + std::optional gpu_tiler_percent; + std::optional gpu_memory_in_use; // System memory the GPU driver has in use. + std::optional gpu_memory_allocated; + std::optional gpu_recoveries; // GPU resets since boot. +}; + +// May block for a few milliseconds (thread enumeration, OS registry reads), so call it off the +// render thread. The first sample has no rates. +class HostStatsSampler final { +public: + HostStatsSampler(); + ~HostStatsSampler(); + HostStatsSampler(const HostStatsSampler&) = delete; + HostStatsSampler& operator=(const HostStatsSampler&) = delete; + + [[nodiscard]] HostStats Sample(); + +private: + struct Impl; + std::unique_ptr m_impl; +}; + +} // namespace Common + +#endif /* KYTY_COMMON_HOST_STATS_H_ */ diff --git a/src/graphics/guest_gpu/graphicsRun.cpp b/src/graphics/guest_gpu/graphicsRun.cpp index e2b06883e..7863836c4 100644 --- a/src/graphics/guest_gpu/graphicsRun.cpp +++ b/src/graphics/guest_gpu/graphicsRun.cpp @@ -1,6 +1,7 @@ #include "graphics/guest_gpu/graphicsRun.h" #include "common/assert.h" +#include "common/debugCounters.h" #include "common/emulatorConfig.h" #include "common/logging/log.h" #include "common/profiler.h" @@ -906,6 +907,7 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun if (draw_count == 0) { return; } + Common::DebugCounters::Add(Common::DebugCounters::Counter::IndirectDraws, draw_count); const auto args_size = indexed ? sizeof(DrawIndexedIndirectArgs) : sizeof(DrawIndirectArgs); EXIT_NOT_IMPLEMENTED(stride_in_bytes < args_size); diff --git a/src/graphics/host_gpu/graphicContext.h b/src/graphics/host_gpu/graphicContext.h index 3aa09cda1..971cc983e 100644 --- a/src/graphics/host_gpu/graphicContext.h +++ b/src/graphics/host_gpu/graphicContext.h @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -99,6 +100,14 @@ struct GraphicContext { [[nodiscard]] bool CanReportMemoryUsage() const noexcept { return memory_budget_ext_enabled; } [[nodiscard]] uint64_t GetDeviceMemoryUsage() const; [[nodiscard]] uint64_t GetTotalMemoryBudget() const; + + struct HeapUsage { + uint64_t usage = 0; // Whole process, as reported by VK_EXT_memory_budget. + uint64_t budget = 0; + uint32_t allocations = 0; + bool device_local = false; + }; + [[nodiscard]] uint32_t GetHeapUsage(std::span heaps) const; [[nodiscard]] bool CreateImage(const vk::ImageCreateInfo& info, VulkanImage& image); void DeleteImage(VulkanImage& image); diff --git a/src/graphics/host_gpu/renderer/cache/bufferCache.cpp b/src/graphics/host_gpu/renderer/cache/bufferCache.cpp index e2c3802f3..d452db969 100644 --- a/src/graphics/host_gpu/renderer/cache/bufferCache.cpp +++ b/src/graphics/host_gpu/renderer/cache/bufferCache.cpp @@ -2,6 +2,7 @@ #include "common/alignment.h" #include "common/assert.h" +#include "common/debugCounters.h" #include "common/logging/log.h" #include "common/profiler.h" #include "graphics/guest_gpu/graphicsRun.h" @@ -177,6 +178,7 @@ bool BufferCache::DownloadBufferMemory(Buffer& buffer, uint64_t vaddr, uint64_t if (copies.empty()) { return false; } + Common::DebugCounters::Add(Common::DebugCounters::Counter::BufferDownloadBytes, total_size); auto [mapped, offset] = m_download_buffer.Map(total_size, 64); std::unique_ptr temporary; @@ -467,6 +469,7 @@ vk::Buffer BufferCache::UploadCopies(Buffer& buffer, std::span c if (copies.empty()) { return nullptr; } + Common::DebugCounters::Add(Common::DebugCounters::Counter::BufferUploadBytes, total_size); const auto source = UploadMemory(m_graphics, m_scheduler, m_staging_buffer, total_size, 4, vk::BufferUsageFlagBits::eTransferSrc, [&](uint8_t* mapped, uint64_t base_offset) { @@ -658,6 +661,7 @@ void BufferCache::RunGarbageCollector() { } else { m_memory_tracker.UntrackMemory(buffer.CpuAddress(), buffer.Size()); DeleteBuffer(id); + Common::DebugCounters::Add(Common::DebugCounters::Counter::BufferEvictions); } return ++retire_count == limit; }); diff --git a/src/graphics/host_gpu/renderer/cache/textureCache.cpp b/src/graphics/host_gpu/renderer/cache/textureCache.cpp index 2f591439e..6e173b09a 100644 --- a/src/graphics/host_gpu/renderer/cache/textureCache.cpp +++ b/src/graphics/host_gpu/renderer/cache/textureCache.cpp @@ -2,6 +2,7 @@ #include "common/alignment.h" #include "common/assert.h" +#include "common/debugCounters.h" #include "common/emulatorConfig.h" #include "common/logging/log.h" #include "common/profiler.h" @@ -954,6 +955,7 @@ void TextureCache::UploadImage(Image& image, Buffer& source, uint64_t source_off const auto linear = m_tiler.Detile(source.Handle(), source_offset, info); const auto copies = info.BufferCopies(linear.offset); destination.Upload(copies, linear.buffer, linear.offset, linear.size); + Common::DebugCounters::Add(Common::DebugCounters::Counter::TextureUploadBytes, linear.size); return; } @@ -999,6 +1001,7 @@ void TextureCache::UploadImage(Image& image, Buffer& source, uint64_t source_off copies[0].imageExtent = info.extent; } destination.Upload(copies, linear.buffer, linear.offset, linear.size); + Common::DebugCounters::Add(Common::DebugCounters::Counter::TextureUploadBytes, linear.size); } void TextureCache::InitializeImage(ImageId id) { @@ -1774,6 +1777,7 @@ bool TextureCache::DownloadImageMemory(ImageId id) { download.Flush(offset, range.size); DownloadImage(image, download, offset, range.size, image.info.resources.levels); + Common::DebugCounters::Add(Common::DebugCounters::Counter::TextureDownloadBytes, range.size); vk::BufferMemoryBarrier barrier {}; barrier.srcAccessMask = vk::AccessFlagBits::eMemoryWrite | vk::AccessFlagBits::eTransferWrite | vk::AccessFlagBits::eShaderWrite; @@ -1943,6 +1947,7 @@ void TextureCache::RunGarbageCollector() { } } FreeImage(id); + Common::DebugCounters::Add(Common::DebugCounters::Counter::TextureEvictions); if (m_total_used_memory < m_critical_gc_memory && aggressive) { deletions >>= 2; aggressive = false; diff --git a/src/graphics/host_gpu/renderer/commandScheduler.cpp b/src/graphics/host_gpu/renderer/commandScheduler.cpp index bb59bba6b..afab5519e 100644 --- a/src/graphics/host_gpu/renderer/commandScheduler.cpp +++ b/src/graphics/host_gpu/renderer/commandScheduler.cpp @@ -1,6 +1,7 @@ #include "graphics/host_gpu/renderer/commandScheduler.h" #include "common/assert.h" +#include "common/debugCounters.h" #include "common/logging/log.h" #include "graphics/host_gpu/graphicContext.h" @@ -373,6 +374,7 @@ uint64_t CommandScheduler::Submit(SubmitInfo submit) { result = graphics.queue.submit(1, &submit_info, nullptr); } + Common::DebugCounters::Add(Common::DebugCounters::Counter::QueueSubmits); if (result != vk::Result::eSuccess) { ReportVulkanFatal("vkQueueSubmit", result, tick, m_command.m_debug_op, diff --git a/src/graphics/host_gpu/renderer/masterSemaphore.cpp b/src/graphics/host_gpu/renderer/masterSemaphore.cpp index 7a79187a9..24f2269cf 100644 --- a/src/graphics/host_gpu/renderer/masterSemaphore.cpp +++ b/src/graphics/host_gpu/renderer/masterSemaphore.cpp @@ -1,6 +1,7 @@ #include "graphics/host_gpu/renderer/masterSemaphore.h" #include "common/assert.h" +#include "common/debugCounters.h" #include "graphics/host_gpu/graphicContext.h" namespace Libs::Graphics { @@ -49,6 +50,8 @@ void MasterSemaphore::Wait(uint64_t tick) { wait_info.pSemaphores = &m_semaphore; wait_info.pValues = &tick; + Common::DebugCounters::ScopedTimer timer(Common::DebugCounters::Counter::GpuWaits, + Common::DebugCounters::Counter::GpuWaitNs); const auto result = m_graphics.device.waitSemaphores(&wait_info, UINT64_MAX); EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess); Refresh(); diff --git a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp index 5aca239d6..8fa5954c8 100644 --- a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp @@ -1,6 +1,7 @@ #include "graphics/host_gpu/renderer/pipeline/pipelineCache.h" #include "common/assert.h" +#include "common/debugCounters.h" #include "common/emulatorConfig.h" #include "common/file.h" #include "common/logging/log.h" @@ -24,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -299,6 +301,9 @@ struct PipelineCache::ProgramCache { ShaderRecompiler::IR::ResourceSpecialization specialization, uint32_t push_data_start_dword, std::span function_code) { + Common::DebugCounters::ScopedTimer timer(Common::DebugCounters::Counter::ShadersCompiled, + Common::DebugCounters::Counter::ShaderCompileNs, + Common::DebugCounters::Peak::ShaderCompileNs); auto result = ShaderRecompiler::CompileProgram(std::move(translated), options, specialization, push_data_start_dword); if (!ValidateShaderSpirv(options.dump_label, options.shader_hash, result.spirv, @@ -993,8 +998,13 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline( auto cached = std::make_unique(); LogPipelineTrace("CreatePipelineInternal begin", vs_id, ps_id); - CreatePipelineInternal(m_graphics, *cached, rendering, key.vertex_input, vertex_info, - ps_input_info, programs, static_params, m_driver_cache); + { + Common::DebugCounters::ScopedTimer timer(Common::DebugCounters::Counter::PipelinesCreated, + Common::DebugCounters::Counter::PipelineCreateNs, + Common::DebugCounters::Peak::PipelineCreateNs); + CreatePipelineInternal(m_graphics, *cached, rendering, key.vertex_input, vertex_info, + ps_input_info, programs, static_params, m_driver_cache); + } LogPipelineTrace("CreatePipelineInternal done", vs_id, ps_id); EXIT_NOT_IMPLEMENTED(cached->pipeline == nullptr); @@ -1024,7 +1034,13 @@ PipelineCache::GetComputePipeline(const ShaderComputeInputInfo& input_info, } auto cached = std::make_unique(); - CreatePipelineInternal(m_graphics, *cached, input_info, compute_program.module, m_driver_cache); + { + Common::DebugCounters::ScopedTimer timer(Common::DebugCounters::Counter::PipelinesCreated, + Common::DebugCounters::Counter::PipelineCreateNs, + Common::DebugCounters::Peak::PipelineCreateNs); + CreatePipelineInternal(m_graphics, *cached, input_info, compute_program.module, + m_driver_cache); + } EXIT_NOT_IMPLEMENTED(cached->pipeline == nullptr); EXIT_NOT_IMPLEMENTED(cached->pipeline_layout == nullptr); diff --git a/src/graphics/host_gpu/renderer/renderCompute.cpp b/src/graphics/host_gpu/renderer/renderCompute.cpp index 1b2f17bd4..ae7fa68bf 100644 --- a/src/graphics/host_gpu/renderer/renderCompute.cpp +++ b/src/graphics/host_gpu/renderer/renderCompute.cpp @@ -1,5 +1,6 @@ #include "common/assert.h" #include "common/common.h" +#include "common/debugCounters.h" #include "common/emulatorConfig.h" #include "common/file.h" #include "common/logging/log.h" @@ -432,6 +433,7 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer, vk_buffer.bindPipeline(vk::PipelineBindPoint::eCompute, pipeline.pipeline); vk_buffer.dispatch(thread_group_x, thread_group_y, thread_group_z); + Common::DebugCounters::Add(Common::DebugCounters::Counter::Dispatches); // The removed host fence also ordered read-only dispatches before later writers. ShaderAccessBarrier(vk_buffer, vk::PipelineStageFlagBits::eComputeShader); @@ -501,6 +503,7 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer, 1, &barrier, 0, nullptr, 0, nullptr); vk_buffer.bindPipeline(vk::PipelineBindPoint::eCompute, pipeline.pipeline); vk_buffer.dispatchIndirect(args_buffer->Handle(), args_offset); + Common::DebugCounters::Add(Common::DebugCounters::Counter::Dispatches); ShaderAccessBarrier(vk_buffer, vk::PipelineStageFlagBits::eComputeShader); ResetBindings(); } diff --git a/src/graphics/host_gpu/renderer/renderDraw.cpp b/src/graphics/host_gpu/renderer/renderDraw.cpp index 28581695d..ee8bad43e 100644 --- a/src/graphics/host_gpu/renderer/renderDraw.cpp +++ b/src/graphics/host_gpu/renderer/renderDraw.cpp @@ -2,6 +2,7 @@ #include "common/assert.h" #include "common/common.h" +#include "common/debugCounters.h" #include "common/emulatorConfig.h" #include "common/file.h" #include "common/logging/log.h" @@ -1233,6 +1234,8 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff } else { EmitDrawPrimitives(ucfg, vk_buffer, draw, emit); } + Common::DebugCounters::Add(Common::DebugCounters::Counter::Draws); + Common::DebugCounters::Add(Common::DebugCounters::Counter::DrawInstances, draw.instance_count); if (!draw.IsIndexed()) { SetDrawDebugPhase(buffer, submit_id, draw, 0x600u); diff --git a/src/graphics/host_gpu/vma.cpp b/src/graphics/host_gpu/vma.cpp index f7b512f32..944066fff 100644 --- a/src/graphics/host_gpu/vma.cpp +++ b/src/graphics/host_gpu/vma.cpp @@ -97,6 +97,24 @@ uint64_t GraphicContext::GetDeviceMemoryUsage() const { return usage; } +uint32_t GraphicContext::GetHeapUsage(std::span heaps) const { + if (allocator == nullptr) { + return 0; + } + VmaBudget budgets[VK_MAX_MEMORY_HEAPS] {}; + vmaGetHeapBudgets(allocator, budgets); + const auto count = physical_device_memory_properties.memoryHeapCount; + for (uint32_t heap = 0; heap < count; heap++) { + const auto& properties = physical_device_memory_properties.memoryHeaps[heap]; + heaps[heap] = {.usage = budgets[heap].usage, + .budget = budgets[heap].budget, + .allocations = budgets[heap].statistics.allocationCount, + .device_local = static_cast(properties.flags & + vk::MemoryHeapFlagBits::eDeviceLocal)}; + } + return count; +} + uint64_t GraphicContext::GetTotalMemoryBudget() const { if (allocator == nullptr) { return 0; diff --git a/src/graphics/presentation/performanceOverlay.cpp b/src/graphics/presentation/performanceOverlay.cpp new file mode 100644 index 000000000..ac8da4cc9 --- /dev/null +++ b/src/graphics/presentation/performanceOverlay.cpp @@ -0,0 +1,747 @@ +#include "graphics/presentation/performanceOverlay.h" + +#include "common/debugCounters.h" +#include "common/emulatorConfig.h" +#include "common/hostStats.h" +#include "graphics/host_gpu/graphicContext.h" +#include "imgui.h" +#include "kernel/memory.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Libs::Graphics { + +namespace { + +using Clock = std::chrono::steady_clock; +using Counter = Common::DebugCounters::Counter; +using Peak = Common::DebugCounters::Peak; + +constexpr auto REFRESH_INTERVAL = std::chrono::seconds(1); +constexpr size_t FRAME_HISTORY = 120; +constexpr float FRAME_GRAPH_MS = 50.0f; +constexpr float FRAME_TIME_GOOD = 17.5f; +constexpr float FRAME_TIME_MEDIUM = 34.0f; +constexpr size_t COUNTER_COUNT = static_cast(Counter::Count); + +// Tables as wide as their columns; the default fills the window, which an auto-resizing +// window then never shrinks. +constexpr ImGuiTableFlags TABLE_FLAGS = + ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoHostExtendX; + +constexpr ImU32 GOOD = IM_COL32(96, 220, 120, 255); +constexpr ImU32 MEDIUM = IM_COL32(230, 195, 60, 255); +constexpr ImU32 BAD = IM_COL32(235, 80, 70, 255); +constexpr ImU32 NEUTRAL = IM_COL32(160, 160, 160, 255); + +struct FrameWindow { + std::array intervals_ms {}; + size_t next = 0; + size_t count = 0; + uint32_t flips = 0; + float max_ms = 0.0f; + uint32_t guest_width = 0; + uint32_t guest_height = 0; + Clock::time_point last_flip = {}; + Clock::time_point window_start = {}; +}; + +// Shown values, recomputed once per refresh interval. +struct Snapshot { + double fps = 0.0; + double frame_ms = 0.0; + double max_frame_ms = 0.0; + double low_fps = 0.0; // From the 99th percentile. + double stalled_s = 0.0; + uint32_t guest_width = 0; + uint32_t guest_height = 0; + std::array rates {}; + std::array totals {}; + uint64_t shader_compile_peak_ns = 0; + uint64_t pipeline_create_peak_ns = 0; + Common::HostStats host; + std::optional device_memory; + uint64_t device_budget = 0; + std::array heaps {}; + uint32_t heap_count = 0; + std::array history {}; + size_t history_count = 0; + Clock::time_point sample_time {}; + LibKernel::Memory::DebugStats guest_memory; +}; + +std::mutex g_frame_mutex; +FrameWindow g_frames; +std::atomic g_generation {0}; +std::atomic g_details {false}; +std::atomic g_revision {0}; + +std::mutex g_host_mutex; +Common::HostStats g_host_stats; +uint64_t g_host_generation = 0; +std::condition_variable_any g_sampler_wait; +std::jthread g_sampler; + +void SetOverlayEnabled(bool enabled) { + { + std::scoped_lock lock(g_host_mutex); + if (enabled) { + std::scoped_lock frame_lock(g_frame_mutex); + g_frames = {}; + g_generation.fetch_add(1, std::memory_order_release); + } + Common::DebugCounters::SetEnabled(enabled); + } + g_sampler_wait.notify_all(); +} + +bool GuestStalled(Clock::time_point now) { + std::scoped_lock lock(g_frame_mutex); + return now - g_frames.last_flip >= REFRESH_INTERVAL; +} + +// Host statistics can block for milliseconds (IOKit, thread enumeration), so sample them off +// the present path. While the guest stops flipping (loading screens), ask for one repaint per +// interval. +void SamplerThread(const std::stop_token& stop) { + std::optional sampler; + uint64_t sampled_generation = 0; + std::unique_lock lock(g_host_mutex); + while (!stop.stop_requested()) { + g_sampler_wait.wait(lock, stop, [] { return Common::DebugCounters::Enabled(); }); + if (stop.stop_requested()) { + break; + } + const auto generation = g_generation.load(std::memory_order_acquire); + lock.unlock(); + if (!sampler || generation != sampled_generation) { + sampler.emplace(); + sampled_generation = generation; + } + auto stats = sampler->Sample(); + lock.lock(); + g_host_stats = std::move(stats); + g_host_generation = generation; + if (Common::DebugCounters::Enabled() && GuestStalled(Clock::now())) { + g_revision.fetch_add(1, std::memory_order_acq_rel); + } + g_sampler_wait.wait_for(lock, stop, REFRESH_INTERVAL, [generation] { + return !Common::DebugCounters::Enabled() || + generation != g_generation.load(std::memory_order_acquire); + }); + } +} + +void EnsureSampler() { + static std::once_flag once; + std::call_once(once, [] { g_sampler = std::jthread(SamplerThread); }); +} + +ImU32 FrameTimeColor(double ms) { + return ms <= FRAME_TIME_GOOD ? GOOD : ms <= FRAME_TIME_MEDIUM ? MEDIUM : BAD; +} + +// Green below `warn`, yellow below `bad`, red above. +ImU32 LevelColor(double value, double warn, double bad) { + return value < warn ? GOOD : value < bad ? MEDIUM : BAD; +} + +ImVec4 Color(ImU32 color) { + return ImGui::ColorConvertU32ToFloat4(color); +} + +std::string Bytes(uint64_t bytes) { + constexpr double GIB = 1024.0 * 1024.0 * 1024.0; + constexpr double MIB = 1024.0 * 1024.0; + char buffer[32]; + if (static_cast(bytes) >= GIB) { + std::snprintf(buffer, sizeof(buffer), "%.2f GB", static_cast(bytes) / GIB); + } else { + std::snprintf(buffer, sizeof(buffer), "%.0f MB", static_cast(bytes) / MIB); + } + return buffer; +} + +std::string Bytes(std::optional bytes) { + return bytes ? Bytes(*bytes) : "n/a"; +} + +std::string Throughput(double bytes_per_second) { + char buffer[32]; + std::snprintf(buffer, sizeof(buffer), "%.1f MB/s", bytes_per_second / (1024.0 * 1024.0)); + return buffer; +} + +std::string Percent(std::optional value) { + if (!value) { + return "n/a"; + } + char buffer[16]; + std::snprintf(buffer, sizeof(buffer), "%.0f%%", *value); + return buffer; +} + +void Row(const char* label, const std::string& value, std::optional color = std::nullopt) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextDisabled("%s", label); + ImGui::TableNextColumn(); + if (color) { + ImGui::TextColored(Color(*color), "%s", value.c_str()); + } else { + ImGui::TextUnformatted(value.c_str()); + } +} + +[[gnu::format(printf, 1, 2)]] std::string Fmt(const char* format, ...) { + char buffer[128]; + va_list args; + va_start(args, format); + std::vsnprintf(buffer, sizeof(buffer), format, args); + va_end(args); + return buffer; +} + +bool BeginSection(const char* title) { + ImGui::SeparatorText(title); + return ImGui::BeginTable(title, 2, TABLE_FLAGS); +} + +void DrawFrameGraph(const std::array& history, size_t count, float width, + float height) { + const ImVec2 origin = ImGui::GetCursorScreenPos(); + auto* draw = ImGui::GetWindowDrawList(); + draw->AddRectFilled(origin, {origin.x + width, origin.y + height}, IM_COL32(0, 0, 0, 90)); + const auto y_of = [&](float ms) { + return origin.y + height - std::min(ms, FRAME_GRAPH_MS) / FRAME_GRAPH_MS * height; + }; + for (const float reference: {1000.0f / 60.0f, 1000.0f / 30.0f}) { + draw->AddLine({origin.x, y_of(reference)}, {origin.x + width, y_of(reference)}, + IM_COL32(255, 255, 255, 45)); + } + const float bar = width / static_cast(FRAME_HISTORY); + for (size_t i = 0; i < count; i++) { + const float ms = history[i]; + const float x = origin.x + width - static_cast(count - i) * bar; + draw->AddRectFilled({x, y_of(ms)}, {x + std::max(bar - 1.0f, 1.0f), origin.y + height}, + FrameTimeColor(ms)); + } + ImGui::Dummy({width, height}); +} + +const char* PressureName(Common::MemoryPressure pressure) { + switch (pressure) { + case Common::MemoryPressure::Normal: return "Normal"; + case Common::MemoryPressure::Warning: return "Warning"; + case Common::MemoryPressure::Critical: return "Critical"; + case Common::MemoryPressure::Unknown: break; + } + return "n/a"; +} + +ImU32 PressureColor(Common::MemoryPressure pressure) { + switch (pressure) { + case Common::MemoryPressure::Normal: return GOOD; + case Common::MemoryPressure::Warning: return MEDIUM; + case Common::MemoryPressure::Critical: return BAD; + case Common::MemoryPressure::Unknown: break; + } + return NEUTRAL; +} + +const char* ThermalName(Common::ThermalPressure thermal) { + switch (thermal) { + case Common::ThermalPressure::Nominal: return "Nominal"; + case Common::ThermalPressure::Moderate: return "Moderate"; + case Common::ThermalPressure::Heavy: return "Heavy (throttling)"; + case Common::ThermalPressure::Critical: return "Critical (throttling)"; + case Common::ThermalPressure::Unknown: break; + } + return "n/a"; +} + +ImU32 ThermalColor(Common::ThermalPressure thermal) { + switch (thermal) { + case Common::ThermalPressure::Nominal: return GOOD; + case Common::ThermalPressure::Moderate: return MEDIUM; + case Common::ThermalPressure::Heavy: + case Common::ThermalPressure::Critical: return BAD; + case Common::ThermalPressure::Unknown: break; + } + return NEUTRAL; +} + +const char* PresentModeName(Config::PresentMode mode) { + switch (mode) { + case Config::PresentMode::Fifo: return "FIFO"; + case Config::PresentMode::Mailbox: return "Mailbox"; + case Config::PresentMode::Immediate: return "Immediate"; + } + return "?"; +} + +double Rate(const Snapshot& shown, Counter counter) { + return shown.rates[static_cast(counter)]; +} + +uint64_t Total(const Snapshot& shown, Counter counter) { + return shown.totals[static_cast(counter)]; +} + +void TransferRow(const Snapshot& shown, const char* label, Counter upload, Counter download) { + Row(label, + Throughput(Rate(shown, upload)) + " up / " + Throughput(Rate(shown, download)) + " down"); +} + +void GpuActivityRows(const Snapshot& shown) { + const auto draws = Rate(shown, Counter::Draws); + Row("Draw calls", Fmt("%.0f/frame %.0f/s", draws / std::max(shown.fps, 1.0), draws)); + // Includes present pacing waits, so a high value alone is not a stall. + Row("CPU waiting on GPU", + Fmt("%.0f ms/s (%.0f waits/s)", Rate(shown, Counter::GpuWaitNs) / 1e6, + Rate(shown, Counter::GpuWaits))); +} + +void CompilationRows(const Snapshot& shown) { + const auto row = [&](const char* label, Counter count_counter, Counter duration_counter, + uint64_t peak_ns) { + const auto count = Total(shown, count_counter); + Row(label, + Fmt("%llu (avg %.1f ms, peak %.1f ms)", static_cast(count), + count != 0 ? static_cast(Total(shown, duration_counter)) / 1e6 / + static_cast(count) + : 0.0, + static_cast(peak_ns) / 1e6), + Rate(shown, count_counter) > 0.0 ? MEDIUM : GOOD); + }; + row("Shader recompiler", Counter::ShadersCompiled, Counter::ShaderCompileNs, + shown.shader_compile_peak_ns); + row("Pipeline compile", Counter::PipelinesCreated, Counter::PipelineCreateNs, + shown.pipeline_create_peak_ns); +} + +void Refresh(Snapshot& shown, const GraphicContext& graphics, Clock::time_point now, + uint64_t generation) { + const double seconds = shown.sample_time == Clock::time_point {} + ? 0.0 + : std::chrono::duration(now - shown.sample_time).count(); + for (size_t i = 0; i < COUNTER_COUNT; i++) { + const auto value = Common::DebugCounters::Get(static_cast(i)); + shown.rates[i] = + seconds > 0.0 ? static_cast(value - shown.totals[i]) / seconds : 0.0; + shown.totals[i] = value; + } + shown.shader_compile_peak_ns = Common::DebugCounters::Get(Peak::ShaderCompileNs); + shown.pipeline_create_peak_ns = Common::DebugCounters::Get(Peak::PipelineCreateNs); + shown.sample_time = now; + { + std::scoped_lock lock(g_host_mutex); + if (g_host_generation == generation) { + shown.host = g_host_stats; + } + } + shown.heap_count = graphics.GetHeapUsage(shown.heaps); + shown.device_memory = + graphics.CanReportMemoryUsage() ? std::optional(0) : std::nullopt; + shown.device_budget = 0; + const bool discrete = + graphics.GetPhysicalDeviceProperties().deviceType == vk::PhysicalDeviceType::eDiscreteGpu; + for (uint32_t i = 0; i < shown.heap_count; i++) { + const auto& heap = shown.heaps[i]; + if (shown.device_memory && (!discrete || heap.device_local)) { + *shown.device_memory += heap.usage; + shown.device_budget += heap.budget; + } + } + if (g_details.load(std::memory_order_acquire)) { + shown.guest_memory = LibKernel::Memory::GetDebugStats(); + } +} + +void DrawCompact(const Snapshot& shown) { + if (!ImGui::BeginTable("##performance", 2, TABLE_FLAGS)) { + return; + } + const auto& host = shown.host; + if (shown.max_frame_ms > 0.0) { + Row("Worst frame", Fmt("%.1f ms", shown.max_frame_ms), FrameTimeColor(shown.max_frame_ms)); + } + Row("CPU", host.process_cpu_percent + ? Fmt("%.0f%% of %u threads", *host.process_cpu_percent, host.logical_cores) + : std::string("n/a")); + Row("GPU", Percent(host.gpu_percent)); + GpuActivityRows(shown); + CompilationRows(shown); + if (shown.device_memory) { + Row("GPU memory", Bytes(*shown.device_memory) + " / " + Bytes(shown.device_budget)); + } + TransferRow(shown, "Texture transfers", Counter::TextureUploadBytes, + Counter::TextureDownloadBytes); + TransferRow(shown, "Buffer transfers", Counter::BufferUploadBytes, + Counter::BufferDownloadBytes); + Row("Emulator RAM", Bytes(host.process_memory)); + if (host.system_memory_used && host.system_memory_total) { + Row("System RAM", + Bytes(*host.system_memory_used) + " / " + Bytes(*host.system_memory_total)); + } + ImGui::EndTable(); +} + +void DrawFramesAndCpu(const Snapshot& shown, vk::Extent2D extent) { + const auto& host = shown.host; + if (BeginSection("Frames")) { + if (shown.low_fps > 0.0) { + Row("1% low", Fmt("%.1f FPS", shown.low_fps), FrameTimeColor(1000.0 / shown.low_fps)); + } + Row("Worst frame", Fmt("%.1f ms", shown.max_frame_ms), FrameTimeColor(shown.max_frame_ms)); + Row("Guest output", shown.guest_width != 0 + ? Fmt("%ux%u", shown.guest_width, shown.guest_height) + : std::string("n/a")); + Row("Window", Fmt("%ux%u %s %u Hz", extent.width, extent.height, + PresentModeName(Config::GetPresentMode()), Config::GetVblankFrequency())); + Row("Flips", Fmt("%.0f/s requested", Rate(shown, Counter::FlipsRequested))); + const auto rejected = Rate(shown, Counter::FlipsRejected); + Row("Flip queue full", Fmt("%.0f/s", rejected), rejected > 0.0 ? BAD : GOOD); + const auto late = Rate(shown, Counter::LateVblanks); + Row("Late vblanks", Fmt("%.0f/s", late), LevelColor(late, 1.0, 10.0)); + Row("Swapchain rebuilds", + Fmt("%llu (dropped %llu)", + static_cast(Total(shown, Counter::SwapchainRecreations)), + static_cast(Total(shown, Counter::DroppedPresents)))); + ImGui::EndTable(); + } + if (BeginSection("CPU")) { + Row("Emulator", + host.process_cpu_percent + ? Fmt("%.0f%% of %u threads (%.1f cores)", *host.process_cpu_percent, + host.logical_cores, *host.process_cpu_percent * host.logical_cores / 100.0) + : std::string("n/a")); + Row("Machine", Percent(host.system_cpu_percent), + host.system_cpu_percent ? std::optional(LevelColor(*host.system_cpu_percent, 70, 90)) + : std::nullopt); + if (host.thread_count) { + Row("Host threads", Fmt("%u", *host.thread_count)); + } + for (const auto& thread: host.busiest_threads) { + Row(Fmt(" %.24s", thread.name.c_str()).c_str(), Fmt("%.0f%%", thread.cpu_percent), + LevelColor(thread.cpu_percent, 70, 95)); + } + if (host.context_switches_per_s) { + Row("Context switches", Fmt("%.0f/s", *host.context_switches_per_s)); + } + if (host.page_faults_per_s) { + Row("Page faults", Fmt("%.0f/s (%.0f from disk)", *host.page_faults_per_s, + host.page_ins_per_s.value_or(0.0))); + } + Row("Thermal", ThermalName(host.thermal_pressure), ThermalColor(host.thermal_pressure)); + const auto emulated = Rate(shown, Counter::EmulatedInstructions); + Row("Emulated instrs", Fmt("%.0f/s", emulated), LevelColor(emulated, 1000, 100000)); + const auto unresolved = Rate(shown, Counter::UnresolvedImportCalls); + Row("Missing HLE calls", + Fmt("%.0f/s (%llu total)", unresolved, + static_cast(Total(shown, Counter::UnresolvedImportCalls))), + unresolved > 0.0 ? MEDIUM : GOOD); + ImGui::EndTable(); + } +} + +void DrawGpu(const Snapshot& shown, const GraphicContext& graphics) { + const auto& host = shown.host; + if (BeginSection("GPU")) { + Row("Device", host.gpu_model.empty() + ? std::string(graphics.GetPhysicalDeviceProperties().deviceName.data()) + : host.gpu_cores + ? Fmt("%s (%u cores)", host.gpu_model.c_str(), *host.gpu_cores) + : host.gpu_model); + Row("Utilization", Percent(host.gpu_percent), + host.gpu_percent ? std::optional(LevelColor(*host.gpu_percent, 80, 95)) : std::nullopt); + if (host.gpu_renderer_percent || host.gpu_tiler_percent) { + Row("Renderer / tiler", + Percent(host.gpu_renderer_percent) + " / " + Percent(host.gpu_tiler_percent)); + } + if (host.gpu_memory_in_use) { + Row("Driver memory", Bytes(*host.gpu_memory_in_use) + " in use / " + + Bytes(host.gpu_memory_allocated) + " allocated"); + } + if (host.gpu_recoveries) { + Row("GPU resets", Fmt("%llu", static_cast(*host.gpu_recoveries)), + *host.gpu_recoveries > 0 ? BAD : GOOD); + } + const double fps = std::max(shown.fps, 1.0); + GpuActivityRows(shown); + Row("Instances", Fmt("%.0f/frame", Rate(shown, Counter::DrawInstances) / fps)); + Row("Indirect draws", Fmt("%.0f/s", Rate(shown, Counter::IndirectDraws))); + Row("Dispatches", Fmt("%.0f/frame %.0f/s", Rate(shown, Counter::Dispatches) / fps, + Rate(shown, Counter::Dispatches))); + Row("Queue submits", Fmt("%.0f/s", Rate(shown, Counter::QueueSubmits))); + const auto faults = Rate(shown, Counter::GpuFaults); + Row("Write-tracking faults", Fmt("%.0f/s", faults), LevelColor(faults, 20000, 100000)); + CompilationRows(shown); + ImGui::EndTable(); + } + if (BeginSection("GPU memory")) { + if (shown.device_memory) { + const double share = shown.device_budget != 0 + ? static_cast(*shown.device_memory) / + static_cast(shown.device_budget) * 100.0 + : 0.0; + Row("Vulkan budget", Bytes(*shown.device_memory) + " / " + Bytes(shown.device_budget), + LevelColor(share, 80, 95)); + } + for (size_t heap = 0; heap < shown.heap_count; heap++) { + const auto& usage = shown.heaps[heap]; + Row(Fmt(" Heap %zu%s", heap, usage.device_local ? " (device)" : "").c_str(), + Bytes(usage.usage) + " / " + Bytes(usage.budget) + + Fmt(" %u allocs", usage.allocations)); + } + TransferRow(shown, "Texture transfers", Counter::TextureUploadBytes, + Counter::TextureDownloadBytes); + Row("Texture evictions", Fmt("%.0f/s", Rate(shown, Counter::TextureEvictions))); + TransferRow(shown, "Buffer transfers", Counter::BufferUploadBytes, + Counter::BufferDownloadBytes); + Row("Buffer evictions", Fmt("%.0f/s", Rate(shown, Counter::BufferEvictions))); + ImGui::EndTable(); + } +} + +void DrawMemoryAndIo(const Snapshot& shown) { + const auto& host = shown.host; + const auto& guest = shown.guest_memory; + if (BeginSection("PS5 memory")) { + const auto direct_used = guest.direct_allocated + guest.pooled_allocated; + Row("Direct", Bytes(direct_used) + " / " + Bytes(guest.direct_total), + LevelColor(guest.direct_total != 0 ? static_cast(direct_used) / + static_cast(guest.direct_total) * 100.0 + : 0.0, + 85, 97)); + Row(" Mapped", Bytes(guest.direct_mapped)); + Row(" Pool", + Bytes(guest.pooled_allocated) + " (" + Bytes(guest.pool_committed) + " committed)"); + Row("Flexible", Bytes(guest.flexible_used) + " / " + Bytes(guest.flexible_total)); + Row("Extended", Bytes(guest.automatic_allocated)); + // Past the pool size the guest sees no page-table entries available. + const auto entries = std::max(guest.cpu_page_entries, guest.gpu_page_entries); + Row("Page table (2 MB)", + Fmt("CPU %llu GPU %llu / %llu", + static_cast(guest.cpu_page_entries), + static_cast(guest.gpu_page_entries), + static_cast(guest.page_entries_total)), + entries > guest.page_entries_total ? BAD + : entries * 10 > guest.page_entries_total * 9 ? MEDIUM + : GOOD); + ImGui::EndTable(); + } + if (BeginSection("Emulator process")) { + Row("Footprint", Bytes(host.process_memory)); + Row("Resident", Bytes(host.process_resident)); + if (host.process_compressed) { + Row("Compressed / swapped", Bytes(*host.process_compressed), + *host.process_compressed > (1ull << 30u) ? MEDIUM : GOOD); + } + Row("File-backed", Bytes(host.process_file_backed)); + Row("Address space", Bytes(host.process_virtual)); + ImGui::EndTable(); + } + if (BeginSection("System memory")) { + if (host.system_memory_used && host.system_memory_total) { + Row("Used", Bytes(*host.system_memory_used) + " / " + Bytes(*host.system_memory_total)); + } + if (host.system_wired) { + Row("Wired", Bytes(*host.system_wired)); + } + if (host.system_compressed) { + Row("Compressed", Bytes(*host.system_compressed)); + } + Row("Cached files", Bytes(host.system_cached)); + Row("Free", Bytes(host.system_free)); + if (host.swap_used) { + Row("Swap", Bytes(*host.swap_used) + " / " + Bytes(host.swap_total), + *host.swap_used > (4ull << 30u) ? MEDIUM : GOOD); + } + Row("Pressure", PressureName(host.memory_pressure), PressureColor(host.memory_pressure)); + ImGui::EndTable(); + } + if (BeginSection("I/O & audio")) { + Row("Disk read / write", Throughput(host.disk_read_per_s.value_or(0.0)) + " / " + + Throughput(host.disk_write_per_s.value_or(0.0))); + Row("Guest file read / write", Throughput(Rate(shown, Counter::GuestFileReadBytes)) + + " / " + + Throughput(Rate(shown, Counter::GuestFileWriteBytes))); + const auto underruns = Rate(shown, Counter::AudioUnderruns); + Row("Audio underruns", + Fmt("%.0f/s (%llu total)", underruns, + static_cast(Total(shown, Counter::AudioUnderruns))), + underruns > 0.0 ? BAD : GOOD); + ImGui::EndTable(); + } +} + +} // namespace + +void PerformanceOverlayRecordFlip(uint32_t guest_width, uint32_t guest_height) noexcept { + if (!Common::DebugCounters::Enabled()) { + return; + } + std::scoped_lock lock(g_frame_mutex); + const auto now = Clock::now(); + if (g_frames.window_start == Clock::time_point {}) { + g_frames.window_start = now; + } + auto& frames = g_frames; + if (frames.last_flip != Clock::time_point {}) { + const float ms = std::chrono::duration(now - frames.last_flip).count(); + frames.intervals_ms[frames.next] = ms; + frames.next = (frames.next + 1) % FRAME_HISTORY; + frames.count = std::min(frames.count + 1, FRAME_HISTORY); + frames.max_ms = std::max(frames.max_ms, ms); + } + frames.last_flip = now; + frames.guest_width = guest_width; + frames.guest_height = guest_height; + frames.flips++; +} + +bool PerformanceOverlayEnabled() noexcept { + return Common::DebugCounters::Enabled(); +} + +void TogglePerformanceOverlay() noexcept { + SetOverlayEnabled(!Common::DebugCounters::Enabled()); + g_revision.fetch_add(1, std::memory_order_acq_rel); +} + +void TogglePerformanceOverlayDetails() noexcept { + if (Common::DebugCounters::Enabled()) { + g_details.store(!g_details.load(std::memory_order_acquire), std::memory_order_release); + } else { + g_details.store(true, std::memory_order_release); + SetOverlayEnabled(true); + } + g_revision.fetch_add(1, std::memory_order_acq_rel); +} + +uint64_t PerformanceOverlayRevision() noexcept { + return g_revision.load(std::memory_order_acquire); +} + +float DrawPerformanceOverlay(const GraphicContext& graphics, vk::Extent2D extent) { + if (!PerformanceOverlayEnabled()) { + return 0.0f; + } + EnsureSampler(); + + static Snapshot shown; + static uint64_t shown_generation = 0; + static bool shown_details = false; + const auto generation = g_generation.load(std::memory_order_acquire); + const bool details = g_details.load(std::memory_order_acquire); + const auto now = Clock::now(); + if (generation != shown_generation) { + shown = {}; + shown_generation = generation; + } + if (shown.sample_time == Clock::time_point {} || now - shown.sample_time >= REFRESH_INTERVAL || + details != shown_details) { + { + std::scoped_lock lock(g_frame_mutex); + auto& frames = g_frames; + shown.history_count = frames.count; + for (size_t i = 0; i < frames.count; i++) { + shown.history[i] = + frames.intervals_ms[(frames.next + FRAME_HISTORY - frames.count + i) % + FRAME_HISTORY]; + } + if (frames.window_start == Clock::time_point {}) { + frames.window_start = now; + } + const double seconds = std::chrono::duration(now - frames.window_start).count(); + shown.fps = seconds > 0.0 ? frames.flips / seconds : 0.0; + shown.frame_ms = frames.flips > 0 ? seconds * 1000.0 / frames.flips : 0.0; + shown.max_frame_ms = frames.max_ms; + shown.stalled_s = frames.last_flip == Clock::time_point {} + ? 0.0 + : std::chrono::duration(now - frames.last_flip).count(); + shown.guest_width = frames.guest_width; + shown.guest_height = frames.guest_height; + frames.flips = 0; + frames.max_ms = 0.0f; + frames.window_start = now; + } + shown_details = details; + if (shown.history_count > 0) { + auto sorted = shown.history; + const auto end = sorted.begin() + shown.history_count; + const auto percentile = sorted.begin() + shown.history_count * 99 / 100; + std::nth_element(sorted.begin(), percentile, end); + shown.low_fps = *percentile > 0.0f ? 1000.0 / *percentile : 0.0; + } + Refresh(shown, graphics, now, generation); + } + + const float scale = std::clamp(std::min(static_cast(extent.width) / 1920.0f, + static_cast(extent.height) / 1080.0f), + 0.75f, 1.5f); + ImGui::SetNextWindowPos({static_cast(extent.width) - 12.0f * scale, 12.0f * scale}, + ImGuiCond_Always, {1.0f, 0.0f}); + ImGui::SetNextWindowBgAlpha(details ? 0.82f : 0.72f); + constexpr ImGuiWindowFlags FLAGS = + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoBringToFrontOnFocus | + ImGuiWindowFlags_AlwaysAutoResize; + ImGui::PushFont(nullptr, (details ? 13.0f : 15.0f) * scale); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f * scale); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {10.0f * scale, 8.0f * scale}); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {8.0f * scale, 2.0f * scale}); + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, {4.0f * scale, 1.0f * scale}); + if (ImGui::Begin("##kyty_performance_overlay", nullptr, FLAGS)) { + if (shown.fps > 0.0) { + ImGui::TextColored(Color(FrameTimeColor(shown.frame_ms)), "%.1f FPS %.1f ms", + shown.fps, shown.frame_ms); + } else if (shown.stalled_s >= 1.0) { + ImGui::TextColored(Color(BAD), "No frames for %.0f s", shown.stalled_s); + } else { + ImGui::TextDisabled("Waiting for frames"); + } + DrawFrameGraph(shown.history, shown.history_count, (details ? 330.0f : 260.0f) * scale, + 46.0f * scale); + if (details) { + // Three columns keep the detailed panel within a 720p window. + if (ImGui::BeginTable("##details", 3, TABLE_FLAGS)) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + DrawFramesAndCpu(shown, extent); + ImGui::TableNextColumn(); + DrawGpu(shown, graphics); + ImGui::TableNextColumn(); + DrawMemoryAndIo(shown); + ImGui::EndTable(); + } + } else { + DrawCompact(shown); + } +#if defined(__APPLE__) + ImGui::TextDisabled(details ? "Cmd+Shift+P compact Cmd+P hide" + : "Cmd+Shift+P details Cmd+P hide"); +#else + ImGui::TextDisabled(details ? "Shift+F2 compact F2 hide" : "Shift+F2 details F2 hide"); +#endif + } + const float bottom = ImGui::GetWindowPos().y + ImGui::GetWindowSize().y; + ImGui::End(); + ImGui::PopStyleVar(4); + ImGui::PopFont(); + return bottom; +} + +} // namespace Libs::Graphics diff --git a/src/graphics/presentation/performanceOverlay.h b/src/graphics/presentation/performanceOverlay.h new file mode 100644 index 000000000..8b7b318eb --- /dev/null +++ b/src/graphics/presentation/performanceOverlay.h @@ -0,0 +1,30 @@ +#ifndef EMULATOR_SRC_GRAPHICS_PRESENTATION_PERFORMANCEOVERLAY_H_ +#define EMULATOR_SRC_GRAPHICS_PRESENTATION_PERFORMANCEOVERLAY_H_ + +#include "graphics/host_gpu/vulkanCommon.h" + +#include + +namespace Libs::Graphics { + +struct GraphicContext; + +// Called for each main-bus guest flip, with the guest's video-out size. +void PerformanceOverlayRecordFlip(uint32_t guest_width, uint32_t guest_height) noexcept; + +[[nodiscard]] bool PerformanceOverlayEnabled() noexcept; +void TogglePerformanceOverlay() noexcept; +// Switches between the compact and the detailed panel, showing the panel if it was hidden. +void TogglePerformanceOverlayDetails() noexcept; + +// Changes when the panel needs a repaint without a guest flip (toggled, or the guest stalled). +[[nodiscard]] uint64_t PerformanceOverlayRevision() noexcept; + +// Must run between ImGui::NewFrame() and ImGui::Render(), on the present thread while the +// graphics subsystem is alive. Draws in the top-right corner and returns the panel's bottom +// edge, or 0 when hidden. +float DrawPerformanceOverlay(const GraphicContext& graphics, vk::Extent2D extent); + +} // namespace Libs::Graphics + +#endif // EMULATOR_SRC_GRAPHICS_PRESENTATION_PERFORMANCEOVERLAY_H_ diff --git a/src/graphics/presentation/systemOverlay.cpp b/src/graphics/presentation/systemOverlay.cpp index 845780f84..9993b8363 100644 --- a/src/graphics/presentation/systemOverlay.cpp +++ b/src/graphics/presentation/systemOverlay.cpp @@ -1,7 +1,5 @@ #include "graphics/presentation/systemOverlay.h" -#include - #include "common/assert.h" #include "common/emulatorConfig.h" #include "common/file.h" @@ -11,10 +9,11 @@ #include "gpu_blit_shaders/gpu_imgui_hdr_spv.h" #include "graphics/host_gpu/graphicContext.h" #include "graphics/presentation/ddsImage.h" +#include "graphics/presentation/performanceOverlay.h" #include "graphics/presentation/window/hostInput.h" #include "imgui.h" -#include "imgui_internal.h" #include "imgui_impl_vulkan.h" +#include "imgui_internal.h" #include "libs/controller.h" #include "libs/dialog.h" #include "libs/ime.h" @@ -22,6 +21,7 @@ #include "loader/systemContent.h" #include "stb_image.h" +#include #include #include #include @@ -712,7 +712,8 @@ SystemOverlayVisualState GetSystemOverlayVisualState() noexcept { return {core.active || dialog.active || system.active || trophy_active || splash || (cursor.enabled && cursor.cursor_visible), core.revision + dialog.revision + system.revision + (trophy_active ? 1u : 0u) + - (splash ? 2u : 0u) + static_cast(cursor.revision)}; + (splash ? 2u : 0u) + static_cast(cursor.revision) + + PerformanceOverlayRevision()}; } bool ProcessSystemOverlayInput(const SDL_Event& event) { @@ -1662,8 +1663,7 @@ struct SystemOverlay::Impl { } } - - void DrawTrophyNotification(vk::Extent2D frame_extent, Clock::time_point now) { + void DrawTrophyNotification(vk::Extent2D frame_extent, Clock::time_point now, float min_top) { std::erase_if(retired_trophy_images, [](const auto& texture) { if (texture->Status != ImTextureStatus_Destroyed) { return false; @@ -1731,7 +1731,7 @@ struct SystemOverlay::Impl { const float alpha = std::min(fade_in, fade_out); const float slide = (1.0f - fade_in) * (width + 24.0f); const ImVec2 top_left {static_cast(frame_extent.width) - 32.0f - width + slide, - 32.0f}; + std::max(32.0f, min_top + 12.0f)}; const ImVec2 bottom_right {top_left.x + width, top_left.y + height}; auto* draw = ImGui::GetForegroundDrawList(); const auto fade = [alpha](int r, int g, int b, int a) { @@ -1799,7 +1799,7 @@ struct SystemOverlay::Impl { const auto now = Clock::now(); if (!has_overlay && !g_trophy_notification_active.load(std::memory_order_acquire) && !g_splash_active.load(std::memory_order_acquire) && - !GetHostInputTouchPadVisualState().enabled) { + !GetHostInputTouchPadVisualState().enabled && !PerformanceOverlayEnabled()) { return false; } const auto prepared_session = snapshot.session; @@ -1830,6 +1830,7 @@ struct SystemOverlay::Impl { ImGui_ImplVulkan_NewFrame(); ImGui::NewFrame(); DrawSplash(frame_extent); + const float performance_bottom = DrawPerformanceOverlay(graphics, frame_extent); GetOverlaySnapshot(&snapshot); if (has_overlay && snapshot.session == prepared_session) { if (snapshot.session.kind == OverlayKind::Dialog) { @@ -1838,7 +1839,7 @@ struct SystemOverlay::Impl { DrawIme(snapshot.ime, frame_extent); } } - DrawTrophyNotification(frame_extent, now); + DrawTrophyNotification(frame_extent, now, performance_bottom); DrawTouchPadCursor(GetHostInputTouchPadVisualState()); ImGui::Render(); extent = frame_extent; diff --git a/src/graphics/presentation/videoOut.cpp b/src/graphics/presentation/videoOut.cpp index 22f00feb4..f93caa31c 100644 --- a/src/graphics/presentation/videoOut.cpp +++ b/src/graphics/presentation/videoOut.cpp @@ -3,6 +3,7 @@ #include "common/abi.h" #include "common/assert.h" #include "common/common.h" +#include "common/debugCounters.h" #include "common/emulatorConfig.h" #include "common/logging/log.h" #include "common/profiler.h" @@ -15,6 +16,7 @@ #include "graphics/host_gpu/renderer/image/imageInfo.h" #include "graphics/host_gpu/renderer/render.h" #include "graphics/host_gpu/renderer/renderContext.h" +#include "graphics/presentation/performanceOverlay.h" #include "graphics/presentation/presenter.h" #include "graphics/presentation/renderDoc.h" #include "kernel/pthread.h" @@ -909,6 +911,9 @@ void VideoOutDriver::Impl::PresentThread(std::stop_token token) { const auto frame_end = Common::Timer::QueryPerformanceCounter(); total_wait += static_cast(period) - static_cast(frame_end - frame_begin); + if (total_wait < 0) { + Common::DebugCounters::Add(Common::DebugCounters::Counter::LateVblanks); + } } } @@ -938,6 +943,7 @@ bool FlipQueue::Reserve(VideoOutConfig& cfg, int index, int64_t flip_arg, FlipRe m_done_cond_var.SignalAll(); } if (cfg.flip_status.flipPendingNum >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) { + Common::DebugCounters::Add(Common::DebugCounters::Counter::FlipsRejected); return false; } auto& pending = source == FlipRequestSource::GpuEop ? m_requests : m_cpu_requests; @@ -965,6 +971,7 @@ bool FlipQueue::Reserve(VideoOutConfig& cfg, int index, int64_t flip_arg, FlipRe pending.push_back(r); request_id = r.id; + Common::DebugCounters::Add(Common::DebugCounters::Counter::FlipsRequested); cfg.flip_status.flipPendingNum++; cfg.flip_status.submitProcessTimeCounter = r.submit_ptc; @@ -1243,6 +1250,9 @@ bool FlipQueue::Flip(uint32_t micros) { } if (due) { m_presenter.Present(std::span(layers.data(), count)); + if (requests[0].cfg->bus == 0) { + Graphics::PerformanceOverlayRecordFlip(requests[0].cfg->width, requests[0].cfg->height); + } } m_mutex.Lock(); diff --git a/src/graphics/presentation/window/hostInput.cpp b/src/graphics/presentation/window/hostInput.cpp index 7c4ed81c3..48f2dbb42 100644 --- a/src/graphics/presentation/window/hostInput.cpp +++ b/src/graphics/presentation/window/hostInput.cpp @@ -254,8 +254,8 @@ public: } const bool reserved = binding.key == SDLK_ESCAPE || binding.key == SDLK_F1 || - binding.key == SDLK_F7 || binding.key == SDLK_F8 || - binding.key == SDLK_F11; + binding.key == SDLK_F2 || binding.key == SDLK_F7 || + binding.key == SDLK_F8 || binding.key == SDLK_F11; if (binding.control == INVALID_CONTROL || reserved || (binding.key == SDLK_UNKNOWN && binding.mouse_button == 0)) { EXIT("Invalid input mapping: %s\n", value.c_str()); diff --git a/src/graphics/presentation/window/swapchain.cpp b/src/graphics/presentation/window/swapchain.cpp index 2a67094eb..853bd8db4 100644 --- a/src/graphics/presentation/window/swapchain.cpp +++ b/src/graphics/presentation/window/swapchain.cpp @@ -1,5 +1,6 @@ #include "common/assert.h" #include "common/common.h" +#include "common/debugCounters.h" #include "common/emulatorConfig.h" #include "common/logging/log.h" #include "common/profiler.h" @@ -10,6 +11,7 @@ #include "graphics/host_gpu/renderer/render.h" #include "graphics/host_gpu/renderer/renderContext.h" #include "graphics/host_gpu/vulkanCommon.h" +#include "graphics/presentation/performanceOverlay.h" #include "graphics/presentation/presenter.h" #include "graphics/presentation/systemOverlay.h" #include "graphics/presentation/videoOut.h" @@ -324,6 +326,7 @@ struct Presenter::Impl { void RecoverSwapchain(Swapchain::Status status) { LOGF("Recovering Vulkan swapchain%s\n", status == Swapchain::Status::SurfaceLost ? " and surface" : ""); + Common::DebugCounters::Add(Common::DebugCounters::Counter::SwapchainRecreations); swapchain.Recreate(status == Swapchain::Status::SurfaceLost); frames.SetFormat(swapchain.Format()); } @@ -1039,8 +1042,8 @@ void Presenter::Impl::Present() { // present_mutex owns the prepared frames and this scheduler. Only queue submission // is shared with rendering, and it has its own queue_mutex. auto& command = present_scheduler.BeginCommand(); - const bool draw_system_overlay = - overlay_visual.active && swapchain.PrepareSystemOverlay(); + const bool draw_system_overlay = (overlay_visual.active || PerformanceOverlayEnabled()) && + swapchain.PrepareSystemOverlay(); swapchain.RecordPresentCommands(command, layers[0].frame, layers[1], draw_system_overlay); const auto tick = swapchain.Submit(present_scheduler); @@ -1060,6 +1063,7 @@ void Presenter::Impl::Present() { return; } LOGF("Vulkan presentation retry exhausted; dropping frame\n"); + Common::DebugCounters::Add(Common::DebugCounters::Counter::DroppedPresents); } void Presenter::Discard(Frame& frame) { diff --git a/src/graphics/presentation/window/window.cpp b/src/graphics/presentation/window/window.cpp index 206c67fd6..689086249 100644 --- a/src/graphics/presentation/window/window.cpp +++ b/src/graphics/presentation/window/window.cpp @@ -1,21 +1,20 @@ #include "graphics/presentation/window.h" -#include - #include "common/assert.h" #include "common/common.h" #include "common/emulatorConfig.h" #include "common/file.h" #include "common/logging/log.h" #include "common/profiler.h" +#include "common/stringUtils.h" #include "common/systemInfo.h" #include "common/threads.h" #include "common/timer.h" -#include "common/stringUtils.h" #include "graphics/host_gpu/graphicContext.h" #include "graphics/host_gpu/renderer/render.h" #include "graphics/host_gpu/renderer/renderContext.h" #include "graphics/host_gpu/vulkanCommon.h" +#include "graphics/presentation/performanceOverlay.h" #include "graphics/presentation/renderDoc.h" #include "graphics/presentation/systemOverlay.h" #include "graphics/presentation/window/hostInput.h" @@ -24,13 +23,14 @@ #include "libs/controller.h" #include "loader/systemContent.h" +#include #include +#include #include #include #include #include #include -#include #define STB_IMAGE_IMPLEMENTATION #define STBI_NO_SIMD @@ -459,19 +459,37 @@ void WindowContext::ProcessDisplayEvent(const SDL_DisplayEvent& display) { void WindowContext::ProcessEvent(double time_s) { auto& game = loop; auto* event = &game.event; - if ((event->type == SDL_EVENT_KEY_DOWN || event->type == SDL_EVENT_KEY_UP) && - event->key.key == SDLK_F7) { - if (event->type == SDL_EVENT_KEY_DOWN && event->key.repeat == 0) { - HostInputToggleMouseToJoystick(); + if (event->type == SDL_EVENT_KEY_DOWN || event->type == SDL_EVENT_KEY_UP) { + const bool pressed = event->type == SDL_EVENT_KEY_DOWN && event->key.repeat == 0; + switch (event->key.key) { + case SDLK_F7: + if (pressed) { + HostInputToggleMouseToJoystick(); + } + return; + case SDLK_F8: + if (pressed) { + Controller::CycleKeyboardPlayer(); + } + return; +#if defined(__APPLE__) + case SDLK_P: + if ((event->key.mod & SDL_KMOD_GUI) == 0) { + break; + } + [[fallthrough]]; +#endif + case SDLK_F2: + if (pressed) { + if ((event->key.mod & SDL_KMOD_SHIFT) != 0) { + TogglePerformanceOverlayDetails(); + } else { + TogglePerformanceOverlay(); + } + } + return; + default: break; } - return; - } - if ((event->type == SDL_EVENT_KEY_DOWN || event->type == SDL_EVENT_KEY_UP) && - event->key.key == SDLK_F8) { - if (event->type == SDL_EVENT_KEY_DOWN && event->key.repeat == 0) { - Controller::CycleKeyboardPlayer(); - } - return; } if (ProcessSystemOverlayInput(*event)) { return; diff --git a/src/kernel/fileSystem.cpp b/src/kernel/fileSystem.cpp index b44f64fea..4e4d7567b 100644 --- a/src/kernel/fileSystem.cpp +++ b/src/kernel/fileSystem.cpp @@ -4,6 +4,7 @@ #include "common/assert.h" #include "common/common.h" #include "common/dateTime.h" +#include "common/debugCounters.h" #include "common/emulatorConfig.h" #include "common/file.h" #include "common/hash.h" @@ -669,6 +670,7 @@ int64_t KYTY_SYSV_ABI KernelRead(int d, void* buf, size_t nbytes) { std::min(nbytes, remaining)); uint32_t bytes_read = 0; file->f.Read(buf, static_cast(nbytes), &bytes_read); + Common::DebugCounters::Add(Common::DebugCounters::Counter::GuestFileReadBytes, bytes_read); file->mutex.Unlock(); @@ -731,6 +733,7 @@ int64_t KYTY_SYSV_ABI KernelWrite(int d, const void* buf, size_t nbytes) { file->f.Seek(file->f.Size()); } file->f.Write(buf, static_cast(nbytes), &bytes_written); + Common::DebugCounters::Add(Common::DebugCounters::Counter::GuestFileWriteBytes, bytes_written); if (file->sync_writes) { file->f.Flush(); } @@ -795,6 +798,7 @@ int64_t KYTY_SYSV_ABI KernelPread(int d, void* buf, size_t nbytes, int64_t offse uint32_t bytes_read = 0; file->f.Seek(offset); file->f.Read(buf, static_cast(nbytes), &bytes_read); + Common::DebugCounters::Add(Common::DebugCounters::Counter::GuestFileReadBytes, bytes_read); file->f.Seek(pos); file->mutex.Unlock(); @@ -896,6 +900,7 @@ int64_t KYTY_SYSV_ABI KernelPreadv(int d, const KernelIovec* iov, int iovcnt, in Memory::InvalidateMemory(reinterpret_cast(buffer.iov_base), count); uint32_t bytes = 0; file->f.Read(buffer.iov_base, count, &bytes); + Common::DebugCounters::Add(Common::DebugCounters::Counter::GuestFileReadBytes, bytes); bytes_read += bytes; remaining -= bytes; if (bytes < count) { @@ -945,6 +950,7 @@ int64_t KYTY_SYSV_ABI KernelPwrite(int d, const void* buf, size_t nbytes, int64_ uint32_t bytes_written = 0; file->f.Seek(file->append ? file->f.Size() : static_cast(offset)); file->f.Write(buf, static_cast(nbytes), &bytes_written); + Common::DebugCounters::Add(Common::DebugCounters::Counter::GuestFileWriteBytes, bytes_written); if (file->sync_writes) { file->f.Flush(); } @@ -1016,6 +1022,7 @@ static int64_t Writev(int d, const KernelIovec* iov, int iovcnt, std::optionalf.Write(buffer.iov_base, static_cast(buffer.iov_len), &bytes); + Common::DebugCounters::Add(Common::DebugCounters::Counter::GuestFileWriteBytes, bytes); bytes_written += bytes; if (bytes < buffer.iov_len) { break; diff --git a/src/kernel/memory.cpp b/src/kernel/memory.cpp index 441eecaf9..2edeb1097 100644 --- a/src/kernel/memory.cpp +++ b/src/kernel/memory.cpp @@ -4414,4 +4414,39 @@ int KYTY_SYSV_ABI KernelMemoryPoolGetBlockStats(KernelMemoryPoolBlockStats* outp return OK; } +DebugStats GetDebugStats() { + DebugStats stats; + stats.direct_total = PhysicalMemory::Size(); + stats.flexible_total = FlexibleMemory::Size(); + stats.pool_committed = g_memory_pool_committed.load(std::memory_order_relaxed); + stats.page_entries_total = PAGE_TABLE_POOL_ENTRIES; + if (g_physical_memory != nullptr) { + Common::LockGuard lock(g_physical_memory->GetMutex()); + for (const auto& [start, block]: g_physical_memory->GetPhysicalBlocks()) { + switch (block.kind) { + case PhysicalMemory::AllocationKind::Direct: + stats.direct_allocated += block.size; + break; + case PhysicalMemory::AllocationKind::Pooled: + stats.pooled_allocated += block.size; + break; + case PhysicalMemory::AllocationKind::Automatic: + stats.automatic_allocated += block.size; + break; + } + } + for (const auto& mapping: g_physical_memory->GetMappings()) { + stats.direct_mapped += mapping.map_size; + } + } + if (g_flexible_memory != nullptr) { + stats.flexible_used = stats.flexible_total - g_flexible_memory->Available(); + } + if (g_virtual_ranges != nullptr) { + stats.cpu_page_entries = g_virtual_ranges->CountPageTableEntries(false); + stats.gpu_page_entries = g_virtual_ranges->CountPageTableEntries(true); + } + return stats; +} + } // namespace Libs::LibKernel::Memory diff --git a/src/kernel/memory.h b/src/kernel/memory.h index af1231f0c..e350d755d 100644 --- a/src/kernel/memory.h +++ b/src/kernel/memory.h @@ -15,6 +15,23 @@ namespace Libs::LibKernel::Memory { void Initialize(); void Shutdown(); +// Guest memory usage for the performance panel. Takes only bookkeeping locks, so it never +// waits on the GPU; call it only while the memory subsystem is initialized. +struct DebugStats { + uint64_t direct_total = 0; + uint64_t direct_allocated = 0; // sceKernelAllocateDirectMemory. + uint64_t pooled_allocated = 0; // Memory pool expansions. + uint64_t automatic_allocated = 0; // Extended memory. + uint64_t direct_mapped = 0; + uint64_t flexible_total = 0; + uint64_t flexible_used = 0; + uint64_t pool_committed = 0; + uint64_t cpu_page_entries = 0; // 2 MiB page-table entries in use. + uint64_t gpu_page_entries = 0; + uint64_t page_entries_total = 0; // Per side. +}; +[[nodiscard]] DebugStats GetDebugStats(); + struct Lifecycle { static constexpr const char* name = "Memory"; static constexpr auto initialize = Libs::LibKernel::Memory::Initialize; diff --git a/src/launcher/forms/configuration_edit_dialog.ui b/src/launcher/forms/configuration_edit_dialog.ui index bf56d1c10..74fc5b277 100644 --- a/src/launcher/forms/configuration_edit_dialog.ui +++ b/src/launcher/forms/configuration_edit_dialog.ui @@ -242,6 +242,12 @@ Auto-hide cursor + + + Show FPS, frame times, CPU/GPU usage, RAM and memory pressure in the game window. Press F2 (Cmd+P on macOS) to toggle it, and add Shift for the detailed view. + Show performance panel + + Skip startup logos and notices in supported games. diff --git a/src/launcher/include/configuration.h b/src/launcher/include/configuration.h index af03a9548..8a3a820d1 100644 --- a/src/launcher/include/configuration.h +++ b/src/launcher/include/configuration.h @@ -130,6 +130,7 @@ public: bool tessellation_enabled = false; bool trophy_enabled = true; int trophy_duration_seconds = 6; + bool perf_overlay_enabled = false; bool skip_notice_screen = false; bool network_connected = false; int vblank_frequency = 60; @@ -168,6 +169,7 @@ public: tessellation_enabled = other.tessellation_enabled; trophy_enabled = other.trophy_enabled; trophy_duration_seconds = other.trophy_duration_seconds; + perf_overlay_enabled = other.perf_overlay_enabled; skip_notice_screen = other.skip_notice_screen; network_connected = other.network_connected; vblank_frequency = other.vblank_frequency; diff --git a/src/launcher/src/configuration.cpp b/src/launcher/src/configuration.cpp index 5f394b6c6..a6e63dfba 100644 --- a/src/launcher/src/configuration.cpp +++ b/src/launcher/src/configuration.cpp @@ -17,6 +17,7 @@ QVariantMap Configuration::GameSettings() const { KYTY_CFG_SET(tessellation_enabled); KYTY_CFG_SET(trophy_enabled); KYTY_CFG_SET(trophy_duration_seconds); + KYTY_CFG_SET(perf_overlay_enabled); KYTY_CFG_SET(skip_notice_screen); KYTY_CFG_SET(network_connected); KYTY_CFG_SET(vblank_frequency); @@ -65,6 +66,7 @@ void Configuration::ReadGameSettingsValues(const Settings& s) { KYTY_CFG_GET(tessellation_enabled); trophy_enabled = s.value("trophy_enabled", trophy_enabled).toBool(); KYTY_CFG_GET(trophy_duration_seconds); + KYTY_CFG_GET(perf_overlay_enabled); KYTY_CFG_GET(skip_notice_screen); KYTY_CFG_GET(network_connected); vblank_frequency = s.value("vblank_frequency", vblank_frequency).toInt(); diff --git a/src/launcher/src/configurationEditDialog.cpp b/src/launcher/src/configurationEditDialog.cpp index 1e6c69e33..dcfcdfeb5 100644 --- a/src/launcher/src/configurationEditDialog.cpp +++ b/src/launcher/src/configurationEditDialog.cpp @@ -292,6 +292,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) { m_ui->checkBox_network_connected->setChecked(info.network_connected); m_ui->checkBox_trophy_notifications->setChecked(info.trophy_enabled); m_ui->spinBox_trophy_duration->setValue(info.trophy_duration_seconds); + m_ui->checkBox_perf_overlay->setChecked(info.perf_overlay_enabled); m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency); m_ui->comboBox_console_language->clear(); m_ui->comboBox_console_language->addItems(CONSOLE_LANGUAGE_NAMES); @@ -484,6 +485,7 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo info.network_connected = ui.checkBox_network_connected->isChecked(); info.trophy_enabled = ui.checkBox_trophy_notifications->isChecked(); info.trophy_duration_seconds = ui.spinBox_trophy_duration->value(); + info.perf_overlay_enabled = ui.checkBox_perf_overlay->isChecked(); info.vblank_frequency = ui.spinBox_vblank_frequency->value(); info.console_language = ui.comboBox_console_language->currentIndex(); info.vulkan_validation_enabled = ui.checkBox_vulkan_validation->isChecked(); diff --git a/src/launcher/src/inputMappingDialog.cpp b/src/launcher/src/inputMappingDialog.cpp index d880349cf..ec5560dae 100644 --- a/src/launcher/src/inputMappingDialog.cpp +++ b/src/launcher/src/inputMappingDialog.cpp @@ -203,7 +203,8 @@ public: auto* layout = new QVBoxLayout(this); m_label = new QLabel( - tr("Press a key or mouse button.\nF1, F7, and F11 are reserved; Esc cancels."), this); + tr("Press a key or mouse button.\nF1, F2, F7, F8, and F11 are reserved; Esc cancels."), + this); m_label->setAlignment(Qt::AlignCenter); layout->addWidget(m_label); } @@ -219,7 +220,8 @@ protected: reject(); return; } - if (event->key() == Qt::Key_F1 || event->key() == Qt::Key_F7 || + if (event->key() == Qt::Key_F1 || event->key() == Qt::Key_F2 || + event->key() == Qt::Key_F7 || event->key() == Qt::Key_F8 || event->key() == Qt::Key_F11) { m_label->setText(tr("That key is reserved by the emulator.")); return; diff --git a/src/launcher/src/mainDialog.cpp b/src/launcher/src/mainDialog.cpp index 90debf16e..e33373166 100644 --- a/src/launcher/src/mainDialog.cpp +++ b/src/launcher/src/mainDialog.cpp @@ -264,6 +264,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) { args << "--sync-raw-image-buffers" << BoolArg(info.sync_raw_image_buffers); args << "--trophy-notifications" << BoolArg(info.trophy_enabled); args << "--trophy-duration" << QString::number(info.trophy_duration_seconds); + args << "--perf-overlay" << BoolArg(info.perf_overlay_enabled); args << "--skip-notice-screen" << BoolArg(info.skip_notice_screen); args << "--network-connected" << BoolArg(info.network_connected); if (info.tessellation_enabled) { diff --git a/src/libs/audio.cpp b/src/libs/audio.cpp index 1aa13b2cc..5af755acb 100644 --- a/src/libs/audio.cpp +++ b/src/libs/audio.cpp @@ -1,6 +1,7 @@ #include "libs/audio.h" #include "common/assert.h" +#include "common/debugCounters.h" #include "common/emulatorConfig.h" #include "common/logging/log.h" #include "common/threads.h" @@ -403,6 +404,9 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking, float queued = SDL_GetAudioStreamQueued(port->stream); } if (queued < static_cast(prepared_size)) { + if (port->queue_primed) { + Common::DebugCounters::Add(Common::DebugCounters::Counter::AudioUnderruns); + } port->queue_primed = false; } } diff --git a/src/loader/runtimeLinker.cpp b/src/loader/runtimeLinker.cpp index e03768983..10557aec4 100644 --- a/src/loader/runtimeLinker.cpp +++ b/src/loader/runtimeLinker.cpp @@ -2,6 +2,7 @@ #include "common/assert.h" #include "common/common.h" +#include "common/debugCounters.h" #include "common/emulatorConfig.h" #include "common/file.h" #include "common/hostException.h" @@ -190,6 +191,7 @@ static uint64_t RegisterStubbedImport(uint32_t index, const Program* program, } static KYTY_SYSV_ABI uint64_t UnresolvedImportStub(uint64_t record_id) { + Common::DebugCounters::Add(Common::DebugCounters::Counter::UnresolvedImportCalls); const auto log_index = g_unresolved_stub_call_log_count.fetch_add(1); if (log_index < 1024) { if (record_id < g_stubbed_imports.size()) { @@ -652,6 +654,7 @@ static bool KytyExceptionHandler(const Common::HostException::ExceptionInfo& exc if (info->type == Common::HostException::ExceptionType::IllegalInstruction && Loader::X64InstructionEmulator::TryEmulate(info->native_context)) { + Common::DebugCounters::Add(Common::DebugCounters::Counter::EmulatedInstructions); return true; } @@ -666,6 +669,7 @@ static bool KytyExceptionHandler(const Common::HostException::ExceptionInfo& exc case CoreAccess::Unknown: return false; } if (Libs::LibKernel::Memory::HandleGpuFault(access, info->access_violation_vaddr)) { + Common::DebugCounters::Add(Common::DebugCounters::Counter::GpuFaults); return true; } } diff --git a/src/main.cpp b/src/main.cpp index 1846af168..fec8b2cfb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -95,6 +95,7 @@ static void PrintUsage() { " --trophy-notifications Show trophy unlock toasts and play their sound.\n"); ::printf( " --trophy-duration Trophy toast duration, 5-10. Default: 6.\n"); + ::printf(" --perf-overlay Show the performance panel (F2 or Cmd+P).\n"); ::printf(" --playgo-hack Use the supplied PlayGo stub fallback.\n"); ::printf( " --skip-notice-screen Skip startup logos and notices in supported games.\n" @@ -486,6 +487,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he ::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str()); return false; } + } else if (arg == "--perf-overlay") { + if (!ParseBool(value, options.config.perf_overlay_enabled)) { + ::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str()); + return false; + } } else if (arg == "--keymap") { const auto split = value.find('='); if (split == std::string::npos || split == 0 || split + 1 == value.size()) {