Compare commits

...

34 Commits

Author SHA1 Message Date
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
Felipe 415387f00d Support multiple controllers for local multiplayer (#1583)
* Support multiple controllers for local co-op
2026-10-17 13:15:22 +02:00
Matthew Hurley 890393fbe6 vulkan: start without fragmentShaderBarycentric on GPUs that lack it (#1590)
* vulkan: start without fragmentShaderBarycentric on GPUs that lack it

Pascal (GTX 10xx), RDNA 1 and older drivers have no
VK_KHR_fragment_shader_barycentric, so device selection rejected them
with 'Could not find suitable device' (#1432, #1416, #762). The SPIR-V
backend only emits the capability for pixel shaders with
per-vertex/barycentric inputs, so make the extension and feature
optional: warn at selection and creation, enable only when supported.
Shaders that need custom barycentrics fail at pipeline creation
instead of blocking startup for every game.

* vulkan: fix macOS build and warn about unsupported GPUs

---------

Co-authored-by: Matthew Hurley <171001439+theantipopau@users.noreply.github.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-17 12:55:33 +02:00
nmzik 9f88043ebe shader: use compact integer RTZ half conversion 2026-10-17 12:23:12 +02:00
nmzik c2173801d4 shader: skip resources in proven empty unsigned loops 2026-10-17 10:38:14 +02:00
nmzik 56d03f16e9 network: UDPP2P 2026-10-17 10:24:45 +02:00
nmzik 3ecc6b7d1b network: implement POSIX socket shutdown on Linux 2026-10-17 10:10:16 +02:00
nmzik 5bf37f52ad renderer: clear compressed stencil with pending depth metadata 2026-10-17 09:50:11 +02:00
nmzik 57791645e4 renderer: track attachment load clears by image aspect 2026-10-17 09:34:51 +02:00
nmzik d1fe1324c8 shader: keep coherent 64bit atomic 2026-10-17 07:52:20 +02:00
nmzik b485dd24be system service: stub 2026-10-17 07:30:21 +02:00
nmzik ae25558321 shader: size NGG primitive outputs from amplification 2026-10-17 06:54:45 +02:00
nmzik ec08ea197b shader: new fill pattern 2026-10-17 06:24:29 +02:00
nmzik f6f1ac8b40 shader: batch dependent folds to avoid repeated SSA graph walks 2026-10-17 06:20:02 +02:00
Nawid3333 b82512be06 shader: read float inline constants on 64-bit operands as f64 bits (#1576)
A float inline constant (240-248) read by a 64-bit operand is the f64 bit
pattern for integer B64 opcodes too: AMD's compilers emit
v_lshrrev_b64 v[4:5], v0, 1.0 for 0x3ff0000000000000 >> lane. ReadU32Pair
returned {f32 bits, 0}, so only F64-typed reads had the right value.

Based on TheCruZ's 284deab28 (closed #1219).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 05:21:06 +02:00
Nawid3333 ba8b0c85b8 shader: keep quiet NaNs with payload bits out of the packed f16 min/max sNaN path (#1579)
V_PK_MIN_F16 and V_PK_MAX_F16 return the quieted NaN for a signaling NaN
operand and the other operand for a quiet one (per half, as V_MIN_F16 and
V_MAX_F16 do). The signaling test masked the half with 0x7c00 and checked
for a nonzero payload, so a quiet NaN with payload bits such as 0x7e01 was
taken as signaling and returned. Mask with 0x7e00 so the quiet bit must be
clear.

Two more vectors in PackedMinMaxF16NanAndSignedZeroEdges cover it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 05:12:37 +02:00
Nawid3333 b6bd9ecae4 shader: read float inline constants on 16-bit operands as half bits (#1582)
The 16-bit encodings of the float inline constants are fixed: the exact
halves of 0.5, 1.0, 2.0 and 4.0 and their negations, and 0x3118 for
1/(2*pi). Read16LaneBits converted the 32-bit value at run time instead,
through PackHalf2x16, which the AMD driver rounds toward zero, so
1/(2*pi) became 0x3117. Use the encodings directly.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 05:05:40 +02:00
Felipe b95ea4bb5f Support mountable-content DLC (addcont with real content) (#1581)
Support mountable-content DLC (addcont) and fix PlayGo chunk count query
2026-10-17 04:44:28 +02:00
Nawid3333 e93b4b0378 shader: support returning 32-bit LDS compare-exchange (DS_CMPST_RTN_B32) (#1578)
* shader: support DS_CMPST_RTN_B32

Assassin's Creed Valhalla (PPSA01490, #281) stops on DS opcode 0x30 in a
compute shader. DS compares DATA and stores DATA2, the opposite of
BUFFER_ATOMIC_CMPSWAP, so the 32-bit atomic path swaps the two for this
opcode, as the 64-bit form does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* shader: reject unsupported GDS compare-store variant

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-17 04:37:16 +02:00
nmzik 0fd41ae206 Fix Linux CI xxHash include path (#1580)
Fix xxHash include path capitalization for test builds
2026-10-17 04:37:00 +02:00
nmzik 9202bddad2 recognize verified pattern fills
Co-authored-by: Juan Pablo Farias <juanpablo.profissional.a.f@gmail.com>
2026-10-17 04:09:54 +02:00
nmzik 32587337b5 renderdoc: advance capture flips independently of logging 2026-10-17 04:09:54 +02:00
Çağatay Güçlü 02ead0e2e1 shader: 64-bit buffer atomic add (BUFFER_ATOMIC_ADD_X2) 2026-10-17 04:09:54 +02:00
NeySlim 4f1f3b1bb3 shader: make the f32 clamp modifier turn NaN into 0 2026-10-17 04:09:53 +02:00
Nawid3333 42bb0da520 loader: keep the caller's flags in the TLS stubs (#1571)
A patched TLS load calls TlsRegStub and SafeCall, which moved RSP past the
red zone with sub/add rsp,0x80. ADD and SUB write the arithmetic flags, so
the sub ran before SafeCall's pushfq saved them and the add ran after its
popfq restored them: guest code that compares, loads fs:[0] and then
branches on the flags could branch the wrong way. lea moves RSP without
touching the flags. The entry lea plus a two-byte nop keeps every patched
offset in both stubs; the exit lea is one byte longer and moves only ret.

TlsStubsPreserveFlags calls the stubs with two flag patterns and checks
the status flags, the output register and RAX; it fails with the old
bytes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 00:05:08 +02:00
nmzik 5dec523143 Stub RazorCpu profiling markers in a dedicated library 2026-10-16 19:16:42 +02:00
nmzik d01633e18e Revert "Fix scalar buffer reads across sparse guest mappings"
This reverts commit d7974dc763.
2026-10-16 19:13:36 +02:00
nmzik e7ac242039 Load packaged system modules through the native module lifecycle 2026-10-16 19:13:36 +02:00
nmzik d7974dc763 Fix scalar buffer reads across sparse guest mappings 2026-10-16 18:20:31 +02:00
shadowbeat070 b899717900 renderer: log unsupported depth feedback once 2026-10-16 17:24:45 +02:00
117 changed files with 4085 additions and 523 deletions
+1 -1
View File
@@ -350,7 +350,7 @@ set(inc_headers
${KYTY_THIRD_PARTY_DIR}/SPIRV-Tools/include
${KYTY_THIRD_PARTY_DIR}/SPIRV-Headers/include
${KYTY_THIRD_PARTY_DIR}/fmt/include
${KYTY_THIRD_PARTY_DIR}/xxhash
${KYTY_THIRD_PARTY_DIR}/xxHash
${KYTY_THIRD_PARTY_DIR}/cpuinfo/include
${KYTY_THIRD_PARTY_DIR}/renderdoc
${KYTY_THIRD_PARTY_DIR}/stb
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "تم كسب الكأس!"
"trophy.toast.earned": "تم كسب الكأس!",
"user.guest": "ضيف"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofej získána!"
"trophy.toast.earned": "Trofej získána!",
"user.guest": "Host"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofæ optjent!"
"trophy.toast.earned": "Trofæ optjent!",
"user.guest": "Gæst"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trophäe erhalten!"
"trophy.toast.earned": "Trophäe erhalten!",
"user.guest": "Gast"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Κερδίσατε ένα τρόπαιο!"
"trophy.toast.earned": "Κερδίσατε ένα τρόπαιο!",
"user.guest": "Επισκ."
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trophy earned!"
"trophy.toast.earned": "Trophy earned!",
"user.guest": "Guest"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trophy earned!"
"trophy.toast.earned": "Trophy earned!",
"user.guest": "Guest"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "¡Ganaste un trofeo!"
"trophy.toast.earned": "¡Ganaste un trofeo!",
"user.guest": "Invitado"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "¡Ganaste un trofeo!"
"trophy.toast.earned": "¡Ganaste un trofeo!",
"user.guest": "Invitado"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Pokaali ansaittu!"
"trophy.toast.earned": "Pokaali ansaittu!",
"user.guest": "Vieras"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trophée obtenu !"
"trophy.toast.earned": "Trophée obtenu !",
"user.guest": "Invité"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trophée obtenu !"
"trophy.toast.earned": "Trophée obtenu !",
"user.guest": "Invité"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trófea megszerezve!"
"trophy.toast.earned": "Trófea megszerezve!",
"user.guest": "Vendég"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofi diperoleh!"
"trophy.toast.earned": "Trofi diperoleh!",
"user.guest": "Tamu"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofeo ottenuto!"
"trophy.toast.earned": "Trofeo ottenuto!",
"user.guest": "Ospite"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "トロフィーを獲得しました!"
"trophy.toast.earned": "トロフィーを獲得しました!",
"user.guest": "ゲスト"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "트로피를 획득했습니다!"
"trophy.toast.earned": "트로피를 획득했습니다!",
"user.guest": "게스트"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofee behaald!"
"trophy.toast.earned": "Trofee behaald!",
"user.guest": "Gast"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofé opptjent!"
"trophy.toast.earned": "Trofé opptjent!",
"user.guest": "Gjest"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Zdobyto trofeum!"
"trophy.toast.earned": "Zdobyto trofeum!",
"user.guest": "Gość"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Troféu conquistado!"
"trophy.toast.earned": "Troféu conquistado!",
"user.guest": "Convidado"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Troféu conquistado!"
"trophy.toast.earned": "Troféu conquistado!",
"user.guest": "Convidado"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofeu câștigat!"
"trophy.toast.earned": "Trofeu câștigat!",
"user.guest": "Invitat"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Трофей получен!"
"trophy.toast.earned": "Трофей получен!",
"user.guest": "Гость"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofé intjänad!"
"trophy.toast.earned": "Trofé intjänad!",
"user.guest": "Gäst"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "ได้รับถ้วยรางวัลแล้ว!"
"trophy.toast.earned": "ได้รับถ้วยรางวัลแล้ว!",
"user.guest": "แขก"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Kupa kazanıldı!"
"trophy.toast.earned": "Kupa kazanıldı!",
"user.guest": "Misafir"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Đã nhận cúp!"
"trophy.toast.earned": "Đã nhận cúp!",
"user.guest": "Khách"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "已获得奖杯!"
"trophy.toast.earned": "已获得奖杯!",
"user.guest": "访客"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "已獲得獎盃!"
"trophy.toast.earned": "已獲得獎盃!",
"user.guest": "訪客"
}
+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_ */
+29
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();
}
@@ -24,8 +26,10 @@ void Load(const ConfigOptions& cfg) {
EXIT_IF(cfg.user_name.empty() || cfg.user_name.size() > MAX_USER_NAME_LENGTH);
EXIT_IF(!IsConfiguredUserIdValid(cfg.user_id));
EXIT_IF(cfg.controller_speaker_volume > 100 || cfg.controller_vibration_intensity > 100);
EXIT_IF(cfg.keyboard_player > static_cast<uint32_t>(MAX_PLAYERS));
*g_config = cfg;
Common::DebugCounters::SetEnabled(cfg.perf_overlay_enabled);
}
uint32_t GetScreenWidth() {
@@ -60,6 +64,27 @@ uint32_t GetControllerVibrationIntensity() {
return g_config->controller_vibration_intensity;
}
uint32_t GetKeyboardPlayer() {
return g_config->keyboard_player;
}
int32_t GetPlayerUserId(int player) {
return g_config->user_id + player * PLAYER_USER_ID_STEP;
}
int GetPlayerFromUserId(int32_t user_id) {
for (int player = 0; player < MAX_PLAYERS; player++) {
if (user_id == GetPlayerUserId(player)) {
return player;
}
}
return -1;
}
bool IsGuestUserId(int32_t user_id) {
return GetPlayerFromUserId(user_id) > 0;
}
PresentMode GetPresentMode() {
return g_config->present_mode;
}
@@ -164,6 +189,10 @@ bool SyncRawImageBuffersEnabled() {
return g_config->sync_raw_image_buffers;
}
bool SupportZmaskEnabled() {
return g_config->support_zmask;
}
bool TessellationEnabled() {
return g_config->tessellation_enabled;
}
+20 -1
View File
@@ -6,6 +6,7 @@
#include <cstddef>
#include <array>
#include <filesystem>
#include <limits>
#include <optional>
#include <string>
#include <vector>
@@ -35,10 +36,16 @@ constexpr uint32_t MAX_CONSOLE_LANGUAGE = 29;
constexpr std::size_t MAX_USER_NAME_LENGTH = 16;
constexpr int32_t DEFAULT_USER_ID = 1000;
constexpr int MAX_PLAYERS = 4;
constexpr int32_t PLAYER_USER_ID_STEP = 0x100;
constexpr int32_t MAX_USER_ID =
std::numeric_limits<int32_t>::max() - (MAX_PLAYERS - 1) * PLAYER_USER_ID_STEP;
constexpr bool IsConfiguredUserIdValid(int32_t user_id) {
constexpr int32_t USER_ID_EVERYONE = 0xfe;
constexpr int32_t USER_ID_SYSTEM = 0xff;
return user_id >= 0 && user_id != USER_ID_EVERYONE && user_id != USER_ID_SYSTEM;
return user_id >= 0 && user_id <= MAX_USER_ID && user_id != USER_ID_EVERYONE &&
user_id != USER_ID_SYSTEM;
}
struct ConfigOptions {
@@ -50,6 +57,8 @@ struct ConfigOptions {
std::optional<ControllerColor> controller_color;
uint32_t controller_speaker_volume = 50;
uint32_t controller_vibration_intensity = 100;
// 0 shares player 1 with the first gamepad; 1..MAX_PLAYERS dedicates that player to the keyboard.
uint32_t keyboard_player = 0;
PresentMode present_mode = PresentMode::Mailbox;
int32_t gpu_index = -1;
bool fullscreen_enabled = false;
@@ -77,9 +86,11 @@ struct ConfigOptions {
bool renderdoc_enabled = false;
bool readback_linear_images = false;
bool sync_raw_image_buffers = false;
bool support_zmask = false;
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
@@ -100,6 +111,13 @@ const std::string& GetAudioInputDevice();
const std::optional<ControllerColor>& GetControllerColor();
uint32_t GetControllerSpeakerVolume();
uint32_t GetControllerVibrationIntensity();
uint32_t GetKeyboardPlayer();
// Player 0 is the configured user; later players derive their IDs from it.
int32_t GetPlayerUserId(int player);
// Returns -1 when the user ID does not belong to any player.
int GetPlayerFromUserId(int32_t user_id);
// Players after the first are guests: they have no profile, trophies or NP account.
bool IsGuestUserId(int32_t user_id);
PresentMode GetPresentMode();
int32_t GetGpuIndex();
bool FullscreenEnabled();
@@ -134,6 +152,7 @@ bool GpuAssistedValidationEnabled();
bool RenderDocEnabled();
bool ReadbackLinearImagesEnabled();
bool SyncRawImageBuffersEnabled();
bool SupportZmaskEnabled();
bool TessellationEnabled();
bool TrophyEnabled();
uint32_t GetTrophyDurationSeconds();
+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_ */
+2
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"
@@ -906,6 +907,7 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
if (draw_count == 0) {
return;
}
Common::DebugCounters::Add(Common::DebugCounters::Counter::IndirectDraws, draw_count);
const auto args_size = indexed ? sizeof(DrawIndexedIndirectArgs) : sizeof(DrawIndirectArgs);
EXIT_NOT_IMPLEMENTED(stride_in_bytes < args_size);
+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;
});
+16 -15
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,15 +263,6 @@ 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);
}
}
UnregisterImage(id);
if (m_scheduler.Active()) {
m_scheduler.DeferOperation([this, id] { m_slot_images.erase(id); });
@@ -724,6 +716,13 @@ ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingTyp
"TextureCache: unsupported unequal-sample depth overlap copy (%u -> %u)\n",
cached.backing.samples, replacement.backing.samples);
}
if (info.IsDepth() && info.metadata.kind == ImageMetadataKind::Htile) {
// Copied depth supersedes pending clears from the previous native image.
const auto metadata = m_surface_metas.find(info.metadata.range.address);
if (metadata != m_surface_metas.end()) {
metadata->second.clear_mask.reset();
}
}
FreeImage(cached_id);
return replacement_id;
}
@@ -954,6 +953,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 +999,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 +1040,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 +1418,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 +1772,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;
@@ -1881,6 +1880,7 @@ void TextureCache::UnmapMemory(uint64_t address, uint64_t size) {
EXIT("TextureCache: invalid unmap range\n");
}
std::scoped_lock lock {m_lock};
// HTILE belongs to guest memory, independently of native image replacements.
for (auto metadata = m_surface_metas.begin(); metadata != m_surface_metas.end();) {
const auto base = metadata->first;
if (base >= address && base < address + size) {
@@ -1943,6 +1943,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;
};
@@ -220,8 +220,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 +232,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;
}
}
}
}
@@ -311,7 +311,6 @@ void RenderExecutor::ResolveRenderDepthTarget(CommandBuffer& buffer, RenderDepth
}
r.desc = MakeDepthTargetDesc(buffer, z);
r.depth_clear_enable = rc.depth_clear_enable;
r.depth_load_clear_enable = r.depth_clear_enable;
r.depth_clear_value = hw.GetDepthClearValue();
r.depth_test_enable = dc.z_enable;
r.depth_write_enable = r.depth_test_enable && dc.z_write_enable &&
@@ -421,9 +420,9 @@ vk::ImageAspectFlags RenderDepthInfo::AttachmentWriteAspects() const {
}
const auto available = ImageViewOps::DepthAspectMask(format);
vk::ImageAspectFlags writes {};
vk::ImageAspectFlags writes = load_clear_aspects & available;
if ((available & vk::ImageAspectFlagBits::eDepth) &&
(depth_load_clear_enable || depth_write_enable)) {
depth_write_enable) {
writes |= vk::ImageAspectFlagBits::eDepth;
}
if (!(available & vk::ImageAspectFlagBits::eStencil)) {
@@ -461,8 +460,7 @@ vk::ImageAspectFlags RenderDepthInfo::AttachmentWriteAspects() const {
(can_pass && depth_pass && state.passOp != vk::StencilOp::eKeep) ||
(can_pass && depth_fail && state.depthFailOp != vk::StencilOp::eKeep);
};
if (stencil_clear_enable ||
(stencil_test_enable && (face_writes(stencil_front) || face_writes(stencil_back)))) {
if (stencil_test_enable && (face_writes(stencil_front) || face_writes(stencil_back))) {
writes |= vk::ImageAspectFlagBits::eStencil;
}
return writes;
@@ -22,7 +22,7 @@ struct RenderDepthInfo {
// Discovery keeps guest image information but can remap the view into a larger cache image.
TextureCache::ImageDesc desc;
bool depth_clear_enable = false;
bool depth_load_clear_enable = false;
vk::ImageAspectFlags load_clear_aspects;
float depth_clear_value = 0.0f;
bool depth_test_enable = false;
// Effective draw writes; discovery applies test, target-write and clear controls.
@@ -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();
@@ -126,9 +126,13 @@ NativeStorageBuffer(RenderContext& context, const PreparedBindings::BufferSource
if (size > graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange) {
EXIT("storage buffer range is unsupported\n");
}
auto [buffer, offset] = context.GetBufferCache().ObtainBuffer(
address, size, resource.written,
const bool metadata_read = resource.read && Config::SupportZmaskEnabled() &&
(address & 0x7fffu) == 0 &&
context.GetTextureCache().IsMeta(address);
auto [buffer, offset] = context.GetBufferCache().ObtainBuffer(
address, size, resource.written || metadata_read,
resource.formatted || (resource.read && Config::SyncRawImageBuffersEnabled()), id);
if (metadata_read) buffer->Fill(offset, size, 0xf); // HTILE ZMask::kUncompressed.
const auto aligned_offset = Common::AlignDown(offset, alignment);
const auto adjustment = offset - aligned_offset;
const auto max_range = graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange;
@@ -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>
@@ -299,6 +301,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,
@@ -993,8 +998,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 +1034,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);
@@ -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"
@@ -94,9 +95,10 @@ bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t grou
constexpr std::array formats {
Prospero::BufferFormat::k32UInt, Prospero::BufferFormat::k32_32UInt,
Prospero::BufferFormat::k32_32_32UInt, Prospero::BufferFormat::k32_32_32_32UInt};
// Offset-or-swizzle bounds do not describe the element range proved by the shader.
if (descriptor.Stride() != element_size || descriptor.Format() != formats[fill.words - 1] ||
descriptor.SwizzleEnabled() || descriptor.IndexStride() != 0 || descriptor.AddTid() ||
descriptor.Base48() == 0) {
descriptor.OutOfBounds() == 3 || descriptor.Base48() == 0) {
return false;
}
if (input.threads_num[0] == 0 || input.threads_num[0] != fill.group_stride[0] ||
@@ -108,7 +110,11 @@ bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t grou
? group_x
: static_cast<uint64_t>(group_x) * input.threads_num[0];
const auto size = descriptor.GetSize();
if (invocations != descriptor.NumRecords() || size == 0 || size > UINT32_MAX ||
const auto elements = descriptor.NumRecords();
const bool covered = fill.bounded_elements != 0
? fill.bounded_elements == elements && invocations >= elements
: invocations == elements;
if (!covered || invocations > uint64_t {UINT32_MAX} + 1 || size == 0 || size > UINT32_MAX ||
(input.dispatch_thread_dimensions &&
(group_x % input.threads_num[0] != 0 || input.dispatch_threads_num[0] != group_x ||
input.dispatch_threads_num[1] != 1 || input.dispatch_threads_num[2] != 1))) {
@@ -252,10 +258,6 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
return;
}
if (sh_ctx.GetCs().cs_regs.data_addr == 0) {
return;
}
constexpr uint32_t DISPATCH_INITIATOR_USE_THREAD_DIMENSIONS = 1u << 5u;
constexpr uint32_t DISPATCH_INITIATOR_BASE_BITS = 0x41u;
constexpr uint32_t DISPATCH_INITIATOR_MODIFIER_BITS = 0xa038u;
@@ -431,6 +433,7 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
vk_buffer.bindPipeline(vk::PipelineBindPoint::eCompute, pipeline.pipeline);
vk_buffer.dispatch(thread_group_x, thread_group_y, thread_group_z);
Common::DebugCounters::Add(Common::DebugCounters::Counter::Dispatches);
// The removed host fence also ordered read-only dispatches before later writers.
ShaderAccessBarrier(vk_buffer, vk::PipelineStageFlagBits::eComputeShader);
@@ -500,6 +503,7 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
1, &barrier, 0, nullptr, 0, nullptr);
vk_buffer.bindPipeline(vk::PipelineBindPoint::eCompute, pipeline.pipeline);
vk_buffer.dispatchIndirect(args_buffer->Handle(), args_offset);
Common::DebugCounters::Add(Common::DebugCounters::Counter::Dispatches);
ShaderAccessBarrier(vk_buffer, vk::PipelineStageFlagBits::eComputeShader);
ResetBindings();
}
+18 -9
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/logging/log.h"
@@ -507,14 +508,15 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
}
const auto image_view = cache.FindDepthTarget(depth.image_id, depth.desc);
const auto& metadata = depth.desc.info.metadata;
if (metadata.kind == ImageMetadataKind::Htile && depth.depth_clear_enable &&
!cache.ClearMeta(metadata.range.address)) {
EXIT("failed to acquire HTile metadata for a depth clear\n");
}
const bool meta_clear =
metadata.kind == ImageMetadataKind::Htile &&
cache.IsMetaCleared(metadata.range.address, depth.desc.view_info.base_layer);
depth.depth_load_clear_enable = depth.depth_clear_enable || meta_clear;
depth.load_clear_aspects = {};
if (depth.depth_clear_enable || meta_clear)
depth.load_clear_aspects |= vk::ImageAspectFlagBits::eDepth;
// Apply pending clears to compressed stencil as well as depth.
if (depth.stencil_clear_enable || (meta_clear && metadata.stencil_compressed))
depth.load_clear_aspects |= vk::ImageAspectFlagBits::eStencil;
if (meta_clear &&
!cache.TouchMeta(metadata.range.address, depth.desc.view_info.base_layer, false)) {
EXIT("failed to consume HTile clear state\n");
@@ -536,7 +538,10 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
}
}
if (feedback_aspects && !m_context.GetGraphics().attachment_feedback_loop_enabled) {
EXIT("depth attachment feedback loop is not supported by the host\n");
static std::atomic_bool logged = false;
if (!logged.exchange(true, std::memory_order_relaxed)) {
std::printf("depth attachment feedback loop is not supported by the host\n");
}
}
auto layout = depth_attachment_layout(depth);
if (sampled_aspects & ~DepthReadableAspects(layout)) {
@@ -564,9 +569,11 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
attachment.clear_value[0] = std::bit_cast<uint32_t>(depth.depth_clear_value);
attachment.clear_value[1] = depth.stencil_clear_value;
attachment.has_depth = static_cast<bool>(aspects & vk::ImageAspectFlagBits::eDepth);
attachment.depth_clear = depth.depth_load_clear_enable;
attachment.depth_clear =
static_cast<bool>(depth.load_clear_aspects & vk::ImageAspectFlagBits::eDepth);
attachment.has_stencil = static_cast<bool>(aspects & vk::ImageAspectFlagBits::eStencil);
attachment.stencil_clear = depth.stencil_clear_enable;
attachment.stencil_clear =
static_cast<bool>(depth.load_clear_aspects & vk::ImageAspectFlagBits::eStencil);
}
if (color_count == 0 && !depth.image_id) {
const auto& limits = buffer.GetGraphics().GetPhysicalDeviceProperties().limits;
@@ -1057,7 +1064,7 @@ bool RenderExecutor::CopyDepthToColor(CommandBuffer& buffer, const DrawCallInfo&
state.ps_input_info.ps_sample_mask_export_enable);
vk::ImageAspectFlags feedback;
const auto rendering = AcquireRenderTargets(buffer, &color, 1, depth, feedback);
if (depth.depth_load_clear_enable) {
if (depth.load_clear_aspects) {
m_context.GetCommandScheduler().BeginRendering(rendering);
}
auto& cache = m_context.GetTextureCache();
@@ -1227,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_
+2 -1
View File
@@ -162,7 +162,8 @@ void RenderDocOnGuestFlip(RenderContext& renderer) {
return;
}
if (state == RenderDocState::Capturing) {
LOGF("RenderDoc: captured guest flip %u/2\n", ++g_captured_flips);
++g_captured_flips;
LOGF("RenderDoc: captured guest flip %u/2\n", g_captured_flips);
if (g_captured_flips < 2) {
return;
}
+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,7 +254,8 @@ public:
}
const bool reserved = binding.key == SDLK_ESCAPE || binding.key == SDLK_F1 ||
binding.key == SDLK_F7 || 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) {
@@ -266,7 +266,6 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
#endif
#if !defined(__APPLE__)
check_feature(device_features2.features.depthClamp, "depthClamp");
check_feature(fragment_barycentric.fragmentShaderBarycentric, "fragmentShaderBarycentric");
#endif
check_feature(features12.samplerMirrorClampToEdge, "samplerMirrorClampToEdge",
@@ -498,7 +497,21 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
image_atomic_int64.pNext = supported_features2.pNext;
supported_features2.pNext = &image_atomic_int64;
}
const bool barycentric_extension =
HasExtension(device_extensions, VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME);
vk::PhysicalDeviceFragmentShaderBarycentricFeaturesKHR supported_barycentric {};
if (barycentric_extension) {
supported_barycentric.pNext = supported_features2.pNext;
supported_features2.pNext = &supported_barycentric;
}
physical_device.getFeatures2(&supported_features2);
const bool barycentric_supported =
barycentric_extension && supported_barycentric.fragmentShaderBarycentric == VK_TRUE;
if (!barycentric_supported) {
Log::WriteToConsoleAndLog(
"WARNING: Your GPU is not fully supported (fragmentShaderBarycentric is unavailable). "
"Some games may render incorrectly or crash.\n");
}
graphics.shader_image_int64_atomics_enabled = image_atomic_int64.shaderImageInt64Atomics;
auto features12 = WindowContext::RequiredVulkan12Features();
@@ -610,9 +623,13 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
robustness2.pNext = &features12;
#else
vk::PhysicalDeviceFragmentShaderBarycentricFeaturesKHR fragment_barycentric {};
fragment_barycentric.pNext = &features12;
fragment_barycentric.fragmentShaderBarycentric = VK_TRUE;
robustness2.pNext = &fragment_barycentric;
if (barycentric_supported) {
fragment_barycentric.pNext = &features12;
fragment_barycentric.fragmentShaderBarycentric = VK_TRUE;
robustness2.pNext = &fragment_barycentric;
} else {
robustness2.pNext = &features12;
}
#endif
robustness2.robustBufferAccess2 = supported_robustness2.robustBufferAccess2;
robustness2.robustImageAccess2 = supported_robustness2.robustImageAccess2;
@@ -966,7 +983,6 @@ void WindowContext::CreateVulkan() {
#else
device_extensions.push_back(VK_EXT_DEPTH_CLIP_ENABLE_EXTENSION_NAME);
device_extensions.push_back(VK_EXT_COLOR_WRITE_ENABLE_EXTENSION_NAME);
device_extensions.push_back(VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME);
#endif
#ifdef KYTY_ENABLE_DEBUG_PRINTF
@@ -1024,7 +1040,8 @@ void WindowContext::CreateVulkan() {
VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME,
VK_EXT_MESH_SHADER_EXTENSION_NAME,
VK_KHR_WORKGROUP_MEMORY_EXPLICIT_LAYOUT_EXTENSION_NAME,
VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME}) {
VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME,
VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME}) {
if (HasExtension(available_extensions, extension)) {
device_extensions.push_back(extension);
}
+34 -9
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,12 +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 (ProcessSystemOverlayInput(*event)) {
return;
@@ -67,6 +67,44 @@ uint32_t EmitExt(EmitterState& state, uint32_t type, uint32_t opcode,
return result;
}
uint32_t EmitF32ToF16RtzBits(EmitterState& state, uint32_t value) {
// Native half conversion causes regressions; use compact integer RTZ conversion.
const auto bits = EmitBitcastF32ToU32(state, value);
const auto abs = EmitAndConstant(state, bits, 0x7fffffffu);
const auto sign = EmitAndConstant(
state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), bits, ConstantU32(state, 16)),
0x8000u);
const auto exp = Binary(state, spv::OpShiftRightLogical, TypeU32(state), abs,
ConstantU32(state, 23));
const auto mant = EmitAndConstant(state, abs, 0x007fffffu);
// Align the hidden bit to the half subnormal mantissa. Even unused shifts must be < 32.
const auto shift = EmitUMin32(
state, Binary(state, spv::OpISub, TypeU32(state), ConstantU32(state, 126), exp),
ConstantU32(state, 31));
const auto sub = Binary(state, spv::OpShiftRightLogical, TypeU32(state),
EmitOrU32(state, mant, ConstantU32(state, 0x00800000u)), shift);
// Drop 13 mantissa bits and rebias the exponent; finite overflow saturates to max half.
const auto truncated = Binary(state, spv::OpShiftRightLogical, TypeU32(state), abs,
ConstantU32(state, 13));
const auto normal = EmitUMin32(
state, Binary(state, spv::OpISub, TypeU32(state), truncated, ConstantU32(state, 0x1c000u)),
ConstantU32(state, 0x7bffu));
const auto finite = Select(state, TypeU32(state),
EmitCompareU32Constant(state, spv::OpULessThan, abs, 0x38800000u),
sub, normal);
const auto nan = EmitOrU32(state, EmitAndConstant(state, truncated, 0x03ffu),
ConstantU32(state, 0x7e00u));
const auto special = Select(state, TypeU32(state),
EmitCompareU32Constant(state, spv::OpIEqual, mant, 0),
ConstantU32(state, 0x7c00u), nan);
const auto magnitude = Select(
state, TypeU32(state),
EmitCompareU32Constant(state, spv::OpUGreaterThanEqual, abs, 0x7f800000u), special, finite);
return EmitOrU32(state, sign, magnitude);
}
uint32_t EmitF32ToU32(EmitterState& state, uint32_t src, bool signed_value) {
const auto trunc = EmitTruncF32Value(state, src);
const auto converted_raw = state.builder.AllocateId();
@@ -210,28 +248,18 @@ uint32_t EmitCompositeExtractU32x2(EmitterState& state, uint32_t arg0, IR::Value
}
uint32_t EmitPackFloat2x16Rtz(EmitterState& state, uint32_t arg0, uint32_t arg1) {
const auto pair = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 2), pair, arg0, arg1);
const auto packed = EmitPackHalf2x16(state, pair);
const auto back = EmitExt(state, TypeF32Vector(state, 2), GLSLstd450UnpackHalf2x16, {packed});
auto result = packed;
for (uint32_t i = 0; i < 2; i++) {
const auto rounded = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeExtract, TypeF32(state), rounded, back, i);
const auto above =
Binary(state, spv::OpFOrdGreaterThan, TypeBool(state),
EmitExt(state, TypeF32(state), GLSLstd450FAbs, {rounded}),
EmitExt(state, TypeF32(state), GLSLstd450FAbs, {i == 0 ? arg0 : arg1}));
result = Binary(state, spv::OpISub, TypeU32(state), result,
Select(state, TypeU32(state), above,
ConstantU32(state, i == 0 ? 1u : 0x10000u), ConstantU32(state, 0)));
}
return result;
const auto low = EmitF32ToF16RtzBits(state, arg0);
const auto high = Binary(state, spv::OpShiftLeftLogical, TypeU32(state),
EmitF32ToF16RtzBits(state, arg1), ConstantU32(state, 16));
return EmitOrU32(state, low, high);
}
uint32_t EmitFPSaturate32(EmitterState& state, uint32_t arg0) {
return EmitExt(state, TypeF32(state), GLSLstd450FClamp,
{arg0, ConstantF32(state, 0), ConstantF32(state, 0x3f800000u)});
// The clamp modifier runs in DX10 clamp mode: NaN becomes 0. FClamp leaves NaN undefined.
const auto clamped = EmitExt(state, TypeF32(state), GLSLstd450FClamp,
{arg0, ConstantF32(state, 0), ConstantF32(state, 0x3f800000u)});
return Select(state, TypeF32(state), EmitClassifyF32(state, arg0).nan, ConstantF32(state, 0),
clamped);
}
uint32_t EmitSMulHi(EmitterState& state, uint32_t arg0, uint32_t arg1) {
@@ -296,6 +296,7 @@ inline constexpr auto EmitBufferAtomicSMax32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicUMax32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicAnd32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicAnd64 = EmitBufferAtomic64;
inline constexpr auto EmitBufferAtomicIAdd64 = EmitBufferAtomic64;
inline constexpr auto EmitBufferAtomicOr32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicOr64 = EmitBufferAtomic64;
inline constexpr auto EmitBufferAtomicXor32 = EmitAtomic32;
@@ -325,6 +326,7 @@ inline constexpr auto EmitSharedAtomicSMin32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicUMin32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicSMax32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicUMax64 = EmitSharedAtomic64;
inline constexpr auto EmitSharedAtomicCmpSwap32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicCmpSwap64 = EmitSharedAtomic64;
inline constexpr auto EmitSharedAtomicUMax32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicAnd32 = EmitAtomic32;
@@ -420,6 +420,7 @@ void EnsureLdsStorage(EmitterState& state);
struct MemoryResourceAccess {
IR::ResourceKind kind = IR::ResourceKind::None;
uint32_t object_pointer = 0;
uint32_t descriptor_index = 0;
uint32_t length = 0;
uint32_t byte_offset = 0;
uint32_t element_bits = 32;
@@ -1075,6 +1075,63 @@ uint32_t LoadIndirectFormattedX(ValueEmitContext& ctx, const IR::Inst& inst) {
});
}
uint32_t LoadRawBuffer(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem,
const MemoryResourceAccess& resource, uint32_t components) {
auto& state = ctx.state;
const auto scalar = [&] {
std::array<uint32_t, 4> values {};
for (uint32_t component = 0; component < components; component++) {
values[component] =
LoadWordPrepared(ctx, inst, RebaseRawComponent(mem, component), resource);
}
return ConstructU32Composite(state, components, values);
};
if (!mem.IsCoherentBufferPairLoad(inst.GetOpcode())) return scalar();
const auto first = ByteAddress(ctx, inst, mem, resource.byte_offset);
const auto second = ByteAddress(ctx, inst, RebaseRawComponent(mem, 1), resource.byte_offset);
const auto wide =
PrepareStorageBufferResourceAccess(state, mem, state.storage_buffers[3],
resource.non_uniform ? resource.descriptor_index : 0u);
const auto index =
Binary(state, spv::OpShiftRightLogical, TypeU32(state), first, ConstantU32(state, 3));
auto atomic =
Binary(state, spv::OpIEqual, TypeBool(state),
Binary(state, spv::OpBitwiseAnd, TypeU32(state), first, ConstantU32(state, 7)),
ConstantU32(state, 0));
atomic = AndCondition(state, atomic,
Binary(state, spv::OpIEqual, TypeBool(state), second,
EmitAddU32(state, first, ConstantU32(state, 4))));
// The 64-bit alias requires the entire pair; partial and swizzled reads retain DWORD bounds.
atomic = AndCondition(state, atomic, EmitMemoryElementInBounds(state, wide, index));
const auto atomic_label = state.builder.AllocateId(), scalar_label = state.builder.AllocateId();
const auto merge = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone);
state.builder.AddFunction(spv::OpBranchConditional, atomic, atomic_label, scalar_label);
EmitLabel(state, atomic_label);
const auto pointer = EmitStorageBufferElementPointer(
state, wide, index, TypeStorageBufferElementPointer(state, 64));
const auto snapshot = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAtomicLoad, TypeU64(state), snapshot, pointer,
ConstantU32(state, spv::ScopeDevice),
ConstantU32(state, spv::MemorySemanticsAcquireMask |
spv::MemorySemanticsUniformMemoryMask));
const std::array<uint32_t, 4> words {
EmitCompositeExtractU64(state, snapshot, IR::Value(0u)),
EmitCompositeExtractU64(state, snapshot, IR::Value(1u))};
const auto pair = ConstructU32Composite(state, 2, words);
state.builder.AddFunction(spv::OpBranch, merge);
EmitLabel(state, scalar_label);
const auto fallback = scalar();
const auto scalar_exit = state.current_label;
state.builder.AddFunction(spv::OpBranch, merge);
EmitLabel(state, merge);
const auto result = state.builder.AllocateId();
state.builder.AddFunction(spv::OpPhi, TypeU32Composite(state, 2), result, pair, atomic_label,
fallback, scalar_exit);
return result;
}
uint32_t LoadBuffer(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t components) {
auto& state = ctx.state;
const auto mem = ctx.Memory(inst);
@@ -1100,12 +1157,7 @@ uint32_t LoadBuffer(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t compon
return LoadFormattedPrepared(ctx, inst, selected, resource, info, components);
}
if (resource.object_pointer == 0u) return ConstantU32CompositeZero(state, components);
std::array<uint32_t, 4> values {};
for (uint32_t component = 0; component < components; component++) {
values[component] =
LoadWordPrepared(ctx, inst, RebaseRawComponent(selected, component), resource);
}
return ConstructU32Composite(state, components, values);
return LoadRawBuffer(ctx, inst, selected, resource, components);
});
});
}
@@ -207,10 +207,11 @@ MemoryResourceAccess PrepareStorageBufferResourceAccess(EmitterState& state,
const bool non_uniform = array_index != 0u;
const auto static_index = state.program.info.buffers.at(mem.resource).descriptor_index;
if (!non_uniform) array_index = ConstantU32(state, static_index);
MemoryResourceAccess access {
.kind = mem.kind,
.non_uniform = non_uniform,
.memory_access = mem.coherent ? spv::MemoryAccessVolatileMask : spv::MemoryAccessMaskNone};
MemoryResourceAccess access {.kind = mem.kind,
.descriptor_index = array_index,
.non_uniform = non_uniform,
.memory_access = mem.coherent ? spv::MemoryAccessVolatileMask
: spv::MemoryAccessMaskNone};
access.object_pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain, buffer.pointer_type, access.object_pointer,
buffer.variable, array_index);
@@ -504,6 +505,7 @@ uint32_t EncodePackedColorComponent(EmitterState& state, const Format::BufferFor
spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
switch (opcode) {
case IR::ValueOpcode::SharedAtomicCmpSwap32:
case IR::ValueOpcode::SharedAtomicCmpSwap64:
case IR::ValueOpcode::ImageAtomicCompareSwap32:
case IR::ValueOpcode::BufferAtomicCmpSwap32: return spv::OpAtomicCompareExchange;
@@ -516,6 +518,7 @@ spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
case IR::ValueOpcode::ImageAtomicIAdd64:
case IR::ValueOpcode::BufferAtomicIAdd32:
case IR::ValueOpcode::SharedAtomicIAdd64:
case IR::ValueOpcode::BufferAtomicIAdd64:
case IR::ValueOpcode::SharedAtomicIAdd32: return spv::OpAtomicIAdd;
case IR::ValueOpcode::BufferAtomicISub32:
case IR::ValueOpcode::SharedAtomicISub32: return spv::OpAtomicISub;
@@ -560,8 +563,12 @@ uint32_t EmitAtomicOperation(ValueEmitContext& ctx, const IR::Inst& inst, uint32
const bool wide = inst.GetType() == IR::Type::U64;
const auto type = wide ? TypeU64(ctx.state) : TypeU32(ctx.state);
if (opcode == spv::OpAtomicCompareExchange) {
const auto desired = ctx.Arg(inst, inst.NumArgs() - 3);
const auto comparator = ctx.Arg(inst, inst.NumArgs() - 2);
auto desired = ctx.Arg(inst, inst.NumArgs() - 3);
auto comparator = ctx.Arg(inst, inst.NumArgs() - 2);
if (inst.GetOpcode() == IR::ValueOpcode::SharedAtomicCmpSwap32) {
// RDNA2 DS compares DATA and stores DATA2, opposite to BUFFER atomics.
std::swap(desired, comparator);
}
ctx.state.builder.AddFunction(
spv::OpAtomicCompareExchange, TypeU32(ctx.state), old, pointer,
ConstantU32(ctx.state, scope), ConstantU32(ctx.state, spv::MemorySemanticsMaskNone),
@@ -67,6 +67,7 @@ constexpr MemoryOpcodeInfo MUBUF_OPCODE_LIST[] = {
{0x3fu, Opcode::BUFFER_ATOMIC_FMIN, 1, 32},
{0x40u, Opcode::BUFFER_ATOMIC_FMAX, 1, 32},
{0x50u, Opcode::BUFFER_ATOMIC_SWAP_X2, 2, 32},
{0x52u, Opcode::BUFFER_ATOMIC_ADD_X2, 2, 32},
{0x59u, Opcode::BUFFER_ATOMIC_AND_X2, 2, 32},
{0x5au, Opcode::BUFFER_ATOMIC_OR_X2, 2, 32},
{0x80u, Opcode::BUFFER_LOAD_FORMAT_D16_X, 1, 16, false, false, true},
@@ -114,6 +115,7 @@ constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = {
{0x27u, Opcode::DS_MIN_RTN_U32, 1, 32}, {0x28u, Opcode::DS_MAX_RTN_U32, 1, 32},
{0x29u, Opcode::DS_AND_RTN_B32, 1, 32}, {0x2au, Opcode::DS_OR_RTN_B32, 1, 32},
{0x2bu, Opcode::DS_XOR_RTN_B32, 1, 32}, {0x2du, Opcode::DS_WRXCHG_RTN_B32, 1, 32},
{0x30u, Opcode::DS_CMPST_RTN_B32, 1, 32},
{0x35u, Opcode::DS_SWIZZLE_B32, 1, 32}, {0x36u, Opcode::DS_READ_B32, 1, 32},
{0x37u, Opcode::DS_READ2_B32, 2, 32}, {0x38u, Opcode::DS_READ2ST64_B32, 2, 32},
{0x39u, Opcode::DS_READ_I8, 1, 8, true}, {0x3au, Opcode::DS_READ_U8, 1, 8},
@@ -200,6 +202,7 @@ bool IsDsAtomicOpcode(Opcode opcode) {
case Opcode::DS_MAX_U32:
case Opcode::DS_MAX_RTN_U32:
case Opcode::DS_MAX_RTN_U64:
case Opcode::DS_CMPST_RTN_B32:
case Opcode::DS_CMPST_RTN_B64:
case Opcode::DS_AND_B32:
case Opcode::DS_AND_RTN_B32:
@@ -220,6 +223,7 @@ uint32_t DsSourceCount(Opcode opcode) {
case Opcode::DS_WRITE2ST64_B32:
case Opcode::DS_WRITE2_B64:
case Opcode::DS_WRITE2ST64_B64:
case Opcode::DS_CMPST_RTN_B32:
case Opcode::DS_CMPST_RTN_B64:
case Opcode::DS_MSKOR_B32: return 3u;
case Opcode::DS_MIN_F32:
@@ -454,6 +458,9 @@ void DecodeDs(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
if (inst.opcode == Opcode::DS_SWIZZLE_B32 && inst.offset >= 0xe000u) {
SetUnsupported(inst, Family::DS, opcode, "DS swizzle FFT mode is not implemented");
}
if (inst.gds && inst.opcode == Opcode::DS_CMPST_RTN_B32) {
SetUnsupported(inst, Family::DS, opcode, "DS compare-store return B32 GDS variant is not implemented");
}
if (inst.gds &&
(inst.opcode == Opcode::DS_SWIZZLE_B32 || inst.opcode == Opcode::DS_PERMUTE_B32 ||
inst.opcode == Opcode::DS_BPERMUTE_B32 ||
@@ -670,6 +670,7 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::BUFFER_ATOMIC_CMPSWAP:
case Opcode::BUFFER_ATOMIC_SWAP_X2:
case Opcode::BUFFER_ATOMIC_ADD:
case Opcode::BUFFER_ATOMIC_ADD_X2:
case Opcode::BUFFER_ATOMIC_SUB:
case Opcode::BUFFER_ATOMIC_SMIN:
case Opcode::BUFFER_ATOMIC_UMIN:
@@ -717,6 +718,7 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::DS_MAX_U32:
case Opcode::DS_MAX_RTN_U32:
case Opcode::DS_MAX_RTN_U64:
case Opcode::DS_CMPST_RTN_B32:
case Opcode::DS_CMPST_RTN_B64:
case Opcode::DS_AND_B32:
case Opcode::DS_AND_RTN_B32:
@@ -586,6 +586,7 @@ enum class Opcode {
BUFFER_ATOMIC_CMPSWAP,
BUFFER_ATOMIC_SWAP_X2,
BUFFER_ATOMIC_ADD,
BUFFER_ATOMIC_ADD_X2,
BUFFER_ATOMIC_SUB,
BUFFER_ATOMIC_SMIN,
BUFFER_ATOMIC_UMIN,
@@ -631,6 +632,7 @@ enum class Opcode {
DS_MAX_U32,
DS_MAX_RTN_U32,
DS_MAX_RTN_U64,
DS_CMPST_RTN_B32,
DS_CMPST_RTN_B64,
DS_AND_B32,
DS_AND_RTN_B32,
@@ -27,7 +27,7 @@ void Translator::PackedFloat16(const Decoder::Instruction& inst, IR::ValueOpcode
if (quiet_snan) {
const auto quiet_snan_lane = [&](const Decoder::Operand& operand, bool high) {
const auto bits = ReadU16LaneAsU32(operand, high, false);
const auto exponent = ir.BitwiseAnd(bits, IR::U32(IR::Value(0x7c00u)));
const auto exponent = ir.BitwiseAnd(bits, IR::U32(IR::Value(0x7e00u)));
const auto payload = ir.BitwiseAnd(bits, IR::U32(IR::Value(0x01ffu)));
const auto snan = ir.LogicalAnd(ir.IEqual(exponent, IR::U32(IR::Value(0x7c00u))),
ir.INotEqual(payload, IR::U32(IR::Value(0u))));
@@ -1046,6 +1046,8 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicFMin32);
case Decoder::Opcode::BUFFER_ATOMIC_FMAX:
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicFMax32);
case Decoder::Opcode::BUFFER_ATOMIC_ADD_X2:
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicIAdd64);
case Decoder::Opcode::DS_ADD_U32:
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicIAdd32, false);
@@ -1083,6 +1085,8 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicUMax32, true);
case Decoder::Opcode::DS_MAX_RTN_U64:
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicUMax64, true);
case Decoder::Opcode::DS_CMPST_RTN_B32:
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicCmpSwap32, true);
case Decoder::Opcode::DS_CMPST_RTN_B64:
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicCmpSwap64, true);
case Decoder::Opcode::DS_AND_B32:
@@ -233,13 +233,6 @@ IR::Value Translator::ReadOperand(const Decoder::Operand& operand, IR::Type type
if (type == IR::Type::F64) {
if (operand.kind == Decoder::OperandKind::LiteralConstant) {
pair = {IR::U32(IR::Value(0u)), IR::U32(IR::Value(operand.value))};
} else if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
const auto bits = operand.value == 0x3e22f983u
? 0x3fc45f306dc9c882ull
: std::bit_cast<uint64_t>(static_cast<double>(
std::bit_cast<float>(operand.value)));
pair = {IR::U32(IR::Value(static_cast<uint32_t>(bits))),
IR::U32(IR::Value(static_cast<uint32_t>(bits >> 32u)))};
}
if (operand.absolute) {
pair[1] = ir.BitwiseAnd(pair[1], IR::U32(IR::Value(0x7fffffffu)));
@@ -488,6 +481,14 @@ std::array<IR::U32, 2> Translator::ReadU32Pair(const Decoder::Operand& operand)
if (operand.kind == Decoder::OperandKind::VccLo) {
return {ir.GetVccLo(), ir.GetVccHi()};
}
if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
const auto bits =
operand.value == 0x3e22f983u
? 0x3fc45f306dc9c882ull
: std::bit_cast<uint64_t>(static_cast<double>(std::bit_cast<float>(operand.value)));
return {IR::U32(IR::Value(static_cast<uint32_t>(bits))),
IR::U32(IR::Value(static_cast<uint32_t>(bits >> 32u)))};
}
const auto low = ApplyBitSourceModifiers(operand, ReadRawU32(operand));
IR::U32 high(IR::Value(0u));
if (operand.kind == Decoder::OperandKind::Sgpr || operand.kind == Decoder::OperandKind::Vgpr) {
@@ -546,9 +547,11 @@ IR::U32 Translator::Read16LaneBits(const Decoder::Operand& operand, bool high_la
auto bits = ReadRawU32(operand);
if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
// Inline floats encode the operand's width before SDWA or OP_SEL selects bits.
const auto half = IR::F16(ir.Emit(IR::ValueOpcode::ConvertF16F32, {ir.BitCastF32(bits)}));
const auto half_bits = IR::U16(ir.Emit(IR::ValueOpcode::BitCastU16F16, {half}));
bits = IR::U32(ir.Emit(IR::ValueOpcode::ConvertU32U16, {half_bits}));
const auto value = operand.value;
bits = IR::U32(IR::Value(value == 0x3e22f983u
? 0x3118u
: ((value >> 16u) & 0x8000u) |
((((value >> 23u) & 0xffu) - 112u) << 10u)));
}
bits = ApplyBitSourceModifiers(raw_operand, bits);
uint32_t offset = (high_lane ? operand.op_sel_hi : operand.op_sel) ? 16u : 0u;
@@ -25,6 +25,7 @@ struct UniformFill {
std::array<uint32_t, 3> group_stride {};
uint32_t words = 0;
uint32_t value = 0;
uint32_t bounded_elements = 0;
bool operator==(const UniformFill&) const = default;
};
@@ -82,6 +82,11 @@ struct MemoryInfo {
bool coherent = false;
bool planning_only = false;
[[nodiscard]] bool IsCoherentBufferPairLoad(ValueOpcode opcode) const {
return kind == ResourceKind::Buffer && opcode == ValueOpcode::LoadBufferU32x2 && coherent &&
!typed && !formatted && data_bits == 32u;
}
[[nodiscard]] bool SupportsIndirectBufferLoad(ValueOpcode opcode) const {
if (typed) return false;
if (data_bits == 8u) return !formatted && opcode == ValueOpcode::LoadBufferU8;
@@ -622,6 +627,8 @@ struct CompiledShaderInfo {
struct UniformFillPlan {
UniformFill fill;
std::array<Value, 4> values;
Value pattern_size;
Value element_count;
};
// Resource analysis retained by the shader cache. It owns immutable descriptor/SRT,
@@ -118,6 +118,7 @@ BufferAccess BufferAccessOf(ValueOpcode opcode) {
case ValueOpcode::BufferAtomicCmpSwap32:
case ValueOpcode::BufferAtomicSwap64:
case ValueOpcode::BufferAtomicIAdd32:
case ValueOpcode::BufferAtomicIAdd64:
case ValueOpcode::BufferAtomicISub32:
case ValueOpcode::BufferAtomicSMin32:
case ValueOpcode::BufferAtomicUMin32:
@@ -137,6 +138,7 @@ BufferAccess BufferAccessOf(ValueOpcode opcode) {
uint32_t BufferComponentCount(ValueOpcode opcode) {
switch (opcode) {
case ValueOpcode::BufferAtomicSwap64:
case ValueOpcode::BufferAtomicIAdd64:
case ValueOpcode::BufferAtomicAnd64:
case ValueOpcode::BufferAtomicOr64:
case ValueOpcode::LoadBufferU32x2:
@@ -176,6 +178,7 @@ SharedAccess SharedAccessOf(ValueOpcode opcode) {
case ValueOpcode::SharedAtomicSMax32:
case ValueOpcode::SharedAtomicUMax32:
case ValueOpcode::SharedAtomicUMax64:
case ValueOpcode::SharedAtomicCmpSwap32:
case ValueOpcode::SharedAtomicCmpSwap64:
case ValueOpcode::SharedAtomicAnd32:
case ValueOpcode::SharedAtomicOr32:
@@ -268,6 +268,7 @@ VALUE_OPCODE(BufferAtomicSwap32, U32, BufferResource, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicCmpSwap32, U32, BufferResource, U32, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicSwap64, U64, BufferResource, U32, U32, U32, U64, U1)
VALUE_OPCODE(BufferAtomicIAdd32, U32, BufferResource, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicIAdd64, U64, BufferResource, U32, U32, U32, U64, U1)
VALUE_OPCODE(BufferAtomicISub32, U32, BufferResource, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicSMin32, U32, BufferResource, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicUMin32, U32, BufferResource, U32, U32, U32, U32, U1)
@@ -306,6 +307,7 @@ VALUE_OPCODE(SharedAtomicUMin32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicSMax32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicUMax32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicUMax64, U64, U32, U64, U1)
VALUE_OPCODE(SharedAtomicCmpSwap32, U32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicCmpSwap64, U64, U32, U64, U64, U1)
VALUE_OPCODE(SharedAtomicAnd32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicOr32, U32, U32, U32, U1)
@@ -51,11 +51,13 @@ public:
}
void QueueUsers(const std::vector<Use>& users) {
++generation;
affected.clear();
for (const auto& use: users) affected.push_back(use.user);
}
void FlushUsers() {
++generation;
// Pattern folds inspect nested operands even when intermediate operations
// stay dynamic. Visit consumers once per change, including cyclic Phi edges.
// stay dynamic. Visit their union once per drain, including cyclic Phi edges.
for (size_t i = 0; i < affected.size(); ++i) {
auto* inst = affected[i];
const auto found = locations.find(inst);
@@ -64,6 +66,7 @@ public:
Queue(inst);
for (const auto& use: inst->Uses()) affected.push_back(use.user);
}
affected.clear();
}
Block::iterator Prepend(Block& block, Block::iterator before, ValueOpcode opcode,
@@ -977,27 +980,30 @@ void ConstantPropagationPass(const BlockList& blocks, uint32_t wave_size) {
std::vector<Use> users;
std::vector<Value> operands;
const auto drain = [&] {
for (size_t i = 0; i < worklist.pending.size(); ++i) {
auto* inst = worklist.pending[i];
auto& location = worklist.locations.at(inst);
location.queued = false;
const auto opcode = inst->GetOpcode();
if (opcode == ValueOpcode::Identity || opcode == ValueOpcode::Void) continue;
const auto where = location.instruction;
users.assign(inst->Uses().begin(), inst->Uses().end());
operands.clear();
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) operands.push_back(inst->Arg(arg));
mask_projection.Fold(*inst, wave_size);
FoldInstruction(*inst->Parent(), where, worklist);
bool changed = opcode != inst->GetOpcode() || operands.size() != inst->NumArgs();
for (size_t arg = 0; !changed && arg < operands.size(); ++arg)
changed = operands[arg] != inst->Arg(arg);
if (changed) {
worklist.QueueUsers(users);
worklist.Queue(inst);
do {
for (size_t i = 0; i < worklist.pending.size(); ++i) {
auto* inst = worklist.pending[i];
auto& location = worklist.locations.at(inst);
location.queued = false;
const auto opcode = inst->GetOpcode();
if (opcode == ValueOpcode::Identity || opcode == ValueOpcode::Void) continue;
const auto where = location.instruction;
users.assign(inst->Uses().begin(), inst->Uses().end());
operands.clear();
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) operands.push_back(inst->Arg(arg));
mask_projection.Fold(*inst, wave_size);
FoldInstruction(*inst->Parent(), where, worklist);
bool changed = opcode != inst->GetOpcode() || operands.size() != inst->NumArgs();
for (size_t arg = 0; !changed && arg < operands.size(); ++arg)
changed = operands[arg] != inst->Arg(arg);
if (changed) {
worklist.QueueUsers(users);
worklist.Queue(inst);
}
}
}
worklist.pending.clear();
worklist.pending.clear();
worklist.FlushUsers();
} while (!worklist.pending.empty());
};
drain();
// Normalize retained PHI/select values only after every supported field read has
@@ -1409,7 +1409,8 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
}
if (op != ValueOpcode::LogicalAnd && op != ValueOpcode::LogicalOr &&
op != ValueOpcode::LogicalNot && op != ValueOpcode::IEqual32 &&
op != ValueOpcode::INotEqual32 && op != ValueOpcode::ULessThanEqual32 &&
op != ValueOpcode::INotEqual32 && op != ValueOpcode::ULessThan32 &&
op != ValueOpcode::ULessThanEqual32 &&
op != ValueOpcode::SelectU1 && op != ValueOpcode::SelectU32) return finish(opaque());
auto& node = plan.value_storage.emplace_back(op);
for (size_t index = 0; index < inst->NumArgs(); ++index)
@@ -1424,10 +1425,11 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
for (auto it = first; it != plan.value_storage.end(); ++it) it->Invalidate();
plan.value_storage.erase(first, plan.value_storage.end());
}
plan.uniform_fill = AnalyzeUniformFill(program);
for (uint32_t i = 0; i < plan.uniform_fill.fill.words; ++i) {
plan.uniform_fill.values[i] = Clone(plan.uniform_fill.values[i]);
}
plan.uniform_fill = program.uniform_fill.fill.kind != UniformFillKind::None
? program.uniform_fill : AnalyzeUniformFill(program);
for (auto& value: plan.uniform_fill.values) value = Clone(value);
plan.uniform_fill.pattern_size = Clone(plan.uniform_fill.pattern_size);
plan.uniform_fill.element_count = Clone(plan.uniform_fill.element_count);
plan.capture_specialization_reads |=
capture_indirect_reads || capture_control_reads || !plan.source_reads.empty();
if (plan.capture_specialization_reads) {
@@ -1513,15 +1515,21 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
}
snapshot.uniform_fill = {};
const auto& fill = program.uniform_fill;
const auto words = fill.fill.words;
auto words = fill.fill.words;
std::array<uint32_t, 4> stored {};
bool uniform_fill = words != 0;
if (uniform_fill && !fill.pattern_size.IsEmpty())
uniform_fill = clean.Evaluate(fill.pattern_size, words) && words >= 1 && words <= 4;
uint32_t elements = 0;
if (uniform_fill && !fill.element_count.IsEmpty())
uniform_fill = clean.Evaluate(fill.element_count, elements) && elements != 0;
for (uint32_t i = 0; i < words && uniform_fill; ++i) {
uniform_fill = clean.Evaluate(fill.values[i], stored[i]) && stored[i] == stored[0];
}
if (uniform_fill) {
snapshot.uniform_fill = fill.fill;
snapshot.uniform_fill.value = stored[0];
snapshot.uniform_fill.bounded_elements = elements;
}
const auto evaluate = [&](uint32_t source, DescriptorValue& value, bool written = false) {
if (source >= program.descriptor_sources.size()) {
@@ -15,6 +15,8 @@
#include <tuple>
#include <unordered_map>
#include <utility>
#define XXH_INLINE_ALL
#include <xxhash.h>
namespace Libs::Graphics::ShaderRecompiler::IR {
namespace {
@@ -4040,11 +4042,71 @@ private:
bool m_shader_writes = false;
};
UniformFillPlan RecognizePatternFill(Program& program, const Decoder::Program& decoded,
ShaderStageInputInfo input) {
struct Pattern {
size_t code_words;
uint64_t hash;
uint32_t wave_size_mask;
bool bounded;
};
constexpr std::array patterns {
// Bounded 64-lane variant.
Pattern {.code_words = 69, .hash = 0x0704b9818e3cba06ull, .wave_size_mask = 64, .bounded = true},
// Dispatch thread dimensions, 64-lane variants.
Pattern {.code_words = 42, .hash = 0x92ce57f21310ef02ull, .wave_size_mask = 64, .bounded = false},
Pattern {.code_words = 40, .hash = 0x374161027f58bee7ull, .wave_size_mask = 64, .bounded = false},
Pattern {.code_words = 40, .hash = 0x7d7172c56c2e296dull, .wave_size_mask = 64, .bounded = false},
// Dispatch thread dimensions, valid in both wave sizes.
Pattern {.code_words = 38, .hash = 0xacbda1a1ccdd4198ull, .wave_size_mask = 32 | 64, .bounded = false},
};
if (program.stage != ShaderType::Compute || input.compute == nullptr ||
decoded.instructions.empty() || program.user_data_base != 0 || program.user_data_count < 10 ||
program.info.buffers.size() != 1 || !program.info.images.empty()) return {};
const auto& cs = *input.compute;
const bool matching_inputs = cs.workgroup_register == 10 && cs.thread_ids_num == 1 &&
cs.group_id[0] && !cs.group_id[1] && !cs.group_id[2] && cs.threads_num[0] == 64 &&
cs.threads_num[1] == 1 && cs.threads_num[2] == 1 && cs.float_mode == 0xc0;
if (!matching_inputs) return {};
const auto& last = decoded.instructions.back();
const size_t words = last.pc / sizeof(uint32_t) + last.word_count;
if (words > decoded.code.size() ||
std::ranges::find(patterns, words, &Pattern::code_words) == patterns.end()) return {};
// Hash executable instructions through S_ENDPGM, excluding binary metadata.
// Verified variants implement Destination[id] = value[id % pattern_size].
const auto hash = XXH3_64bits(decoded.code.data(), words * sizeof(uint32_t));
const auto pattern = std::ranges::find_if(patterns, [&](const Pattern& candidate) {
return candidate.code_words == words && candidate.hash == hash;
});
if (pattern == patterns.end() || (program.wave_size & pattern->wave_size_mask) == 0 ||
cs.dispatch_thread_dimensions != !pattern->bounded) return {};
std::array<Value, 10> userdata;
for (auto* block: program.blocks) {
for (auto& inst: *block) {
if (inst.GetOpcode() != ValueOpcode::GetUserData) continue;
const auto reg = RegIndex(inst.Arg(0).ScalarRegister());
if (reg < userdata.size()) userdata[reg] = Value(&inst);
}
}
for (uint32_t reg = 4; reg < 10; ++reg)
if ((reg != 8 || pattern->bounded) && userdata[reg].IsEmpty()) return {};
UniformFillPlan result;
result.fill.kind = UniformFillKind::Buffer;
result.fill.words = 1;
result.fill.group_stride[0] = 64;
std::copy_n(userdata.begin() + 4, 4, result.values.begin());
result.pattern_size = userdata[9];
// Only the bounded variant reads an explicit count; the other uses thread dimensions.
if (pattern->bounded) result.element_count = userdata[8];
return result;
}
} // namespace
void TrackResources(Program& program, const Decoder::Program& decoded, const CFG::Graph& native_cfg,
ShaderStageInputInfo input) {
Tracker(program, decoded, native_cfg, input).Run();
program.uniform_fill = RecognizePatternFill(program, decoded, input);
}
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -340,6 +340,7 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs
}
if (kind == ResourceKind::Buffer) {
info.coherent_buffers |= memory.coherent;
info.buffer_int64_atomics |= memory.IsCoherentBufferPairLoad(op);
if (memory.resource >= info.buffers.size()) {
return Fail("buffer operation has invalid resource metadata");
}
@@ -1054,7 +1054,13 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
}
return false;
case ValueOpcode::ULessThan32:
if (binary()) {
// No U32 is below zero, including an unknown loop-carried value.
if (!Arg(inst, 1, b)) return false;
if (static_cast<uint32_t>(b) == 0u) {
result = 0u;
return true;
}
if (Arg(inst, 0, a)) {
result = static_cast<uint32_t>(a) < static_cast<uint32_t>(b);
return true;
}
+5 -5
View File
@@ -583,16 +583,16 @@ ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context&
mesh.input_primitive, sh.m_vgtGsOutPrimType, sh.m_vgtGsMaxVertOut,
group.primitive_group_size, group.vertex_group_size, mesh.max_vertices);
}
mesh.max_primitives = mesh.fast_launch ? mesh.max_vertices :
native_ngg ? mesh.InputPrimitiveCount(mesh.max_vertices) :
group.primitive_group_size * (sh.m_vgtGsMaxVertOut - 2u);
mesh.primitives_per_group = std::min({static_cast<uint32_t>(group.primitive_group_size),
mesh.InputPrimitiveCount(group.vertex_group_size),
native_ngg ? mesh.max_primitives :
native_ngg ? mesh.InputPrimitiveCount(mesh.max_vertices) :
mesh.max_vertices / sh.m_vgtGsMaxVertOut});
EXIT_IF(mesh.primitives_per_group == 0u);
mesh.vertices_per_group = mesh.InputVertexCount(mesh.primitives_per_group);
const auto threads = native_ngg && !mesh.fast_launch ? mesh.vertices_per_group : mesh.max_vertices;
mesh.max_primitives = mesh.primitives_per_group * (sh.m_geNggSubgrpCntl & 0x1ffu);
const auto threads = std::max(native_ngg && !mesh.fast_launch ? mesh.vertices_per_group
: mesh.max_vertices,
mesh.max_primitives);
mesh.threads_num[0] = ((threads + mesh.wave_size - 1u) / mesh.wave_size) * mesh.wave_size;
mesh.threads_num[1] = mesh.threads_num[2] = 1u;
return params;
+15 -2
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"
@@ -338,14 +339,20 @@ static bool HasWindowsForbiddenFilenameCharacter(const std::string& relative_pat
std::filesystem::path MountPoints::ResolvePath(const std::string& mounted_name) {
Common::LockGuard lock(m_mutex);
// Guests join mount points and names with extra slashes (e.g. "/addcont0//file").
std::string normalized = Common::FixFilenameSlash(mounted_name);
normalized.erase(std::unique(normalized.begin(), normalized.end(),
[](char a, char b) { return a == '/' && b == '/'; }),
normalized.end());
// Match the entire guest path so a mount root works with or without a trailing slash.
const auto mounted_path = Common::FixDirectorySlash(mounted_name);
const auto mounted_path = Common::FixDirectorySlash(normalized);
const auto it = std::find_if(
m_mount_pairs.begin(), m_mount_pairs.end(),
[&mounted_path](const MountPair& p) { return mounted_path.starts_with(p.point); });
if (it != m_mount_pairs.end()) {
const auto& p = *it;
auto rel_path = Common::RemoveFirst(Common::FixFilenameSlash(mounted_name), p.point.size());
auto rel_path = Common::RemoveFirst(normalized, p.point.size());
while (rel_path.starts_with('/')) {
rel_path = Common::RemoveFirst(rel_path, 1);
}
@@ -663,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();
@@ -725,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();
}
@@ -789,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();
@@ -890,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) {
@@ -939,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();
}
@@ -1010,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;
+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;
+24
View File
@@ -0,0 +1,24 @@
#ifndef EMULATOR_SRC_KERNEL_MODULE_H_
#define EMULATOR_SRC_KERNEL_MODULE_H_
#include "common/abi.h"
#include <cstddef>
#include <cstdint>
namespace Libs::LibKernel {
using KernelModule = int32_t;
struct KernelLoadModuleOpt;
struct KernelUnloadModuleOpt;
KernelModule KYTY_SYSV_ABI KernelLoadStartModule(const char* module_file_name, size_t args,
const void* argp, uint32_t flags,
const KernelLoadModuleOpt* opt, int* res);
int KYTY_SYSV_ABI KernelStopUnloadModule(KernelModule handle, size_t args, const void* argp,
uint32_t flags, const KernelUnloadModuleOpt* opt,
int* res);
} // namespace Libs::LibKernel
#endif // EMULATOR_SRC_KERNEL_MODULE_H_
@@ -66,7 +66,6 @@
<item row="1" column="1">
<widget class="QSpinBox" name="spinBox_user_id">
<property name="toolTip"><string>Local user ID exposed to games</string></property>
<property name="maximum"><number>2147483647</number></property>
</widget>
</item>
<item row="2" column="0">
@@ -175,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>
@@ -243,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>
@@ -347,6 +346,16 @@
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_keyboard_player">
<property name="text"><string>Keyboard player:</string></property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="comboBox_keyboard_player">
<property name="toolTip"><string>Gamepads fill the remaining players in connection order. Every player uses the same input mapping.</string></property>
</widget>
</item>
</layout>
</widget>
</item>
@@ -468,6 +477,31 @@
</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>
<item>
<widget class="QCheckBox" name="checkBox_support_zmask">
<property name="toolTip"><string>Can restore missing geometry (High on Life 2).</string></property>
<property name="text"><string>Support ZMask</string></property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="debugSpacer">
<property name="orientation"><enum>Qt::Vertical</enum></property>
+9
View File
@@ -48,11 +48,14 @@ struct ControllerSettings {
QString color;
int speaker_volume = 50;
int vibration_intensity = 100;
// 0 shares player 1 with the first gamepad; 1..MAX_PLAYERS dedicates that player to the keyboard.
int keyboard_player = 0;
void WriteSettings(QSettings* s) const {
s->setValue("controller_color", color);
s->setValue("controller_speaker_volume", speaker_volume);
s->setValue("controller_vibration_intensity", vibration_intensity);
s->setValue("controller_keyboard_player", keyboard_player);
}
void ReadSettings(QSettings* s) {
@@ -65,6 +68,8 @@ struct ControllerSettings {
};
speaker_volume = read_percent("controller_speaker_volume", 50);
vibration_intensity = read_percent("controller_vibration_intensity", 100);
keyboard_player = qBound(0, s->value("controller_keyboard_player", 0).toInt(),
Config::MAX_PLAYERS);
}
};
@@ -122,9 +127,11 @@ public:
bool hide_cursor_enabled = false;
bool readback_linear_images = false;
bool sync_raw_image_buffers = false;
bool support_zmask = false;
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;
@@ -160,9 +167,11 @@ public:
hide_cursor_enabled = other.hide_cursor_enabled;
readback_linear_images = other.readback_linear_images;
sync_raw_image_buffers = other.sync_raw_image_buffers;
support_zmask = other.support_zmask;
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;
+4
View File
@@ -14,9 +14,11 @@ QVariantMap Configuration::GameSettings() const {
KYTY_CFG_SET(hide_cursor_enabled);
KYTY_CFG_SET(readback_linear_images);
KYTY_CFG_SET(sync_raw_image_buffers);
KYTY_CFG_SET(support_zmask);
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);
@@ -62,9 +64,11 @@ void Configuration::ReadGameSettingsValues(const Settings& s) {
KYTY_CFG_GET(hide_cursor_enabled);
KYTY_CFG_GET(readback_linear_images);
KYTY_CFG_GET(sync_raw_image_buffers);
KYTY_CFG_GET(support_zmask);
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();
@@ -209,10 +209,19 @@ static void ListInit(QComboBox* combo, T value) {
void ConfigurationEditDialog::Init(const Configuration& info) {
m_ui->lineEdit_user_name->setMaxLength(static_cast<int>(Config::MAX_USER_NAME_LENGTH));
m_ui->lineEdit_user_name->setText(info.user_name);
m_ui->spinBox_user_id->setMaximum(Config::MAX_USER_ID);
m_ui->spinBox_user_id->setValue(info.user_id);
UpdateControllerColorButton(m_ui->button_controller_color, info.controller.color);
m_ui->slider_controller_vibration->setValue(info.controller.vibration_intensity);
m_ui->slider_controller_volume->setValue(info.controller.speaker_volume);
auto* keyboard_player = m_ui->comboBox_keyboard_player;
keyboard_player->clear();
keyboard_player->addItem(tr("Shared with player 1 (default)"), 0);
for (int player = 1; player <= Config::MAX_PLAYERS; player++) {
keyboard_player->addItem(tr("Player %1").arg(player), player);
}
keyboard_player->setCurrentIndex(
qMax(0, keyboard_player->findData(info.controller.keyboard_player)));
auto* microphone = m_ui->comboBox_audio_input_device;
microphone->clear();
microphone->addItem(tr("None"), QString {});
@@ -278,11 +287,13 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
m_ui->checkBox_hide_cursor->setChecked(info.hide_cursor_enabled);
m_ui->checkBox_readback->setChecked(info.readback_linear_images);
m_ui->checkBox_sync_raw_image_buffers->setChecked(info.sync_raw_image_buffers);
m_ui->checkBox_support_zmask->setChecked(info.support_zmask);
m_ui->checkBox_tessellation->setChecked(info.tessellation_enabled);
m_ui->checkBox_skip_notice_screen->setChecked(info.skip_notice_screen);
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);
@@ -458,6 +469,7 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
info.controller.color = ui.button_controller_color->property("controllerColor").toString();
info.controller.vibration_intensity = ui.slider_controller_vibration->value();
info.controller.speaker_volume = ui.slider_controller_volume->value();
info.controller.keyboard_player = ui.comboBox_keyboard_player->currentData().toInt();
}
info.screen_resolution =
TextToEnum<Configuration::Resolution>(ui.comboBox_screen_resolution->currentText());
@@ -469,11 +481,13 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
info.hide_cursor_enabled = ui.checkBox_hide_cursor->isChecked();
info.readback_linear_images = ui.checkBox_readback->isChecked();
info.sync_raw_image_buffers = ui.checkBox_sync_raw_image_buffers->isChecked();
info.support_zmask = ui.checkBox_support_zmask->isChecked();
info.tessellation_enabled = ui.checkBox_tessellation->isChecked();
info.skip_notice_screen = ui.checkBox_skip_notice_screen->isChecked();
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;
+3
View File
@@ -246,6 +246,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
}
args << "--controller-volume" << QString::number(info.controller.speaker_volume);
args << "--controller-vibration" << QString::number(info.controller.vibration_intensity);
args << "--keyboard-player" << QString::number(info.controller.keyboard_player);
args << "--present-mode" << EnumToText(info.present_mode);
if (info.gpu_index >= 0) {
args << "--gpu" << QString::number(info.gpu_index);
@@ -261,8 +262,10 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
}
args << "--readback-linear-images" << BoolArg(info.readback_linear_images);
args << "--sync-raw-image-buffers" << BoolArg(info.sync_raw_image_buffers);
args << "--support-zmask" << BoolArg(info.support_zmask);
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) {
+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;
}
}
+245 -124
View File
@@ -18,6 +18,7 @@
#include <atomic>
#include <cmath>
#include <cstring>
#include <memory>
#include <vector>
namespace Libs::Controller {
@@ -107,18 +108,19 @@ struct ControllerState {
class GameController {
public:
GameController() = default;
explicit GameController(int player): m_player(player) {}
KYTY_CLASS_NO_COPY(GameController);
[[nodiscard]] int Handle() const { return PAD_HANDLE + m_player; }
int InitPort();
int OpenPort(int user_id, int type, int index);
int GetPortHandle(int user_id, int type, int index);
int ClosePort(int handle);
int CheckPortHandle(int handle);
void Connect(int id);
void Disconnect(int id);
void SetDevices(int active_id);
void Button(int id, uint32_t button, bool down);
void Axis(int id, Axis axis, int value);
void RightStick(int id, int x, int y);
@@ -128,7 +130,6 @@ public:
void ResetOrientation();
void ToggleGyroEmulation(int id);
void ResetInputState();
void ReleaseHostPads();
void GetConnectionInfo(bool* flag, int* count);
void SetVibration(uint8_t large_motor, uint8_t small_motor);
int GetActiveControllerId();
@@ -145,8 +146,10 @@ private:
enum class PortState { Uninitialized, Closed, Open };
int CheckPortHandleLocked(int handle) const;
[[nodiscard]] bool PadOpenArgsAreValid(int user_id, int type, int index) const;
const int m_player;
void CheckActive();
void AddState();
// Emulated tilt: the right stick drives pitch and roll instead of the stick axes.
void UpdateEmulatedTilt();
@@ -157,7 +160,6 @@ private:
Common::Mutex m_mutex;
PortState m_port_state = PortState::Uninitialized;
std::vector<int> m_connected_ids;
int m_active_id = -1;
bool m_connected = false;
int m_connected_count = 0;
@@ -184,7 +186,39 @@ private:
std::array<std::atomic<uint32_t>, 3> m_setting_steps {};
};
static GameController* g_controller = nullptr;
static std::array<std::unique_ptr<GameController>, Config::MAX_PLAYERS> g_players;
// Maps host gamepads to players; the keyboard follows the configured keyboard player.
static Common::Mutex g_router_mutex;
static std::vector<std::pair<int, int>> g_pad_players;
static uint32_t g_keyboard_player = 0;
static void RefreshPlayers();
static void StopHostPad(SDL_Gamepad* pad);
static int KeyboardPlayer() {
return g_keyboard_player == 0 ? 0 : static_cast<int>(g_keyboard_player) - 1;
}
// Caller holds g_router_mutex.
static GameController* PlayerForDevice(int id) {
if (id == HOST_INPUT_CONTROLLER_ID) {
return g_players[KeyboardPlayer()].get();
}
for (const auto& [pad, player]: g_pad_players) {
if (pad == id) {
return player >= 0 ? g_players[player].get() : nullptr;
}
}
return nullptr;
}
static GameController* PlayerForHandle(int handle) {
return handle >= PAD_HANDLE && handle < PAD_HANDLE + Config::MAX_PLAYERS
? g_players[handle - PAD_HANDLE].get()
: nullptr;
}
static void pad_fill_data(PadData* data, const ControllerState& state, bool connected,
int connected_count) {
@@ -228,13 +262,15 @@ constexpr std::array SPEAKER_VOLUME = {1.0f, 0.0f, 0.02f, 0.12f, 0.42f};
constexpr std::array INTENSITY = {1.0f, 0.0f, 0.33f, 0.66f};
void CycleSetting(Setting setting) {
if (g_controller != nullptr) {
g_controller->CycleSetting(setting);
for (const auto& player: g_players) {
if (player != nullptr) {
player->CycleSetting(setting);
}
}
}
float GetSettingScale(Setting setting) {
return g_controller != nullptr ? g_controller->GetSettingScale(setting) : 1.0f;
return g_players[0] != nullptr ? g_players[0]->GetSettingScale(setting) : 1.0f;
}
void GameController::CycleSetting(Setting setting) {
@@ -404,35 +440,53 @@ static int32_t TriggerEffectState(const PadTriggerEffectCommand& command, int va
}
void Initialize() {
EXIT_IF(g_controller != nullptr);
EXIT_IF(g_players[0] != nullptr);
g_controller = new GameController;
g_controller->Connect(HOST_INPUT_CONTROLLER_ID);
for (int i = 0; i < Config::MAX_PLAYERS; i++) {
g_players[i] = std::make_unique<GameController>(i);
}
g_keyboard_player = Config::GetKeyboardPlayer();
RefreshPlayers();
}
void Shutdown() {
EmergencyShutdown();
delete g_controller;
g_controller = nullptr;
for (auto& player: g_players) {
player.reset();
}
}
void EmergencyShutdown() {
if (g_controller != nullptr) {
g_controller->ReleaseHostPads();
Common::LockGuard lock(g_router_mutex);
DualSenseHaptics::Shutdown();
std::vector<SDL_Gamepad*> pads;
for (const auto& [id, player]: g_pad_players) {
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(id))) {
StopHostPad(pad);
pads.push_back(pad);
}
}
if (!pads.empty()) {
SDL_Delay(RELEASE_FLUSH_MS);
for (auto* pad: pads) {
SDL_CloseGamepad(pad);
}
}
g_pad_players.clear();
for (const auto& player: g_players) {
if (player != nullptr) {
player->SetDevices(-1);
}
}
}
void GameController::Connect(int id) {
void GameController::SetDevices(int active_id) {
Common::LockGuard lock(m_mutex);
if (std::find(m_connected_ids.begin(), m_connected_ids.end(), id) != m_connected_ids.end()) {
if (m_active_id == active_id) {
return;
}
m_connected_ids.push_back(id);
if (id != HOST_INPUT_CONTROLLER_ID) {
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(id));
if (active_id != HOST_INPUT_CONTROLLER_ID && active_id != -1) {
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(active_id));
pad != nullptr) {
if (const auto& color = Config::GetControllerColor()) {
(void)SDL_SetGamepadLED(pad, (*color)[0], (*color)[1], (*color)[2]);
@@ -446,42 +500,11 @@ void GameController::Connect(int id) {
}
}
CheckActive();
}
void GameController::Disconnect(int id) {
Common::LockGuard lock(m_mutex);
const auto it = std::find(m_connected_ids.begin(), m_connected_ids.end(), id);
EXIT_IF(it == m_connected_ids.end());
m_connected_ids.erase(it);
CheckActive();
}
void GameController::CheckActive() {
int new_active_id = -1;
bool new_connected = false;
if (!m_connected_ids.empty()) {
new_active_id = m_connected_ids[0];
for (const auto id: m_connected_ids) {
if (id != HOST_INPUT_CONTROLLER_ID) {
new_active_id = id;
break;
}
}
new_connected = true;
}
if (m_connected == new_connected && m_active_id == new_active_id) {
return;
}
const bool new_connected = active_id != -1;
if (!m_connected && new_connected) {
m_connected_count++;
}
m_active_id = new_active_id;
m_active_id = active_id;
m_connected = new_connected;
m_state = {};
m_gyro_time = 0;
@@ -513,7 +536,7 @@ void GameController::AddState() {
void GameController::Button(int id, uint32_t button, bool down) {
Common::LockGuard lock(m_mutex);
// The keyboard shares the player-1 pad with the active gamepad.
// Keyboard events are routed to the configured player's port.
if (m_active_id == id || id == HOST_INPUT_CONTROLLER_ID) {
m_state.time = LibKernel::KernelGetProcessTime();
@@ -798,39 +821,16 @@ void GameController::ResetInputState() {
AddState();
}
void GameController::ReleaseHostPads() {
Common::LockGuard lock(m_mutex);
DualSenseHaptics::Shutdown();
std::vector<SDL_Gamepad*> pads;
for (const auto id: m_connected_ids) {
if (id == HOST_INPUT_CONTROLLER_ID) {
continue;
}
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(id));
pad != nullptr) {
if (SDL_GetGamepadType(pad) == SDL_GAMEPAD_TYPE_PS5) {
DualSenseEffects effect {};
effect.enable_bits = 0x0c;
effect.right_trigger[0] = 0x05;
effect.left_trigger[0] = 0x05;
(void)SDL_SendGamepadEffect(pad, &effect, sizeof(effect));
}
(void)SDL_RumbleGamepad(pad, 0, 0, 0);
(void)SDL_SetGamepadLED(pad, 0, 0, 0);
pads.push_back(pad);
}
static void StopHostPad(SDL_Gamepad* pad) {
if (SDL_GetGamepadType(pad) == SDL_GAMEPAD_TYPE_PS5) {
DualSenseEffects effect {};
effect.enable_bits = 0x0c;
effect.right_trigger[0] = 0x05;
effect.left_trigger[0] = 0x05;
(void)SDL_SendGamepadEffect(pad, &effect, sizeof(effect));
}
if (!pads.empty()) {
SDL_Delay(RELEASE_FLUSH_MS);
for (auto* pad: pads) {
SDL_CloseGamepad(pad);
}
}
m_connected_ids.clear();
CheckActive();
(void)SDL_RumbleGamepad(pad, 0, 0, 0);
(void)SDL_SetGamepadLED(pad, 0, 0, 0);
}
void GameController::SetVibration(uint8_t large_motor, uint8_t small_motor) {
@@ -847,7 +847,9 @@ void GameController::ApplyVibration() {
const auto scale = duration != 0 ? GetSettingScale(Setting::VibrationIntensity) : 0.0f;
const auto large_motor = Scale(m_vibration[0], scale);
const auto small_motor = Scale(m_vibration[1], scale);
if (DualSenseHaptics::SetVibration(m_active_id, large_motor, small_motor, duration)) {
// The DualSense haptics backend drives a single pad, so only player 1 uses it.
if (m_player == 0 &&
DualSenseHaptics::SetVibration(m_active_id, large_motor, small_motor, duration)) {
return;
}
@@ -999,49 +1001,149 @@ int GameController::ReadStates(ControllerState* states, int states_num, bool* fl
return ret_num;
}
// Keep every opened pad, including those waiting for a free player slot.
// Caller holds g_router_mutex.
static void RefreshPlayers() {
std::array<bool, Config::MAX_PLAYERS> taken {};
const int keyboard = KeyboardPlayer();
if (g_keyboard_player != 0) {
taken[keyboard] = true;
}
for (auto& [id, player]: g_pad_players) {
if (g_keyboard_player != 0 && player == keyboard) {
player = -1;
}
if (player >= 0) {
taken[player] = true;
}
}
for (auto& [id, player]: g_pad_players) {
if (player >= 0) {
continue;
}
const auto free = std::find(taken.begin(), taken.end(), false);
if (free != taken.end()) {
player = static_cast<int>(free - taken.begin());
*free = true;
LOGF("Controller %d assigned to player %d\n", id, player + 1);
}
}
std::array<int, Config::MAX_PLAYERS> active;
active.fill(-1);
active[keyboard] = HOST_INPUT_CONTROLLER_ID;
for (const auto& [id, player]: g_pad_players) {
if (player >= 0) {
active[player] = id;
}
}
// Stop old owners before configuring new owners of the same physical pad.
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
const int previous = g_players[player]->GetActiveControllerId();
if (previous != active[player]) {
if (player == 0) {
(void)DualSenseHaptics::SetVibration(previous, 0, 0, 0);
}
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(previous))) {
StopHostPad(pad);
}
}
}
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
g_players[player]->SetDevices(active[player]);
}
}
void Connect(int id) {
g_controller->Connect(id);
Common::LockGuard lock(g_router_mutex);
if (std::any_of(g_pad_players.begin(), g_pad_players.end(),
[id](const auto& pad) { return pad.first == id; })) {
return;
}
g_pad_players.emplace_back(id, -1);
RefreshPlayers();
}
void Disconnect(int id) {
g_controller->Disconnect(id);
Common::LockGuard lock(g_router_mutex);
std::erase_if(g_pad_players, [id](const auto& pad) { return pad.first == id; });
RefreshPlayers();
}
void CycleKeyboardPlayer() {
Common::LockGuard lock(g_router_mutex);
g_keyboard_player = (g_keyboard_player + 1) % (Config::MAX_PLAYERS + 1);
RefreshPlayers();
if (g_keyboard_player == 0) {
LOGF("Keyboard shared with player 1\n");
} else {
LOGF("Keyboard assigned to player %u\n", g_keyboard_player);
}
}
void SetButton(int id, uint32_t button, bool down) {
g_controller->Button(id, button, down);
Common::LockGuard lock(g_router_mutex);
if (auto* player = PlayerForDevice(id)) {
player->Button(id, button, down);
}
}
void SetAxis(int id, Axis axis, int value) {
g_controller->Axis(id, axis, value);
Common::LockGuard lock(g_router_mutex);
if (auto* player = PlayerForDevice(id)) {
player->Axis(id, axis, value);
}
}
void SetRightStick(int id, int x, int y) {
g_controller->RightStick(id, x, y);
Common::LockGuard lock(g_router_mutex);
if (auto* player = PlayerForDevice(id)) {
player->RightStick(id, x, y);
}
}
void SetTouchPad(int id, int finger, bool down, float x, float y) {
g_controller->TouchPad(id, finger, down, x, y);
Common::LockGuard lock(g_router_mutex);
if (auto* player = PlayerForDevice(id)) {
player->TouchPad(id, finger, down, x, y);
}
}
void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us) {
g_controller->Motion(id, sensor, data, time_us);
Common::LockGuard lock(g_router_mutex);
if (auto* player = PlayerForDevice(id)) {
player->Motion(id, sensor, data, time_us);
}
}
void ToggleGyroEmulation(int id) {
g_controller->ToggleGyroEmulation(id);
Common::LockGuard lock(g_router_mutex);
if (auto* player = PlayerForDevice(id)) {
player->ToggleGyroEmulation(id);
}
}
void ResetInputState() {
g_controller->ResetInputState();
for (const auto& player: g_players) {
if (player != nullptr) {
player->ResetInputState();
}
}
}
int GetActiveControllerId() {
return g_controller != nullptr ? g_controller->GetActiveControllerId() : -1;
return g_players[0] != nullptr ? g_players[0]->GetActiveControllerId() : -1;
}
static bool PadOpenArgsAreValid(int user_id, int type, int index) {
// The host keyboard and gamepad feed one standard port for the configured user.
return user_id == Config::GetUserId() && type == 0 && index == 0;
bool IsPlayerConnected(int player) {
if (player < 0 || player >= Config::MAX_PLAYERS || g_players[player] == nullptr) {
return false;
}
return g_players[player]->GetActiveControllerId() != -1;
}
bool GameController::PadOpenArgsAreValid(int user_id, int type, int index) const {
// The host keyboard and gamepads of a player feed one standard port for that player's user.
return user_id == Config::GetPlayerUserId(m_player) && type == 0 && index == 0;
}
int GameController::InitPort() {
@@ -1064,7 +1166,7 @@ int GameController::OpenPort(int user_id, int type, int index) {
return PAD_ERROR_ALREADY_OPENED;
}
m_port_state = PortState::Open;
return PAD_HANDLE;
return Handle();
}
int GameController::GetPortHandle(int user_id, int type, int index) {
@@ -1072,15 +1174,16 @@ int GameController::GetPortHandle(int user_id, int type, int index) {
if (m_port_state == PortState::Uninitialized) {
return PAD_ERROR_NOT_INITIALIZED;
}
return m_port_state == PortState::Open && PadOpenArgsAreValid(user_id, type, index) ? PAD_HANDLE
: PAD_ERROR_NO_HANDLE;
return m_port_state == PortState::Open && PadOpenArgsAreValid(user_id, type, index)
? Handle()
: PAD_ERROR_NO_HANDLE;
}
int GameController::CheckPortHandleLocked(int handle) const {
if (m_port_state == PortState::Uninitialized) {
return PAD_ERROR_NOT_INITIALIZED;
}
return m_port_state == PortState::Open && handle == PAD_HANDLE ? OK : PAD_ERROR_INVALID_HANDLE;
return m_port_state == PortState::Open && handle == Handle() ? OK : PAD_ERROR_INVALID_HANDLE;
}
int GameController::CheckPortHandle(int handle) {
@@ -1098,12 +1201,19 @@ int GameController::ClosePort(int handle) {
}
int PadCheckHandle(int handle) {
return g_controller->CheckPortHandle(handle);
if (g_players[0] == nullptr) {
return PAD_ERROR_NOT_INITIALIZED;
}
auto* player = PlayerForHandle(handle);
return player != nullptr ? player->CheckPortHandle(handle) : PAD_ERROR_INVALID_HANDLE;
}
int KYTY_SYSV_ABI PadInit() {
PRINT_NAME();
return g_controller->InitPort();
for (const auto& player: g_players) {
(void)player->InitPort();
}
return OK;
}
int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param) {
@@ -1115,7 +1225,8 @@ int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param) {
"\t param = 0x%016" PRIx64 "\n",
user_id, type, index, reinterpret_cast<uint64_t>(param));
return g_controller->OpenPort(user_id, type, index);
const int player = Config::GetPlayerFromUserId(user_id);
return player >= 0 ? g_players[player]->OpenPort(user_id, type, index) : PAD_ERROR_INVALID_ARG;
}
int KYTY_SYSV_ABI PadGetHandle(int user_id, int type, int index) {
@@ -1126,13 +1237,19 @@ int KYTY_SYSV_ABI PadGetHandle(int user_id, int type, int index) {
"\t index = %d\n",
user_id, type, index);
return g_controller->GetPortHandle(user_id, type, index);
const int player = Config::GetPlayerFromUserId(user_id);
return player >= 0 ? g_players[player]->GetPortHandle(user_id, type, index)
: PAD_ERROR_NO_HANDLE;
}
int KYTY_SYSV_ABI PadClose(int handle) {
PRINT_NAME();
LOGF("\t handle = %d\n", handle);
return g_controller->ClosePort(handle);
auto* player = PlayerForHandle(handle);
if (player == nullptr) {
return PAD_ERROR_INVALID_HANDLE;
}
return player->ClosePort(handle);
}
int KYTY_SYSV_ABI PadSetMotionSensorState(int handle, bool enable) {
@@ -1143,7 +1260,7 @@ int KYTY_SYSV_ABI PadSetMotionSensorState(int handle, bool enable) {
}
LOGF("\t enable = %s\n", (enable ? "true" : "false"));
g_controller->SetMotionSensorState(enable);
PlayerForHandle(handle)->SetMotionSensorState(enable);
return OK;
}
@@ -1167,7 +1284,7 @@ int KYTY_SYSV_ABI PadResetOrientation(int handle) {
return error;
}
g_controller->ResetOrientation();
PlayerForHandle(handle)->ResetOrientation();
return OK;
}
@@ -1183,7 +1300,7 @@ int KYTY_SYSV_ABI PadGetControllerInformation(int handle, PadControllerInformati
int connected_count = 0;
bool connected = false;
g_controller->GetConnectionInfo(&connected, &connected_count);
PlayerForHandle(handle)->GetConnectionInfo(&connected, &connected_count);
std::memset(info, 0, sizeof(*info));
@@ -1228,7 +1345,7 @@ int KYTY_SYSV_ABI PadReadState(int handle, PadData* data) {
bool connected = false;
ControllerState state;
g_controller->ReadState(&state, &connected, &connected_count);
PlayerForHandle(handle)->ReadState(&state, &connected, &connected_count);
pad_fill_data(data, state, connected, connected_count);
@@ -1252,11 +1369,12 @@ int KYTY_SYSV_ABI PadRead(int handle, PadData* data, int num) {
bool connected = false;
ControllerState states[64] = {};
int ret_num = g_controller->ReadStates(states, num, &connected, &connected_count);
auto* player = PlayerForHandle(handle);
int ret_num = player->ReadStates(states, num, &connected, &connected_count);
if (!connected || ret_num == 0) {
if (connected) {
g_controller->ReadState(&states[0], &connected, &connected_count);
player->ReadState(&states[0], &connected, &connected_count);
}
ret_num = 1;
}
@@ -1270,6 +1388,7 @@ int KYTY_SYSV_ABI PadRead(int handle, PadData* data, int num) {
int KYTY_SYSV_ABI PadSetVibration(int handle, const PadVibrationParam* param) {
PRINT_NAME();
Common::LockGuard lock(g_router_mutex);
if (const auto error = PadCheckHandle(handle); error != OK) {
return error;
@@ -1282,7 +1401,7 @@ int KYTY_SYSV_ABI PadSetVibration(int handle, const PadVibrationParam* param) {
"\t small_motor = %d\n",
static_cast<int>(param->large_motor), static_cast<int>(param->small_motor));
g_controller->SetVibration(param->large_motor, param->small_motor);
PlayerForHandle(handle)->SetVibration(param->large_motor, param->small_motor);
return OK;
}
@@ -1299,6 +1418,7 @@ int KYTY_SYSV_ABI PadResetLightBar(int handle) {
int KYTY_SYSV_ABI PadSetLightBar(int handle, const PadLightBarParam* param) {
PRINT_NAME();
Common::LockGuard lock(g_router_mutex);
if (const auto error = PadCheckHandle(handle); error != OK) {
return error;
@@ -1307,13 +1427,14 @@ int KYTY_SYSV_ABI PadSetLightBar(int handle, const PadLightBarParam* param) {
return PAD_ERROR_INVALID_ARG;
}
g_controller->SetLightBar(param->r, param->g, param->b);
PlayerForHandle(handle)->SetLightBar(param->r, param->g, param->b);
return OK;
}
int KYTY_SYSV_ABI PadSetTriggerEffect(int handle, const PadTriggerEffectParam* param) {
PRINT_NAME();
Common::LockGuard lock(g_router_mutex);
if (const auto error = PadCheckHandle(handle); error != OK) {
return error;
@@ -1322,7 +1443,7 @@ int KYTY_SYSV_ABI PadSetTriggerEffect(int handle, const PadTriggerEffectParam* p
return PAD_ERROR_INVALID_ARG;
}
return g_controller->SetTriggerEffect(*param) ? OK : PAD_ERROR_INVALID_ARG;
return PlayerForHandle(handle)->SetTriggerEffect(*param) ? OK : PAD_ERROR_INVALID_ARG;
}
int KYTY_SYSV_ABI PadGetTriggerEffectState(int handle, PadTriggerEffectStateInformation* info) {
@@ -1337,7 +1458,7 @@ int KYTY_SYSV_ABI PadGetTriggerEffectState(int handle, PadTriggerEffectStateInfo
return PAD_ERROR_INVALID_ARG;
}
g_controller->GetTriggerEffectState(info);
PlayerForHandle(handle)->GetTriggerEffectState(info);
return OK;
}
+4
View File
@@ -68,6 +68,8 @@ inline int controller_get_axis(int min, int max, int value) {
void Connect(int id);
void Disconnect(int id);
// Moves the keyboard to the next player (shared with player 1, then 1-4) while the game runs.
void CycleKeyboardPlayer();
void SetButton(int id, uint32_t button, bool down);
void SetAxis(int id, Axis axis, int value);
void SetRightStick(int id, int x, int y);
@@ -76,6 +78,8 @@ void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us);
void ToggleGyroEmulation(int id);
void ResetInputState();
int GetActiveControllerId();
// True when the player has a keyboard or gamepad assigned.
bool IsPlayerConnected(int player);
enum class Setting { SpeakerVolume, VibrationIntensity, TriggerEffectIntensity };
+22
View File
@@ -25,6 +25,9 @@
#include <string>
#include <thread>
#include <unordered_map>
#include <atomic>
#include <cinttypes>
#include <iterator>
#include <variant>
#include <vector>
@@ -176,6 +179,14 @@ static int GetHostPathStat(const std::filesystem::path& host_path, LibKernel::Fi
return OK;
}
// Mounts can appear after a failed lookup (e.g. additional content), so forget negative results.
void InvalidateFailedResolves() {
std::scoped_lock lock(g_mutex);
for (auto it = g_resolved_paths.begin(); it != g_resolved_paths.end();) {
it = it->second.result != OK ? g_resolved_paths.erase(it) : std::next(it);
}
}
static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size) {
if (guest_path == nullptr || guest_path[0] == '\0') {
return LibKernel::KERNEL_ERROR_EINVAL;
@@ -225,6 +236,10 @@ static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size)
log_missing = true;
}
}
if (!log_missing && info.result == OK && path.starts_with("/addcont")) {
LOGF("\tAPR resolve: %s -> %s (%" PRIu64 " bytes)\n", guest_path,
Common::PathToString(info.host_path).c_str(), info.file_size);
}
if (log_missing) {
LOGF("\tAPR resolve missing path: %s -> %s\n", guest_path, Common::PathToString(info.host_path).c_str());
}
@@ -1181,6 +1196,13 @@ static int ExecuteCommand(const CommandBufferState::Command& entry) {
uint64_t bytes_read = 0;
auto result = ReadHostFileToGuest(host_path, command.file_offset, command.destination,
command.size, &bytes_read);
static std::atomic<uint32_t> s_logged_dlc_reads {0};
if (host_path.filename() == "re_dlc_000.pak" && s_logged_dlc_reads.fetch_add(1) < 32) {
LOGF("\tAPR read dlc: id=0x%08" PRIx32 ", offset=0x%016" PRIx64 ", size=0x%016" PRIx64
", read=0x%016" PRIx64 ", result=0x%08" PRIx32 "\n",
command.file_id, command.file_offset, command.size, bytes_read,
static_cast<uint32_t>(result));
}
if (result != OK) {
LOGF("\tAPR submit read failed: id=0x%08" PRIx32 ", result=0x%08" PRIx32 ", path=%s\n",
command.file_id, static_cast<uint32_t>(result), Common::PathToString(host_path).c_str());
+20 -5
View File
@@ -22,13 +22,16 @@
namespace Libs {
namespace AprShared {
void InvalidateFailedResolves();
}
LIB_VERSION("AppContent", 1, "AppContentUtil", 1, 1);
namespace AppContent {
constexpr uint32_t APP_CONTENT_APPPARAM_SKU_FLAG_FULL = 3;
constexpr int APP_CONTENT_ERROR_PARAMETER = -2133262334; /* 0x80D90002 */
constexpr int APP_CONTENT_ERROR_BUSY = -2133262333; /* 0x80D90003 */
constexpr int APP_CONTENT_ERROR_NOT_MOUNTED = -2133262332; /* 0x80D90004 */
constexpr int APP_CONTENT_ERROR_NOT_FOUND = -2133262331; /* 0x80D90005 */
constexpr int APP_CONTENT_ERROR_MOUNT_FULL = -2133262330; /* 0x80D90006 */
@@ -207,7 +210,10 @@ int KYTY_SYSV_ABI AppContentAddcontMount(uint32_t servic
for (const auto& m: g_addcont_mounts) {
if (m.label == entry->entitlement_label) {
return APP_CONTENT_ERROR_BUSY;
const auto guest_point = m.point.substr(0, m.point.size() - 1);
std::strncpy(mount_point->data, guest_point.c_str(), sizeof(mount_point->data) - 1);
LOGF("\t already mounted %s\n", guest_point.c_str());
return OK;
}
}
@@ -232,7 +238,15 @@ int KYTY_SYSV_ABI AppContentAddcontMount(uint32_t servic
LOGF("\t mounted %s -> %s\n", mount.point.c_str(),
Common::PathToString(entry->mount_dir).c_str());
std::strncpy(mount_point->data, mount.point.c_str(), sizeof(mount_point->data) - 1);
AprShared::InvalidateFailedResolves();
// Titles scan /download0/pdlc for downloaded content; it must exist even when empty.
if (const auto pdlc = LibKernel::FileSystem::GetRealFilename("/download0/pdlc"); !pdlc.empty()) {
std::filesystem::create_directories(pdlc, ec);
}
const auto guest_point = mount.point.substr(0, mount.point.size() - 1);
std::strncpy(mount_point->data, guest_point.c_str(), sizeof(mount_point->data) - 1);
g_addcont_mounts.push_back(std::move(mount));
return OK;
@@ -251,8 +265,8 @@ int KYTY_SYSV_ABI AppContentAddcontUnmount(const AppContentMountPoint* mount_poi
return APP_CONTENT_ERROR_NOT_MOUNTED;
}
const std::string point(mount_point->data,
strnlen(mount_point->data, sizeof(mount_point->data)));
const std::string point = Common::FixDirectorySlash(
std::string_view(mount_point->data, strnlen(mount_point->data, sizeof(mount_point->data))));
std::lock_guard lock(g_addcont_mutex);
@@ -263,6 +277,7 @@ int KYTY_SYSV_ABI AppContentAddcontUnmount(const AppContentMountPoint* mount_poi
}
LibKernel::FileSystem::Umount(point);
g_addcont_mounts.erase(it);
AprShared::InvalidateFailedResolves();
return OK;
}
+13 -7
View File
@@ -11,6 +11,7 @@
#include "kernel/eventQueue.h"
#include "kernel/fileSystem.h"
#include "kernel/memory.h"
#include "kernel/module.h"
#include "kernel/pthread.h"
#include "kernel/semaphore.h"
#include "kernel/syncOnAddress.h"
@@ -65,7 +66,6 @@ LIB_DEFINE(InitLibKernel_1_Apr);
namespace LibKernel {
using KernelModule = int32_t;
using get_thread_atexit_count_func_t = KYTY_SYSV_ABI int (*)(KernelModule);
using thread_atexit_report_func_t = KYTY_SYSV_ABI void (*)(KernelModule);
@@ -1201,9 +1201,9 @@ static int KYTY_SYSV_ABI KernelRaiseException(Pthread thread, int signum) {
return OK;
}
static KYTY_SYSV_ABI KernelModule KernelLoadStartModule(const char* module_file_name, size_t args,
const void* argp, uint32_t flags,
const KernelLoadModuleOpt* opt, int* res) {
KernelModule KYTY_SYSV_ABI KernelLoadStartModule(const char* module_file_name, size_t args,
const void* argp, uint32_t flags,
const KernelLoadModuleOpt* opt, int* res) {
PRINT_NAME();
// EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
@@ -1255,9 +1255,9 @@ static KYTY_SYSV_ABI KernelModule KernelLoadStartModule(const char* module_file_
return static_cast<KernelModule>(handle);
}
static int KYTY_SYSV_ABI KernelStopUnloadModule(KernelModule handle, size_t args, const void* argp,
uint32_t flags, const KernelUnloadModuleOpt* opt,
int* res) {
int KYTY_SYSV_ABI KernelStopUnloadModule(KernelModule handle, size_t args, const void* argp,
uint32_t flags, const KernelUnloadModuleOpt* opt,
int* res) {
PRINT_NAME();
// EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
@@ -2006,6 +2006,11 @@ int KYTY_SYSV_ABI listen(int s, int backlog) {
return Network::Net::Listen(s, backlog);
}
int KYTY_SYSV_ABI shutdown(int s, int how) {
PRINT_NAME();
return Network::Net::Shutdown(s, how);
}
int KYTY_SYSV_ABI accept(int s, void* addr, uint32_t* addrlen) {
PRINT_NAME();
return Network::Net::Accept(s, addr, addrlen);
@@ -2242,6 +2247,7 @@ LIB_DEFINE(InitLibKernel_1_Posix) {
LIB_FUNC("KuOmgKoqCdY", Posix::bind);
LIB_FUNC("XVL8So3QJUk", Posix::connect);
LIB_FUNC("pxnCmagrtao", Posix::listen);
LIB_FUNC("TUuiYS2kE8s", Posix::shutdown);
LIB_FUNC("3e+4Iv7IJ8U", Posix::accept);
LIB_FUNC("RenI1lL1WFk", Posix::getsockname);
LIB_FUNC("6O8EwYOgH9Y", Posix::getsockopt);
+8 -2
View File
@@ -221,11 +221,17 @@ int KYTY_SYSV_ABI PlayGoGetChunkId(int handle, uint16_t* out_chunk_id_list,
if (out_entries == nullptr) {
return PLAYGO_ERROR_BAD_POINTER;
}
if (number_of_entries != 0 && out_chunk_id_list == nullptr) {
return PLAYGO_ERROR_BAD_POINTER;
if (out_chunk_id_list != nullptr && number_of_entries == 0) {
return PLAYGO_ERROR_BAD_SIZE;
}
ensure_chunks_loaded();
// A null output buffer asks for the total chunk count.
if (out_chunk_id_list == nullptr) {
*out_entries = g_chunks_num;
return OK;
}
const uint32_t entries = std::min(number_of_entries, g_chunks_num);
for (uint32_t i = 0; i < entries; i++) {
out_chunk_id_list[i] = static_cast<uint16_t>(i);

Some files were not shown because too many files have changed in this diff Show More