Compare commits

...

31 Commits

Author SHA1 Message Date
nmzik e5f0ee7fc2 gpu: preserve finite buffer descriptors across sparse PRT holes 2026-10-19 05:26:41 +02:00
nmzik 5f29374d5b gpu: approximate PPSA24156 EQAA with 2x MSAA and warn once 2026-10-19 05:17:32 +02:00
nmzik 2c730e58ea revert HTile lifetime changes and opt-in ZMask support 2026-10-19 05:04:56 +02:00
Pixeluted e87fb8c859 kernel: hand guest mutex ownership directly to FIFO waiters 2026-10-19 05:04:56 +02:00
Pixeluted 440245f616 gpu: bound overlapping image copies by both native backings 2026-10-19 05:04:56 +02:00
nmzik 05d25ef28c revert 01ce97388: restore compute metadata clear consumption 2026-10-19 05:04:56 +02:00
Pixeluted cad6aafdc5 shader: detect and loudly skip unsupported GPU-buffer callables 2026-10-19 05:04:56 +02:00
nmzik defb2f11d3 Share median classifications and preserve RDNA2 signed-zero selection 2026-10-19 02:34:25 +02:00
nmzik 75d7a4311d Share repeated SSA bitcasts and guard emitted shader size 2026-10-19 02:34:25 +02:00
nmzik bb505a3ae8 Recognize packed descriptor keys in GPU sampler waterfalls 2026-10-19 02:34:25 +02:00
nmzik ab8566591f shader: link multiple compute helpers without synthetic return loops 2026-10-19 02:34:25 +02:00
nmzik b2d3ce6dfc shader: size expanded sampler bindings independently of native roots 2026-10-19 02:34:25 +02:00
nmzik 09c993dfe0 shader: query dimensions through shared indirect image selection 2026-10-19 02:34:25 +02:00
nmzik de81300b28 shader: bound indexed material image selectors before heap expansion 2026-10-19 02:34:25 +02:00
nmzik ac3711ce7c shader: select the primary mip for linear explicit-LOD gathers 2026-10-19 02:34:24 +02:00
nmzik 8b51370ca1 shader: eliminate predicates for empty branch detours 2026-10-19 02:34:24 +02:00
nmzik e9bcc77e0b shader: retain resource guards for 64-bit scalar comparisons 2026-10-18 20:22:54 +02:00
nmzik 8639e0de81 shader: share indirect image accesses by type and guard emitted size 2026-10-18 20:16:49 +02:00
nmzik 7675579e79 kernel: implement POSIX fsync through the shared file API 2026-10-18 20:15:53 +02:00
nmzik 831862a50b razor: stub 2026-10-18 19:29:54 +02:00
nmzik 099de8dea6 graphics: publish each PM4 WRITE_DATA word exactly once 2026-10-18 16:17:51 +02:00
opcod3 f4bdb52922 agc: restore verified fused shader size name 2026-10-18 16:12:28 +02:00
nmzik 22ef16a044 shader: implement PS5 vector data cache invalidation 2026-10-18 15:57:14 +02:00
nmzik 37df7f3278 Show pipeline cache sizes in MB and remove obsolete texture folder cleanup 2026-10-18 03:57:45 +02:00
nmzik 33208a545a shader: remove artificial sampled image and sampler pair limit 2026-10-18 03:20:38 +02:00
Santiago Paquete 4059179e9d graphics: write proven compute buffer fills to guest memory (#1628)
Avoid readbacks for compute metadata fills

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-18 05:42:14 +02:00
nmzik fe3857a171 graphics: materialize CMASK clears for multisampled targets (PPSA13794) 2026-10-18 02:37:56 +02:00
nmzik c3e7ec8285 graphics: fix Jusant regression from d8c4a9af3 2026-10-18 01:03:37 +02:00
nmzik 763d3be3a9 graphics: add opt-in ZMask support and launcher rendering options 2026-10-17 17:50:02 +02:00
nmzik d8c4a9af38 graphics: preserve HTILE metadata across native image replacement 2026-10-17 17:31:25 +02:00
Juan Pablo Farias 6aa3846fb2 feat: add an in-game performance panel (#1090)
Add in-game performance panel

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-17 14:47:51 +02:00
79 changed files with 4080 additions and 731 deletions
+4
View File
@@ -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})
+117
View File
@@ -0,0 +1,117 @@
#ifndef KYTY_COMMON_DEBUG_COUNTERS_H_
#define KYTY_COMMON_DEBUG_COUNTERS_H_
#include <atomic>
#include <chrono>
#include <cstdint>
namespace Common::DebugCounters {
static_assert(std::atomic<uint64_t>::is_always_lock_free && std::atomic<bool>::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<bool> g_enabled {false};
inline std::atomic<uint64_t> g_counters[static_cast<int>(Counter::Count)] {};
inline std::atomic<uint64_t> g_peaks[static_cast<int>(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<int>(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<uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(
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<int>(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<int>(counter)].load(std::memory_order_relaxed);
}
[[nodiscard]] inline uint64_t Get(Peak peak) noexcept {
return Detail::g_peaks[static_cast<int>(peak)].load(std::memory_order_relaxed);
}
} // namespace Common::DebugCounters
#endif /* KYTY_COMMON_DEBUG_COUNTERS_H_ */
+3
View File
@@ -1,6 +1,7 @@
#include "common/emulatorConfig.h"
#include "common/assert.h"
#include "common/debugCounters.h"
#include <algorithm>
#include <memory>
@@ -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<uint32_t>(MAX_PLAYERS));
*g_config = cfg;
Common::DebugCounters::SetEnabled(cfg.perf_overlay_enabled);
}
uint32_t GetScreenWidth() {
+1
View File
@@ -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
+580
View File
@@ -0,0 +1,580 @@
#include "common/hostStats.h"
#include "common/config.h"
#include <algorithm>
#include <chrono>
#include <thread>
#include <unordered_map>
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h> // IWYU pragma: keep
// clang-format off
#include <psapi.h>
// clang-format on
#elif defined(__APPLE__)
#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOKitLib.h>
#include <cstring>
#include <libproc.h>
#include <mach/mach.h>
#include <notify.h>
#include <sys/resource.h>
#include <sys/sysctl.h>
#include <unistd.h>
#else
#include <filesystem>
#include <fstream>
#include <sstream>
#include <sys/resource.h>
#include <unistd.h>
#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<uint64_t> process_cpu_ns;
std::optional<uint64_t> system_busy;
std::optional<uint64_t> system_total;
std::optional<uint64_t> context_switches;
std::optional<uint64_t> page_faults;
std::optional<uint64_t> page_ins;
std::optional<uint64_t> disk_read;
std::optional<uint64_t> disk_write;
std::unordered_map<uint64_t, Thread> threads;
uint64_t wall_ns = 0;
};
uint64_t WallNanoseconds() {
return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count());
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
uint64_t FileTimeTo100Ns(const FILETIME& time) {
return (static_cast<uint64_t>(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<uint64_t>(time.tv_sec) * 1000000000u +
static_cast<uint64_t>(time.tv_usec) * 1000u;
};
now.process_cpu_ns = to_ns(usage.ru_utime) + to_ns(usage.ru_stime);
now.context_switches = static_cast<uint64_t>(usage.ru_nvcsw + usage.ru_nivcsw);
now.page_faults = static_cast<uint64_t>(usage.ru_minflt + usage.ru_majflt);
now.page_ins = static_cast<uint64_t>(usage.ru_majflt);
}
#if defined(__APPLE__)
template <typename T>
std::optional<T> 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<uint32_t> gpu_cores;
~PlatformState() {
if (thermal_ready) {
notify_cancel(thermal_token);
}
mach_port_deallocate(mach_task_self(), host);
}
};
std::optional<int64_t> DictionaryNumber(CFDictionaryRef dictionary, CFStringRef key) {
const auto* value = CFDictionaryGetValue(dictionary, key);
int64_t number = 0;
if (value == nullptr || CFGetTypeID(value) != CFNumberGetTypeID() ||
!CFNumberGetValue(static_cast<CFNumberRef>(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<CFStringRef>(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<CFDataRef>(value);
const auto* bytes = reinterpret_cast<const char*>(CFDataGetBytePtr(data));
const auto length = static_cast<size_t>(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<double>& target, std::optional<int64_t> value) {
if (value) {
target = std::max(target.value_or(0.0), std::min(100.0, static_cast<double>(*value)));
}
};
const auto add = [](std::optional<uint64_t>& target, std::optional<int64_t> value) {
if (value) {
target = target.value_or(0) + static_cast<uint64_t>(*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<CFNumberRef>(cores), kCFNumberSInt64Type,
&count) &&
count > 0) {
state.gpu_cores = static_cast<uint32_t>(count);
}
CFRelease(cores);
}
}
auto* statistics = IORegistryEntryCreateCFProperty(service, CFSTR("PerformanceStatistics"),
kCFAllocatorDefault, 0);
if (statistics != nullptr) {
if (CFGetTypeID(statistics) == CFDictionaryGetTypeID()) {
const auto* values = static_cast<CFDictionaryRef>(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<thread_info_t>(&identifier), &size) == KERN_SUCCESS &&
thread_info(threads[i], THREAD_BASIC_INFO, reinterpret_cast<thread_info_t>(&basic),
&basic_size) == KERN_SUCCESS) {
const auto to_ns = [](const time_value_t& time) {
return static_cast<uint64_t>(time.seconds) * 1000000000u +
static_cast<uint64_t>(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<thread_info_t>(&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<vm_address_t>(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<host_info_t>(&load),
&load_count) == KERN_SUCCESS) {
const uint64_t busy = static_cast<uint64_t>(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<task_info_t>(&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<rusage_info_t*>(&usage)) == 0) {
now.disk_read = usage.ri_diskio_bytesread;
now.disk_write = usage.ri_diskio_byteswritten;
}
stats.system_memory_total = SysctlValue<uint64_t>("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<host_info64_t>(&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<uint64_t>(vm.wire_count) * page;
stats.system_compressed = static_cast<uint64_t>(vm.compressor_page_count) * page;
stats.system_cached =
(static_cast<uint64_t>(vm.external_page_count) + vm.purgeable_count) * page;
stats.system_free = static_cast<uint64_t>(vm.free_count) * page;
}
if (const auto swap = SysctlValue<xsw_usage>("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<int>("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<std::string, uint64_t>;
// Reads "<key>: <number> [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<uint64_t> 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<double>(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<uint64_t>(static_cast<double>(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<double>(*available) / static_cast<double>(*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<Impl>()) {}
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<double>(now.wall_ns - last.wall_ns) / 1e9;
if (m_impl->has_last && elapsed > 0.0) {
const auto rate = [&](std::optional<uint64_t> current,
std::optional<uint64_t> previous) -> std::optional<double> {
if (!current || !previous || *current < *previous) {
return std::nullopt;
}
return static_cast<double>(*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<double>(*now.system_busy - *last.system_busy) /
static_cast<double>(*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<double>(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
+86
View File
@@ -0,0 +1,86 @@
#ifndef KYTY_COMMON_HOST_STATS_H_
#define KYTY_COMMON_HOST_STATS_H_
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <vector>
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<double> process_cpu_percent; // Share of all logical cores, 0-100.
std::optional<double> system_cpu_percent; // Whole machine, 0-100.
std::optional<uint32_t> thread_count;
std::vector<ThreadUsage> busiest_threads; // Busiest first.
std::optional<double> context_switches_per_s;
std::optional<double> page_faults_per_s;
std::optional<double> page_ins_per_s; // Faults that read from disk.
ThermalPressure thermal_pressure = ThermalPressure::Unknown;
// Emulator process memory.
std::optional<uint64_t> process_memory; // Physical footprint / resident set.
std::optional<uint64_t> process_resident;
std::optional<uint64_t> process_compressed; // Compressed or swapped out.
std::optional<uint64_t> process_file_backed;
std::optional<uint64_t> process_virtual; // Reserved address space.
// System memory.
std::optional<uint64_t> system_memory_used;
std::optional<uint64_t> system_memory_total;
std::optional<uint64_t> system_wired;
std::optional<uint64_t> system_compressed;
std::optional<uint64_t> system_cached; // File cache, reclaimable.
std::optional<uint64_t> system_free;
std::optional<uint64_t> swap_used;
std::optional<uint64_t> swap_total;
MemoryPressure memory_pressure = MemoryPressure::Unknown;
// Disk I/O of the emulator process.
std::optional<double> disk_read_per_s;
std::optional<double> disk_write_per_s;
// GPU, whole device (macOS IOAccelerator).
std::string gpu_model;
std::optional<uint32_t> gpu_cores;
std::optional<double> gpu_percent;
std::optional<double> gpu_renderer_percent;
std::optional<double> gpu_tiler_percent;
std::optional<uint64_t> gpu_memory_in_use; // System memory the GPU driver has in use.
std::optional<uint64_t> gpu_memory_allocated;
std::optional<uint64_t> 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<Impl> m_impl;
};
} // namespace Common
#endif /* KYTY_COMMON_HOST_STATS_H_ */
+1 -1
View File
@@ -367,7 +367,7 @@ bool SysFileDeleteFile(const std::filesystem::path& name) {
bool SysFileFlush(sys_file_t& f) {
if (f.type == SYS_FILE_FILE && f.f != nullptr) {
return (fflush(f.f) == 0);
return fflush(f.f) == 0 && fsync(fileno(f.f)) == 0;
}
return false;
-38
View File
@@ -89,42 +89,6 @@ static void MountSandboxDirs() {
MountOrCreateDir("_TempData/" + title_id, "/temp");
}
static bool ClearDirectoryContents(const std::filesystem::path& dir) {
bool ok = true;
for (const auto& entry: Common::File::GetDirEntries(dir)) {
if (entry.name == "." || entry.name == "..") {
continue;
}
auto path = dir / entry.name;
if (entry.is_file) {
Common::File::RemoveReadonly(path);
ok = Common::File::DeleteFile(path) && ok;
} else {
ok = ClearDirectoryContents(path) && ok;
ok = Common::File::DeleteDirectory(path) && ok;
}
}
return ok;
}
static void ClearDebugTextureFolder() {
const std::string debug_texture_folder = "_Textures";
if (!Common::File::IsDirectoryExisting(debug_texture_folder)) {
Common::File::CreateDirectories(debug_texture_folder);
return;
}
if (!ClearDirectoryContents(debug_texture_folder)) {
LOGF_COLOR(Log::Color::BrightYellow, "TextureDump: failed to completely clear %s\n",
debug_texture_folder.c_str());
}
}
static void Init(const Config::ConfigOptions& cfg, const std::filesystem::path& param_json,
Common::Subsystems& subsystems) {
EXIT_IF(!Common::Thread::IsMainThread());
@@ -198,8 +162,6 @@ void Run(const RunOptions& options) {
Common::Subsystems subsystems(true);
Init(options.config, param_json, subsystems);
ClearDebugTextureFolder();
PrintSystemInfo();
std::string title_id;
Loader::SystemContentParamSfoGetString("TITLE_ID", &title_id);
+7 -6
View File
@@ -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"
@@ -342,12 +343,11 @@ void CommandProcessor::WriteData(uint32_t* dst, const uint32_t* src, uint32_t dw
return;
}
if (write_one_address) {
for (uint32_t i = 0; i < dw_num; i++) {
dst[0] = src[i];
}
} else {
memcpy(dst, src, static_cast<size_t>(dw_num) * sizeof(uint32_t));
// Completion labels can be reused immediately; memcpy may write a destination twice.
auto* output = static_cast<volatile uint32_t*>(dst);
for (uint32_t i = 0; i < dw_num; i++) {
*output = src[i];
output += !write_one_address;
}
}
@@ -906,6 +906,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);
+9
View File
@@ -8,6 +8,7 @@
#include <map>
#include <mutex>
#include <span>
#include <tuple>
#include <vector>
#include <vk_mem_alloc.h>
@@ -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<HeapUsage, VK_MAX_MEMORY_HEAPS> heaps) const;
[[nodiscard]] bool CreateImage(const vk::ImageCreateInfo& info, VulkanImage& image);
void DeleteImage(VulkanImage& image);
+4
View File
@@ -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<Buffer> temporary;
@@ -467,6 +469,7 @@ vk::Buffer BufferCache::UploadCopies(Buffer& buffer, std::span<vk::BufferCopy> 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;
});
+10 -14
View File
@@ -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"
@@ -262,14 +263,8 @@ void TextureCache::DeleteImage(ImageId id) {
EXIT("TextureCache: deleting a GPU-modified image without resolving its contents\n");
}
m_download_images.erase(id);
if (image->info.HasMetadata()) {
const auto metadata = m_surface_metas.find(image->info.metadata.range.address);
if (metadata != m_surface_metas.end() &&
image->info.metadata.kind == ImageMetadataKind::Htile &&
metadata->second.type == MetaDataInfo::Type::HTile) {
// A later binding may have reused this address for another metadata type.
m_surface_metas.erase(metadata);
}
if (image->info.metadata.kind == ImageMetadataKind::Htile) {
m_surface_metas.erase(image->info.metadata.range.address);
}
UnregisterImage(id);
if (m_scheduler.Active()) {
@@ -954,6 +949,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 +995,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) {
@@ -1039,7 +1036,7 @@ void TextureCache::MaterializeColorClear(ImageId id, const ImageDesc& desc,
std::scoped_lock lock {m_lock};
auto& image = m_slot_images[id];
image.info.metadata = desc.info.metadata;
// Native color metadata must not retain a reused HTile/CMask/FMask clear flag.
// Native color metadata must not retain a reused HTile clear flag.
m_surface_metas.erase(range.address);
if (range.size == 0 || desc.info.resources.levels != 1) {
return;
@@ -1417,11 +1414,8 @@ vk::ImageView TextureCache::FindDepthTarget(ImageId id, const ImageDesc& desc) {
image.info.metadata = desc.info.metadata;
if (desc.info.HasMetadata()) {
const auto [metadata, inserted] = m_surface_metas.try_emplace(desc.info.metadata.range.address);
if (inserted) {
metadata->second.type = MetaDataInfo::Type::HTile;
if (image.info.htile_initial_clear) {
metadata->second.clear_mask.set();
}
if (inserted && image.info.htile_initial_clear) {
metadata->second.clear_mask.set();
}
}
RefreshImage(id);
@@ -1774,6 +1768,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 +1938,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;
-3
View File
@@ -81,9 +81,6 @@ public:
private:
struct MetaDataInfo {
enum class Type : uint8_t { CMask, FMask, HTile };
Type type;
// PS5 DB_DEPTH_VIEW encodes 13-bit base and last array-slice indices.
std::bitset<8192> clear_mask;
};
@@ -72,7 +72,16 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
return;
}
const auto samples = render_sample_count(rt.attrib.num_fragments);
if (samples == 0 || rt.attrib.num_samples != rt.attrib.num_fragments) {
// TODO(PPSA24156): Replace this 2x MSAA approximation with proper 4-coverage/2-fragment EQAA.
const bool approximate_eqaa = rt.attrib.num_samples == 2 && rt.attrib.num_fragments == 1;
if (approximate_eqaa) {
static std::atomic_bool logged = false;
if (!logged.exchange(true, std::memory_order_relaxed)) {
Log::WriteToConsoleAndLog(
"Warning: approximating 4-coverage/2-fragment EQAA with 2x MSAA.\n");
}
}
if (samples == 0 || (rt.attrib.num_samples != rt.attrib.num_fragments && !approximate_eqaa)) {
EXIT("unsupported render-target sample configuration: samples=%u fragments=%u\n",
rt.attrib.num_samples, rt.attrib.num_fragments);
}
@@ -220,8 +229,7 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
}
const bool has_dcc = rt.info.dcc_compression_enable && rt.dcc_addr.addr != 0;
const bool has_cmask = !rt.info.dcc_compression_enable && rt.info.cmask_fast_clear_enable &&
rt.cmask.addr != 0 && samples == 1 &&
!rt.info.fmask_compression_enable &&
rt.cmask.addr != 0 &&
!rt.attrib3.write_vrs_rate_hint_to_cmask;
if (has_dcc || has_cmask) {
TileSizeAlign metadata_size {};
@@ -233,7 +241,7 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
} else {
(void)TileGetCmaskSize(width, height, layers, levels, metadata_size);
}
// DCC owns the clear when both planes are enabled; single-sample CMASK stays expanded.
// DCC owns the clear when both planes are enabled.
desc.info.metadata.kind = has_dcc ? ImageMetadataKind::Dcc : ImageMetadataKind::Cmask;
desc.info.metadata.range = {has_dcc ? rt.dcc_addr.addr : rt.cmask.addr, metadata_size.size};
desc.info.metadata.clear_word =
@@ -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,
-10
View File
@@ -294,16 +294,6 @@ static void RtCheck(const HW::RenderTarget& rt) {
EXIT_NOT_IMPLEMENTED(rt.dcc.overwrite_combiner_disable != false);
// EXIT_NOT_IMPLEMENTED(rt.dcc.data_write_on_dcc_clear_to_reg != false);
EXIT_NOT_IMPLEMENTED(rt.dcc.dcc_clear_key_enable != false);
if (rt.cmask.addr != 0x0000000000000000 || rt.fmask.addr != 0x0000000000000000 ||
rt.dcc_addr.addr != 0x0000000000000000) {
static bool logged = false;
if (!logged) {
LOGF("RenderTarget: temporary: ignoring PS5 metadata addresses cmask=0x%016" PRIx64
" fmask=0x%016" PRIx64 " dcc=0x%016" PRIx64 "\n",
rt.cmask.addr, rt.fmask.addr, rt.dcc_addr.addr);
logged = true;
}
}
}
}
@@ -353,8 +353,12 @@ void Image::CopyImage(Image& source) {
std::vector<vk::ImageCopy> copies;
copies.reserve(levels);
for (uint32_t level = 0; level < levels; level++) {
const auto width = std::max(source.backing.extent.width >> level, 1u);
const auto height = std::max(source.backing.extent.height >> level, 1u);
// Overlapping cache images can differ in size; copying the source extent into a smaller
// destination writes out of bounds and loses the device.
const auto width = std::min(std::max(source.backing.extent.width >> level, 1u),
std::max(backing.extent.width >> level, 1u));
const auto height = std::min(std::max(source.backing.extent.height >> level, 1u),
std::max(backing.extent.height >> level, 1u));
const auto depth = std::max(base_depth >> level, 1u);
const auto [source_layers, destination_layers] = SanitizeCopyLayers(source, *this, depth);
vk::ImageCopy copy {};
@@ -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();
@@ -764,7 +764,10 @@ void RenderExecutor::FindBuffers(std::span<PreparedBindings* const> stages) {
size = std::min({size, uint64_t {256} * 1024 * 1024, limit});
}
}
size = Libs::LibKernel::Memory::ClampRangeSize(address, size);
// Finite GPU descriptors can span sparse PRT holes; clamp only oversized/sentinel ranges.
if (size >= (uint64_t {1} << 30u) || descriptor.NumRecords() == UINT32_MAX) {
size = Libs::LibKernel::Memory::ClampRangeSize(address, size);
}
source = {address, size, cache.FindBuffer(address, size)};
}
}
@@ -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 <array>
#include <atomic>
#include <cctype>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <fmt/format.h>
@@ -273,6 +275,7 @@ struct PipelineCache::ProgramCache {
ShaderRecompiler::IR::ResourceSnapshot resources;
ShaderRecompiler::IR::ResourceSpecialization specialization;
std::vector<Permutation> permutations;
bool skip_dispatch = false;
};
struct ProgramKeyHash {
@@ -299,6 +302,9 @@ struct PipelineCache::ProgramCache {
ShaderRecompiler::IR::ResourceSpecialization specialization,
uint32_t push_data_start_dword,
std::span<const uint32_t> 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,
@@ -355,6 +361,7 @@ struct PipelineCache::ProgramCache {
bool source_changed = false;
if (entry != programs.end()) {
auto& cached = entry->second;
if (cached.skip_dispatch) return {};
Permutation* permutation = nullptr;
if (cached.call_source) {
source_changed = cached.RefreshCallSource(runtime);
@@ -414,10 +421,18 @@ struct PipelineCache::ProgramCache {
DumpShaderOriginal(stage_name, options.shader_hash, params.code);
ShaderRecompiler::ShaderSource source;
if (entry == programs.end()) source = ShaderRecompiler::PrepareShaderSource(params.code, options);
if (source.unsupported_call_pc != UINT32_MAX) {
PipelineCacheLog("!!! TEMPORARY COMPUTE DISPATCH SKIP !!! shader=0x{:016x} pc=0x{:08x} "
"reason=unsupported GPU-selected callable target; rendering is incomplete",
params.hash, source.unsupported_call_pc);
programs.try_emplace(lookup_key, ShaderRecompiler::IR::ResourcePlan{}, nullptr)
.first->second.skip_dispatch = true;
return {};
}
auto* current = entry == programs.end() ? &source : entry->second.call_source.get();
ShaderRecompiler::TranslateResult translated;
if (current != nullptr) {
if (current->call) {
if (!current->calls.empty()) {
if (entry == programs.end()) ShaderRecompiler::RefreshShaderSource(*current, runtime);
translated = ShaderRecompiler::TranslateProgram(current->linked->decoded, options);
translated.program.source_reads = current->reads;
@@ -428,7 +443,7 @@ struct PipelineCache::ProgramCache {
translated = ShaderRecompiler::TranslateProgram(params.code, options);
}
if (entry == programs.end()) {
auto retained = source.call ? std::make_unique<ShaderRecompiler::ShaderSource>(std::move(source)) : nullptr;
auto retained = !source.calls.empty() ? std::make_unique<ShaderRecompiler::ShaderSource>(std::move(source)) : nullptr;
entry = programs.try_emplace(lookup_key,
ShaderRecompiler::IR::ExtractResourcePlan(translated.program), std::move(retained)).first;
EXIT_IF(!ShaderRecompiler::IR::MaterializeResources(
@@ -575,8 +590,8 @@ void PipelineCache::InitializeDriverCache() {
return;
}
if (!initial_data.empty()) {
PipelineCacheLog("Vulkan pipeline cache: loaded {} bytes from {}", initial_data.size(),
path);
PipelineCacheLog("Vulkan pipeline cache: loaded {:.2f} MB from {}",
static_cast<double>(initial_data.size()) / 1'000'000.0, path);
} else {
PipelineCacheLog("Vulkan pipeline cache: initialized empty");
}
@@ -616,8 +631,8 @@ void PipelineCache::Save() {
}
if (result != vk::Result::eSuccess || size == 0 ||
size > std::numeric_limits<uint32_t>::max()) {
PipelineCacheLog("Vulkan pipeline cache: save failed ({}, {} bytes)",
vk::to_string(result), size);
PipelineCacheLog("Vulkan pipeline cache: save failed ({}, {:.2f} MB)",
vk::to_string(result), static_cast<double>(size) / 1'000'000.0);
return;
}
payload.resize(size);
@@ -649,7 +664,8 @@ void PipelineCache::Save() {
Common::PathToString(m_driver_cache_path));
return;
}
PipelineCacheLog("Vulkan pipeline cache: saved {} bytes to {}", payload.size(),
PipelineCacheLog("Vulkan pipeline cache: saved {:.2f} MB to {}",
static_cast<double>(payload.size()) / 1'000'000.0,
Common::PathToString(m_driver_cache_path));
m_driver_cache_dirty = false;
}
@@ -993,8 +1009,13 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(
auto cached = std::make_unique<Pipeline>();
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 +1045,13 @@ PipelineCache::GetComputePipeline(const ShaderComputeInputInfo& input_info,
}
auto cached = std::make_unique<Pipeline>();
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);
@@ -196,21 +196,27 @@ static void CreateDescriptorLayout(GraphicContext& graphics, PipelineCache::Pipe
const auto& limits = graphics.GetPhysicalDeviceProperties().limits;
uint64_t descriptor_count = 0;
uint64_t storage_buffers = 0;
std::array<uint64_t, 32> stage_buffers {}, stage_resources {};
uint64_t samplers = 0;
std::array<uint64_t, 32> stage_buffers {}, stage_resources {}, stage_samplers {};
for (const auto& binding: bindings) {
descriptor_count += binding.descriptorCount;
const bool storage = binding.descriptorType == vk::DescriptorType::eStorageBuffer;
const bool sampler = binding.descriptorType == vk::DescriptorType::eSampler;
if (storage) storage_buffers += binding.descriptorCount;
if (sampler) samplers += binding.descriptorCount;
for (auto stages = static_cast<uint32_t>(binding.stageFlags); stages != 0; stages &= stages - 1u) {
const auto stage = std::countr_zero(stages);
if (storage) stage_buffers[stage] += binding.descriptorCount;
if (binding.descriptorType != vk::DescriptorType::eSampler)
if (sampler) stage_samplers[stage] += binding.descriptorCount;
else
stage_resources[stage] += binding.descriptorCount;
if (stage_buffers[stage] > limits.maxPerStageDescriptorStorageBuffers ||
stage_resources[stage] > limits.maxPerStageResources) {
stage_resources[stage] > limits.maxPerStageResources ||
stage_samplers[stage] > limits.maxPerStageDescriptorSamplers) {
EXIT("native descriptor stage %u exceeds device limits: buffers=%" PRIu64
"/%u resources=%" PRIu64 "/%u\n", stage, stage_buffers[stage],
limits.maxPerStageDescriptorStorageBuffers, stage_resources[stage], limits.maxPerStageResources);
"/%u resources=%" PRIu64 "/%u samplers=%" PRIu64 "/%u\n", stage, stage_buffers[stage],
limits.maxPerStageDescriptorStorageBuffers, stage_resources[stage], limits.maxPerStageResources,
stage_samplers[stage], limits.maxPerStageDescriptorSamplers);
}
}
}
@@ -218,6 +224,10 @@ static void CreateDescriptorLayout(GraphicContext& graphics, PipelineCache::Pipe
EXIT("native storage-buffer descriptors exceed device limit: %" PRIu64 "/%u\n",
storage_buffers, limits.maxDescriptorSetStorageBuffers);
}
if (samplers > limits.maxDescriptorSetSamplers) {
EXIT("native sampler descriptors exceed device limit: %" PRIu64 "/%u\n",
samplers, limits.maxDescriptorSetSamplers);
}
pipeline.uses_push_descriptors = descriptor_count <= graphics.max_push_descriptors;
vk::DescriptorSetLayoutCreateInfo create {};
+5 -5
View File
@@ -201,11 +201,11 @@ private:
void BindImage(ImageId id, bool storage);
void BindRenderTarget(ImageId id);
void ResetBindings();
void UpdateComputeMetaClears(const ShaderComputeInputInfo& input,
const CommandBuffer& buffer);
[[nodiscard]] bool TryConsumeComputeImageClear(const ShaderComputeInputInfo& input,
CommandBuffer& command, uint32_t group_x,
uint32_t group_y, uint32_t group_z, uint32_t mode);
[[nodiscard]] bool TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
const CommandBuffer& buffer);
[[nodiscard]] bool TryConsumeComputeFill(const ShaderComputeInputInfo& input,
CommandBuffer& command, uint32_t group_x,
uint32_t group_y, uint32_t group_z, uint32_t mode);
RenderContext& m_context;
GraphicsBindings m_graphics_bindings;
@@ -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"
@@ -50,8 +51,8 @@ static bool FillSourcesDisjoint(std::span<const ShaderRecompiler::IR::Descriptor
return true;
}
void RenderExecutor::UpdateComputeMetaClears(const ShaderComputeInputInfo& input,
const CommandBuffer& buffer) {
bool RenderExecutor::TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
const CommandBuffer& buffer) {
const auto& program = *input.stage.program;
const auto& resources = *input.stage.resources;
if (resources.buffers.size() != program.info.buffers.size()) {
@@ -61,9 +62,10 @@ void RenderExecutor::UpdateComputeMetaClears(const ShaderComputeInputInfo& input
for (uint32_t i = 0; i < program.info.buffers.size(); i++) {
const auto& resource = program.info.buffers[i];
const auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(resources.buffers[i]);
// Reads and transforms of metadata do not establish a clear state.
// A metadata resource that is also read is not proven to be a full overwrite. Execute it
// conservatively instead of replacing the dispatch with a coarse full-surface clear.
if ((!resource.written || resource.read) && cache.IsMeta(descriptor.Base48())) {
return;
return false;
}
}
@@ -73,10 +75,13 @@ void RenderExecutor::UpdateComputeMetaClears(const ShaderComputeInputInfo& input
if (resource.written) {
const auto descriptor =
DecodeNativeDescriptor<ShaderBufferResource>(resources.buffers[i]);
(void)cache.ClearMeta(descriptor.Base48());
if (cache.ClearMeta(descriptor.Base48())) {
return true;
}
}
}
}
return false;
}
bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t group_x,
@@ -127,9 +132,9 @@ bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t grou
return true;
}
bool RenderExecutor::TryConsumeComputeImageClear(const ShaderComputeInputInfo& input,
CommandBuffer& command, uint32_t group_x,
uint32_t group_y, uint32_t group_z, uint32_t mode) {
bool RenderExecutor::TryConsumeComputeFill(const ShaderComputeInputInfo& input,
CommandBuffer& command, uint32_t group_x,
uint32_t group_y, uint32_t group_z, uint32_t mode) {
const auto& program = *input.stage.program;
const auto& resources = *input.stage.resources;
const auto& fill = resources.uniform_fill;
@@ -185,14 +190,26 @@ bool RenderExecutor::TryConsumeComputeImageClear(const ShaderComputeInputInfo& i
size)) {
return false;
}
if (!cache.ClearImageFromBuffer(command, descriptor.Base48(), size, packed_clear)) {
return false;
const bool image_clear =
cache.ClearImageFromBuffer(command, descriptor.Base48(), size, packed_clear);
if (!image_clear) {
// CPU fills invalidate overlapping cached images. Preserve the native dispatch for
// partial or ambiguous image backings, including pooled metadata aliases.
if ((descriptor.Base48() & 3u) != 0 ||
!m_context.IsMapped(descriptor.Base48(), size) ||
cache.IsRegionRegistered(descriptor.Base48(), size)) {
return false;
}
// Metadata need not have an image backing yet. Keep proven fills in guest memory
// when CPU-owned, so color-clear discovery can read them without a GPU download.
m_context.GetBufferCache().FillBuffer(descriptor.Base48(), size, packed_clear, false);
}
static std::atomic<uint32_t> logged_clears {0};
if (logged_clears.fetch_add(1, std::memory_order_relaxed) < 32) {
LOGF("GraphicsRenderDispatchDirect: compute image clear shader=0x%016" PRIx64
LOGF("GraphicsRenderDispatchDirect: compute %s shader=0x%016" PRIx64
" addr=0x%016" PRIx64 " size=0x%016" PRIx64 " value=0x%08" PRIx32 "\n",
input.stage.program->shader_hash, descriptor.Base48(), size, packed_clear);
image_clear ? "image clear" : "buffer fill", input.stage.program->shader_hash,
descriptor.Base48(), size, packed_clear);
}
return true;
}
@@ -296,6 +313,7 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
}
const auto compute_program =
m_context.GetPipelineCache().GetComputeProgram(cs_regs, sh_regs, input_info);
if (!compute_program) return;
if (use_thread_dimensions) {
input_info.dispatch_threads_num[0] = thread_group_x;
input_info.dispatch_threads_num[1] = thread_group_y;
@@ -304,14 +322,12 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
const auto& program = *input_info.stage.program;
const auto& resources = *input_info.stage.resources;
if (resources.specialization_reads.empty()) {
// Cached metadata addresses can be reused as ordinary buffers; preserve their guest writes.
UpdateComputeMetaClears(input_info, buffer);
if (TryConsumeComputeImageClear(input_info, buffer, thread_group_x, thread_group_y,
thread_group_z, mode)) {
ResetBindings();
return;
}
if (resources.specialization_reads.empty() &&
(TryConsumeComputeMetaClear(input_info, buffer) ||
TryConsumeComputeFill(input_info, buffer, thread_group_x, thread_group_y,
thread_group_z, mode))) {
ResetBindings();
return;
}
if (use_thread_dimensions) {
@@ -432,6 +448,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);
@@ -462,6 +479,7 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
input_info.indirect_args_address = args_addr;
const auto compute_program = m_context.GetPipelineCache().GetComputeProgram(
cs_regs, buffer.GetRegisters().GetShaderRegisters(), input_info);
if (!compute_program) return;
buffer.EndRendering();
auto& pipeline = m_context.GetPipelineCache().GetComputePipeline(input_info, compute_program);
auto& bindings = m_compute_bindings;
@@ -501,6 +519,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();
}
@@ -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);
+18
View File
@@ -97,6 +97,24 @@ uint64_t GraphicContext::GetDeviceMemoryUsage() const {
return usage;
}
uint32_t GraphicContext::GetHeapUsage(std::span<HeapUsage, VK_MAX_MEMORY_HEAPS> 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<bool>(properties.flags &
vk::MemoryHeapFlagBits::eDeviceLocal)};
}
return count;
}
uint64_t GraphicContext::GetTotalMemoryBudget() const {
if (allocator == nullptr) {
return 0;
@@ -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 <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdarg>
#include <cstdio>
#include <mutex>
#include <optional>
#include <stop_token>
#include <thread>
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<size_t>(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<float, FRAME_HISTORY> 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<double, COUNTER_COUNT> rates {};
std::array<uint64_t, COUNTER_COUNT> totals {};
uint64_t shader_compile_peak_ns = 0;
uint64_t pipeline_create_peak_ns = 0;
Common::HostStats host;
std::optional<uint64_t> device_memory;
uint64_t device_budget = 0;
std::array<GraphicContext::HeapUsage, VK_MAX_MEMORY_HEAPS> heaps {};
uint32_t heap_count = 0;
std::array<float, FRAME_HISTORY> 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<uint64_t> g_generation {0};
std::atomic<bool> g_details {false};
std::atomic<uint64_t> 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<Common::HostStatsSampler> 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<double>(bytes) >= GIB) {
std::snprintf(buffer, sizeof(buffer), "%.2f GB", static_cast<double>(bytes) / GIB);
} else {
std::snprintf(buffer, sizeof(buffer), "%.0f MB", static_cast<double>(bytes) / MIB);
}
return buffer;
}
std::string Bytes(std::optional<uint64_t> 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<double> 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<ImU32> 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<float, FRAME_HISTORY>& 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<float>(FRAME_HISTORY);
for (size_t i = 0; i < count; i++) {
const float ms = history[i];
const float x = origin.x + width - static_cast<float>(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<size_t>(counter)];
}
uint64_t Total(const Snapshot& shown, Counter counter) {
return shown.totals[static_cast<size_t>(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<unsigned long long>(count),
count != 0 ? static_cast<double>(Total(shown, duration_counter)) / 1e6 /
static_cast<double>(count)
: 0.0,
static_cast<double>(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<double>(now - shown.sample_time).count();
for (size_t i = 0; i < COUNTER_COUNT; i++) {
const auto value = Common::DebugCounters::Get(static_cast<Counter>(i));
shown.rates[i] =
seconds > 0.0 ? static_cast<double>(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<uint64_t>(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<unsigned long long>(Total(shown, Counter::SwapchainRecreations)),
static_cast<unsigned long long>(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<unsigned long long>(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<unsigned long long>(*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<double>(*shown.device_memory) /
static_cast<double>(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<double>(direct_used) /
static_cast<double>(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<unsigned long long>(guest.cpu_page_entries),
static_cast<unsigned long long>(guest.gpu_page_entries),
static_cast<unsigned long long>(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<unsigned long long>(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<float, std::milli>(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<double>(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<double>(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<float>(extent.width) / 1920.0f,
static_cast<float>(extent.height) / 1080.0f),
0.75f, 1.5f);
ImGui::SetNextWindowPos({static_cast<float>(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
@@ -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 <cstdint>
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_
+10 -9
View File
@@ -1,7 +1,5 @@
#include "graphics/presentation/systemOverlay.h"
#include <SDL3/SDL.h>
#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 <SDL3/SDL.h>
#include <algorithm>
#include <array>
#include <atomic>
@@ -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<decltype(core.revision)>(cursor.revision)};
(splash ? 2u : 0u) + static_cast<decltype(core.revision)>(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<float>(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;
+10
View File
@@ -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<int64_t>(period) - static_cast<int64_t>(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();
@@ -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());
@@ -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) {
+34 -16
View File
@@ -1,21 +1,20 @@
#include "graphics/presentation/window.h"
#include <SDL3/SDL.h>
#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 <SDL3/SDL.h>
#include <cstdlib>
#include <filesystem>
#include <fmt/format.h>
#include <memory>
#include <string>
#include <vector>
#include <vulkan/vk_platform.h>
#include <filesystem>
#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;
@@ -481,18 +481,14 @@ Decoder::Program DecodeFusedProgram(std::span<const uint32_t> front, std::span<c
return result;
}
void PrepareCallTarget(ShaderSource& source, const CompileOptions& options) {
void PrepareCallTargets(ShaderSource& source, const CompileOptions& options) {
using namespace IR;
EXIT_NOT_IMPLEMENTED(options.stage != ShaderType::Compute);
for (uint32_t i = 0; i < source.decoded.instructions.size(); ++i) {
if (source.decoded.instructions[i].opcode != Decoder::Opcode::S_SWAPPC_B64) continue;
EXIT_NOT_IMPLEMENTED(source.call.has_value() || options.stage != ShaderType::Compute);
source.call = ShaderSource::Call {.instruction = i};
source.calls.push_back({.instruction = i});
}
EXIT_IF(options.input_info.compute == nullptr);
const auto& call = source.decoded.instructions[source.call->instruction];
EXIT_NOT_IMPLEMENTED(call.src0.kind != Decoder::OperandKind::Sgpr ||
call.dst.kind != Decoder::OperandKind::Sgpr ||
call.src0.reg != call.dst.reg || call.dst.reg >= 105u);
const auto graph = CFG::BuildGraph(source.decoded);
Program query;
query.stage = options.stage;
@@ -524,7 +520,11 @@ void PrepareCallTarget(ShaderSource& source, const CompileOptions& options) {
return value;
};
Register = [&](uint32_t before, uint32_t code) -> Value {
const auto index = CFG::FindScalarDefinition(source.decoded, graph, before, code);
std::vector<uint32_t> crossed_calls;
const auto index = CFG::FindScalarDefinition(source.decoded, graph, before, code, crossed_calls);
for (const auto crossed: crossed_calls) {
std::ranges::find(source.calls, crossed, &ShaderSource::Call::instruction)->preserved_registers.set(code);
}
if (index == UINT32_MAX) {
EXIT_IF(code < query.user_data_base || code - query.user_data_base >= query.user_data_count);
return IREmitter(&block).GetUserData(static_cast<ScalarReg>(code));
@@ -557,18 +557,30 @@ void PrepareCallTarget(ShaderSource& source, const CompileOptions& options) {
EXIT_IF(!producer.ready || !producer.values.contains(code));
return producer.values.at(code).Resolve();
};
for (uint32_t i = 0; i < 2u; ++i) {
const auto value = Register(source.call->instruction, call.src0.reg + i);
EXIT_IF(!ValidateRuntimeValue(query, value, RuntimeValueType::Integer));
auto& root = query.value_storage.emplace_back(ValueOpcode::ReferenceU32);
root.SetArg(0, value);
source.call->target[i] = Value(&root);
for (auto& site: source.calls) {
const auto& call = source.decoded.instructions[site.instruction];
EXIT_NOT_IMPLEMENTED(call.src0.kind != Decoder::OperandKind::Sgpr ||
call.dst.kind != Decoder::OperandKind::Sgpr ||
call.src0.reg != call.dst.reg || call.dst.reg >= 105u);
for (uint32_t i = 0; i < 2u; ++i) {
const auto value = Register(site.instruction, call.src0.reg + i);
if (!ValidateRuntimeValue(query, value, RuntimeValueType::Integer)) {
EXIT_IF(value.Instruction()->GetOpcode() != ValueOpcode::ReadConstBuffer);
source.unsupported_call_pc = call.pc;
source.calls.clear();
return;
}
auto& root = query.value_storage.emplace_back(ValueOpcode::ReferenceU32);
root.SetArg(0, value);
site.target[i] = Value(&root);
}
}
RewriteToSsa(query.blocks);
ConstantPropagationPass(query.blocks, query.wave_size);
RemoveIdentities(query);
EliminateDeadCode(query.blocks);
for (auto& value: source.call->target) value = value.Instruction()->Arg(0).Resolve();
for (auto& site: source.calls)
for (auto& value: site.target) value = value.Instruction()->Arg(0).Resolve();
query.value_storage.clear();
for (auto& inst: block) {
inst.SetParent(nullptr);
@@ -595,7 +607,7 @@ ShaderSource PrepareShaderSource(std::span<const uint32_t> code, const CompileOp
Decoder::DecodeProgram(code, source.decoded);
}
if (source.decoded.has_swap_pc) {
PrepareCallTarget(source, options);
PrepareCallTargets(source, options);
source.code.assign(code.begin(), code.end());
source.decoded.code = source.code;
}
@@ -603,88 +615,122 @@ ShaderSource PrepareShaderSource(std::span<const uint32_t> code, const CompileOp
}
const Decoder::Program& RefreshShaderSource(ShaderSource& source, const IR::SrtRuntime& runtime) {
EXIT_IF(source.unsupported_call_pc != UINT32_MAX);
auto& reads = source.reads;
reads.clear();
IR::SrtReadCapture capture(runtime, reads);
const auto clean = IR::CleanRuntime(capture.ObservedRuntime());
IR::SrtWalker walker(source.call_targets, clean);
uint32_t low = 0, high = 0;
const auto& call = source.decoded.instructions[source.call->instruction];
if (!walker.Evaluate(source.call->target[0], low) || !walker.Evaluate(source.call->target[1], high))
EXIT("shader call at pc 0x%08x has an unavailable scalar target", call.pc);
// SWAPPC reads the old aliased pair and ignores the target's low two bits.
const auto address = ((uint64_t {high} << 32u) | low) & ~uint64_t {3};
EXIT_IF(address == 0u);
for (auto& site: source.calls) {
uint32_t low = 0, high = 0;
if (!walker.Evaluate(site.target[0], low) || !walker.Evaluate(site.target[1], high))
EXIT("shader call at pc 0x%08x has an unavailable scalar target",
source.decoded.instructions[site.instruction].pc);
// SWAPPC reads the old aliased pair and ignores the target's low two bits.
const auto address = ((uint64_t {high} << 32u) | low) & ~uint64_t {3};
EXIT_IF(address == 0u);
site.address = address;
}
const auto query_reads = reads.size();
if (source.linked) {
auto& linked = *source.linked;
const auto function = std::span(linked.code).subspan(source.code.size());
if (clean.read_memory(clean.userdata, address, linked.observed_function) &&
std::ranges::equal(function, linked.observed_function)) {
return linked.decoded;
bool unchanged = true;
for (size_t i = 0; i < source.calls.size(); ++i) {
const auto& function = linked.functions[i];
const auto expected = std::span(linked.code).subspan(function.offset, function.size);
const auto preceding = std::span(source.calls).first(i);
const auto previous = std::ranges::find(preceding, source.calls[i].address, &ShaderSource::Call::address);
if (previous != preceding.end()) {
const auto& observed = linked.functions[previous - preceding.begin()];
if (std::ranges::equal(expected, std::span(linked.code).subspan(observed.offset, observed.size))) continue;
unchanged = false;
break;
}
linked.observed_function.resize(function.size);
if (!clean.read_memory(clean.userdata, source.calls[i].address, linked.observed_function) ||
!std::ranges::equal(expected, linked.observed_function)) {
unchanged = false;
break;
}
}
if (unchanged) return linked.decoded;
reads.resize(query_reads);
}
ShaderSource::Linked linked {.code = source.code,
.decoded = source.decoded};
ShaderSource::Linked linked {.code = source.code, .decoded = source.decoded};
const auto& last = source.decoded.instructions.back();
EXIT_NOT_IMPLEMENTED(last.opcode != Decoder::Opcode::S_ENDPGM);
// Keep the caller's data/footer bytes at their native offsets. The leaf's virtual CFG
// PCs follow decoded instructions; its physical code is independently read and owned.
const auto function_pc = last.pc + last.word_count * sizeof(uint32_t);
for (uint32_t index = 0;;) {
// The observed queue helper is a scalar leaf. Scalar encodings need at most one literal.
EXIT_IF(index >= 64u || address > UINT64_MAX - (uint64_t {index} + 2u) * sizeof(uint32_t));
std::array<uint32_t, 2> words {};
if (!clean.read_memory(clean.userdata, address + uint64_t {index} * sizeof(uint32_t),
std::span(words).first(1)))
EXIT("shader call at pc 0x%08x cannot read leaf code", call.pc);
const auto family = Decoder::GetInstructionFamily(words[0]);
EXIT_NOT_IMPLEMENTED(family != Decoder::Family::SOP1 && family != Decoder::Family::SOP2 &&
family != Decoder::Family::SOPC && family != Decoder::Family::SOPK &&
family != Decoder::Family::SOPP);
Decoder::Instruction inst;
Decoder::DecodeInstruction(words, 0u, inst);
if (inst.word_count == 2u) {
if (!clean.read_memory(clean.userdata, address + uint64_t {index + 1u} * sizeof(uint32_t),
std::span(words).last(1)))
EXIT("shader call at pc 0x%08x cannot read leaf literal", call.pc);
inst = {};
Decoder::DecodeInstruction(words, 0u, inst);
// Keep the caller's data/footer at native offsets. Expand leaf CFGs per call site
// before SSA, so return edges neither require a dispatcher nor merge resource roots.
uint32_t function_pc = last.pc + last.word_count * sizeof(uint32_t);
std::map<uint64_t, std::vector<Decoder::Instruction>> functions;
for (size_t i = 0; i < source.calls.size(); ++i) {
const auto& site = source.calls[i];
const auto& call = source.decoded.instructions[site.instruction];
const auto address = source.calls[i].address;
auto [entry, inserted] = functions.try_emplace(address);
auto& function = entry->second;
if (inserted) {
for (uint32_t index = 0;;) {
std::array<uint32_t, Decoder::MaxInstructionRawWords> words {};
EXIT_IF(index > UINT32_MAX / sizeof(uint32_t) - words.size() ||
address > UINT64_MAX - (uint64_t {index} + words.size()) * sizeof(uint32_t));
uint32_t count = 1;
Decoder::Instruction inst;
for (;;) {
if (!clean.read_memory(clean.userdata, address + uint64_t {index} * sizeof(uint32_t),
std::span(words).first(count)))
EXIT("shader call at pc 0x%08x cannot read leaf code", call.pc);
inst = {};
Decoder::DecodeInstruction(words, 0u, inst);
if (inst.word_count <= count) break;
count = inst.word_count;
}
inst.pc = index * sizeof(uint32_t);
if (Decoder::IsDirectBranch(inst.opcode)) inst.branch_target += inst.pc;
index += inst.word_count;
EXIT_NOT_IMPLEMENTED(inst.opcode == Decoder::Opcode::S_SWAPPC_B64 ||
inst.opcode == Decoder::Opcode::S_GETPC_B64 ||
inst.opcode == Decoder::Opcode::S_ENDPGM);
function.push_back(inst);
if (inst.opcode == Decoder::Opcode::S_SETPC_B64) break;
}
}
inst.pc = index * sizeof(uint32_t);
if (Decoder::IsDirectBranch(inst.opcode)) inst.branch_target += inst.pc;
linked.code.insert(linked.code.end(), inst.raw, inst.raw + inst.word_count);
index += inst.word_count;
EXIT_NOT_IMPLEMENTED(inst.opcode == Decoder::Opcode::S_SWAPPC_B64 ||
inst.opcode == Decoder::Opcode::S_GETPC_B64 ||
inst.opcode == Decoder::Opcode::S_ENDPGM);
EXIT_IF(CFG::MayWriteScalarRegister(inst, call.dst.reg) || CFG::MayWriteScalarRegister(inst, call.dst.reg + 1u));
const bool returns = inst.opcode == Decoder::Opcode::S_SETPC_B64;
if (returns) {
EXIT_NOT_IMPLEMENTED(inst.src0.kind != Decoder::OperandKind::Sgpr || inst.src0.reg != call.dst.reg);
inst.opcode = Decoder::Opcode::S_BRANCH;
inst.branch_target = call.pc + call.word_count * sizeof(uint32_t);
} else if (Decoder::IsDirectBranch(inst.opcode)) {
EXIT_IF(inst.branch_target > UINT32_MAX - function_pc);
inst.branch_target += function_pc;
const uint32_t function_size = function.back().pc + function.back().word_count * sizeof(uint32_t);
EXIT_IF(function_pc > UINT32_MAX - function_size);
const auto function_offset = static_cast<uint32_t>(linked.code.size());
for (auto inst: function) {
EXIT_IF(CFG::MayWriteScalarRegister(inst, call.dst.reg) ||
CFG::MayWriteScalarRegister(inst, call.dst.reg + 1u));
for (uint32_t code = 0; code < site.preserved_registers.size(); ++code) {
if (site.preserved_registers[code] && CFG::MayWriteScalarRegister(inst, code))
EXIT("shader call at pc 0x%08x clobbers scalar target dependency s%u", call.pc, code);
}
linked.code.insert(linked.code.end(), inst.raw, inst.raw + inst.word_count);
if (inst.opcode == Decoder::Opcode::S_SETPC_B64) {
EXIT_NOT_IMPLEMENTED(inst.src0.kind != Decoder::OperandKind::Sgpr || inst.src0.reg != call.dst.reg);
inst.opcode = Decoder::Opcode::S_BRANCH;
inst.branch_target = call.pc + call.word_count * sizeof(uint32_t);
} else if (Decoder::IsDirectBranch(inst.opcode)) {
EXIT_IF(inst.branch_target >= function_size);
inst.branch_target += function_pc;
}
inst.pc += function_pc;
linked.decoded.instructions.push_back(std::move(inst));
}
inst.pc += function_pc;
linked.decoded.instructions.push_back(std::move(inst));
if (returns) break;
}
const auto function_end = linked.decoded.instructions.back().pc +
linked.decoded.instructions.back().word_count * sizeof(uint32_t);
for (size_t i = source.decoded.instructions.size(); i + 1u < linked.decoded.instructions.size(); ++i) {
const auto& inst = linked.decoded.instructions[i];
if (Decoder::IsDirectBranch(inst.opcode))
EXIT_IF(inst.branch_target < function_pc || inst.branch_target >= function_end);
linked.functions.push_back({function_offset, function_size / 4u});
linked.decoded.instructions[site.instruction].branch_target = function_pc;
function_pc += function_size;
}
reads.resize(query_reads);
reads.emplace_back(address, (linked.code.size() - source.code.size()) * sizeof(uint32_t));
linked.decoded.instructions[source.call->instruction].branch_target = function_pc;
for (size_t i = 0; i < source.calls.size(); ++i) {
const auto preceding = std::span(source.calls).first(i);
if (std::ranges::find(preceding, source.calls[i].address, &ShaderSource::Call::address) == preceding.end())
reads.emplace_back(source.calls[i].address, linked.functions[i].size * sizeof(uint32_t));
}
linked.decoded.code = linked.code;
linked.observed_function.resize(linked.code.size() - source.code.size());
linked.observed_function.resize(std::ranges::max_element(linked.functions, {},
&ShaderSource::Linked::Function::size)->size);
source.linked = std::move(linked);
++source.revision;
return source.linked->decoded;
@@ -8,6 +8,7 @@
#include "graphics/shader/shader.h"
#include <array>
#include <bitset>
#include <optional>
#include <span>
#include <vector>
@@ -47,16 +48,21 @@ struct ShaderSource {
struct Call {
uint32_t instruction;
std::array<IR::Value, 2> target;
std::bitset<128> preserved_registers;
uint64_t address = 0;
};
struct Linked {
struct Function { uint32_t offset; uint32_t size; };
std::vector<uint32_t> code;
Decoder::Program decoded;
std::vector<Function> functions;
std::vector<uint32_t> observed_function;
};
std::vector<uint32_t> code;
Decoder::Program decoded;
IR::ResourcePlan call_targets;
std::optional<Call> call;
std::vector<Call> calls;
uint32_t unsupported_call_pc = UINT32_MAX;
std::optional<Linked> linked;
uint64_t revision = 0;
std::vector<std::pair<uint64_t, uint64_t>> reads;
@@ -160,19 +160,6 @@ uint32_t EmitFPMad32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
return sum;
}
uint32_t EmitFPMedTri32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
const auto min_ab = EmitMinMaxF32Value(state, a, b, false);
const auto min3 = EmitMinMaxF32Value(state, min_ab, c, false);
const auto max_ab = EmitMinMaxF32Value(state, a, b, true);
const auto high_min = EmitMinMaxF32Value(state, max_ab, c, false);
const auto median = EmitMinMaxF32Value(state, min_ab, high_min, true);
const auto nan_ab = Binary(state, spv::OpLogicalOr, TypeBool(state),
EmitClassifyF32(state, a).nan, EmitClassifyF32(state, b).nan);
const auto any_nan =
Binary(state, spv::OpLogicalOr, TypeBool(state), nan_ab, EmitClassifyF32(state, c).nan);
return Select(state, TypeF32(state), any_nan, min3, median);
}
uint32_t EmitFindUMsb64(EmitterState& state, uint32_t value) {
// GLSL FindUMsb is restricted to 32-bit components.
const auto unpacked = Unary(state, spv::OpBitcast, TypeU32Vector(state, 2), value);
@@ -100,10 +100,9 @@ uint32_t EmitClassMaskF16(EmitterState& state, uint32_t bits, uint32_t mask) {
return EmitClassMaskBits(state, bits, mask, true);
}
uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value) {
const auto lhs_class = EmitClassifyF32(state, lhs);
const auto rhs_class = EmitClassifyF32(state, rhs);
static uint32_t EmitMinMaxF32(EmitterState& state, uint32_t lhs, uint32_t rhs,
const F32Class& lhs_class, const F32Class& rhs_class,
bool max_value) {
const auto numeric_cond = state.builder.AllocateId();
state.builder.AddFunction(max_value ? spv::OpFOrdGreaterThanEqual : spv::OpFOrdLessThan,
TypeBool(state), numeric_cond, lhs, rhs);
@@ -121,6 +120,34 @@ uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, boo
return EmitBitcastU32ToF32(state, result_bits);
}
uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value) {
const auto lhs_class = EmitClassifyF32(state, lhs);
const auto rhs_class = EmitClassifyF32(state, rhs);
return EmitMinMaxF32(state, lhs, rhs, lhs_class, rhs_class, max_value);
}
uint32_t EmitFPMedTri32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
const auto a_class = EmitClassifyF32(state, a);
const auto b_class = EmitClassifyF32(state, b);
const auto c_class = EmitClassifyF32(state, c);
const auto min_ab = EmitMinMaxF32(state, a, b, a_class, b_class, false);
const auto min_ab_class = EmitClassifyF32(state, min_ab);
const auto min3 = EmitMinMaxF32(state, min_ab, c, min_ab_class, c_class, false);
const auto max_ab = EmitMinMaxF32(state, a, b, a_class, b_class, true);
const auto max_ab_class = EmitClassifyF32(state, max_ab);
const auto max3 = EmitMinMaxF32(state, max_ab, c, max_ab_class, c_class, true);
const auto max_bc = EmitMinMaxF32(state, b, c, b_class, c_class, true);
const auto max_ac = EmitMinMaxF32(state, a, c, a_class, c_class, true);
// RDNA2 compares numerically against MAX3, including ties between signed zeros.
const auto a_is_max = Binary(state, spv::OpFOrdEqual, TypeBool(state), max3, a);
const auto b_is_max = Binary(state, spv::OpFOrdEqual, TypeBool(state), max3, b);
const auto median = Select(state, TypeF32(state), a_is_max, max_bc,
Select(state, TypeF32(state), b_is_max, max_ac, max_ab));
const auto nan_ab = EmitLogicalOrBool(state, a_class.nan, b_class.nan);
const auto any_nan = EmitLogicalOrBool(state, nan_ab, c_class.nan);
return Select(state, TypeF32(state), any_nan, min3, median);
}
uint32_t EmitFlushF32DenormToSignedZero(EmitterState& state, uint32_t value) {
const auto bits = state.builder.AllocateId();
const auto abs_bits = state.builder.AllocateId();
@@ -655,6 +655,14 @@ void EmitStoreCompletion(EmitterState& state) {
spv::MemorySemanticsImageMemoryMask));
}
void EmitDataCacheInvalidate(EmitterState& state) {
// RDNA2 GL0/GL1 invalidation makes vector memory visible without a workgroup rendezvous.
state.builder.AddFunction(
spv::OpMemoryBarrier, ConstantU32(state, spv::ScopeDevice),
ConstantU32(state, spv::MemorySemanticsAcquireMask | spv::MemorySemanticsUniformMemoryMask |
spv::MemorySemanticsImageMemoryMask));
}
void EmitBarrier(EmitterState& state) {
const auto tessellation = state.program.stage == ShaderType::TessellationControl;
if (!tessellation && ShaderWorkgroupInput(state.program.stage, state.input_info) == nullptr) {
@@ -97,10 +97,14 @@ uint32_t GatherMip(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Memory
ConstantF32Value(state, 1.0f / 256.0f));
};
const auto sampler_control = ctx.Arg(inst, 6);
const auto mip_filter = Extract(sampler_control, 26, 2);
const auto preclamp = Binary(state, spv::OpINotEqual, TypeBool(state),
Extract(sampler_control, 28, 1), ConstantU32(state, 0));
const auto half = ConstantF32Value(state, 0.5f);
const auto zero = ZeroF32(state);
// Point selects the nearest mip; linear gathers return the lower, primary mip only.
const auto rounding = Select(state, TypeF32(state),
Binary(state, spv::OpIEqual, TypeBool(state), mip_filter, ConstantU32(state, 1)),
ConstantF32Value(state, 0.5f), zero);
const auto base = Unary(state, spv::OpConvertUToF, TypeF32(state),
Extract(ctx.Arg(inst, 4), 12, 4));
const auto minimum = Binary(state, spv::OpFSub, TypeF32(state),
@@ -108,19 +112,19 @@ uint32_t GatherMip(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Memory
const auto last = ConstantF32Value(state, static_cast<float>(mip_count - 1u));
auto lod = Binary(state, spv::OpFAdd, TypeF32(state),
AddressF32(ctx, mem, address, layout.lod),
Select(state, TypeF32(state), preclamp, half, zero));
Select(state, TypeF32(state), preclamp, rounding, zero));
lod = EmitGlsl<GLSLstd450FClamp, IR::Type::F32>(
state, lod, LodFixed(ctx.Arg(inst, 5), 0), LodFixed(ctx.Arg(inst, 5), 12));
// T# minimum LOD is in physical mip space and applies after the S# view-relative clamp.
lod = EmitGlsl<GLSLstd450FMax, IR::Type::F32>(state, lod, minimum);
lod = EmitGlsl<GLSLstd450FClamp, IR::Type::F32>(state, lod, zero, last);
lod = Binary(state, spv::OpFAdd, TypeF32(state), lod,
Select(state, TypeF32(state), preclamp, zero, half));
Select(state, TypeF32(state), preclamp, zero, rounding));
// The clamped value is nonnegative, so conversion performs the integer mip rounding.
const auto mip = EmitUMin32(state, Unary(state, spv::OpConvertFToU, TypeU32(state), lod),
ConstantU32(state, mip_count - 1u));
const auto mip_none = Binary(state, spv::OpIEqual, TypeBool(state),
Extract(sampler_control, 26, 2), ConstantU32(state, 0));
mip_filter, ConstantU32(state, 0));
return Select(state, TypeU32(state), mip_none, ConstantU32(state, 0), mip);
}
@@ -284,10 +288,10 @@ uint32_t ResultVector(ValueEmitContext& ctx, uint32_t value,
}
uint32_t QueryDimensions(ValueEmitContext& ctx, const IR::MemoryInfo& mem,
const IR::Inst& address) {
const auto dimension = ctx.state.program.info.images.at(mem.resource).dimension;
const IR::Inst& address, uint32_t resource, uint32_t array_index) {
const auto dimension = ctx.state.program.info.images.at(resource).dimension;
const auto& info = ImageDimensionInfoFor(dimension);
const auto image = LoadImageDescriptor(ctx.state, mem.resource);
const auto image = LoadImageDescriptor(ctx.state, resource, 0u, array_index);
const auto size = ctx.state.builder.AllocateId();
if (info.multisampled != 0u) {
ctx.state.builder.AddFunction(spv::OpImageQuerySize,
@@ -689,68 +693,58 @@ auto EmitImageAccess(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t resul
}
const auto selected = EmitIndirectResourceIndex(
state, key, image.indirect_mapping_offset, image.indirect_search_iterations, 0u);
struct ImageRun {
uint32_t first;
uint32_t count;
uint32_t resource;
uint32_t slot_bias;
const auto SameType = [&](uint32_t lhs, uint32_t rhs) {
const auto& first = state.program.info.images[lhs];
const auto& second = state.program.info.images[rhs];
return first.dimension == second.dimension && first.cube == second.cube &&
IR::DescriptorBindingForImage(first) == IR::DescriptorBindingForImage(second) &&
(inst.GetOpcode() != IR::ValueOpcode::ImageWrite ||
first.shader_swizzle == second.shader_swizzle) &&
first.mip_count == 1u && second.mip_count == 1u;
};
std::vector<ImageRun> runs;
for (uint32_t ordinal = 0; ordinal < image.indirect_resources.size(); ++ordinal) {
const auto resource = image.indirect_resources[ordinal];
const auto& candidate = state.program.info.images[resource];
const auto kind = *IR::DescriptorBindingForImage(candidate);
if (!runs.empty()) {
auto& run = runs.back();
const auto& first = state.program.info.images[run.resource];
const auto next_slot = run.slot_bias + ordinal;
if (candidate.dimension == first.dimension && candidate.cube == first.cube &&
IR::DescriptorBindingForImage(first) == kind &&
(inst.GetOpcode() != IR::ValueOpcode::ImageWrite ||
candidate.shader_swizzle == first.shader_swizzle) &&
candidate.mip_count == 1u && first.mip_count == 1u &&
(ordinal == 1u || candidate.descriptor_index == next_slot)) {
// Native roots precede appended children; only the root may have a slot gap.
if (ordinal == 1u)
run.slot_bias = candidate.descriptor_index - ordinal;
++run.count;
continue;
}
}
runs.push_back({ordinal, 1u, resource,
candidate.descriptor_index - ordinal});
// Descriptor order is unrelated to image type. Share each typed access and
// select its descriptor slot independently, including gaps between native roots.
struct ImageGroup { uint32_t resource; uint32_t count; };
std::vector<ImageGroup> groups;
std::vector<uint32_t> ordinal_groups;
for (const auto resource: image.indirect_resources) {
const auto found = std::ranges::find_if(groups, [&](const ImageGroup& group) {
return SameType(resource, group.resource);
});
ordinal_groups.push_back(static_cast<uint32_t>(found - groups.begin()));
if (found == groups.end()) groups.push_back({resource, 1u});
else ++found->count;
}
const auto EmitRun = [&](uint32_t index) {
const auto& run = runs[index];
if (run.count == 1u) return emit(run.resource, 0u);
auto slot = Binary(state, spv::OpIAdd, TypeU32(state), selected,
ConstantU32(state, run.slot_bias));
if (run.first == 0u) {
const auto root_slot = image.descriptor_index;
if (root_slot != run.slot_bias) {
const auto is_root = Binary(state, spv::OpIEqual, TypeBool(state), selected,
ConstantU32(state, 0u));
const auto root_or_child = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelect, TypeU32(state), root_or_child,
is_root, ConstantU32(state, root_slot), slot);
slot = root_or_child;
}
}
return emit(run.resource, slot);
};
auto run_index = ConstantU32(state, 0u);
for (uint32_t index = 1; index < runs.size(); ++index) {
const bool dynamic_slot = std::ranges::any_of(groups, [](const ImageGroup& group) {
return group.count > 1u;
});
auto group_index = ConstantU32(state, 0u);
auto slot_bias = ConstantU32(state, image.descriptor_index);
auto previous_bias = image.descriptor_index;
for (uint32_t ordinal = 1; ordinal < image.indirect_resources.size(); ++ordinal) {
const auto& candidate = state.program.info.images[image.indirect_resources[ordinal]];
const auto bias = candidate.descriptor_index - ordinal;
const bool select_group = ordinal_groups[ordinal] != ordinal_groups[ordinal - 1u];
const bool select_slot = dynamic_slot && bias != previous_bias;
previous_bias = bias;
if (!select_group && !select_slot) continue;
const auto at_or_after = Binary(state, spv::OpUGreaterThanEqual, TypeBool(state),
selected, ConstantU32(state, runs[index].first));
const auto next = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelect, TypeU32(state), next, at_or_after,
ConstantU32(state, index), run_index);
run_index = next;
selected, ConstantU32(state, ordinal));
if (select_group) group_index = Select(state, TypeU32(state), at_or_after,
ConstantU32(state, ordinal_groups[ordinal]), group_index);
if (select_slot) slot_bias = Select(state, TypeU32(state), at_or_after,
ConstantU32(state, bias), slot_bias);
}
return runs.size() == 1u
? EmitRun(0u)
: EmitIndexSwitch(state, run_index, static_cast<uint32_t>(runs.size()),
result_type, EmitRun);
const auto slot = dynamic_slot
? Binary(state, spv::OpIAdd, TypeU32(state), selected, slot_bias) : 0u;
const auto EmitGroup = [&](uint32_t index) {
const auto& group = groups[index];
return emit(group.resource, group.count == 1u ? 0u : slot);
};
return groups.size() == 1u
? EmitGroup(0u)
: EmitIndexSwitch(state, group_index, static_cast<uint32_t>(groups.size()),
result_type, EmitGroup);
}
template <typename Emit>
@@ -852,7 +846,10 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto* address = ctx.ImageAddress(inst.Arg(image_info.needs_sampler ? 2 : 1));
if (op == IR::ValueOpcode::ImageQueryDimensions) {
state.builder.RequireCapability(spv::CapabilityImageQuery);
ctx.Define(inst, QueryDimensions(ctx, mem, *address));
ctx.Define(inst, EmitImageAccess(ctx, inst, TypeU32Vector(state, 4),
[&](uint32_t resource, uint32_t array_index) {
return QueryDimensions(ctx, mem, *address, resource, array_index);
}));
return;
}
if (image_info.needs_sampler &&
@@ -218,6 +218,7 @@ inline constexpr auto EmitTtraceData = EmitVoid;
inline constexpr auto EmitInstPrefetch = EmitVoid;
void EmitBarrier(EmitterState& state);
void EmitStoreCompletion(EmitterState& state);
void EmitDataCacheInvalidate(EmitterState& state);
void EmitMeshAllocate(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitMeshDrawParameter(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitGetTessellationAttribute(ValueEmitContext& ctx, const IR::Inst& inst);
@@ -307,6 +307,7 @@ void EmitBlock(ValueEmitContext& ctx, const IR::Block* block, EmitInstruction&&
ctx.state.lane_half = half;
if (half == 0 || (inst.GetOpcode() != IR::ValueOpcode::Barrier &&
inst.GetOpcode() != IR::ValueOpcode::StoreCompletion &&
inst.GetOpcode() != IR::ValueOpcode::DataCacheInvalidate &&
inst.GetOpcode() != IR::ValueOpcode::MeshAllocate)) {
emit_instruction(lane, inst);
}
@@ -8,6 +8,8 @@
#include <deque>
#include <list>
#include <map>
#include <optional>
#include <set>
#include <span>
namespace Libs::Graphics::ShaderRecompiler::CFG {
@@ -1650,6 +1652,32 @@ Graph BuildGraph(const Decoder::Program& program) {
}
SortUnique(graph.code_table_load_pcs);
if (!indirect_setpc && std::ranges::any_of(program.instructions, [](const auto& inst) {
return inst.opcode == Opcode::S_SWAPPC_B64 && inst.branch_target != UINT32_MAX;
})) {
// Linked leaf bodies follow the caller in native PC order. Visit them in
// execution order so returning to the caller does not manufacture goto loops.
std::vector<bool> visited(graph.blocks.size(), false);
std::vector<std::pair<uint32_t, bool>> pending {{graph.entry_block, false}};
std::vector<BasicBlock> ordered;
ordered.reserve(graph.blocks.size());
while (!pending.empty()) {
const auto [id, finished] = pending.back();
pending.pop_back();
if (finished) {
ordered.push_back(std::move(graph.blocks[id]));
continue;
}
if (visited[id]) continue;
visited[id] = true;
pending.emplace_back(id, true);
for (const auto successor: graph.blocks[id].successors)
pending.emplace_back(successor, false);
}
std::ranges::reverse(ordered);
ApplyBlockOrder(graph, std::move(ordered));
}
RecomputeAnalyses(graph);
if (indirect_setpc) {
@@ -1681,33 +1709,56 @@ Graph Structurize(const Graph& graph) {
}
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code) {
uint32_t destination = UINT32_MAX;
// Scalar ALU pair widths are not all decoded. The typed producer must confirm this word.
return InstructionWritesScalarCode(inst, code) ||
(ScalarOperandCode(inst.dst, destination) && code == destination + 1u);
// Vector comparisons and carry results can write a scalar pair in wave64.
if (inst.family == Decoder::Family::VOP1 || inst.family == Decoder::Family::VOP2 ||
inst.family == Decoder::Family::VOPC || inst.family == Decoder::Family::VOP3 ||
inst.family == Decoder::Family::VOP3P) {
uint32_t destination = 0;
for (const auto* operand: {&inst.dst, &inst.dst2})
if (ScalarOperandCode(*operand, destination) && code >= destination && code <= destination + 1u)
return true;
}
return InstructionWritesScalarCode(inst, code);
}
uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& graph,
uint32_t before, uint32_t code) {
uint32_t before, uint32_t code, std::vector<uint32_t>& crossed_calls) {
EXIT_IF(before >= program.instructions.size());
const auto found = std::ranges::find_if(graph.blocks, [before](const auto& block) {
return block.inst_begin <= before && before < block.inst_end;
});
EXIT_IF(found == graph.blocks.end());
const auto* block = &*found;
for (size_t depth = 0; depth < graph.blocks.size(); ++depth) {
for (uint32_t i = before; i > block->inst_begin;) {
const auto& inst = program.instructions[--i];
EXIT_IF(inst.opcode == Opcode::S_SWAPPC_B64);
if (MayWriteScalarRegister(inst, code)) return i;
}
if (block->id == graph.entry_block) return UINT32_MAX;
EXIT_IF(block->predecessors.size() != 1u);
block = graph.FindBlock(block->predecessors[0]);
std::vector<std::pair<uint32_t, uint32_t>> pending {{found->id, before}};
std::set<std::pair<uint32_t, uint32_t>> visited;
std::optional<uint32_t> definition;
while (!pending.empty()) {
const auto point = pending.back();
pending.pop_back();
if (!visited.insert(point).second) continue;
const auto* block = graph.FindBlock(point.first);
EXIT_IF(block == nullptr);
before = block->inst_end;
uint32_t index = UINT32_MAX;
for (uint32_t i = point.second; i > block->inst_begin;) {
const auto& inst = program.instructions[--i];
if (inst.opcode == Opcode::S_SWAPPC_B64) {
EXIT_NOT_IMPLEMENTED(MayWriteScalarRegister(inst, code));
crossed_calls.push_back(i);
continue;
}
if (MayWriteScalarRegister(inst, code)) { index = i; break; }
}
if (index != UINT32_MAX || block->id == graph.entry_block) {
EXIT_IF(definition && *definition != index);
definition = index;
continue;
}
EXIT_IF(block->predecessors.empty());
for (const auto predecessor: block->predecessors) {
pending.emplace_back(predecessor, graph.blocks[predecessor].inst_end);
}
}
EXIT("scalar shader call source has cyclic reaching definitions");
EXIT_IF(!definition);
return *definition;
}
std::string BranchConditionToString(BranchCondition condition) {
@@ -115,7 +115,7 @@ struct Graph {
Graph BuildGraph(const Decoder::Program& program);
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code);
uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& graph,
uint32_t before, uint32_t code);
uint32_t before, uint32_t code, std::vector<uint32_t>& crossed_calls);
// Returns structured control flow or failure diagnostics without changing the native graph.
// On failure, failure_block is an original block ID or UINT32_MAX.
Graph Structurize(const Graph& graph);
@@ -70,6 +70,8 @@ constexpr MemoryOpcodeInfo MUBUF_OPCODE_LIST[] = {
{0x52u, Opcode::BUFFER_ATOMIC_ADD_X2, 2, 32},
{0x59u, Opcode::BUFFER_ATOMIC_AND_X2, 2, 32},
{0x5au, Opcode::BUFFER_ATOMIC_OR_X2, 2, 32},
{0x71u, Opcode::BUFFER_GL0_INV, 0, 0},
{0x72u, Opcode::BUFFER_GL1_INV, 0, 0},
{0x80u, Opcode::BUFFER_LOAD_FORMAT_D16_X, 1, 16, false, false, true},
{0x83u, Opcode::BUFFER_LOAD_FORMAT_D16_XYZW, 2, 16, false, false, true},
{0x84u, Opcode::BUFFER_STORE_FORMAT_D16_X, 1, 16, false, false, true},
@@ -312,6 +314,9 @@ void DecodeMubuf(uint32_t pc, std::span<const uint32_t> code, uint32_t word_inde
if (inst.opcode == Opcode::UNSUPPORTED) {
SetUnsupported(inst, Family::MUBUF, opcode, "MUBUF opcode is not implemented");
}
if (inst.opcode == Opcode::BUFFER_GL0_INV || inst.opcode == Opcode::BUFFER_GL1_INV) {
return;
}
DecodeVectorGpr(vdata, inst.dst);
if (inst.opcode == Opcode::BUFFER_LOAD_FORMAT_D16_XYZW ||
@@ -164,9 +164,23 @@ void DecodeSop1(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
switch (inst.opcode) {
case Opcode::S_SWAPPC_B64:
inst.branch_target = UINT32_MAX;
inst.data_dwords = 2;
[[fallthrough]];
case Opcode::S_MOV_B64:
case Opcode::S_CMOV_B64:
case Opcode::S_NOT_B64:
case Opcode::S_WQM_B64:
case Opcode::S_BREV_B64:
case Opcode::S_BITSET0_B64:
case Opcode::S_BITSET1_B64:
case Opcode::S_AND_SAVEEXEC_B64:
case Opcode::S_ORN2_SAVEEXEC_B64:
case Opcode::S_QUADMASK_B64:
case Opcode::S_ANDN1_SAVEEXEC_B64:
case Opcode::S_BITREPLICATE_B64_B32:
inst.data_dwords = 2u;
break;
case Opcode::S_GETPC_B64:
inst.data_dwords = 2u;
inst.src_count = 0;
DecodeScalarDestination(sdst, pc, inst.dst);
return;
@@ -206,6 +220,8 @@ void DecodeSop2(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
DecodeScalarDestination(sdst, pc, inst.dst);
DecodeBinarySources(pc, code, word_index, inst, ssrc0, ssrc1);
if (opcode == 0x0bu || (opcode >= 0x0fu && opcode <= 0x25u && (opcode & 1u)) || opcode == 0x29u)
inst.data_dwords = 2u;
}
void DecodeSopk(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
@@ -566,6 +566,9 @@ std::string InstructionToString(const Instruction& inst) {
OperandToString(inst.src0).c_str()));
case Opcode::S_BARRIER:
return WithUnsupportedReason(inst, fmt::format("0x{:08x}: s_barrier", inst.pc));
case Opcode::BUFFER_GL0_INV:
case Opcode::BUFFER_GL1_INV:
return fmt::format("0x{:08x}: {}", inst.pc, magic_enum::enum_name(inst.opcode));
case Opcode::V_NOP:
return WithUnsupportedReason(inst, fmt::format("0x{:08x}: v_nop", inst.pc));
case Opcode::S_ENDPGM:
@@ -599,6 +599,8 @@ enum class Opcode {
BUFFER_ATOMIC_XOR,
BUFFER_ATOMIC_FMIN,
BUFFER_ATOMIC_FMAX,
BUFFER_GL0_INV,
BUFFER_GL1_INV,
FLAT_LOAD_UBYTE,
FLAT_LOAD_SBYTE,
FLAT_LOAD_USHORT,
@@ -940,6 +940,10 @@ void Translator::DS_PERMUTE(const Decoder::Instruction& inst, bool backward) {
void Translator::EmitMemory(const Decoder::Instruction& inst) {
switch (inst.opcode) {
case Decoder::Opcode::BUFFER_GL0_INV:
case Decoder::Opcode::BUFFER_GL1_INV:
ir.Emit(IR::ValueOpcode::DataCacheInvalidate);
return;
case Decoder::Opcode::IMAGE_BVH_INTERSECT_RAY: {
const auto result = ir.Emit(IR::ValueOpcode::BvhIntersect,
{ConstructU32x4(inst.src1, 4), MakeImageAddress(inst, inst.src0), ir.GetExec()},
@@ -495,7 +495,6 @@ struct ShaderInfo {
static constexpr uint32_t MaxBufferRoots = 64;
static constexpr uint32_t MaxImages = 64;
static constexpr uint32_t MaxSamplers = 32;
static constexpr uint32_t MaxSampledPairs = 64;
std::vector<BufferResource> buffers;
std::vector<ImageResource> images;
@@ -94,6 +94,7 @@ bool HasSideEffects(ValueOpcode opcode) {
case ValueOpcode::SetTessellationAttribute:
case ValueOpcode::MeshAllocate:
case ValueOpcode::StoreCompletion:
case ValueOpcode::DataCacheInvalidate:
case ValueOpcode::Barrier: return true;
default: return false;
}
@@ -350,6 +350,7 @@ VALUE_OPCODE(SetAttribute, Void, U32x4, U1)
VALUE_OPCODE(ControlNop, Void)
VALUE_OPCODE(StoreCompletion, Void)
VALUE_OPCODE(DataCacheInvalidate, Void)
VALUE_OPCODE(Barrier, Void)
VALUE_OPCODE(Sendmsg, Void)
VALUE_OPCODE(TtraceData, Void)
@@ -1029,6 +1029,19 @@ void ConstantPropagationPass(const BlockList& blocks, uint32_t wave_size) {
}
drain();
} while (normalized);
// Repeated register reads share a bitcast only within the same SSA block.
std::unordered_map<const Inst*, Inst*> bitcasts;
for (auto* block: blocks) {
bitcasts.clear();
for (auto& inst: *block) {
if (inst.GetOpcode() != ValueOpcode::BitCastF32U32 &&
inst.GetOpcode() != ValueOpcode::BitCastU32F32) continue;
const auto* source = Arg(inst, 0).TryInstruction();
if (source == nullptr) continue;
const auto [entry, inserted] = bitcasts.try_emplace(source, &inst);
if (!inserted) Replace(inst, Value(entry->second));
}
}
}
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -30,32 +30,62 @@ void RemoveIdentities(Program& program) {
}
void EliminateDeadCode(const BlockList& blocks) {
std::vector<Inst*> pending;
for (auto* block: blocks) {
for (auto& inst: *block) {
// Retained resource-planning instructions live outside the block list.
inst.live = inst.MayHaveSideEffects() || std::ranges::any_of(inst.Uses(),
[](const Use& use) { return use.user->Parent() == nullptr; });
if (inst.live) pending.push_back(&inst);
}
}
while (!pending.empty()) {
const auto* inst = pending.back();
pending.pop_back();
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) {
auto* input = inst->Arg(arg).TryInstruction();
if (input != nullptr && input->Parent() != nullptr && !input->live) {
input->live = true;
pending.push_back(input);
bool changed;
do {
std::vector<Inst*> pending;
for (auto* block: blocks) {
for (auto& inst: *block) {
// Retained resource-planning instructions live outside the block list.
inst.live = inst.MayHaveSideEffects() || std::ranges::any_of(inst.Uses(),
[](const Use& use) { return use.user->Parent() == nullptr; });
if (inst.live) pending.push_back(&inst);
}
}
}
// Unlink every dead operand before freeing nodes in a dead Phi cycle.
for (auto* block: blocks)
for (auto& inst: *block)
if (!inst.live) inst.Invalidate();
for (auto* block: blocks)
std::erase_if(block->Instructions(), [](const Inst& inst) { return !inst.live; });
while (!pending.empty()) {
const auto* inst = pending.back();
pending.pop_back();
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) {
auto* input = inst->Arg(arg).TryInstruction();
if (input != nullptr && input->Parent() != nullptr && !input->live) {
input->live = true;
pending.push_back(input);
}
}
}
// Unlink every dead operand before freeing nodes in a dead Phi cycle.
for (auto* block: blocks)
for (auto& inst: *block)
if (!inst.live) inst.Invalidate();
for (auto* block: blocks)
std::erase_if(block->Instructions(), [](const Inst& inst) { return !inst.live; });
changed = false;
for (auto* block: blocks) {
const auto& term = block->terminator;
if (term.kind != CFG::TerminatorKind::ConditionalBranch || term.loop_header ||
block->condition.IsImmediate()) continue;
for (const bool take_true: {false, true}) {
const auto* empty = take_true ? term.false_block : term.true_block;
const auto* merge = take_true ? term.true_block : term.false_block;
if (empty == nullptr || merge == nullptr || !empty->empty() ||
empty->terminator.loop_header || merge->terminator.loop_header ||
empty->terminator.kind != CFG::TerminatorKind::Branch ||
empty->terminator.true_block != merge ||
std::ranges::any_of(*merge, [](const Inst& inst) {
return inst.GetOpcode() == ValueOpcode::Phi;
})) continue;
// Keep the CFG and SSA edges intact; only the empty detour's predicate is dead.
for (auto& inst: *block) {
if (inst.GetOpcode() == ValueOpcode::Reference &&
inst.Arg(0).Resolve() == block->condition.Resolve())
inst.SetArg(0, Value(take_true));
}
block->condition = Value(take_true);
changed = true;
break;
}
}
} while (changed);
}
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -292,11 +292,23 @@ bool NormalizeIndirectBuffer(DescriptorValue& value, bool indexed, const BufferR
bool IsBoundedDescriptorTable(const ResourcePlan& program,
const DescriptorSource::IndirectDescriptor& indirect) {
const auto* table = Source(program, indirect.table_source);
return indirect.sources.empty() && !indirect.selector &&
return indirect.sources.empty() && (!indirect.selector || indirect.table_scalar) &&
(indirect.key_count.IsEmpty() || indirect.table_maximum) && indirect.workgroup_axis == UINT32_MAX &&
table != nullptr && table->dword_count == 4u;
}
bool CanReadIndexedSelector(const ShaderBufferResource& material, uint32_t offset) {
if (material.Type() != 0u || material.SwizzleEnabled() || material.AddTid() ||
material.OutOfBounds() != 0u) return false;
if (material.Format() == Prospero::BufferFormat::kInvalid || material.NumRecords() == 0u ||
offset >= material.Stride()) return true;
return material.Base48() != 0u &&
((material.Base48() | material.Stride() | offset) & 3u) == 0u &&
material.NumRecords() <= MaxIndirectDescriptorProbes &&
uint64_t {material.NumRecords() - 1u} * material.Stride() + offset + 4u <=
uint64_t {UINT32_MAX} + 1u;
}
bool EvaluateIndirectCount(const DescriptorSource::IndirectDescriptor& indirect,
const SrtRuntime& runtime, SrtWalker& clean, uint32_t& count) {
if (indirect.workgroup_axis != UINT32_MAX) {
@@ -490,6 +502,15 @@ bool ResolveSelectorKeys(const DescriptorSource::IndirectDescriptor& indirect,
keys.push_back(key);
mask &= mask - 1u;
}
} else if (selector->stride == 0u) {
ShaderBufferResource material;
if (!DecodeBufferDescriptor(material_value, material) ||
!CanReadIndexedSelector(material, selector->offset)) return false;
if (material.Format() != Prospero::BufferFormat::kInvalid && material.NumRecords() != 0u &&
selector->offset < material.Stride() &&
!read_keys(material.Base48(), material.GetSize(), selector->offset,
material.Stride(), material.NumRecords())) return false;
keys.push_back(0u); // Every out-of-bounds indexed load returns zero.
} else {
ShaderBufferResource material;
if (!DecodeBufferDescriptor(material_value, material) || selector->stride < 4u ||
@@ -524,29 +545,29 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
ResourceSnapshot& snapshot,
std::vector<DescriptorValue>& descriptors,
std::vector<Specialization>& specializations,
uint32_t maximum_resources, Normalize&& normalize) {
Normalize&& normalize) {
const auto& indirect = *program.descriptor_sources[source_index].indirect_descriptor;
const auto descriptor_bytes = read_dwords * sizeof(uint32_t);
const auto& sources = indirect.sources;
const auto* selector = indirect.selector ? &*indirect.selector : nullptr;
const bool byte_keys = IsBoundedDescriptorTable(program, indirect);
auto* selector_keys = selector != nullptr
? &program.selector_keys[program.selector_groups[source_index]] : nullptr;
auto& keys = selector_keys != nullptr ? selector_keys->values : program.material_keys;
auto& byte_offsets = program.material_keys;
const auto children_begin = descriptors.size();
const auto root_resource = specializations[resource_index];
const auto intern_candidate = [&](const DescriptorValue& candidate, uint32_t& ordinal) {
ordinal = 0u;
if (candidate == descriptors[resource_index]) return true;
const auto intern_candidate = [&](const DescriptorValue& candidate) {
if (candidate == descriptors[resource_index]) return 0u;
const auto found = std::find(descriptors.begin() + children_begin, descriptors.end(), candidate);
ordinal = static_cast<uint32_t>(found - descriptors.begin() - children_begin + 1u);
const auto ordinal = static_cast<uint32_t>(found - descriptors.begin() - children_begin + 1u);
if (found == descriptors.end()) {
if (descriptors.size() >= maximum_resources) return false;
descriptors.push_back(candidate);
auto child = root_resource;
child.indirect_root = resource_index;
specializations.push_back(child);
}
return true;
return ordinal;
};
uint64_t table_base = 0;
uint64_t table_size = UINT64_MAX; // Scalar addresses have no buffer descriptor bounds.
@@ -558,7 +579,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
"indirect {} resource={} source={} table_source={} words={:08x},{:08x},{:08x},{:08x} "
"base={:#x} size={} type={} runtime_stride={} records={} swizzle={} addtid={} oob={} "
"maximum={} offset={} immediate={} record_bytes={} child_count={} child_stride={} "
"child_offset={} indexed_pointer={} candidates={} capacity={} {}",
"child_offset={} indexed_pointer={} candidates={} {}",
reason, resource_index, source_index, indirect.table_source,
table_value.dwords[0], table_value.dwords[1], table_value.dwords[2], table_value.dwords[3],
table_base, table_size, table.Type(), table.Stride(), table.NumRecords(),
@@ -567,7 +588,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
indirect.table_offset, indirect.table_immediate, indirect.table_record_bytes,
pointer != nullptr ? pointer->count : 1u, pointer != nullptr ? pointer->stride : 0u,
pointer != nullptr ? pointer->offset : 0u, pointer != nullptr && pointer->indexed,
descriptors.size(), maximum_resources, details));
descriptors.size(), details));
};
const auto read_failed = [&](std::string_view reason, uint32_t row, uint32_t child,
uint64_t base, uint64_t offset, size_t bytes) {
@@ -583,7 +604,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
if (sources.empty()) {
if (selector == nullptr) keys.clear();
DescriptorValue material_value;
if ((selector != nullptr && !selector_keys->resolved &&
if ((selector != nullptr && (byte_keys || !selector_keys->resolved) &&
!clean.EvaluateDescriptor(selector->source, material_value)) ||
!clean.EvaluateDescriptor(indirect.table_source, table_value)) {
return false;
@@ -596,16 +617,40 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
} else {
return false;
}
if (IsBoundedDescriptorTable(program, indirect)) {
if (byte_keys && table_size > UINT32_MAX) return failed("table geometry");
bool use_selector = selector != nullptr;
if (byte_keys && selector != nullptr) {
ShaderBufferResource material;
if (!DecodeBufferDescriptor(material_value, material)) return false;
use_selector = CanReadIndexedSelector(material, selector->offset);
if (use_selector && material.Format() != Prospero::BufferFormat::kInvalid &&
material.NumRecords() != 0u && selector->offset < material.Stride()) {
const auto begin = material.Base48() + selector->offset;
const auto end = begin + uint64_t {material.NumRecords() - 1u} * material.Stride() + 4u;
for (uint32_t i = 0; i < program.info.buffers.size(); ++i) {
const auto& buffer = program.info.buffers[i];
if (!buffer.written) continue;
ShaderBufferResource written;
if (program.descriptor_sources[buffer.source].indirect_descriptor ||
!DecodeBufferDescriptor(snapshot.buffers[i], written) ||
(written.Base48() != 0u && written.GetSize() != 0u &&
begin < written.Base48() + written.GetSize() &&
written.Base48() < end)) {
use_selector = false;
break;
}
}
}
}
if (byte_keys && !use_selector) {
if (!indirect.table_scalar && table.Format() == Prospero::BufferFormat::kInvalid) {
descriptors[resource_index] = {.dword_count = dword_count};
return true;
}
if (table_size > UINT32_MAX ||
(indirect.table_record_bytes != 0u &&
if (indirect.table_record_bytes != 0u &&
(table.Type() != 0u || table.SwizzleEnabled() || table.AddTid() ||
table.OutOfBounds() != 0u || (table_base & 3u) != 0u ||
(table.Stride() & 3u) != 0u || table.Stride() < indirect.table_record_bytes)))
(table.Stride() & 3u) != 0u || table.Stride() < indirect.table_record_bytes))
return failed("table geometry");
const auto* pointer = indirect.pointer_table ? &*indirect.pointer_table : nullptr;
const bool indexed_pointer = pointer != nullptr && pointer->indexed;
@@ -640,10 +685,11 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
return failed("candidate count", fmt::format("rows={} limit={}", count, MaxIndirectDescriptorProbes));
// Affine SMEM offsets wrap in U32 before adding the aligned immediate.
// Image heaps retain one bulk snapshot and their absolute byte-offset mapping.
auto& heap_words = program.material_keys;
if (image_heap) {
keys.resize(table_size / 4u);
if (!ReadScalarTable(table_base, table_size, 0u, runtime, keys))
return read_failed("image heap read", 0u, 0u, table_base, 0u, keys.size() * sizeof(uint32_t));
heap_words.resize(table_size / 4u);
if (!ReadScalarTable(table_base, table_size, 0u, runtime, heap_words))
return read_failed("image heap read", 0u, 0u, table_base, 0u, heap_words.size() * sizeof(uint32_t));
}
const auto mapping_offset = snapshot.flattened_srt.size();
descriptors[resource_index] = {.dword_count = dword_count};
@@ -670,15 +716,14 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
std::span(candidate.dwords).first(read_dwords))) return false;
} else {
const auto table_word = table_offset / 4u;
std::copy_n(keys.begin() + table_word,
std::min<size_t>(read_dwords, keys.size() - table_word),
std::copy_n(heap_words.begin() + table_word,
std::min<size_t>(read_dwords, heap_words.size() - table_word),
candidate.dwords.begin());
}
if (!normalize(candidate))
return normalization_failed(table_offset, word, step,
indirect.table_maximum ? "key" : "probe");
uint32_t ordinal;
if (!intern_candidate(candidate, ordinal)) return false;
const auto ordinal = intern_candidate(candidate);
if (ordinal == 0u) continue;
const auto mapping_word = image_heap ? table_offset / 4u : word * child_count + child;
snapshot.flattened_srt.resize(mapping_offset + 2u + mapping_word, 0u);
@@ -722,21 +767,38 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
selector_keys->resolved = true;
}
}
if (byte_keys) {
// Keep cached selector values independent of each destination table's
// U32 arithmetic, and use the same byte keys as the full-heap GPU path.
byte_offsets.clear();
byte_offsets.reserve(keys.size());
for (const auto key: keys) {
const auto offset = uint64_t {(key * indirect.table_stride + indirect.table_offset) & ~3u} +
(indirect.table_immediate & ~3u);
byte_offsets.push_back(offset <= UINT32_MAX ? static_cast<uint32_t>(offset) : UINT32_MAX);
}
std::ranges::sort(byte_offsets);
byte_offsets.erase(std::unique(byte_offsets.begin(), byte_offsets.end()), byte_offsets.end());
}
}
const auto mapping_offset = snapshot.flattened_srt.size();
const auto key_count = sources.empty() ? keys.size() : sources.size();
const auto selected_keys = byte_keys ? std::span<const uint32_t>(byte_offsets)
: std::span<const uint32_t>(keys);
const auto key_count = sources.empty() ? selected_keys.size() : sources.size();
snapshot.flattened_srt.resize(mapping_offset + 1u + key_count * 2u);
snapshot.flattened_srt[mapping_offset] = static_cast<uint32_t>(key_count);
for (uint32_t entry = 0; entry < key_count; ++entry) {
const auto key = sources.empty() ? keys[entry] : entry;
const auto key = sources.empty() ? selected_keys[entry] : entry;
uint64_t table_offset = 0;
DescriptorValue candidate;
candidate.dword_count = dword_count;
if (sources.empty()) {
table_offset = uint64_t {(key * indirect.table_stride + indirect.table_offset) & ~3u} +
(indirect.table_immediate & ~3u);
if (!ReadScalarTable(table_base, table_size, table_offset, runtime,
table_offset = byte_keys ? key
: uint64_t {(key * indirect.table_stride + indirect.table_offset) & ~3u} +
(indirect.table_immediate & ~3u);
if (!(byte_keys && key == UINT32_MAX) &&
!ReadScalarTable(table_base, table_size, table_offset, runtime,
std::span(candidate.dwords).first(read_dwords))) {
return read_failed("table descriptor read", key, 0u, table_base, table_offset, descriptor_bytes);
}
@@ -746,10 +808,10 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
if (!normalize(candidate))
return normalization_failed(table_offset, key, indirect.table_stride, "key");
uint32_t ordinal = 0;
if (entry == 0) {
if (entry == 0 && !byte_keys) {
descriptors[resource_index] = candidate;
} else if (!intern_candidate(candidate, ordinal)) {
return false;
} else {
ordinal = intern_candidate(candidate);
}
snapshot.flattened_srt[mapping_offset + 1u + entry * 2u] = key;
snapshot.flattened_srt[mapping_offset + 2u + entry * 2u] = ordinal;
@@ -772,9 +834,8 @@ struct SamplerPlan {
uint32_t source;
SamplerClass type;
};
std::array<std::array<uint32_t, 2>, ShaderInfo::MaxSamplers> mapping;
std::array<Binding, ShaderInfo::MaxSamplers> bindings;
uint32_t sampler_count = 0;
std::vector<std::array<uint32_t, 2>> mapping;
std::vector<Binding> bindings;
};
template <typename T, typename Keep>
@@ -913,6 +974,7 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
if (i >= program.info.images.size() && source != nullptr &&
source->indirect_descriptor &&
IsBoundedDescriptorTable(program, *source->indirect_descriptor) &&
specialization.images[base_index].indirect_search_iterations == 0u &&
!source->indirect_descriptor->pointer_table) {
const auto& indirect = *source->indirect_descriptor;
const size_t mapping = specialization.images[base_index].indirect_mapping_offset;
@@ -997,7 +1059,6 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
is_1d_or_2d(image_class.dimension);
if ((!same_coordinates && !(is_2d(image.dimension) && is_2d(image_class.dimension)) &&
!unfiltered_coordinates) ||
image.mip_count != image_class.mip_count ||
image.conversion_format != image_class.conversion_format ||
(!storage_swizzle && image.shader_swizzle != image_class.shader_swizzle)) {
return SpecializationFail(
@@ -1011,11 +1072,9 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
}
bool BuildSamplerPlan(const ShaderInfo& base, SamplerPlan& plan) {
if (base.samplers.size() > plan.mapping.size()) {
return false;
}
std::array<uint8_t, ShaderInfo::MaxSamplers> usage {};
plan.sampler_count = static_cast<uint32_t>(base.samplers.size());
plan.mapping.resize(base.samplers.size());
plan.bindings.resize(base.samplers.size());
std::vector<uint8_t> usage(base.samplers.size());
for (const auto& pair: base.sampled_pairs) {
if (pair.image >= base.images.size() || pair.sampler >= base.samplers.size()) {
return false;
@@ -1037,8 +1096,8 @@ bool BuildSamplerPlan(const ShaderInfo& base, SamplerPlan& plan) {
bool first = true;
for (uint32_t type = 0; type < mapping.size(); type++) {
if ((classes & (1u << type)) == 0u) continue;
const auto target = first ? index : plan.sampler_count++;
if (target >= ShaderInfo::MaxSamplers) return false;
const auto target = first ? index : static_cast<uint32_t>(plan.bindings.size());
if (!first) plan.bindings.emplace_back();
mapping[type] = target;
plan.bindings[target] = {index, static_cast<SamplerClass>(type)};
first = false;
@@ -1544,9 +1603,9 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
return true;
};
const auto resource_failed = [&](std::string_view kind, uint32_t resource, uint32_t source,
size_t candidates, uint32_t capacity) {
return SpecializationFail(fmt::format("indirect {} resource={} source={} candidates={} capacity={}",
kind, resource, source, candidates, capacity));
size_t candidates) {
return SpecializationFail(fmt::format("indirect {} resource={} source={} candidates={}",
kind, resource, source, candidates));
};
snapshot.buffers.resize(program.info.buffers.size());
specialization.buffers.assign(program.info.buffers.size(), {});
@@ -1563,12 +1622,12 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
if (active.empty() || active[base.source]) {
if (!MaterializeIndirectDescriptor(
program, base.source, i, 4u, 4u, observed, clean, snapshot,
snapshot.buffers, specialization.buffers, UINT32_MAX,
snapshot.buffers, specialization.buffers,
[&](DescriptorValue& value) {
return NormalizeIndirectBuffer(
value, source->indirect_descriptor->indexed, base);
}))
return resource_failed("buffer", i, base.source, snapshot.buffers.size(), UINT32_MAX);
return resource_failed("buffer", i, base.source, snapshot.buffers.size());
}
} else if (!evaluate(base.source, snapshot.buffers[i], base.written)) {
return false;
@@ -1627,14 +1686,14 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
const bool bounded_table = IsBoundedDescriptorTable(program, indirect);
if (!MaterializeIndirectDescriptor(
program, image.source, i, 8u, image.r128 ? 4u : 8u, observed, clean, snapshot, snapshot.images,
specialization.images, UINT32_MAX, [&](DescriptorValue& value) {
specialization.images, [&](DescriptorValue& value) {
// A broad heap also contains resources for other typed image operations.
if (NullImageDescriptor(value) || !ValidImageDescriptor(value, image.r128) ||
(bounded_table && DescriptorDimension(value, image.dimension) != image.dimension))
value.dwords.fill(0u);
return true;
})) {
return resource_failed("image", i, image.source, snapshot.images.size(), UINT32_MAX);
return resource_failed("image", i, image.source, snapshot.images.size());
}
} else {
if (!evaluate(image.source, snapshot.images[i], image.written)) {
@@ -1660,9 +1719,9 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
snapshot.samplers[i] = {.dword_count = 4u};
if ((active.empty() || active[source]) && !MaterializeIndirectDescriptor(
program, source, i, 4u, 4u, observed, clean, snapshot, snapshot.samplers,
specialization.samplers, ShaderInfo::MaxSamplers,
specialization.samplers,
[](DescriptorValue&) { return true; }))
return resource_failed("sampler", i, source, snapshot.samplers.size(), ShaderInfo::MaxSamplers);
return resource_failed("sampler", i, source, snapshot.samplers.size());
} else {
if (indirect != nullptr) source = indirect->sources[0];
if (!evaluate(source, snapshot.samplers[i])) return false;
@@ -1671,11 +1730,13 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
if (program.info.samplers[base_index].gather_lod) {
const auto control = snapshot.samplers[i].dwords[2];
const auto filter = (control >> 26u) & 3u;
// MipNone always selects the base level. Explicit point gathers currently require
// encoded-zero primary and secondary bias; linear primary-mip selection is unsupported.
if (filter > 1u || (filter == 1u && (control & 0xfffffu) != 0u)) {
const auto perf_mip = (snapshot.samplers[i].dwords[1] >> 24u) & 15u;
// MipNone always selects the base level. Point/linear selection requires zero
// primary/secondary bias; linear mip snapping is not implemented.
if (filter > 2u || (filter != 0u && (control & 0xfffffu) != 0u) ||
(filter == 2u && perf_mip != 0u)) {
return SpecializationFail(
"explicit-LOD gather requires mip filtering None or Point with zero LOD biases");
"explicit-LOD gather requires None, unbiased Point, or unbiased Linear without mip snapping");
}
}
}
@@ -1768,8 +1829,8 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
SamplerPlan sampler_plan;
EXIT_IF(!BuildSamplerPlan(program.info, sampler_plan));
const auto original_sampler_count = samplers.size();
samplers.reserve(sampler_plan.sampler_count);
for (uint32_t index = 0; index < sampler_plan.sampler_count; index++) {
samplers.reserve(sampler_plan.bindings.size());
for (uint32_t index = 0; index < sampler_plan.bindings.size(); index++) {
const auto& binding = sampler_plan.bindings[index];
if (index >= original_sampler_count) {
samplers.push_back(samplers[binding.source]);
@@ -1778,7 +1839,7 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
samplers[index].force_point_filtering = binding.type == SamplerClass::Integer;
samplers[index].integer_border = binding.type != SamplerClass::Float;
}
for (uint32_t index = 0; index < sampler_plan.sampler_count; index++) {
for (uint32_t index = 0; index < sampler_plan.bindings.size(); index++) {
const auto& binding = sampler_plan.bindings[index];
for (auto& candidate: samplers[index].indirect_resources) {
candidate = sampler_plan.mapping[candidate][static_cast<uint32_t>(binding.type)];
@@ -1905,6 +1966,7 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
EXIT_IF(image.indirect_root == memory.resource &&
inst.GetOpcode() != ValueOpcode::ImageSampleRaw &&
inst.GetOpcode() != ValueOpcode::ImageGatherRaw &&
inst.GetOpcode() != ValueOpcode::ImageQueryDimensions &&
inst.GetOpcode() != ValueOpcode::ImageRead &&
inst.GetOpcode() != ValueOpcode::ImageWrite);
}
@@ -1558,14 +1558,49 @@ private:
return read;
}
const Inst* IndexedMaterialRead(const Inst* read, Value active,
DescriptorSource::IndirectDescriptor::SelectorRead& selector) const {
uint32_t lane = 0;
if (read != nullptr && read->GetOpcode() == ValueOpcode::CompositeExtractU32x2) {
if (!ImmediateU32(read->Arg(1), lane) || lane >= 2u) return nullptr;
read = read->Arg(0).Resolve().TryInstruction();
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32x2) return nullptr;
} else if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32) {
return nullptr;
}
const auto& memory = m_program.memory_info[read->Flags<MemoryFlags>().index];
if (memory.kind != ResourceKind::Buffer || memory.typed || memory.formatted ||
!memory.idxen || memory.offen || memory.offset > UINT32_MAX - lane * 4u ||
read->Arg(2).Resolve() != Value(0u) || read->Arg(3).Resolve() != Value(0u) ||
!EquivalentValue(m_program, active, read->Arg(4))) return nullptr;
selector.offset = memory.offset + lane * 4u;
return read;
}
bool MatchUniformizedMaterialKey(Value key, const Inst& image,
DescriptorSource::IndirectDescriptor& indirect,
DescriptorSource& material_source) {
DescriptorSource& material_source, bool indexed) {
const auto* selected = UniformizedMaterialValue(key, image);
if (selected == nullptr) return false;
const auto active = selected->Arg(0).Resolve();
DescriptorSource::IndirectDescriptor::SelectorRead selector;
const auto* read = SelectorReadValue(selected->Arg(1), active, selector);
if (indexed) {
read = IndexedMaterialRead(read, active, selector);
// This optional domain is checked against the dispatch's writable buffers.
// Other stages and unbounded/image writes need the original heap domain.
if (read == nullptr || m_program.stage != ShaderType::Compute ||
std::ranges::any_of(m_program.blocks, [](const Block* block) {
return std::ranges::any_of(*block, [](const Inst& inst) {
const auto op = inst.GetOpcode();
return ImageOpcodeInfoOf(op).resource_class == ImageResourceClass::Storage ||
AddressOpcodeInfoOf(op).access == AddressAccess::Write;
});
}) || !MakeRuntimeTableSource(*read, material_source)) return false;
selector.source = InternSource(material_source);
indirect.selector = std::move(selector);
return true;
}
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadAddressU32 ||
read->NumArgs() != 4u || !Implies(active, read->Arg(3)))
return false;
@@ -2815,7 +2850,7 @@ private:
} else result.maximum = std::max(result.maximum, SelectorMaximum(key, {}, 0u, active));
}
}
if (lane != nullptr) result = SelectedFiniteDomain(*lane, handle != nullptr ? lane->Arg(1) : Value {});
if (lane != nullptr) result = SelectedSelectorDomain(*lane, handle != nullptr ? lane->Arg(1) : Value {});
else if (handle == nullptr) result.maximum = SelectorMaximum(key);
if (result.maximum > UINT32_MAX / 4u ||
uint64_t {result.maximum} * indirect.table_stride + indirect.table_offset > UINT32_MAX)
@@ -2971,6 +3006,13 @@ private:
// Keep live descriptor payloads, but project descriptor-only SMEM onto
// the GPU key using the same plan as other indirect image reads.
plan.retain_reads |= table_source.dword_count == 4u;
} else if (!sampler && !plan.table_indexed && table_source.dword_count == 4u &&
MatchUniformizedMaterialKey(key, handle, indirect, material_source, true)) {
// Preserve the heap's byte-offset key so runtime geometry can choose the
// tighter selector domain without changing the GPU's descriptor reads.
indirect.table_scalar = true;
plan.table_read = plan.reads[0];
plan.retain_reads = true;
} else if (sampler && table_source.dword_count == 4u && !plan.table_indexed &&
SetExactTableMaximum(key, indirect, &handle)) {
indirect.table_scalar = true;
@@ -3020,7 +3062,7 @@ private:
}
}
if (indirect.key_count.IsEmpty() &&
!MatchUniformizedMaterialKey(key, handle, indirect, material_source) &&
!MatchUniformizedMaterialKey(key, handle, indirect, material_source, false) &&
!MatchReversedImageRange(key, indirect)) {
const auto maximum = SelectorMaximum(key);
if (maximum >= MaxIndirectDescriptorProbes) return false;
@@ -3064,6 +3106,10 @@ private:
indirect.selector_mask = selected.selector_mask;
indirect.signed_count = selected.signed_count;
indirect.workgroup_axis = selected.workgroup_axis;
if (selected.table_scalar) {
indirect.table_scalar = true;
plan.table_read = plan.reads[0];
}
material_source = m_sources[selected.selector->source];
shared_selector = true;
break;
@@ -3264,26 +3310,13 @@ private:
if (value == nullptr || (selected != nullptr && selected != value)) return false;
selected = value;
}
const auto* read = selected->Arg(1).Resolve().TryInstruction();
uint32_t lane = 0;
if (read != nullptr && read->GetOpcode() == ValueOpcode::CompositeExtractU32x2) {
if (!ImmediateU32(read->Arg(1), lane) || lane >= 2u) return false;
read = read->Arg(0).Resolve().TryInstruction();
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32x2) return false;
} else if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32) {
return false;
}
const auto& memory = m_program.memory_info[read->Flags<MemoryFlags>().index];
uint32_t offset = 1u, scalar = 1u;
if (memory.kind != ResourceKind::Buffer || memory.typed || memory.formatted ||
!memory.idxen || memory.offen || memory.offset > UINT32_MAX - lane * 4u ||
!ImmediateU32(read->Arg(2), offset) || offset != 0u ||
!ImmediateU32(read->Arg(3), scalar) || scalar != 0u ||
!EquivalentValue(m_program, selected->Arg(0), read->Arg(4))) return false;
if (!MakeRuntimeTableSource(*read, material_source) ||
DescriptorSource::IndirectDescriptor::SelectorRead selector;
const auto* read = IndexedMaterialRead(selected->Arg(1).Resolve().TryInstruction(),
selected->Arg(0), selector);
if (read == nullptr || !MakeRuntimeTableSource(*read, material_source) ||
!BoundedSelectedIndex(read->Arg(1), read->Arg(4), indirect)) return false;
indirect.selector.emplace(DescriptorSource::IndirectDescriptor::SelectorRead{.source = InternSource(material_source),
.offset = memory.offset + lane * 4u});
selector.source = InternSource(material_source);
indirect.selector = std::move(selector);
return finish();
}
@@ -3372,10 +3405,12 @@ private:
return found ? SelectorDomain {maximum, counted ? count : Value {}} : SelectorDomain {};
}
SelectorDomain SelectedFiniteDomain(const Inst& lane, Value guard) const {
SelectorDomain SelectedSelectorDomain(const Inst& lane, Value guard) const {
if (guard.IsEmpty() ? !HasActiveLane(lane.Arg(1), lane.Parent())
: !Implies(guard, lane.Arg(1))) return {};
return FiniteSelectorDomain(lane.Arg(0), lane.Arg(1));
auto result = FiniteSelectorDomain(lane.Arg(0), lane.Arg(1));
if (result.maximum == UINT32_MAX) result.maximum = PossibleU32Bits(lane.Arg(0));
return result;
}
uint32_t SelectorMaximum(Value value, SelectorBounds bounds = {}, uint32_t depth = 0,
@@ -3392,7 +3427,7 @@ private:
};
switch (inst->GetOpcode()) {
case ValueOpcode::ReadFirstLane:
return SelectedFiniteDomain(*inst, guard).maximum;
return SelectedSelectorDomain(*inst, guard).maximum;
case ValueOpcode::ReadLane:
// ReadLane ignores EXEC; only context-free bounds on every source lane apply.
return PossibleU32Bits(inst->Arg(0));
@@ -3823,9 +3858,6 @@ private:
return;
}
}
if (m_info.sampled_pairs.size() >= ShaderInfo::MaxSampledPairs) {
Fail(pc, "sampled image/sampler pair limit exceeded");
}
m_info.sampled_pairs.push_back({image, sampler, pc});
}
@@ -509,6 +509,7 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs
case ValueOpcode::ReadConst: info.uses_flattened_srt = true; break;
case ValueOpcode::BitwiseXor32: info.has_bitwise_xor = true; break;
case ValueOpcode::StoreCompletion: info.subgroup_barrier = true; break;
case ValueOpcode::DataCacheInvalidate: info.coherent_buffers = true; break;
case ValueOpcode::BvhIntersect: info.bvh = true; break;
case ValueOpcode::ConditionRef:
info.subgroup_vote |=
@@ -159,9 +159,11 @@ bool IsRuntimeUniformOp(ValueOpcode op) {
case ValueOpcode::ULessThan32:
case ValueOpcode::ULessThanEqual32:
case ValueOpcode::IEqual32:
case ValueOpcode::IEqual64:
case ValueOpcode::UGreaterThan32:
case ValueOpcode::SGreaterThanEqual32:
case ValueOpcode::INotEqual32:
case ValueOpcode::INotEqual64:
case ValueOpcode::LogicalOr:
case ValueOpcode::LogicalAnd:
case ValueOpcode::LogicalXor:
@@ -1053,6 +1055,11 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
return true;
}
return false;
case ValueOpcode::IEqual64:
case ValueOpcode::INotEqual64:
if (!binary()) return false;
result = inst.GetOpcode() == ValueOpcode::IEqual64 ? a == b : a != b;
return true;
case ValueOpcode::ULessThan32:
// No U32 is below zero, including an unknown loop-carried value.
if (!Arg(inst, 1, b)) return false;
+28
View File
@@ -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<uint64_t>(nbytes, remaining));
uint32_t bytes_read = 0;
file->f.Read(buf, static_cast<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint64_t>(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<uint64_t>(offset));
file->f.Write(buf, static_cast<uint32_t>(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::optional<i
}
uint32_t bytes = 0;
file->f.Write(buffer.iov_base, static_cast<uint32_t>(buffer.iov_len), &bytes);
Common::DebugCounters::Add(Common::DebugCounters::Counter::GuestFileWriteBytes, bytes);
bytes_written += bytes;
if (bytes < buffer.iov_len) {
break;
@@ -1319,6 +1326,27 @@ int KYTY_SYSV_ABI KernelFtruncate(int d, int64_t length) {
return OK;
}
int KYTY_SYSV_ABI KernelFsync(int d) {
PRINT_NAME();
if (d < DESCRIPTOR_MIN) {
return KERNEL_ERROR_EBADF;
}
auto* file = g_files->GetFile(d);
if (file == nullptr || !file->opened) {
return KERNEL_ERROR_EBADF;
}
Common::LockGuard lock(file->mutex);
if (file->f.IsInvalid() || !file->f.Flush()) {
return KERNEL_ERROR_EIO;
}
LOGF("\tFsync: %s\n", Common::PathToString(file->real_name).c_str());
return OK;
}
int KYTY_SYSV_ABI KernelUnlink(const char* path) {
PRINT_NAME();
+1
View File
@@ -72,6 +72,7 @@ int KYTY_SYSV_ABI KernelStat(const char* path, FileStat* sb);
int KYTY_SYSV_ABI KernelFstat(int d, FileStat* sb);
int KYTY_SYSV_ABI KernelTruncate(const char* path, int64_t length);
int KYTY_SYSV_ABI KernelFtruncate(int d, int64_t length);
int KYTY_SYSV_ABI KernelFsync(int d);
int KYTY_SYSV_ABI KernelUnlink(const char* path);
int KYTY_SYSV_ABI KernelRename(const char* from, const char* to);
int KYTY_SYSV_ABI KernelGetdirentries(int fd, char* buf, int nbytes, int64_t* basep);
+35
View File
@@ -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
+17
View File
@@ -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;
+74 -48
View File
@@ -343,14 +343,21 @@ static bool KernelClockGettimeSpecial(KernelClockid clock_id, KernelTimespec* tp
#endif
struct PthreadMutexPrivate {
struct Waiter {
Pthread thread = nullptr;
std::condition_variable cv;
Waiter* previous = nullptr;
Waiter* next = nullptr;
};
uint8_t reserved[256];
std::string name;
std::mutex m;
std::condition_variable cv;
Pthread owner = nullptr;
uint32_t count = 0;
int type = 1;
int pprotocol = PTHREAD_PRIO_NONE;
Waiter* first_waiter = nullptr;
Waiter* last_waiter = nullptr;
};
struct PthreadMutexattrPrivate {
@@ -1242,6 +1249,20 @@ static int PthreadRwlockInitNamed(PthreadRwlock* rwlock, const PthreadRwlockattr
const char* name);
static int PthreadCondInitNamed(PthreadCond* cond, const PthreadCondattr* attr, const char* name);
static void NativeMutexRemoveWaiter(PthreadMutexPrivate* mutex,
PthreadMutexPrivate::Waiter* waiter) {
if (waiter->previous != nullptr) {
waiter->previous->next = waiter->next;
} else {
mutex->first_waiter = waiter->next;
}
if (waiter->next != nullptr) {
waiter->next->previous = waiter->previous;
} else {
mutex->last_waiter = waiter->previous;
}
}
static int NativeMutexLock(PthreadMutexPrivate* mutex, KernelUseconds* timeout_us) {
EXIT_IF(mutex == nullptr);
@@ -1258,54 +1279,53 @@ static int NativeMutexLock(PthreadMutexPrivate* mutex, KernelUseconds* timeout_u
mutex->count++;
return OK;
}
if (timeout_us != nullptr) {
if (*timeout_us > 0) {
mutex->cv.wait_for(lock, std::chrono::microseconds(*timeout_us));
}
return ETIMEDOUT;
}
return EDEADLK;
}
if (timeout_us == nullptr) {
while (mutex->owner != nullptr) {
mutex->cv.wait_for(lock, std::chrono::microseconds(SIGNAL_APC_POLL_MICROS));
if (mutex->owner != nullptr) {
lock.unlock();
KernelDispatchPendingSignalForCurrentThread();
lock.lock();
}
}
} else if (*timeout_us == 0) {
if (mutex->owner != nullptr) {
return ETIMEDOUT;
}
} else {
const auto deadline =
std::chrono::steady_clock::now() + std::chrono::microseconds(*timeout_us);
while (mutex->owner != nullptr) {
const auto now = std::chrono::steady_clock::now();
if (now >= deadline) {
return ETIMEDOUT;
}
const auto remaining = deadline - now;
const auto poll = (remaining < std::chrono::microseconds(SIGNAL_APC_POLL_MICROS)
? remaining
: std::chrono::steady_clock::duration(
std::chrono::microseconds(SIGNAL_APC_POLL_MICROS)));
mutex->cv.wait_for(lock, poll);
if (mutex->owner != nullptr) {
lock.unlock();
KernelDispatchPendingSignalForCurrentThread();
lock.lock();
}
}
if (mutex->owner == nullptr) {
mutex->owner = self;
mutex->count = 1;
return OK;
}
if (timeout_us != nullptr && *timeout_us == 0) {
return ETIMEDOUT;
}
mutex->owner = self;
mutex->count = 1;
return OK;
// Guest mutex waits are not cancellation points, unlike the host condition wait.
int previous_cancel_state = 0;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &previous_cancel_state);
PthreadMutexPrivate::Waiter waiter;
waiter.thread = self;
waiter.previous = mutex->last_waiter;
if (mutex->last_waiter != nullptr) {
mutex->last_waiter->next = &waiter;
} else {
mutex->first_waiter = &waiter;
}
mutex->last_waiter = &waiter;
const auto deadline = timeout_us != nullptr
? std::chrono::steady_clock::now() +
std::chrono::microseconds(*timeout_us)
: std::chrono::steady_clock::time_point::max();
int result = OK;
while (mutex->owner != self) {
const auto now = std::chrono::steady_clock::now();
if (now >= deadline) {
NativeMutexRemoveWaiter(mutex, &waiter);
result = ETIMEDOUT;
break;
}
waiter.cv.wait_until(lock, std::min(deadline, now +
std::chrono::microseconds(SIGNAL_APC_POLL_MICROS)));
if (mutex->owner != self) {
lock.unlock();
KernelDispatchPendingSignalForCurrentThread();
lock.lock();
}
}
lock.unlock();
pthread_setcancelstate(previous_cancel_state, nullptr);
return result;
}
static int NativeMutexTrylock(PthreadMutexPrivate* mutex) {
@@ -1358,10 +1378,16 @@ static int NativeMutexUnlock(PthreadMutexPrivate* mutex, uint32_t* recurse = nul
return OK;
}
mutex->owner = nullptr;
mutex->count = 0;
lock.unlock();
mutex->cv.notify_one();
if (auto* waiter = mutex->first_waiter) {
NativeMutexRemoveWaiter(mutex, waiter);
// Transfer ownership in FIFO order before waking the selected waiter.
mutex->owner = waiter->thread;
mutex->count = 1;
waiter->cv.notify_one();
} else {
mutex->owner = nullptr;
mutex->count = 0;
}
return OK;
}
@@ -174,12 +174,6 @@
</widget>
</item>
<item row="7" column="1">
<widget class="QCheckBox" name="checkBox_sync_raw_image_buffers">
<property name="toolTip"><string>Can fix missing textures and incorrect transparency in some games. May reduce performance.</string></property>
<property name="text"><string>Accurate texture synchronization</string></property>
</widget>
</item>
<item row="8" column="1">
<widget class="QCheckBox" name="checkBox_hdr">
<property name="toolTip"><string>Enable HDR10 output for supported games. Requires HDR enabled on the host display.</string></property>
<property name="text"><string>Enable HDR display support</string></property>
@@ -242,6 +236,12 @@
<property name="text"><string>Auto-hide cursor</string></property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_perf_overlay">
<property name="toolTip"><string>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.</string></property>
<property name="text"><string>Show performance panel</string></property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_skip_notice_screen">
<property name="toolTip"><string>Skip startup logos and notices in supported games.</string></property>
@@ -477,6 +477,25 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="rendering_compatibility_group">
<property name="title"><string>Rendering compatibility</string></property>
<layout class="QVBoxLayout" name="renderingCompatibilityLayout">
<item>
<widget class="QLabel" name="label_rendering_compatibility">
<property name="text"><string>These settings may help with rendering bugs at a slight performance cost.</string></property>
<property name="wordWrap"><bool>true</bool></property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_sync_raw_image_buffers">
<property name="toolTip"><string>Can fix missing textures and incorrect transparency (Resident Evil 2).</string></property>
<property name="text"><string>Accurate texture synchronization</string></property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="debugSpacer">
<property name="orientation"><enum>Qt::Vertical</enum></property>
+2
View File
@@ -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;
+2
View File
@@ -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();
@@ -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();
+4 -2
View File
@@ -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;
+1
View File
@@ -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) {
+1 -1
View File
@@ -670,7 +670,7 @@ int KYTY_SYSV_ABI AgcCreateShader(Shader** dst, void* header, const volatile voi
return OK;
}
int KYTY_SYSV_ABI AgcUnknownGetFusedShaderSize(SizeAlign* dst, const Shader* front,
int KYTY_SYSV_ABI AgcGetFusedShaderSize(SizeAlign* dst, const Shader* front,
const Shader* back) {
PRINT_NAME();
+1 -1
View File
@@ -40,7 +40,7 @@ int KYTY_SYSV_ABI AgcInit(uint32_t* state, uint32_t ver);
void* KYTY_SYSV_ABI AgcGetRegisterDefaults2(uint32_t ver);
void* KYTY_SYSV_ABI AgcGetRegisterDefaults2Internal(uint32_t ver);
int KYTY_SYSV_ABI AgcCreateShader(Shader** dst, void* header, const volatile void* code);
int KYTY_SYSV_ABI AgcUnknownGetFusedShaderSize(SizeAlign* dst, const Shader* front,
int KYTY_SYSV_ABI AgcGetFusedShaderSize(SizeAlign* dst, const Shader* front,
const Shader* back);
int KYTY_SYSV_ABI AgcFuseShaderHalves_0200(Shader* fused_result, const Shader* front,
const Shader* back, void* scratch_mem);
+4
View File
@@ -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<int>(prepared_size)) {
if (port->queue_primed) {
Common::DebugCounters::Add(Common::DebugCounters::Counter::AudioUnderruns);
}
port->queue_primed = false;
}
}
+1 -1
View File
@@ -18,7 +18,7 @@ LIB_DEFINE(InitAgcDriver_1) {
LIB_FUNC("2JtWUUiYBXs", Gen5::AgcGetRegisterDefaults2);
LIB_FUNC("wRbq6ZjNop4", Gen5::AgcGetRegisterDefaults2Internal);
LIB_FUNC("f3dg2CSgRKY", Gen5::AgcCreateShader);
LIB_FUNC("dolOmWH+huQ", Gen5::AgcUnknownGetFusedShaderSize);
LIB_FUNC("dolOmWH+huQ", Gen5::AgcGetFusedShaderSize);
LIB_FUNC("fd5Bp5tGTgo", Gen5::AgcFuseShaderHalves_0200);
LIB_FUNC("nApJjpKNBl4", Gen5::AgcFuseShaderHalves);
LIB_FUNC("vcmNN+AAXnY", Gen5::AgcSetCxRegIndirectPatchSetAddress);
+8 -9
View File
@@ -1341,14 +1341,6 @@ static int KYTY_SYSV_ABI KernelGetOperationMode(int* mode, int* submode) {
return OK;
}
static int KYTY_SYSV_ABI KernelFsync(int fd) {
PRINT_NAME();
LOGF("\t fd = %d\n", fd);
return OK;
}
static void KYTY_SYSV_ABI KernelSync() {
PRINT_NAME();
}
@@ -1986,6 +1978,12 @@ int KYTY_SYSV_ABI ftruncate(int d, int64_t length) {
return POSIX_CALL(LibKernel::FileSystem::KernelFtruncate(d, length));
}
int KYTY_SYSV_ABI fsync(int d) {
PRINT_NAME();
return POSIX_CALL(LibKernel::FileSystem::KernelFsync(d));
}
int KYTY_SYSV_ABI socket(int family, int type, int protocol) {
PRINT_NAME();
return Network::Net::Socket(family, type, protocol);
@@ -2174,6 +2172,7 @@ LIB_DEFINE(InitLibKernel_1_Posix) {
LIB_FUNC("E6ao34wPw+U", stat);
LIB_FUNC("JGMio+21L4c", mkdir);
LIB_FUNC("ih4CD9-gghM", Posix::ftruncate);
LIB_FUNC("juWbTNM+8hw", Posix::fsync);
LIB_FUNC("8nY19bKoiZk", Posix::fcntl);
LIB_FUNC("pDuPEf3m4fI", Posix::sem_init);
LIB_FUNC("cDW233RAwWo", Posix::sem_destroy);
@@ -3394,7 +3393,7 @@ LIB_DEFINE(InitLibKernel_1) {
LIB_FUNC("959qrazPIrg", LibKernel::KernelGetProcParam);
LIB_FUNC("tU5e3f9gSiU", LibKernel::KernelIsTrinityMode);
LIB_FUNC("NH6xARDOVv8", LibKernel::KernelGetOperationMode);
LIB_FUNC("fTx66l5iWIA", LibKernel::KernelFsync);
LIB_FUNC("fTx66l5iWIA", FileSystem::KernelFsync);
LIB_FUNC("uvT2iYBBnkY", LibKernel::KernelSync);
LIB_FUNC("HoLVWNanBBc", LibKernel::getpid);
LIB_FUNC("9BcDykPmo1I", LibKernel::get_error_addr);
+5
View File
@@ -24,10 +24,15 @@ static int KYTY_SYSV_ABI RazorCpuPushMarker(const char*, uint32_t, uint32_t) {
return 0;
}
static int KYTY_SYSV_ABI RazorCpuNamedSync(const char*) {
return 0;
}
LIB_DEFINE(InitRazorCpu_1) {
LIB_FUNC("EboejOQvLL4", LibRazorCpu::RazorCpuIsCapturing);
LIB_FUNC("YpkGsMXP3ew", LibRazorCpu::RazorCpuPopMarker);
LIB_FUNC("zw+celG7zSI", LibRazorCpu::RazorCpuPushMarker);
LIB_FUNC("XCuZoBSVFG8", LibRazorCpu::RazorCpuNamedSync);
}
} // namespace LibRazorCpu
+4
View File
@@ -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;
}
}
+6
View File
@@ -95,6 +95,7 @@ static void PrintUsage() {
" --trophy-notifications <true|false> Show trophy unlock toasts and play their sound.\n");
::printf(
" --trophy-duration <seconds> Trophy toast duration, 5-10. Default: 6.\n");
::printf(" --perf-overlay <true|false> Show the performance panel (F2 or Cmd+P).\n");
::printf(" --playgo-hack Use the supplied PlayGo stub fallback.\n");
::printf(
" --skip-notice-screen <true|false> 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()) {
+178 -1
View File
@@ -6,15 +6,18 @@
#include "common/file.h"
#include "ArchiveTestFixture.h"
#include "common/logging/log.h"
#include "common/singleton.h"
#include "common/subsystems.h"
#include "common/threads.h"
#include "common/stringUtils.h"
#include "graphics/presentation/window/windowInternal.h"
#include "kernel/eventQueue.h"
#include "kernel/fileSystem.h"
#include "kernel/pthread.h"
#include "libs/errno.h"
#include "libs/network.h"
#include "loader/additionalContent.h"
#include "loader/runtimeLinker.h"
#include "loader/symbolDatabase.h"
#include <algorithm>
@@ -85,6 +88,148 @@ void Check(bool value, const char *text) {
}
}
template <typename Predicate>
void WaitForMutexTest(Predicate ready) {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
while (!ready()) {
Check(std::chrono::steady_clock::now() < deadline, "mutex waiter made progress");
std::this_thread::yield();
}
}
struct MutexWaiter {
Libs::LibKernel::PthreadMutex *mutex;
Libs::LibKernel::KernelUseconds timeout = 0;
bool cancel = false;
Libs::LibKernel::Pthread thread = nullptr;
std::vector<uint8_t> stack = std::vector<uint8_t>(1024 * 1024);
std::atomic<unsigned> parked{0}, resume{0};
std::atomic<bool> acquired{false}, release{false}, finished{false};
int result = 0;
};
thread_local MutexWaiter *current_mutex_waiter = nullptr;
void KYTY_SYSV_ABI ParkMutexWaiter(int, void *) {
auto &waiter = *current_mutex_waiter;
const auto ticket = waiter.parked.fetch_add(1) + 1;
WaitForMutexTest([&] { return waiter.resume.load() >= ticket; });
}
void *KYTY_SYSV_ABI RunMutexWaiter(void *arg) {
using namespace Libs::LibKernel;
auto &waiter = *static_cast<MutexWaiter *>(arg);
current_mutex_waiter = &waiter;
waiter.result = waiter.timeout == 0 ? PthreadMutexLock(waiter.mutex)
: PthreadMutexTimedlock(waiter.mutex, waiter.timeout);
if (waiter.result == OK) {
waiter.acquired = true;
WaitForMutexTest([&] { return waiter.release.load(); });
Check(PthreadMutexUnlock(waiter.mutex) == OK, "waiter unlocks owned mutex");
}
waiter.finished = true;
if (waiter.cancel) {
PthreadTestcancel();
Check(false, "deferred mutex waiter cancellation remains pending");
}
return nullptr;
}
void QueueMutexWaiter(MutexWaiter &waiter) {
using namespace Libs::LibKernel;
PthreadAttr attr = nullptr;
Check(PthreadAttrInit(&attr) == OK &&
PthreadAttrSetstack(&attr, waiter.stack.data(), waiter.stack.size()) == OK &&
PthreadCreate(&waiter.thread, &attr, RunMutexWaiter, &waiter, "MutexTest") == OK &&
PthreadAttrDestroy(&attr) == OK,
"create guest mutex waiter");
// Pending guest signals are dispatched inside the contended lock, after enqueueing.
// Holding that callback proves queue order without letting the waiter acquire.
PthreadQueuePendingSignal(waiter.thread, 30);
WaitForMutexTest([&] { return waiter.parked.load() == 1; });
}
void TestMutexHandoff() {
using namespace Libs::LibKernel;
Common::Singleton<Loader::RuntimeLinker>::Instance();
Initialize();
PthreadInitSelfForMainThread();
Loader::SymbolDatabase symbols;
Libs::InitLibKernel_1(&symbols);
const auto *install_symbol = symbols.FindByNid("WkwEd3N7w0Y", Loader::SymbolType::Func);
const auto *remove_symbol = symbols.FindByNid("Qhv5ARAoOEc", Loader::SymbolType::Func);
Check(install_symbol != nullptr && remove_symbol != nullptr, "mutex signal exports resolve");
const auto install = reinterpret_cast<int(KYTY_SYSV_ABI *)(int, void *)>(install_symbol->vaddr);
const auto remove = reinterpret_cast<int(KYTY_SYSV_ABI *)(int)>(remove_symbol->vaddr);
Check(install(30, reinterpret_cast<void *>(ParkMutexWaiter)) == OK, "install mutex test signal");
PthreadMutexattr attr = nullptr;
Check(PthreadMutexattrInit(&attr) == OK && PthreadMutexattrSettype(&attr, 2) == OK,
"create recursive mutex attributes");
for (unsigned timed = 0; timed < 3; ++timed) {
PthreadMutex mutex = nullptr;
Check(PthreadMutexInit(&mutex, &attr, "MutexHandoff") == OK &&
PthreadMutexLock(&mutex) == OK && PthreadMutexLock(&mutex) == OK,
"lock recursive mutex twice");
std::array<MutexWaiter, 3> waiters{{{&mutex}, {&mutex}, {&mutex}}};
waiters[timed].timeout = 5000;
for (auto &waiter : waiters) {
QueueMutexWaiter(waiter);
}
waiters[timed].resume = 1;
Check(PthreadJoin(waiters[timed].thread, nullptr) == OK &&
waiters[timed].result == KERNEL_ERROR_ETIMEDOUT,
"timed waiter unlinks from queue head, middle and tail");
Check(PthreadMutexUnlock(&mutex) == OK && PthreadMutexTrylock(&mutex) == OK &&
PthreadMutexUnlock(&mutex) == OK,
"partial recursive unlock retains ownership");
Check(PthreadMutexUnlock(&mutex) == OK &&
PthreadMutexTrylock(&mutex) == KERNEL_ERROR_EBUSY,
"unlock reserves ownership before queued waiter runs");
for (auto &waiter : waiters) {
if (&waiter == &waiters[timed]) {
continue;
}
waiter.resume = 1;
WaitForMutexTest([&] { return waiter.acquired.load(); });
Check(PthreadMutexTrylock(&mutex) == KERNEL_ERROR_EBUSY, "FIFO waiter owns mutex");
waiter.release = true;
WaitForMutexTest([&] { return waiter.finished.load(); });
Check(PthreadJoin(waiter.thread, nullptr) == OK && waiter.result == OK,
"FIFO waiter completes");
}
Check(PthreadMutexLock(&mutex) == OK && PthreadMutexUnlock(&mutex) == OK &&
PthreadMutexDestroy(&mutex) == OK,
"mutex remains usable after timed waiter removal");
}
Check(PthreadMutexattrDestroy(&attr) == OK, "destroy recursive mutex attributes");
PthreadMutex mutex = nullptr;
Check(PthreadMutexInit(&mutex, nullptr, "MutexCancel") == OK &&
PthreadMutexLock(&mutex) == OK &&
PthreadMutexTimedlock(&mutex, 5000) == KERNEL_ERROR_EDEADLK,
"nonrecursive self timed lock reports deadlock");
MutexWaiter waiter{&mutex};
waiter.cancel = true;
QueueMutexWaiter(waiter);
Check(PthreadCancel(waiter.thread) == OK, "cancel queued mutex waiter");
PthreadQueuePendingSignal(waiter.thread, 30);
waiter.resume = 1;
WaitForMutexTest([&] { return waiter.parked.load() == 2; });
Check(PthreadMutexUnlock(&mutex) == OK &&
PthreadMutexTrylock(&mutex) == KERNEL_ERROR_EBUSY,
"canceled waiter retains its queue position");
waiter.release = true;
waiter.resume = 2;
void *result = nullptr;
Check(PthreadJoin(waiter.thread, &result) == OK &&
result == reinterpret_cast<void *>(-1) && waiter.acquired && waiter.finished,
"mutex lock defers cancellation until explicit cancellation point");
Check(PthreadMutexLock(&mutex) == OK && PthreadMutexUnlock(&mutex) == OK &&
PthreadMutexDestroy(&mutex) == OK && remove(30) == OK,
"mutex remains usable after waiter cancellation");
}
class TempDirectory {
public:
TempDirectory() {
@@ -483,6 +628,27 @@ void TestSaveOpenVisibility() {
Check(FileSystem::KernelClose(truncated) == OK, "close truncated save file");
}
void TestFsync() {
Loader::SymbolDatabase symbols;
Libs::InitLibKernel_1(&symbols);
const auto* symbol = symbols.Find(
{"juWbTNM+8hw", "Posix", 1, "libkernel", 1, 1, Loader::SymbolType::Func});
Check(symbol != nullptr, "POSIX fsync export resolves");
const auto fsync = reinterpret_cast<int (KYTY_SYSV_ABI *)(int)>(symbol->vaddr);
Check(fsync(std::numeric_limits<int>::min()) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EBADF,
"invalid fsync descriptor reports POSIX EBADF");
constexpr char Path[] = "/savedata0/fsync.dat";
const int fd = FileSystem::KernelOpen(Path, 0x602, 0777);
Check(fd >= 3 && FileSystem::KernelWrite(fd, "save", 4) == 4 && fsync(fd) == OK &&
FileSystem::KernelLseek(fd, 0, 1) == 4,
"fsync flushes a writable file without changing its offset");
Check(FileSystem::KernelClose(fd) == OK && fsync(fd) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EBADF,
"closed fsync descriptor reports POSIX EBADF");
}
void TestSaveTruncate() {
namespace Kernel = Libs::LibKernel;
Loader::SymbolDatabase symbols;
@@ -973,6 +1139,9 @@ void CheckArchiveMount(const std::filesystem::path &root) {
std::equal(bytes.begin(), bytes.begin() + 9, payload.end() - 9) &&
FileSystem::KernelRead(fd, bytes.data(), bytes.size()) == 0,
"archive reads stop at the member boundary");
Check(FileSystem::KernelFsync(fd) == OK &&
FileSystem::KernelLseek(fd, 0, 1) == payload.size(),
"fsync succeeds on an immutable archive member without changing its offset");
Check(FileSystem::KernelWrite(fd, bytes.data(), 1) ==
Libs::LibKernel::KERNEL_ERROR_EBADF &&
FileSystem::KernelPwrite(fd, bytes.data(), 1, 0) ==
@@ -2236,7 +2405,7 @@ void CheckSocketWakeup() {
} // namespace
int main(int, char**) {
int main(int argc, char** argv) {
Common::InitializeThreads();
Common::Subsystems subsystems;
subsystems.Initialize<Config::Lifecycle>();
@@ -2245,6 +2414,13 @@ int main(int, char**) {
Config::Load(options);
subsystems.Initialize<Log::Lifecycle>();
TestMutexHandoff();
if (argc == 2 && std::string_view(argv[1]) == "--mutex-only") {
subsystems.Destroy();
std::printf("KernelFileSystemTests: mutex cases passed\n");
return 0;
}
Check(SDL_InitSubSystem(SDL_INIT_VIDEO), "initialize Vulkan test video");
auto graphics = std::make_unique<Libs::Graphics::WindowContext>();
graphics->graphic_ctx.screen_width = 64;
@@ -2271,6 +2447,7 @@ int main(int, char**) {
CheckAprPaths(temporary.Path());
FileSystem::Mount(temporary.Path(), "/savedata0");
TestSaveOpenVisibility();
TestFsync();
TestSaveTruncate();
TestSaveWritev();
TestAioBatches();
+448 -17
View File
@@ -593,8 +593,9 @@ void TestExactUnfilteredImageCandidates() {
second[3] |= 1u << 16u;
fill();
plan.info.images[0].mip_mode = ImageMipMode::Dynamic;
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"mixed-dimension acceptance discarded mip-count compatibility");
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
specialization.images[0].mip_count == 1u && specialization.images[1].mip_count == 2u,
"mixed-dimension candidates lost their individual mip counts");
}
// The same image may be merged from several uses; one query keeps the guard strict.
auto merged = MakeIndirectImageFixture(false, 32u, 0u, 0u, false, ValueOpcode::ImageRead, true);
@@ -889,7 +890,7 @@ void TestNestedPointerImageTable() {
"nested pointer descriptors lost their row geometry or captured the GPU selector payload");
}
constexpr uint32_t table = 0x2000u, objects = 0x4000u, stride = 592u, count = 6u;
constexpr uint32_t table = 0x2000u, objects = 0x10000u, stride = 592u, count = 59u;
LinearTestMemory memory;
memory.words.resize((objects + count * 0x430u - memory.base) / 4u);
memory.fail_address = 0x1800u; // The GPU selector's contents are unavailable to the CPU.
@@ -898,7 +899,8 @@ void TestNestedPointerImageTable() {
const auto object = objects + row * 0x430u;
*at(table + row * stride) = object;
const std::array<uint32_t, 8> image{10u + row,
uint32_t(Libs::Graphics::Prospero::BufferFormat::k32Float) << 20u, 0u,
uint32_t(row == count - 1u ? Libs::Graphics::Prospero::BufferFormat::k32UInt
: Libs::Graphics::Prospero::BufferFormat::k32Float) << 20u, 0u,
Libs::Graphics::DstSel(4, 0, 0, 1) | (9u << 28u), 0u, 0u, 0u, 0u};
std::copy(image.begin(), image.end(), at(object + 0x3d0u));
const std::array<uint32_t, 4> sampler{row, 0x00fff000u, 0u, 0u};
@@ -917,6 +919,21 @@ void TestNestedPointerImageTable() {
Check(snapshot.images[row + 1u].dwords[0] == 10u + row &&
snapshot.samplers[row + 1u].dwords[0] == row,
"nested pointer table mixed rows or pointee members");
auto typed = make();
typed->PlanAndTrack();
ApplyResourceSpecialization(typed->program, specialization);
const auto& samplers = typed->program.info.samplers;
Check(samplers.size() == 2u * (count + 1u) && samplers[0].indirect_resources.size() == count + 1u &&
samplers[count + 1u].indirect_resources.size() == count + 1u &&
snapshot.samplers[0].dwords[1] == 0u,
"expanded sampler table lost its null root or numeric-class variants");
for (uint32_t index = 0; index <= count; ++index) {
const auto& integer = samplers[index + count + 1u];
Check(samplers[index].snapshot_index == index && !samplers[index].integer_border &&
integer.snapshot_index == index && integer.integer_border && integer.force_point_filtering &&
samplers[count + 1u].indirect_resources[index] == index + count + 1u,
"large sampler class mapping changed descriptor identity or integer filtering");
}
const auto populated = specialization;
auto equivalent = make(0u, Address::Equivalent);
equivalent->PlanAndTrack();
@@ -939,11 +956,11 @@ void TestNestedPointerImageTable() {
specialization == populated,
"empty-to-positive pointer table refresh did not restore its specialization");
*at(table) = objects + 0x430u;
*at(objects + 0x430u + 0x3d0u) = 42u;
*at(objects + 0x430u + 0x3f8u) = 7u;
*at(objects + 0x430u + 0x3d0u) = 10u + count;
*at(objects + 0x430u + 0x3f8u) = count;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.images.size() == count && snapshot.samplers.size() == count &&
snapshot.images[1].dwords[0] == 42u && snapshot.samplers[1].dwords[0] == 7u,
snapshot.images[1].dwords[0] == 10u + count && snapshot.samplers[1].dwords[0] == count,
"nested descriptor refresh retained stale pointers or failed to deduplicate new pointees");
*at(table) = objects;
@@ -3164,6 +3181,243 @@ void TestUniformizedMaterialImageKeys() {
"packed selector accepted an unproved OR guard, offset domain, or field extraction");
}
void TestIndexedMaterialImageKeys() {
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
const auto make_program = [](ShaderType stage = ShaderType::Compute, bool shared = false) {
Fixture f(stage);
auto *entry = f.block, *body = f.AddBlock();
auto *later = shared ? f.AddBlock() : nullptr;
auto *done = f.AddBlock();
entry->AddBranch(body);
entry->AddBranch(done);
body->AddBranch(shared ? later : done);
if (shared) later->AddBranch(done);
const auto lane = f.Emit(ValueOpcode::GetBuiltin,
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)});
const auto active = f.Emit(ValueOpcode::INotEqual32, {lane, Value(UINT32_MAX)});
entry->condition = f.Emit(ValueOpcode::ConditionRef, {active}, CFG::BranchCondition::ExecNonZero);
entry->terminator = {.kind = CFG::TerminatorKind::ConditionalBranch,
.true_block = body, .false_block = done};
body->terminator = {.kind = CFG::TerminatorKind::Branch, .true_block = shared ? later : done};
if (shared) later->terminator = {.kind = CFG::TerminatorKind::Branch, .true_block = done};
done->terminator.kind = CFG::TerminatorKind::Return;
const auto material = f.Buffer({f.UserData(0), f.UserData(1), f.UserData(2), f.UserData(3)});
const auto heap = f.Buffer({f.UserData(4), f.UserData(5), f.UserData(6), f.UserData(7)});
const auto output = f.Buffer({f.UserData(8), f.UserData(9), f.UserData(10), f.UserData(11)});
const auto sampler = f.Sampler({Value(0u), Value(0u), Value(1u << 26u), Value(0u)});
const auto address = f.ImageAddress();
Value owner_key, shared_image;
for (uint32_t index = 0; index < (shared ? 3u : 2u); ++index) {
const uint32_t field = index == 1u ? 68u : 64u;
auto *use = index == 2u ? later : body;
auto key = owner_key;
if (index < 2u) {
MemoryInfo memory;
memory.kind = ResourceKind::Buffer;
memory.idxen = true;
memory.offset = field;
const auto loaded = f.Emit(ValueOpcode::LoadBufferU32,
{material, lane, Value(0u), Value(0u), active}, f.AddMemory(memory, field), entry);
const auto selected = f.Emit(ValueOpcode::SelectU32, {active, loaded, Value(0xdeadbeefu)}, 0, entry);
key = shared && index == 0u
? f.Emit(ValueOpcode::ReadLane, {selected, Value(0u)}, 0, entry)
: f.Emit(ValueOpcode::ReadFirstLane, {selected, active}, 0, body);
if (shared && index == 0u) {
owner_key = key;
const auto equal = f.Emit(ValueOpcode::IEqual32, {key, selected}, 0, entry);
const auto guard = f.Emit(ValueOpcode::LogicalAnd, {active, equal}, 0, entry);
entry->condition = f.Emit(ValueOpcode::ConditionRef, {guard}, CFG::BranchCondition::ExecNonZero, entry);
}
}
const auto offset = f.Emit(ValueOpcode::ShiftLeftLogical32, {key, Value(5u)}, 0, use);
std::array<Value, 8> words;
for (uint32_t word = 0; word < words.size(); ++word) {
MemoryInfo scalar;
scalar.kind = ResourceKind::ScalarBuffer;
scalar.offset = word * 4u;
words[word] = f.Emit(ValueOpcode::ReadConstBuffer, {heap, offset},
f.AddMemory(scalar, 0x100u + field), use);
}
const auto image_handle = [&] {
return f.Emit(ValueOpcode::GetImageResource,
{words[0], words[1], words[2], words[3], words[4], words[5], words[6], words[7]}, 0, use);
};
MemoryInfo gather;
gather.kind = ResourceKind::Image;
gather.image_dimension = Decoder::ImageDimension::Dim2D;
if (index == 0u)
f.Emit(ValueOpcode::ImageQueryDimensions, {image_handle(), address}, f.AddMemory(gather, 0x180u), body);
const auto image = image_handle();
if (index == 2u) shared_image = image;
gather.image_sample_flags = Decoder::ImageSampleFlagLod;
f.Emit(ValueOpcode::ImageGatherRaw,
{image, sampler, address, words[1], words[3], Value(0u), Value(1u << 26u), active},
f.AddMemory(gather, 0x200u + field), use);
}
MemoryInfo store;
store.kind = ResourceKind::Buffer;
f.Emit(ValueOpcode::StoreBufferU32,
{output, Value(0u), Value(0u), Value(0u), Value(1u), Value(true)},
f.AddMemory(store, 0x300u), done);
f.PlanAndTrack();
if (shared) {
owner_key.ResolveInstruction()->ReplaceUsesWith(Value(12994u));
uint32_t offset = 0u;
Check(SrtWalker(f.program, {}).Evaluate(shared_image.ResolveInstruction()->Arg(0), offset) &&
offset == 12994u * 32u,
"inherited indexed selector changed GPU byte offsets into raw material keys");
}
return std::move(f.program);
};
auto program = make_program();
auto plan = ExtractResourcePlan(program);
Check(plan.info.images.size() == 3u && plan.selector_keys.size() == 2u,
"indexed material fields did not share their query/gather selector domain");
for (const auto &image : plan.info.images) {
const auto &source = *plan.descriptor_sources[image.source].indirect_descriptor;
Check(source.table_scalar && source.selector && source.selector->stride == 0u &&
source.key_count.IsEmpty() && source.selector_first.IsEmpty(),
"indexed material field fell back to an unconditional heap scan");
}
constexpr uint32_t base = 0x1000u, heap = 0x2000u, records = 14517u;
struct Memory {
std::vector<uint32_t> words = std::vector<uint32_t>((heap - base + records * 32u) / 4u);
uint32_t selectors = 0, descriptors = 0;
bool narrow = true, unreadable = false;
} memory;
const auto put = [&](uint32_t key, uint32_t mips) {
auto *words = memory.words.data() + (heap - base + key * 32u) / 4u;
words[0] = 0x100u + key;
words[1] = static_cast<uint32_t>(Libs::Graphics::Prospero::BufferFormat::k16UNorm) << 20u;
words[3] = (9u << 28u) | ((mips - 1u) << 16u) | Libs::Graphics::DstSel(4, 5, 6, 7);
};
for (uint32_t key = 0; key < 64u; ++key) put(key, 1u);
put(12994u, 8u);
put(12996u, 8u);
for (uint32_t row = 0; row < 10u; ++row) {
memory.words[(row * 96u + 64u) / 4u] = 12994u;
memory.words[(row * 96u + 68u) / 4u] = 12996u;
}
std::array<uint32_t, 12> user_data{base, 96u << 16u, 10u, 0x5204u,
heap, 32u << 16u, records, 0x5204u, 0x90000u, 4u << 16u, 16u, 0x5204u};
const auto read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &m = *static_cast<Memory *>(data);
if (address < base || address - base + words.size_bytes() > m.words.size() * 4u) return false;
if (address < heap) {
++m.selectors;
if (m.unreadable) return false;
} else {
++m.descriptors;
const auto key = (address - heap) / 32u;
if (m.narrow && (words.size() != 8u || (key != 0u && key != 12994u && key != 12996u))) return false;
}
std::copy_n(m.words.begin() + (address - base) / 4u, words.size(), words.begin());
return true;
};
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = read};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
memory.selectors == 2u && memory.descriptors == 6u && snapshot.images.size() == 9u,
"indexed selector scanned the broad heap or did not share its captured keys");
ApplyResourceSpecialization(program, specialization);
Check(program.info.images[0].indirect_resources.size() == 3u,
"indirect dimension query lost its specialized candidates");
for (uint32_t root = 0; root < plan.info.images.size(); ++root) {
const auto &image = specialization.images[root];
const auto offset = image.indirect_mapping_offset;
Check(snapshot.images[root].dwords[0] == 0u && image.indirect_search_iterations == 2u &&
snapshot.flattened_srt[offset] == 2u && snapshot.flattened_srt[offset + 1u] == 0u &&
snapshot.flattened_srt[offset + 3u] == (root == 2u ? 12996u : 12994u) * 32u,
"byte-key selector lost native OOB zero, sorted offsets, or the null default");
}
Check(std::ranges::count_if(specialization.images, [](const auto &image) {
return image.mip_count == 8u;
}) == 2u, "indirect gathers lost candidate-specific mip counts");
for (const uint32_t field : {64u, 68u})
Check(std::ranges::find(snapshot.specialization_reads,
std::pair<uint64_t, uint64_t>{base + field, 868u}) != snapshot.specialization_reads.end(),
"indexed selector batch escaped writable-alias tracking");
auto shared_program = make_program(ShaderType::Compute, true);
auto shared = ExtractResourcePlan(shared_program);
Check(MaterializeResources(shared, runtime, snapshot, specialization) && snapshot.images.size() == 9u,
"inherited indexed selector lost its bounded material domain");
memory.words[64u / 4u] = 12996u;
memory.selectors = 0;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
memory.selectors == 2u && snapshot.images.size() == 11u,
"changed material rows reused stale selector keys");
memory.words[64u / 4u] = 12994u;
memory.unreadable = true;
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"unreadable eligible selector silently fell back to the heap");
memory.narrow = false;
const auto original = user_data;
for (const uint32_t variant : {0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u}) {
user_data = original;
if (variant == 0u) user_data[1] |= 1u << 31u;
if (variant == 1u) user_data[3] |= 1u << 23u;
if (variant == 2u) user_data[3] |= 1u << 28u;
if (variant == 3u) user_data[0] += 1u;
if (variant == 4u) user_data[2] = MaxIndirectDescriptorProbes + 1u;
if (variant == 5u) user_data[8] = base + 68u;
if (variant == 6u) user_data[1] = 66u << 16u;
if (variant == 7u) user_data[0] = 0u;
memory.selectors = 0;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
memory.selectors == 0u && snapshot.images.size() > 100u,
"unsupported geometry or writable selector lost the original heap domain");
}
user_data = original;
auto graphics_program = make_program(ShaderType::Pixel);
auto graphics = ExtractResourcePlan(graphics_program);
Check(MaterializeResources(graphics, runtime, snapshot, specialization) && memory.selectors == 0u,
"graphics material lookup acquired unproved selector read dependencies");
memory.unreadable = false;
for (uint32_t row = 0; row < 10u; ++row)
memory.words[(row * 96u + 64u) / 4u] = 0x08000000u; // U32 key*32 wraps to zero.
Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.images.size() == 7u,
"indexed selector lost wrapped scalar descriptor offsets");
for (auto &source : plan.descriptor_sources) {
if (!source.indirect_descriptor) continue;
source.indirect_descriptor->table_offset = 0xffffffe0u;
source.indirect_descriptor->table_immediate = 32u;
}
for (uint32_t row = 0; row < 10u; ++row) {
memory.words[(row * 96u + 64u) / 4u] = row == 0u ? 1u : 0u;
memory.words[(row * 96u + 68u) / 4u] = 0u;
}
memory.selectors = memory.descriptors = 0u;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
memory.selectors == 2u && memory.descriptors == 2u && snapshot.images.size() == 5u,
"overflowed scalar immediate read a descriptor or lost valid wrapped keys");
for (uint32_t root = 0; root < 2u; ++root) {
const auto offset = specialization.images[root].indirect_mapping_offset;
Check(snapshot.images[root].dwords[0] == 0u && snapshot.flattened_srt[offset] == 2u &&
snapshot.flattened_srt[offset + 1u] == 32u && snapshot.flattened_srt[offset + 3u] == UINT32_MAX &&
snapshot.flattened_srt[offset + 4u] == 0u,
"invalid byte key selected a real texture instead of the null root");
}
for (auto &source : plan.descriptor_sources) {
if (!source.indirect_descriptor) continue;
source.indirect_descriptor->table_offset = source.indirect_descriptor->table_immediate = 0u;
}
memory.narrow = true;
memory.unreadable = true;
for (const uint32_t variant : {0u, 1u, 2u, 3u}) {
user_data = original;
if (variant == 0u) user_data[2] = 0u;
if (variant == 1u) user_data[1] = 0u;
if (variant == 2u) user_data[3] = 0u;
if (variant == 3u) user_data[1] = 64u << 16u;
memory.selectors = 0;
Check(MaterializeResources(plan, runtime, snapshot, specialization) && memory.selectors == 0u &&
snapshot.images.size() == 6u,
"native zero/OOB indexed load read its unavailable payload");
}
}
void TestImageDescriptorFields() {
constexpr std::array<std::pair<uint32_t, uint32_t>, 5> reserved{
{{1u, 0x20000000u}, {2u, 0x70003000u}, {4u, 0xe000e000u},
@@ -4929,6 +5183,38 @@ void TestImagesSamplersAndAliases() {
"buffer/image descriptor alias was not linked");
}
void TestIndependentImageSamplerPairs() {
Fixture fixture(ShaderType::Pixel);
const auto address = fixture.ImageAddress();
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
for (uint32_t use = 0; use < 144; ++use) {
const auto pair = use % 72;
const auto pc = use * 4;
const auto image = fixture.Image(
{Value(pair / 8), Value(0u), Value(0u), Value(0u), Value(0u),
Value(0u), Value(0u), Value(0u)}, pc);
const auto sampler = fixture.Sampler(
{Value(pair % 8), Value(0u), Value(0u), Value(0u)}, pc);
fixture.Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, address, Value(true)},
fixture.AddMemory(memory, pc));
}
fixture.PlanAndTrack();
Check(fixture.program.info.images.size() == 9 &&
fixture.program.info.samplers.size() == 8 &&
fixture.program.info.sampled_pairs.size() == 72,
"distinct image/sampler combinations were limited or duplicated");
for (uint32_t index = 0; index < 72; ++index) {
const auto &pair = fixture.program.info.sampled_pairs[index];
Check(pair.image == index / 8 && pair.sampler == index % 8 &&
pair.first_use_pc == index * 4,
"sampled pair lost its independent resources or first use");
}
}
void TestSampleAdjustSamplerScratch() {
Fixture fixture(ShaderType::Pixel);
const auto active = fixture.Emit(
@@ -5145,19 +5431,24 @@ void TestGatherLodSamplerValidation() {
Libs::Graphics::Prospero::BufferFormat::k32Float) << 20u;
user_data[3] = Libs::Graphics::DstSel(4, 5, 6, 7) |
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
user_data[9] = 0xfff000u;
constexpr std::array sampler_controls{
0u, 0x4001u, 1u << 26u, (1u << 26u) | 256u,
(1u << 26u) | (1u << 14u), 2u << 26u, 3u << 26u};
(1u << 26u) | (1u << 14u), 2u << 26u, (2u << 26u) | 256u,
(2u << 26u) | (1u << 14u), 0x0a500000u, 0x1a500000u, 3u << 26u};
for (const auto control : sampler_controls) {
user_data[10] = control;
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const bool supported = !explicit_lod || (control >> 26u) == 0 ||
control == (1u << 26u);
Check(MaterializeResources(plan, {.user_data = user_data}, snapshot,
specialization) == supported,
"explicit gather accepted an unsupported sampler or rejected a valid one");
for (const uint32_t perf_mip : {0u, 1u}) {
user_data[9] = 0xfff000u | (perf_mip << 24u);
user_data[10] = control;
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const auto filter = (control >> 26u) & 3u;
const bool supported = !explicit_lod || filter == 0 ||
(filter <= 2 && (control & 0xfffffu) == 0 &&
(filter != 2 || perf_mip == 0));
Check(MaterializeResources(plan, {.user_data = user_data}, snapshot,
specialization) == supported,
"explicit gather accepted an unsupported sampler or rejected a valid one");
}
}
}
}
@@ -5376,6 +5667,65 @@ void TestSrtFlatteningAndRuntimeMemoization() {
"flattened typed SRT reads were not collected");
}
void TestScalarDataReadControlFlow() {
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
for (const auto comparison : {ValueOpcode::Void, ValueOpcode::IEqual64,
ValueOpcode::INotEqual64}) {
Fixture fixture;
const auto low = fixture.UserData(0);
const auto high = fixture.UserData(1);
const auto address = fixture.Address(low, high);
auto* use = fixture.block;
if (comparison != ValueOpcode::Void) {
const auto pointer = fixture.Emit(ValueOpcode::CompositeConstructU64, {low, high});
auto enabled = fixture.Emit(comparison, {pointer, Value(uint64_t{0})});
if (comparison == ValueOpcode::IEqual64)
enabled = fixture.Emit(ValueOpcode::LogicalNot, {enabled});
const auto lane = fixture.Emit(ValueOpcode::GetBuiltin,
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)});
const auto varying = fixture.Emit(ValueOpcode::INotEqual32, {lane, Value(0u)});
fixture.block->condition = fixture.Emit(ValueOpcode::LogicalAnd, {varying, enabled});
use = fixture.AddBlock();
auto* done = fixture.AddBlock();
fixture.block->AddBranch(use);
fixture.block->AddBranch(done);
use->AddBranch(done);
fixture.block->terminator = {.kind = CFG::TerminatorKind::ConditionalBranch,
.true_block = use, .false_block = done};
use->terminator = {.kind = CFG::TerminatorKind::Branch, .true_block = done};
done->terminator.kind = CFG::TerminatorKind::Return;
}
MemoryInfo scalar;
scalar.kind = ResourceKind::ScalarAddress;
fixture.Emit(ValueOpcode::LoadAddressU32,
{address, Value(0u), Value(0u), Value(true)}, fixture.AddMemory(scalar, 4), use);
fixture.PlanAndTrack();
auto plan = ExtractResourcePlan(fixture.program);
Check(plan.srt_reads.size() == 1 && !fixture.program.info.uses_dma &&
bool(plan.srt_code) == (comparison == ValueOpcode::Void),
"scalar data lost flattening or conditional reads entered the unconditional JIT");
uint32_t reads = 0;
std::array<uint32_t, 2> user_data {};
SrtRuntime runtime {.user_data = user_data,
.read_memory = +[](void* data, uint64_t address, std::span<uint32_t> words) {
++*static_cast<uint32_t*>(data);
if (address == 0) return false;
words[0] = 42;
return true;
}, .userdata = &reads};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const uint64_t pointer : {uint64_t{0}, uint64_t{0x1000}, uint64_t{1} << 32u}) {
if (pointer == 0 && comparison == ValueOpcode::Void) continue;
user_data = {static_cast<uint32_t>(pointer), static_cast<uint32_t>(pointer >> 32u)};
reads = 0;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
reads == (pointer != 0) && snapshot.flattened_srt[0] == (pointer ? 42u : 0u),
"scalar data refresh ignored a mixed EXEC/64-bit null-pointer branch");
}
}
}
void TestDynamicSrtReadRemainsExplicit() {
Fixture fixture;
const auto base =
@@ -7030,6 +7380,83 @@ void TestFiniteImagePhiCycle() {
}
}
void TestPackedDescriptorWaterfall() {
for (const uint32_t variant : {0u, 1u, 2u}) {
Fixture f(ShaderType::Pixel);
const auto packed = f.Emit(ValueOpcode::GetInterpolationParameter,
{Value(1u), Value(1u), Value(2u)});
const auto active = f.Emit(ValueOpcode::ULessThan32,
{f.Emit(ValueOpcode::LaneId), Value(16u)});
const auto image_key = f.Emit(ValueOpcode::BitwiseAnd32, {packed, Value(65535u)});
const auto sampler_key = variant == 2u ? packed :
f.Emit(ValueOpcode::ShiftRightLogical32, {packed, Value(16u)});
auto selected_active = active;
const auto descriptor = [&](Value key, uint32_t first, uint32_t count, uint32_t shift) {
const auto selected = f.Emit(ValueOpcode::ReadFirstLane, {key, active});
selected_active = f.Emit(ValueOpcode::LogicalAnd, {selected_active,
f.Emit(ValueOpcode::IEqual32, {selected, key})});
const auto offset = f.Emit(ValueOpcode::ShiftLeftLogical32, {selected, Value(shift)});
const auto table = f.Buffer({f.UserData(first), f.UserData(first + 1u),
f.UserData(first + 2u), f.UserData(first + 3u)});
std::array<Value, 8> words;
for (uint32_t word = 0; word < count; ++word)
words[word] = f.Emit(ValueOpcode::ReadConstBuffer, {table, offset},
f.AddMemory({.kind = ResourceKind::ScalarBuffer, .offset = word * 4u}, 0x58u));
return words;
};
const auto image = f.Image(descriptor(image_key, 0u, 8u, 5u));
const auto sampler_words = descriptor(sampler_key, 4u, 4u, 4u);
const auto sampler = f.Sampler({sampler_words[0], sampler_words[1],
sampler_words[2], sampler_words[3]});
f.Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, f.ImageAddress(), variant == 1u ? Value(true) : selected_active},
f.AddMemory({.kind = ResourceKind::Image,
.image_dimension = Decoder::ImageDimension::Dim2D}, 0x6cu));
if (variant != 0u) {
CheckFatal([&] { f.PlanAndTrack(); }, "not a valid runtime value",
"packed waterfall accepted widened EXEC or an unbounded sampler key");
continue;
}
f.PlanAndTrack();
const auto plan = ExtractResourcePlan(f.program);
for (const auto source : {plan.info.images[0].source, plan.info.samplers[0].source}) {
const auto& value = plan.descriptor_sources[source];
const auto& table = value.indirect_descriptor;
Check(table && !table->selector && table->table_scalar &&
table->table_stride == value.dword_count * 4u &&
(value.dword_count != 4u || table->table_maximum == 65535u),
"packed waterfall lost its static sampler bit bound or captured the varying key");
}
LinearTestMemory memory;
const std::array<uint32_t, 8> user_data{
uint32_t(memory.base) + 256u, 32u << 16u, 2u, 0x5204u,
uint32_t(memory.base) + 512u, 16u << 16u, 59u, 0x5204u};
for (uint32_t row = 0; row < 2u; ++row) {
const auto word = (256u + row * 32u) / 4u;
memory.words[word] = 0x1000u + row * 0x1000u;
memory.words[word + 1u] = uint32_t(Libs::Graphics::Prospero::BufferFormat::k32Float) << 20u;
memory.words[word + 3u] = Libs::Graphics::DstSel(4, 0, 0, 1) | (9u << 28u);
}
for (uint32_t row = 0; row < 59u; ++row) {
const auto word = (512u + row * 16u) / 4u;
memory.words[word] = row;
memory.words[word + 1u] = 0x00fff000u;
memory.words[word + 2u] = (row & 1u) ? 0x0a500000u : 0u;
}
const SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.images.size() == 3u && snapshot.samplers.size() == 60u,
"packed waterfall ignored the two-image/59-sampler heap bounds or lost the null roots");
Check(std::ranges::all_of(snapshot.specialization_reads, [&](const auto& span) {
return (span.first >= memory.base + 256u && span.first + span.second <= memory.base + 320u) ||
(span.first >= memory.base + 512u && span.first + span.second <= memory.base + 512u + 59u * 16u);
}), "packed waterfall materialization read outside the descriptor heaps");
}
}
void TestFiniteImageWaterfall() {
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
enum class Variant { Valid, Staggered, Cycle, DifferentMask, MixedWord, DifferentSelect, WiderExec, BadLeaf };
@@ -8255,6 +8682,7 @@ int main() {
Run("scalar/vector alias", TestScalarAndVectorBufferAlias);
Run("runtime unsigned min", TestRuntimeUnsignedMinDescriptor);
Run("images and samplers", TestImagesSamplersAndAliases);
Run("independent image/sampler pairs", TestIndependentImageSamplerPairs);
Run("SampleAdjust sampler scratch", TestSampleAdjustSamplerScratch);
Run("FMASK load specialization", TestFmaskLoadSpecialization);
Run("gather LOD sampler validation", TestGatherLodSamplerValidation);
@@ -8269,6 +8697,7 @@ int main() {
Run("guarded direct image table", TestGuardedDirectImageTable);
Run("bounded compute image loop", TestBoundedComputeImageLoop);
Run("uniformized material image keys", TestUniformizedMaterialImageKeys);
Run("indexed material image keys", TestIndexedMaterialImageKeys);
Run("image descriptor fields", TestImageDescriptorFields);
Run("draw-uniform scalar image", TestUniformScalarBufferImage);
Run("compact scalar image table", TestCompactScalarImageTable);
@@ -8278,6 +8707,7 @@ int main() {
Run("shared append selector bounds", TestSharedAppendSelectorBounds);
Run("descending image range", TestDescendingImageRange);
Run("SRT runtime", TestSrtFlatteningAndRuntimeMemoization);
Run("scalar data control flow", TestScalarDataReadControlFlow);
Run("dynamic SRT", TestDynamicSrtReadRemainsExplicit);
Run("writable descriptor phi", TestWritableDescriptorPhi);
Run("bounded formatted buffer store", TestBoundedFormattedBufferStore);
@@ -8292,6 +8722,7 @@ int main() {
Run("packed scalar buffer selection", TestPackedScalarBufferSelection);
Run("conditional sampler phi", TestConditionalSamplerPhi);
Run("finite image phi cycle", TestFiniteImagePhiCycle);
Run("packed descriptor waterfall", TestPackedDescriptorWaterfall);
Run("finite image waterfall", TestFiniteImageWaterfall);
Run("finite inline sampler", TestFiniteInlineSampler);
Run("finite image bit scan sentinel", TestFiniteImageBitScanSentinel);
File diff suppressed because it is too large Load Diff
+169 -7
View File
@@ -45,6 +45,7 @@
#include <span>
#include <sstream>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <vector>
@@ -1967,6 +1968,59 @@ void TestNggVertexEntryState() {
}
}
void TestNativeDataCacheInvalidation() {
using namespace ShaderRecompiler;
// Captured f5edf70dbb1f6057:0x194 and 0x1a4, each followed by S_WAITCNT.
const std::array cache_ops = {0xe1c40000u, 0u, 0xbf8c0000u,
0xe1c80000u, 0u, 0xbf8c0000u, EncodeSopp(0x01)};
Decoder::Program decoded;
Decoder::DecodeProgram(cache_ops, decoded);
for (const uint32_t index : {0u, 2u}) {
const auto &inst = decoded.instructions[index];
Check(inst.opcode == (index == 0 ? Decoder::Opcode::BUFFER_GL0_INV
: Decoder::Opcode::BUFFER_GL1_INV) &&
inst.word_count == 2 && inst.src_count == 0 && inst.data_dwords == 0 &&
inst.dst.kind == Decoder::OperandKind::Unknown,
"RDNA2 cache invalidation acquired a buffer operand or lost its encoding");
}
auto options = MakeCompileOptions(ShaderType::Compute);
options.dump_ir = true;
ShaderComputeInputInfo compute{};
compute.threads_num[1] = compute.threads_num[2] = 1;
options.input_info.compute = &compute;
for (const uint32_t wave_size : {32u, 64u}) {
compute.threads_num[0] = compute.wave_size = options.wave_size = wave_size;
const auto minimal = RecompileForTest(cache_ops, options);
CheckSpirvBinaryValidates(minimal.spirv);
Check(minimal.program.info.buffers.empty(),
"cache invalidation introduced a phantom buffer binding");
const auto source = DisassembleSpirvBinary(minimal.spirv);
const auto first = source.find("OpMemoryBarrier %uint_1 %uint_2114");
const auto second = source.find("OpMemoryBarrier %uint_1 %uint_2114", first + 1);
Check(first != std::string::npos && second != std::string::npos &&
source.find("OpMemoryBarrier", second + 1) == std::string::npos &&
source.find("OpControlBarrier") == std::string::npos,
"cache invalidation must fence device memory once per wave without convergence");
const std::array reload = {
EncodeMubuf0(0x1c, 0, false), EncodeMubuf1(0, 0, 0),
0xe1c40000u, 0u, 0xbf8c0000u,
EncodeMubuf0(0x0c, 0, false), EncodeMubuf1(1, 0, 0),
EncodeMubuf0(0x1c, 4, false), EncodeMubuf1(1, 0, 0), EncodeSopp(0x01),
};
const auto result = RecompileForTest(reload, options);
CheckSpirvBinaryValidates(result.spirv);
const auto store = result.ir_dump.find("StoreBufferU32");
const auto fence = result.ir_dump.find("DataCacheInvalidate");
const auto load = result.ir_dump.find("LoadBufferU32");
Check(store < fence && fence < load && load != std::string::npos &&
result.program.info.coherent_buffers &&
DisassembleSpirvBinary(result.spirv).find(" Coherent") != std::string::npos,
"plain buffer reload lost its cache fence order or coherent visibility");
}
}
void TestNativeStoreCompletion() {
using namespace ShaderRecompiler;
const std::array register_wait = {EncodeSopk(0x17, 4, static_cast<int16_t>(0x9234)), EncodeSopp(0x01)};
@@ -10751,9 +10805,14 @@ void TestMemoryFedScalarLeafCall() {
};
IR::SrtRuntime runtime{.read_memory = read, .userdata = &memory, .read_specialization_memory = read};
const auto options = MakeCompileOptions(ShaderType::Compute);
const uint32_t gpu_callable[] = {EncodeVop1(0x02, 4, 256),
EncodeSmem0(0x09, 14, 0), 4u << 25u, EncodeSop1(0x21, 14, 14), EncodeSopp(0x01)};
const auto skipped = PrepareShaderSource(gpu_callable, options);
Check(skipped.unsupported_call_pc == 12u && skipped.calls.empty(),
"GPU-buffer callable did not report its unsupported call site");
auto source = PrepareShaderSource(shader, options);
Check(source.call && source.decoded.has_swap_pc &&
source.decoded.instructions[source.call->instruction].branch_target == UINT32_MAX,
Check(source.calls.size() == 1 && source.decoded.has_swap_pc &&
source.decoded.instructions[source.calls.front().instruction].branch_target == UINT32_MAX,
"memory-fed SWAPPC did not retain an unresolved target");
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
ExpectFatal([&] { (void)TranslateProgram(source.decoded, options); },
@@ -10761,7 +10820,7 @@ void TestMemoryFedScalarLeafCall() {
#endif
const auto& linked = RefreshShaderSource(source, runtime);
Check(linked.has_swap_pc && source.revision == 1 && linked.instructions.back().branch_target == 0x40 &&
linked.instructions[source.call->instruction].branch_target == 0x44 &&
linked.instructions[source.calls.front().instruction].branch_target == 0x44 &&
source.reads.back() == std::pair<uint64_t, uint64_t>{base + 0x100, 12},
"aliased call target, exact leaf range, or continuation was lost");
const auto* storage = linked.instructions.data();
@@ -10846,14 +10905,14 @@ void TestMemoryFedScalarLeafCall() {
memory.words[96] = EncodeSMovB32(14, 129);
ExpectFatal([&] { (void)RefreshShaderSource(source, runtime); },
"leaf clobbering its return pair was linked");
// AND_B64 writes the high half despite the decoder's generic scalar width.
// AND_B64 must replace the preceding high-half definition.
const uint32_t high_writer[] = {
EncodeSMovB32(15, 129), EncodeSop2(0x0f, 14, 128, 128),
EncodeSop1(0x21, 14, 14), EncodeSopp(0x01)};
auto pair_source = PrepareShaderSource(high_writer, options);
IR::SrtWalker pair_walker(pair_source.call_targets, runtime);
uint32_t high = 1;
Check(pair_walker.Evaluate(pair_source.call->target[1], high) && high == 0,
Check(pair_walker.Evaluate(pair_source.calls.front().target[1], high) && high == 0,
"scalar pair producer used a stale preceding high-half definition");
const uint32_t joined[] = {
EncodeSopp(0x04, 2), EncodeSMovB32(14, 129), EncodeSopp(0x02, 1),
@@ -10862,7 +10921,7 @@ void TestMemoryFedScalarLeafCall() {
"ambiguous reaching definitions were accepted for a call target");
const uint32_t multiple[] = {EncodeSop1(0x21, 14, 14), EncodeSop1(0x21, 14, 14), EncodeSopp(0x01)};
ExpectFatal([&] { (void)PrepareShaderSource(multiple, options); },
"call metadata bypassed the multiple-call rejection");
"a native return pair was accepted as a later call target");
#endif
}
@@ -10909,7 +10968,7 @@ void TestFusedShaderHandoffPreservesRegisters() {
for (const auto handoff: {EncodeSop1(0x20, 0, 6), 0xbefd2106u}) {
front[2] = handoff; // SETPC or captured SWAPPC with NULL destination
auto source = PrepareShaderSource(front, options);
Check(!source.decoded.has_swap_pc && !source.call,
Check(!source.decoded.has_swap_pc && source.calls.empty(),
"fused SETPC handoff entered scalar call preparation");
auto translated = TranslateProgram(source.decoded, options);
uint32_t allocations = 0;
@@ -12911,6 +12970,54 @@ void TestLateConstantPropagation() {
"an ungrounded Phi cycle acquired a constant");
}
void TestRepeatedBitcastPropagation() {
using namespace ShaderRecompiler::IR;
Block entry, left, right;
IREmitter e(&entry), l(&left), r(&right);
const auto raw = e.GetUserData(static_cast<ScalarReg>(0));
const auto other = e.GetUserData(static_cast<ScalarReg>(1));
const auto fp = e.Emit(ValueOpcode::ConvertF32U32, {raw});
for (const auto [opcode, source, distinct] : {
std::tuple{ValueOpcode::BitCastF32U32, Value(raw), Value(other)},
std::tuple{ValueOpcode::BitCastU32F32, fp,
e.Emit(ValueOpcode::ConvertF32U32, {other})}}) {
const auto first = e.Emit(opcode, {source});
const auto duplicate = e.Emit(opcode, {source});
const auto separate = e.Emit(opcode, {distinct});
const auto left_cast = l.Emit(opcode, {source});
const auto left_duplicate = l.Emit(opcode, {source});
const auto right_cast = r.Emit(opcode, {source});
const auto use = opcode == ValueOpcode::BitCastF32U32
? e.Emit(ValueOpcode::FPIsNan32, {duplicate})
: e.Emit(ValueOpcode::IEqual32, {duplicate, Value(0u)});
ConstantPropagationPass({&entry, &left, &right});
Check(duplicate.Resolve() == first && left_duplicate.Resolve() == left_cast &&
use.Instruction()->Arg(0).Resolve() == first,
"repeated same-block casts were not shared with their earlier definition");
Check(separate.Resolve() == separate && left_cast.Resolve() == left_cast &&
right_cast.Resolve() == right_cast && left_cast != first && right_cast != left_cast,
"cast reuse merged distinct values or crossed block boundaries");
}
const auto as_float = e.Emit(ValueOpcode::BitCastF32U32, {raw});
const auto raw_roundtrip = e.Emit(ValueOpcode::BitCastU32F32, {as_float});
const auto as_bits = e.Emit(ValueOpcode::BitCastU32F32, {fp});
const auto float_roundtrip = e.Emit(ValueOpcode::BitCastF32U32, {as_bits});
ConstantPropagationPass({&entry});
Check(raw_roundtrip.Resolve() == raw && float_roundtrip.Resolve() == fp,
"sharing casts prevented inverse casts from folding");
for (const uint32_t bits : {0u, 0x80000000u, 1u, 0x80000001u, 0x7f800000u,
0xff800000u, 0x7fc12345u, 0x7f812345u}) {
const auto constant_float = e.Emit(ValueOpcode::BitCastF32U32, {Value(bits)});
const auto constant_bits = e.Emit(ValueOpcode::BitCastU32F32,
{Value::F32(std::bit_cast<float>(bits))});
ConstantPropagationPass({&entry});
Check(constant_float.Resolve().IsImmediate() &&
std::bit_cast<uint32_t>(constant_float.Resolve().F32Value()) == bits &&
constant_bits.Resolve() == Value(bits),
"constant bitcast folding changed signed zero, subnormal, infinity or NaN bits");
}
}
void TestMaskedShiftConstantPropagation() {
using namespace ShaderRecompiler::IR;
for (const uint32_t count : {0u, 1u, 8u, 31u, 32u, 33u, 40u, 63u, 64u, UINT32_MAX}) {
@@ -15300,6 +15407,58 @@ void TestScalarMemoryLoadCrossesIntoVcc() {
}
}
void TestEmptyBranchScalarReadDce() {
using namespace ShaderRecompiler::IR;
using ShaderRecompiler::CFG::TerminatorKind;
for (const bool reverse : {false, true}) {
for (uint32_t mode = 0; mode < 4; ++mode) {
Block entry, detour, merge;
const BlockList blocks{&entry, &detour, &merge};
entry.AddBranch(&detour);
entry.AddBranch(&merge);
detour.AddBranch(&merge);
entry.terminator.kind = TerminatorKind::ConditionalBranch;
entry.terminator.true_block = reverse ? &merge : &detour;
entry.terminator.false_block = reverse ? &detour : &merge;
entry.terminator.merge_block = &merge;
detour.terminator.kind = TerminatorKind::Branch;
detour.terminator.true_block = &merge;
IREmitter ir(&entry);
const auto address = ir.Emit(ValueOpcode::GetAddressResource,
{Value(0x1000u), Value(0u)});
const auto read = ir.Emit(ValueOpcode::LoadAddressU32,
{address, Value(24u), Value(0u), Value(true)});
const auto predicate = ir.INotEqual(U32(read), U32(Value(0u)));
entry.condition = ir.Emit(ValueOpcode::ConditionRef, {predicate});
ir.Emit(ValueOpcode::Reference, {entry.condition});
ir.Emit(ValueOpcode::Barrier);
detour.AppendNewInst(ValueOpcode::ControlNop, {});
if (mode == 1) detour.AppendNewInst(ValueOpcode::Barrier, {});
if (mode == 2) {
auto& phi = merge.AppendNewInst(ValueOpcode::Phi, {});
phi.SetFlags(Type::U32);
phi.AddPhiOperand(&entry, Value(1u));
phi.AddPhiOperand(&detour, Value(2u));
merge.AppendNewInst(ValueOpcode::ReferenceU32, {Value(&phi)});
}
if (mode == 3) merge.AppendNewInst(ValueOpcode::ReferenceU32, {read});
EliminateDeadCode(blocks);
Check(entry.condition.IsImmediate() == (mode == 0 || mode == 3),
"empty branch cleanup changed side effects or a live Phi");
const auto contains = [&](ValueOpcode op) {
return std::ranges::any_of(entry, [=](const Inst& inst) {
return inst.GetOpcode() == op;
});
};
Check(contains(ValueOpcode::LoadAddressU32) == (mode != 0),
"empty branch cleanup lost a shared read or retained a dead read");
Check(contains(ValueOpcode::Barrier) && entry.ImmSuccessors().size() == 2 &&
merge.ImmPredecessors().size() == 2,
"empty branch cleanup changed earlier side effects or CFG edges");
}
}
}
void TestScalarMemoryUnusedTailDce() {
const uint32_t shader[] = {
EncodeSmem0(0x0c, 16, 0),
@@ -16523,6 +16682,7 @@ int main() {
TestNewShaderDecoderArchitecture();
TestImageAddressOperands();
TestSopkCompareImmediateExtension();
TestNativeDataCacheInvalidation();
TestNativeStoreCompletion();
TestNewShaderRecompilerCapturedVopcSdwaCmpxClass();
TestVopcCmpxClassF16Decoder();
@@ -16616,6 +16776,7 @@ int main() {
TestValuePhiValidation();
TestLaneMaskProjection();
TestLateConstantPropagation();
TestRepeatedBitcastPropagation();
TestMaskedShiftConstantPropagation();
TestU64ShiftConstantPropagation();
TestU64RegisterPairRoundTrip();
@@ -16647,6 +16808,7 @@ int main() {
TestSrtWalkerRealSBufferTranslation();
TestScalarMemorySourcesCapturedBeforeWrites();
TestScalarMemoryLoadCrossesIntoVcc();
TestEmptyBranchScalarReadDce();
TestScalarMemoryUnusedTailDce();
TestResourceTrackingRealDensePatching();
TestDirectTranslationResetsAnalysisState();