mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-22 20:06:54 +02:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3e7ec8285 | |||
| 763d3be3a9 | |||
| d8c4a9af38 | |||
| 6aa3846fb2 | |||
| 415387f00d | |||
| 890393fbe6 | |||
| 9f88043ebe | |||
| c2173801d4 | |||
| 56d03f16e9 | |||
| 3ecc6b7d1b | |||
| 5bf37f52ad | |||
| 57791645e4 | |||
| d1fe1324c8 | |||
| b485dd24be | |||
| ae25558321 | |||
| ec08ea197b | |||
| f6f1ac8b40 |
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "تم كسب الكأس!"
|
||||
"trophy.toast.earned": "تم كسب الكأس!",
|
||||
"user.guest": "ضيف"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofej získána!"
|
||||
"trophy.toast.earned": "Trofej získána!",
|
||||
"user.guest": "Host"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofæ optjent!"
|
||||
"trophy.toast.earned": "Trofæ optjent!",
|
||||
"user.guest": "Gæst"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophäe erhalten!"
|
||||
"trophy.toast.earned": "Trophäe erhalten!",
|
||||
"user.guest": "Gast"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Κερδίσατε ένα τρόπαιο!"
|
||||
"trophy.toast.earned": "Κερδίσατε ένα τρόπαιο!",
|
||||
"user.guest": "Επισκ."
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophy earned!"
|
||||
"trophy.toast.earned": "Trophy earned!",
|
||||
"user.guest": "Guest"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophy earned!"
|
||||
"trophy.toast.earned": "Trophy earned!",
|
||||
"user.guest": "Guest"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "¡Ganaste un trofeo!"
|
||||
"trophy.toast.earned": "¡Ganaste un trofeo!",
|
||||
"user.guest": "Invitado"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "¡Ganaste un trofeo!"
|
||||
"trophy.toast.earned": "¡Ganaste un trofeo!",
|
||||
"user.guest": "Invitado"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Pokaali ansaittu!"
|
||||
"trophy.toast.earned": "Pokaali ansaittu!",
|
||||
"user.guest": "Vieras"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophée obtenu !"
|
||||
"trophy.toast.earned": "Trophée obtenu !",
|
||||
"user.guest": "Invité"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophée obtenu !"
|
||||
"trophy.toast.earned": "Trophée obtenu !",
|
||||
"user.guest": "Invité"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trófea megszerezve!"
|
||||
"trophy.toast.earned": "Trófea megszerezve!",
|
||||
"user.guest": "Vendég"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofi diperoleh!"
|
||||
"trophy.toast.earned": "Trofi diperoleh!",
|
||||
"user.guest": "Tamu"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofeo ottenuto!"
|
||||
"trophy.toast.earned": "Trofeo ottenuto!",
|
||||
"user.guest": "Ospite"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "トロフィーを獲得しました!"
|
||||
"trophy.toast.earned": "トロフィーを獲得しました!",
|
||||
"user.guest": "ゲスト"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "트로피를 획득했습니다!"
|
||||
"trophy.toast.earned": "트로피를 획득했습니다!",
|
||||
"user.guest": "게스트"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofee behaald!"
|
||||
"trophy.toast.earned": "Trofee behaald!",
|
||||
"user.guest": "Gast"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofé opptjent!"
|
||||
"trophy.toast.earned": "Trofé opptjent!",
|
||||
"user.guest": "Gjest"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Zdobyto trofeum!"
|
||||
"trophy.toast.earned": "Zdobyto trofeum!",
|
||||
"user.guest": "Gość"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Troféu conquistado!"
|
||||
"trophy.toast.earned": "Troféu conquistado!",
|
||||
"user.guest": "Convidado"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Troféu conquistado!"
|
||||
"trophy.toast.earned": "Troféu conquistado!",
|
||||
"user.guest": "Convidado"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofeu câștigat!"
|
||||
"trophy.toast.earned": "Trofeu câștigat!",
|
||||
"user.guest": "Invitat"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Трофей получен!"
|
||||
"trophy.toast.earned": "Трофей получен!",
|
||||
"user.guest": "Гость"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofé intjänad!"
|
||||
"trophy.toast.earned": "Trofé intjänad!",
|
||||
"user.guest": "Gäst"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "ได้รับถ้วยรางวัลแล้ว!"
|
||||
"trophy.toast.earned": "ได้รับถ้วยรางวัลแล้ว!",
|
||||
"user.guest": "แขก"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Kupa kazanıldı!"
|
||||
"trophy.toast.earned": "Kupa kazanıldı!",
|
||||
"user.guest": "Misafir"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Đã nhận cúp!"
|
||||
"trophy.toast.earned": "Đã nhận cúp!",
|
||||
"user.guest": "Khách"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "已获得奖杯!"
|
||||
"trophy.toast.earned": "已获得奖杯!",
|
||||
"user.guest": "访客"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "已獲得獎盃!"
|
||||
"trophy.toast.earned": "已獲得獎盃!",
|
||||
"user.guest": "訪客"
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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_ */
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,86 @@
|
||||
#ifndef KYTY_COMMON_HOST_STATS_H_
|
||||
#define KYTY_COMMON_HOST_STATS_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Common {
|
||||
|
||||
enum class MemoryPressure { Unknown, Normal, Warning, Critical };
|
||||
enum class ThermalPressure { Unknown, Nominal, Moderate, Heavy, Critical };
|
||||
|
||||
struct ThreadUsage {
|
||||
std::string name;
|
||||
double cpu_percent = 0.0; // Of one core.
|
||||
};
|
||||
|
||||
// Host-wide and process-wide resource usage. Missing values are std::nullopt; rates are per
|
||||
// second since the previous sample.
|
||||
struct HostStats {
|
||||
uint32_t logical_cores = 0;
|
||||
|
||||
// Processing.
|
||||
std::optional<double> process_cpu_percent; // Share of all logical cores, 0-100.
|
||||
std::optional<double> system_cpu_percent; // Whole machine, 0-100.
|
||||
std::optional<uint32_t> thread_count;
|
||||
std::vector<ThreadUsage> busiest_threads; // Busiest first.
|
||||
std::optional<double> context_switches_per_s;
|
||||
std::optional<double> page_faults_per_s;
|
||||
std::optional<double> page_ins_per_s; // Faults that read from disk.
|
||||
ThermalPressure thermal_pressure = ThermalPressure::Unknown;
|
||||
|
||||
// Emulator process memory.
|
||||
std::optional<uint64_t> process_memory; // Physical footprint / resident set.
|
||||
std::optional<uint64_t> process_resident;
|
||||
std::optional<uint64_t> process_compressed; // Compressed or swapped out.
|
||||
std::optional<uint64_t> process_file_backed;
|
||||
std::optional<uint64_t> process_virtual; // Reserved address space.
|
||||
|
||||
// System memory.
|
||||
std::optional<uint64_t> system_memory_used;
|
||||
std::optional<uint64_t> system_memory_total;
|
||||
std::optional<uint64_t> system_wired;
|
||||
std::optional<uint64_t> system_compressed;
|
||||
std::optional<uint64_t> system_cached; // File cache, reclaimable.
|
||||
std::optional<uint64_t> system_free;
|
||||
std::optional<uint64_t> swap_used;
|
||||
std::optional<uint64_t> swap_total;
|
||||
MemoryPressure memory_pressure = MemoryPressure::Unknown;
|
||||
|
||||
// Disk I/O of the emulator process.
|
||||
std::optional<double> disk_read_per_s;
|
||||
std::optional<double> disk_write_per_s;
|
||||
|
||||
// GPU, whole device (macOS IOAccelerator).
|
||||
std::string gpu_model;
|
||||
std::optional<uint32_t> gpu_cores;
|
||||
std::optional<double> gpu_percent;
|
||||
std::optional<double> gpu_renderer_percent;
|
||||
std::optional<double> gpu_tiler_percent;
|
||||
std::optional<uint64_t> gpu_memory_in_use; // System memory the GPU driver has in use.
|
||||
std::optional<uint64_t> gpu_memory_allocated;
|
||||
std::optional<uint64_t> gpu_recoveries; // GPU resets since boot.
|
||||
};
|
||||
|
||||
// May block for a few milliseconds (thread enumeration, OS registry reads), so call it off the
|
||||
// render thread. The first sample has no rates.
|
||||
class HostStatsSampler final {
|
||||
public:
|
||||
HostStatsSampler();
|
||||
~HostStatsSampler();
|
||||
HostStatsSampler(const HostStatsSampler&) = delete;
|
||||
HostStatsSampler& operator=(const HostStatsSampler&) = delete;
|
||||
|
||||
[[nodiscard]] HostStats Sample();
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> m_impl;
|
||||
};
|
||||
|
||||
} // namespace Common
|
||||
|
||||
#endif /* KYTY_COMMON_HOST_STATS_H_ */
|
||||
@@ -1,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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
+13
-9
@@ -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) {
|
||||
@@ -1774,6 +1775,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 +1883,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 +1946,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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
@@ -432,6 +433,7 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
|
||||
vk_buffer.bindPipeline(vk::PipelineBindPoint::eCompute, pipeline.pipeline);
|
||||
vk_buffer.dispatch(thread_group_x, thread_group_y, thread_group_z);
|
||||
Common::DebugCounters::Add(Common::DebugCounters::Counter::Dispatches);
|
||||
|
||||
// The removed host fence also ordered read-only dispatches before later writers.
|
||||
ShaderAccessBarrier(vk_buffer, vk::PipelineStageFlagBits::eComputeShader);
|
||||
@@ -501,6 +503,7 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
1, &barrier, 0, nullptr, 0, nullptr);
|
||||
vk_buffer.bindPipeline(vk::PipelineBindPoint::eCompute, pipeline.pipeline);
|
||||
vk_buffer.dispatchIndirect(args_buffer->Handle(), args_offset);
|
||||
Common::DebugCounters::Add(Common::DebugCounters::Counter::Dispatches);
|
||||
ShaderAccessBarrier(vk_buffer, vk::PipelineStageFlagBits::eComputeShader);
|
||||
ResetBindings();
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
@@ -567,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;
|
||||
@@ -1060,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();
|
||||
@@ -1230,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);
|
||||
|
||||
@@ -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_
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,23 +248,10 @@ 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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -4047,18 +4047,18 @@ UniformFillPlan RecognizePatternFill(Program& program, const Decoder::Program& d
|
||||
struct Pattern {
|
||||
size_t code_words;
|
||||
uint64_t hash;
|
||||
uint32_t wave_size;
|
||||
uint32_t wave_size_mask;
|
||||
bool bounded;
|
||||
};
|
||||
constexpr std::array patterns {
|
||||
// Bounded 64-lane variant.
|
||||
Pattern {.code_words = 69, .hash = 0x0704b9818e3cba06ull, .wave_size = 64, .bounded = true},
|
||||
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 = 64, .bounded = false},
|
||||
Pattern {.code_words = 40, .hash = 0x374161027f58bee7ull, .wave_size = 64, .bounded = false},
|
||||
Pattern {.code_words = 40, .hash = 0x7d7172c56c2e296dull, .wave_size = 64, .bounded = false},
|
||||
// Dispatch thread dimensions, 32-lane variant.
|
||||
Pattern {.code_words = 38, .hash = 0xacbda1a1ccdd4198ull, .wave_size = 32, .bounded = false},
|
||||
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 ||
|
||||
@@ -4078,7 +4078,7 @@ UniformFillPlan RecognizePatternFill(Program& program, const Decoder::Program& d
|
||||
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 ||
|
||||
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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "common/assert.h"
|
||||
#include "common/common.h"
|
||||
#include "common/dateTime.h"
|
||||
#include "common/debugCounters.h"
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/file.h"
|
||||
#include "common/hash.h"
|
||||
@@ -669,6 +670,7 @@ int64_t KYTY_SYSV_ABI KernelRead(int d, void* buf, size_t nbytes) {
|
||||
std::min<uint64_t>(nbytes, remaining));
|
||||
uint32_t bytes_read = 0;
|
||||
file->f.Read(buf, static_cast<uint32_t>(nbytes), &bytes_read);
|
||||
Common::DebugCounters::Add(Common::DebugCounters::Counter::GuestFileReadBytes, bytes_read);
|
||||
|
||||
file->mutex.Unlock();
|
||||
|
||||
@@ -731,6 +733,7 @@ int64_t KYTY_SYSV_ABI KernelWrite(int d, const void* buf, size_t nbytes) {
|
||||
file->f.Seek(file->f.Size());
|
||||
}
|
||||
file->f.Write(buf, static_cast<uint32_t>(nbytes), &bytes_written);
|
||||
Common::DebugCounters::Add(Common::DebugCounters::Counter::GuestFileWriteBytes, bytes_written);
|
||||
if (file->sync_writes) {
|
||||
file->f.Flush();
|
||||
}
|
||||
@@ -795,6 +798,7 @@ int64_t KYTY_SYSV_ABI KernelPread(int d, void* buf, size_t nbytes, int64_t offse
|
||||
uint32_t bytes_read = 0;
|
||||
file->f.Seek(offset);
|
||||
file->f.Read(buf, static_cast<uint32_t>(nbytes), &bytes_read);
|
||||
Common::DebugCounters::Add(Common::DebugCounters::Counter::GuestFileReadBytes, bytes_read);
|
||||
file->f.Seek(pos);
|
||||
|
||||
file->mutex.Unlock();
|
||||
@@ -896,6 +900,7 @@ int64_t KYTY_SYSV_ABI KernelPreadv(int d, const KernelIovec* iov, int iovcnt, in
|
||||
Memory::InvalidateMemory(reinterpret_cast<uint64_t>(buffer.iov_base), count);
|
||||
uint32_t bytes = 0;
|
||||
file->f.Read(buffer.iov_base, count, &bytes);
|
||||
Common::DebugCounters::Add(Common::DebugCounters::Counter::GuestFileReadBytes, bytes);
|
||||
bytes_read += bytes;
|
||||
remaining -= bytes;
|
||||
if (bytes < count) {
|
||||
@@ -945,6 +950,7 @@ int64_t KYTY_SYSV_ABI KernelPwrite(int d, const void* buf, size_t nbytes, int64_
|
||||
uint32_t bytes_written = 0;
|
||||
file->f.Seek(file->append ? file->f.Size() : static_cast<uint64_t>(offset));
|
||||
file->f.Write(buf, static_cast<uint32_t>(nbytes), &bytes_written);
|
||||
Common::DebugCounters::Add(Common::DebugCounters::Counter::GuestFileWriteBytes, bytes_written);
|
||||
if (file->sync_writes) {
|
||||
file->f.Flush();
|
||||
}
|
||||
@@ -1016,6 +1022,7 @@ static int64_t Writev(int d, const KernelIovec* iov, int iovcnt, std::optional<i
|
||||
}
|
||||
uint32_t bytes = 0;
|
||||
file->f.Write(buffer.iov_base, static_cast<uint32_t>(buffer.iov_len), &bytes);
|
||||
Common::DebugCounters::Add(Common::DebugCounters::Counter::GuestFileWriteBytes, bytes);
|
||||
bytes_written += bytes;
|
||||
if (bytes < buffer.iov_len) {
|
||||
break;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,25 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="rendering_compatibility_group">
|
||||
<property name="title"><string>Rendering compatibility</string></property>
|
||||
<layout class="QVBoxLayout" name="renderingCompatibilityLayout">
|
||||
<item>
|
||||
<widget class="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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "common/abi.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/common.h"
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/file.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/stringUtils.h"
|
||||
@@ -300,15 +301,23 @@ static std::string memory_dir_name(uint32_t slot_id) {
|
||||
return "sce_sdmemory" + (slot_id == 0 ? std::string() : std::to_string(slot_id));
|
||||
}
|
||||
|
||||
static std::filesystem::path save_user_directory(std::string_view title_id, int32_t user_id) {
|
||||
auto directory = std::filesystem::path(SAVE_DATA_DIR);
|
||||
if (Config::IsGuestUserId(user_id)) {
|
||||
directory /= std::filesystem::path("[users]") / std::to_string(user_id);
|
||||
}
|
||||
return directory / title_id;
|
||||
}
|
||||
|
||||
static std::filesystem::path save_directory(std::string_view title_id, std::string_view dir_name,
|
||||
int32_t user_id) {
|
||||
const auto directory = std::filesystem::path(SAVE_DATA_DIR) / title_id / dir_name;
|
||||
for (uint32_t slot = 0; slot < 4; slot++) {
|
||||
if (dir_name == memory_dir_name(slot)) {
|
||||
return directory / std::to_string(user_id);
|
||||
return std::filesystem::path(SAVE_DATA_DIR) / title_id / dir_name /
|
||||
std::to_string(user_id);
|
||||
}
|
||||
}
|
||||
return directory;
|
||||
return save_user_directory(title_id, user_id) / dir_name;
|
||||
}
|
||||
|
||||
static std::filesystem::path memory_directory(int32_t user_id, uint32_t slot_id) {
|
||||
@@ -590,7 +599,7 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
|
||||
|
||||
Common::LockGuard lock(g_mount_mutex);
|
||||
const std::string title_id = cond->title_id != nullptr ? cond->title_id->data : get_title_id();
|
||||
const auto root = std::filesystem::path(SAVE_DATA_DIR) / title_id;
|
||||
const auto root = save_user_directory(title_id, cond->user_id);
|
||||
std::vector<std::string> dir_list;
|
||||
|
||||
if (Common::File::IsDirectoryExisting(root)) {
|
||||
|
||||
@@ -258,6 +258,10 @@ static int KYTY_SYSV_ABI SystemServicePowerTick() {
|
||||
return OK;
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI SystemServiceDisableMusicPlayer() {
|
||||
return OK;
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI SystemServiceReportAbnormalTermination(const void* info) {
|
||||
PRINT_NAME();
|
||||
|
||||
@@ -577,6 +581,7 @@ LIB_DEFINE(InitSystemService_1) {
|
||||
LIB_FUNC("8Lo6Zv94aho", SystemService::SystemServiceDisableNoticeScreenSkipFlagAutoSet);
|
||||
LIB_FUNC("Q3utJvma4Mo", SystemService::SystemServiceSetNoticeScreenSkipFlag);
|
||||
LIB_FUNC("XbbJC3E+L5M", SystemService::SystemServicePowerTick);
|
||||
LIB_FUNC("x1UB9bwDSOw", SystemService::SystemServiceDisableMusicPlayer);
|
||||
LIB_FUNC("3s8cHiCBKBE", SystemService::SystemServiceReportAbnormalTermination);
|
||||
SystemGesture::InitSystemGesture_1(s);
|
||||
}
|
||||
|
||||
+12
-3
@@ -105,6 +105,8 @@ struct TrophyState {
|
||||
const Trophies::Package* package = nullptr;
|
||||
Trophies::UnlockData unlocks;
|
||||
std::filesystem::path path;
|
||||
// Guests (players 2+) have no profile: nothing is loaded, saved or announced.
|
||||
bool guest = false;
|
||||
};
|
||||
|
||||
struct TrophyContext {
|
||||
@@ -153,9 +155,12 @@ static TrophyState* LoadTrophyState(TrophyKey key) {
|
||||
package.event_rules.size(), uds_filename.c_str());
|
||||
}
|
||||
}
|
||||
auto path = Trophies::UnlocksPath({}, title_id, key.first, key.second);
|
||||
auto unlocks = Trophies::LoadUnlockData(path);
|
||||
return &g_states.emplace(key, TrophyState {&package, std::move(unlocks), std::move(path)})
|
||||
const bool guest = Config::IsGuestUserId(key.first);
|
||||
auto path = guest ? std::filesystem::path {}
|
||||
: Trophies::UnlocksPath({}, title_id, key.first, key.second);
|
||||
auto unlocks = guest ? Trophies::UnlockData {} : Trophies::LoadUnlockData(path);
|
||||
return &g_states
|
||||
.emplace(key, TrophyState {&package, std::move(unlocks), std::move(path), guest})
|
||||
.first->second;
|
||||
}
|
||||
|
||||
@@ -240,6 +245,10 @@ static void FillGroupInfo(const TrophyState& state, int group_id, const std::str
|
||||
}
|
||||
|
||||
static void RecordTrophyUnlock(TrophyState& state, int trophy_id, bool from_stat = false) {
|
||||
if (state.guest) {
|
||||
LOGF("[Trophy] guest users cannot earn trophies (trophy %d)\n", trophy_id);
|
||||
return;
|
||||
}
|
||||
const auto found = state.package->trophies.find(trophy_id);
|
||||
if (found == state.package->trophies.end()) {
|
||||
LOGF("[Trophy] ignored unknown trophy ID %d\n", trophy_id);
|
||||
|
||||
+54
-27
@@ -1,12 +1,14 @@
|
||||
#include "common/abi.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/localization.h"
|
||||
#include "libs/controller.h"
|
||||
#include "libs/errno.h"
|
||||
#include "libs/libs.h"
|
||||
#include "loader/symbolDatabase.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace Libs {
|
||||
|
||||
@@ -39,6 +41,15 @@ struct UserServiceGamePresets {
|
||||
|
||||
static_assert(sizeof(UserServiceGamePresets) == 40);
|
||||
|
||||
static bool IsPlayerLoggedIn(int player) {
|
||||
return player == 0 || (player > 0 && Controller::IsPlayerConnected(player));
|
||||
}
|
||||
|
||||
static int GetLoggedInPlayer(int user_id) {
|
||||
const int player = Config::GetPlayerFromUserId(user_id);
|
||||
return IsPlayerLoggedIn(player) ? player : -1;
|
||||
}
|
||||
|
||||
static KYTY_SYSV_ABI int UserServiceInitialize(const void* /*params*/) {
|
||||
PRINT_NAME();
|
||||
|
||||
@@ -66,13 +77,16 @@ static KYTY_SYSV_ABI int UserServiceGetEvent(SceUserServiceEvent* event) {
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(event == nullptr);
|
||||
|
||||
static bool logged_in = false;
|
||||
static bool logged_in[Config::MAX_PLAYERS] = {};
|
||||
|
||||
if (!logged_in) {
|
||||
logged_in = true;
|
||||
event->event_type = UserServiceEventTypeLogin;
|
||||
event->user_id = Config::GetUserId();
|
||||
return OK;
|
||||
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
|
||||
const bool connected = IsPlayerLoggedIn(player);
|
||||
if (connected != logged_in[player]) {
|
||||
logged_in[player] = connected;
|
||||
event->event_type = connected ? UserServiceEventTypeLogin : UserServiceEventTypeLogout;
|
||||
event->user_id = Config::GetPlayerUserId(player);
|
||||
return OK;
|
||||
}
|
||||
}
|
||||
|
||||
return USER_SERVICE_ERROR_NO_EVENT;
|
||||
@@ -83,24 +97,36 @@ static KYTY_SYSV_ABI int UserServiceGetLoginUserIdList(UserServiceLoginUserIdLis
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(user_id_list == nullptr);
|
||||
|
||||
user_id_list->user_id[0] = Config::GetUserId();
|
||||
user_id_list->user_id[1] = -1;
|
||||
user_id_list->user_id[2] = -1;
|
||||
user_id_list->user_id[3] = -1;
|
||||
int count = 0;
|
||||
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
|
||||
if (IsPlayerLoggedIn(player)) {
|
||||
user_id_list->user_id[count++] = Config::GetPlayerUserId(player);
|
||||
}
|
||||
}
|
||||
while (count < Config::MAX_PLAYERS) {
|
||||
user_id_list->user_id[count++] = -1;
|
||||
}
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
static KYTY_SYSV_ABI int UserServiceGetUserName(int user_id, char* name, size_t size) {
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (name == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
const int player = GetLoggedInPlayer(user_id);
|
||||
if (player < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(size < 5);
|
||||
|
||||
const auto& user_name = Config::GetUserName();
|
||||
int s = snprintf(name, size, "%s", user_name.c_str());
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(static_cast<size_t>(s) >= size);
|
||||
const auto user_name =
|
||||
player > 0 ? std::string(Common::Localization::Get(Config::GetConsoleLanguage(), "user.guest")) +
|
||||
" " + std::to_string(player + 1)
|
||||
: Config::GetUserName();
|
||||
if (size <= user_name.size()) {
|
||||
return USER_SERVICE_ERROR_BUFFER_TOO_SHORT;
|
||||
}
|
||||
std::memcpy(name, user_name.c_str(), user_name.size() + 1);
|
||||
|
||||
return OK;
|
||||
}
|
||||
@@ -111,11 +137,12 @@ static KYTY_SYSV_ABI int UserServiceGetUserNumber(int user_id, int32_t* number)
|
||||
if (number == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
const int player = GetLoggedInPlayer(user_id);
|
||||
if (player < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
*number = 1;
|
||||
*number = player + 1;
|
||||
|
||||
return OK;
|
||||
}
|
||||
@@ -126,7 +153,7 @@ static KYTY_SYSV_ABI int UserServiceGetGamePresets(int user_id, UserServiceGameP
|
||||
if (presets == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
@@ -147,7 +174,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityVibration(int user_id, int32
|
||||
if (vibration == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
@@ -163,7 +190,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityTriggerEffect(int user_
|
||||
if (trigger_effect == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
@@ -178,7 +205,7 @@ static KYTY_SYSV_ABI int UserServiceGetAgeLevel(int user_id, uint32_t* age_level
|
||||
if (age_level == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
@@ -194,7 +221,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityChatTranscription(int u
|
||||
if (chat_transcription == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
@@ -210,7 +237,7 @@ UserServiceGetAccessibilityPressAndHoldDelay(int user_id, int32_t* press_and_hol
|
||||
if (press_and_hold_delay == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
@@ -226,7 +253,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityZoomEnabled(int user_id
|
||||
if (zoom_enabled == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
@@ -242,7 +269,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityZoomFollowFocus(int use
|
||||
if (zoom_follow_focus == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
|
||||
+26
-13
@@ -891,6 +891,7 @@ struct P2pEndpoint {
|
||||
uint16_t virtual_port = 0;
|
||||
int nonblocking = 0;
|
||||
int broadcast = 0;
|
||||
int use_crypto = 0;
|
||||
int send_buffer = 9216;
|
||||
int receive_buffer = 40 * 1024;
|
||||
};
|
||||
@@ -1095,6 +1096,7 @@ static int* P2pSocketOption(P2pEndpoint& endpoint, int option) {
|
||||
case 0x1001: return &endpoint.send_buffer;
|
||||
case 0x1002: return &endpoint.receive_buffer;
|
||||
case 0x1200: return &endpoint.nonblocking;
|
||||
case 0x20000: return &endpoint.use_crypto;
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
@@ -2096,19 +2098,15 @@ int KYTY_SYSV_ABI Shutdown(int s, int how) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
if (::shutdown(socket, how) == SOCKET_ERROR) {
|
||||
if (::shutdown(socket, how) != 0) {
|
||||
return SetHostSocketError();
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
if (how == SD_RECEIVE || how == SD_BOTH) {
|
||||
transport->receive_shutdown.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
return 0;
|
||||
#else
|
||||
*Posix::GetErrorAddr() = Posix::POSIX_ENOSYS;
|
||||
return -1;
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI Getsockname(int s, void* addr, uint32_t* addrlen) {
|
||||
@@ -2463,9 +2461,9 @@ int64_t KYTY_SYSV_ABI Recvfrom(int s, void* buf, uint64_t len, int flags, void*
|
||||
return -1;
|
||||
}
|
||||
|
||||
NativeSocket socket = INVALID_NATIVE_SOCKET;
|
||||
std::shared_ptr<SocketTransport> transport;
|
||||
if (!GetSocketBackend(s, &socket, nullptr, &transport)) {
|
||||
NativeSocket socket = INVALID_NATIVE_SOCKET;
|
||||
SocketSlot state;
|
||||
if (!GetSocketBackend(s, &socket, &state)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -2473,6 +2471,21 @@ int64_t KYTY_SYSV_ABI Recvfrom(int s, void* buf, uint64_t len, int flags, void*
|
||||
if (host_flags < 0) {
|
||||
return -1;
|
||||
}
|
||||
if (state.p2p) {
|
||||
if (!state.p2p->bound || (!state.p2p->nonblocking && (flags & 0x80) == 0)) {
|
||||
return SetGuestSocketError(Posix::POSIX_EOPNOTSUPP);
|
||||
}
|
||||
char pending;
|
||||
if (::recv(socket, &pending, sizeof(pending), MSG_PEEK) < 0) {
|
||||
SetHostSocketError();
|
||||
// Winsock reports a peeked datagram's truncation as EMSGSIZE.
|
||||
if (*Posix::GetErrorAddr() != Posix::POSIX_EMSGSIZE) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
// Pending UDPP2P packets still require framing and optional decryption.
|
||||
return SetGuestSocketError(Posix::POSIX_EOPNOTSUPP);
|
||||
}
|
||||
|
||||
const auto host_len = static_cast<SocketIoLength>(
|
||||
std::min<uint64_t>(len, std::numeric_limits<SocketIoLength>::max()));
|
||||
@@ -2480,12 +2493,12 @@ int64_t KYTY_SYSV_ABI Recvfrom(int s, void* buf, uint64_t len, int flags, void*
|
||||
SocketLength host_addrlen = sizeof(host_addr);
|
||||
int64_t result = 0;
|
||||
#if defined(_WIN32)
|
||||
if (transport->type == SOCK_STREAM) {
|
||||
if (state.transport->type == SOCK_STREAM) {
|
||||
if (addr != nullptr &&
|
||||
::getpeername(socket, reinterpret_cast<sockaddr*>(&host_addr), &host_addrlen) != 0) {
|
||||
return SetHostSocketError();
|
||||
}
|
||||
result = ReceiveStream(*transport, static_cast<char*>(buf), host_len, host_flags,
|
||||
result = ReceiveStream(*state.transport, static_cast<char*>(buf), host_len, host_flags,
|
||||
(flags & 0x80) != 0);
|
||||
if (result < 0) {
|
||||
return result;
|
||||
@@ -2493,7 +2506,7 @@ int64_t KYTY_SYSV_ABI Recvfrom(int s, void* buf, uint64_t len, int flags, void*
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
transport.reset();
|
||||
state.transport.reset();
|
||||
if (addr == nullptr) {
|
||||
result = ::recv(socket, static_cast<char*>(buf), host_len, host_flags);
|
||||
} else {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/common.h"
|
||||
#include "common/debugCounters.h"
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/file.h"
|
||||
#include "common/hostException.h"
|
||||
@@ -190,6 +191,7 @@ static uint64_t RegisterStubbedImport(uint32_t index, const Program* program,
|
||||
}
|
||||
|
||||
static KYTY_SYSV_ABI uint64_t UnresolvedImportStub(uint64_t record_id) {
|
||||
Common::DebugCounters::Add(Common::DebugCounters::Counter::UnresolvedImportCalls);
|
||||
const auto log_index = g_unresolved_stub_call_log_count.fetch_add(1);
|
||||
if (log_index < 1024) {
|
||||
if (record_id < g_stubbed_imports.size()) {
|
||||
@@ -652,6 +654,7 @@ static bool KytyExceptionHandler(const Common::HostException::ExceptionInfo& exc
|
||||
|
||||
if (info->type == Common::HostException::ExceptionType::IllegalInstruction &&
|
||||
Loader::X64InstructionEmulator::TryEmulate(info->native_context)) {
|
||||
Common::DebugCounters::Add(Common::DebugCounters::Counter::EmulatedInstructions);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -666,6 +669,7 @@ static bool KytyExceptionHandler(const Common::HostException::ExceptionInfo& exc
|
||||
case CoreAccess::Unknown: return false;
|
||||
}
|
||||
if (Libs::LibKernel::Memory::HandleGpuFault(access, info->access_violation_vaddr)) {
|
||||
Common::DebugCounters::Add(Common::DebugCounters::Counter::GpuFaults);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ static void PrintUsage() {
|
||||
::printf(" --controller-color <#RRGGBB> Override the controller lightbar color.\n");
|
||||
::printf(" --controller-volume <0-100> DualSense speaker volume. Default: 50.\n");
|
||||
::printf(" --controller-vibration <0-100> DualSense vibration intensity. Default: 100.\n");
|
||||
::printf(" --keyboard-player <0-4> Keyboard player; 0 shares player 1 with the gamepad. Default: 0.\n");
|
||||
::printf(
|
||||
" --present-mode <value> Fifo, Mailbox, or Immediate. Default: Mailbox.\n");
|
||||
::printf(
|
||||
@@ -90,10 +91,13 @@ static void PrintUsage() {
|
||||
" --readback-linear-images <true|false> Read back writable linear images on submit.\n");
|
||||
::printf(
|
||||
" --sync-raw-image-buffers <true|false> Synchronize raw reads of GPU images. Default: false.\n");
|
||||
::printf(
|
||||
" --support-zmask <true|false> Fill HTILE buffers with uncompressed ZMask. Default: false.\n");
|
||||
::printf(
|
||||
" --trophy-notifications <true|false> Show trophy unlock toasts and play their sound.\n");
|
||||
::printf(
|
||||
" --trophy-duration <seconds> Trophy toast duration, 5-10. Default: 6.\n");
|
||||
::printf(" --perf-overlay <true|false> Show the performance panel (F2 or Cmd+P).\n");
|
||||
::printf(" --playgo-hack Use the supplied PlayGo stub fallback.\n");
|
||||
::printf(
|
||||
" --skip-notice-screen <true|false> Skip startup logos and notices in supported games.\n"
|
||||
@@ -378,6 +382,12 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
|
||||
::printf("invalid controller vibration intensity: %s\n", value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--keyboard-player") {
|
||||
if (!ParseUint32(value, options.config.keyboard_player) ||
|
||||
options.config.keyboard_player > static_cast<uint32_t>(Config::MAX_PLAYERS)) {
|
||||
::printf("invalid keyboard player: %s\n", value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--present-mode") {
|
||||
if (!ParseEnum(value, options.config.present_mode)) {
|
||||
::printf("invalid present mode: %s\n", value.c_str());
|
||||
@@ -469,6 +479,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
|
||||
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--support-zmask") {
|
||||
if (!ParseBool(value, options.config.support_zmask)) {
|
||||
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--trophy-notifications") {
|
||||
if (!ParseBool(value, options.config.trophy_enabled)) {
|
||||
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
|
||||
@@ -479,6 +494,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
|
||||
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--perf-overlay") {
|
||||
if (!ParseBool(value, options.config.perf_overlay_enabled)) {
|
||||
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--keymap") {
|
||||
const auto split = value.find('=');
|
||||
if (split == std::string::npos || split == 0 || split + 1 == value.size()) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -23,6 +24,9 @@ std::vector<Rumble> rumble;
|
||||
std::vector<Rumble> haptics;
|
||||
std::vector<std::array<Uint8, 32>> effects;
|
||||
bool haptics_handles_rumble = false;
|
||||
std::array<Rumble, 6> pad_rumble {};
|
||||
std::vector<int> closed_pads;
|
||||
std::array<std::array<Uint8, 3>, 6> pad_colors {};
|
||||
} // namespace
|
||||
|
||||
namespace Fake {
|
||||
@@ -30,14 +34,15 @@ Uint64 GetTicks() {
|
||||
return now;
|
||||
}
|
||||
SDL_Gamepad* GetGamepadFromID(SDL_JoystickID id) {
|
||||
return id == 1 || id == 2 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id))
|
||||
return id >= 1 && id <= 5 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id))
|
||||
: nullptr;
|
||||
}
|
||||
SDL_GamepadType GetGamepadType(SDL_Gamepad*) {
|
||||
return SDL_GAMEPAD_TYPE_PS5;
|
||||
}
|
||||
bool RumbleGamepad(SDL_Gamepad*, Uint16 large, Uint16 small, Uint32 duration) {
|
||||
bool RumbleGamepad(SDL_Gamepad* pad, Uint16 large, Uint16 small, Uint32 duration) {
|
||||
rumble.push_back({large, small, duration});
|
||||
pad_rumble[reinterpret_cast<uintptr_t>(pad)] = rumble.back();
|
||||
return true;
|
||||
}
|
||||
bool SendGamepadEffect(SDL_Gamepad*, const void* data, int size) {
|
||||
@@ -47,7 +52,8 @@ bool SendGamepadEffect(SDL_Gamepad*, const void* data, int size) {
|
||||
effects.push_back(effect);
|
||||
return true;
|
||||
}
|
||||
bool SetGamepadLED(SDL_Gamepad*, Uint8, Uint8, Uint8) {
|
||||
bool SetGamepadLED(SDL_Gamepad* pad, Uint8 r, Uint8 g, Uint8 b) {
|
||||
pad_colors[reinterpret_cast<uintptr_t>(pad)] = {r, g, b};
|
||||
return true;
|
||||
}
|
||||
bool GamepadHasSensor(SDL_Gamepad*, SDL_SensorType) {
|
||||
@@ -56,7 +62,9 @@ bool GamepadHasSensor(SDL_Gamepad*, SDL_SensorType) {
|
||||
bool SetGamepadSensorEnabled(SDL_Gamepad*, SDL_SensorType, bool) {
|
||||
return true;
|
||||
}
|
||||
void CloseGamepad(SDL_Gamepad*) {}
|
||||
void CloseGamepad(SDL_Gamepad* pad) {
|
||||
closed_pads.push_back(static_cast<int>(reinterpret_cast<uintptr_t>(pad)));
|
||||
}
|
||||
void Delay(Uint32) {}
|
||||
} // namespace Fake
|
||||
|
||||
@@ -368,6 +376,10 @@ void TestIndependentOutputsAndPadSwitch() {
|
||||
"speaker volume resent controller effects");
|
||||
Connect(2);
|
||||
Disconnect(1);
|
||||
Check(GetActiveControllerId() == HOST_INPUT_CONTROLLER_ID,
|
||||
"disconnecting player 1 stole player 2's pad");
|
||||
Disconnect(2);
|
||||
Connect(2);
|
||||
Check(GetActiveControllerId() == 2, "active controller did not change");
|
||||
haptics.clear();
|
||||
effects.clear();
|
||||
@@ -394,6 +406,126 @@ void TestIndependentOutputsAndPadSwitch() {
|
||||
}
|
||||
}
|
||||
|
||||
void TestPlayerUserIds() {
|
||||
Check(Config::IsConfiguredUserIdValid(Config::MAX_USER_ID) &&
|
||||
!Config::IsConfiguredUserIdValid(Config::MAX_USER_ID + 1) &&
|
||||
!Config::IsConfiguredUserIdValid(254) && !Config::IsConfiguredUserIdValid(255),
|
||||
"configured user IDs did not reserve a valid range for guests");
|
||||
Config::ConfigOptions options;
|
||||
options.user_id = Config::MAX_USER_ID;
|
||||
Config::Load(options);
|
||||
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
|
||||
Check(Config::GetPlayerFromUserId(Config::GetPlayerUserId(player)) == player,
|
||||
"player user ID did not round-trip at the upper limit");
|
||||
}
|
||||
Check(Config::GetPlayerUserId(3) == std::numeric_limits<int32_t>::max() &&
|
||||
Config::GetPlayerFromUserId(-1) == -1,
|
||||
"guest user ID overflowed or accepted an unknown user");
|
||||
Config::Load(Config::ConfigOptions {});
|
||||
}
|
||||
|
||||
void TestMultiplayerRouting() {
|
||||
Controller controller;
|
||||
for (int player = 1; player < Config::MAX_PLAYERS; player++) {
|
||||
Check(PadOpen(Config::GetPlayerUserId(player), 0, 0, nullptr) == player + 1,
|
||||
"a guest could not open its own pad port");
|
||||
}
|
||||
for (int id = 2; id <= 5; id++) {
|
||||
Connect(id);
|
||||
}
|
||||
Connect(2); // Duplicate device reports must not consume another port.
|
||||
const auto read = [](int handle) {
|
||||
PadData data {};
|
||||
Check(PadReadState(handle, &data) == OK, "player pad read failed");
|
||||
return data;
|
||||
};
|
||||
SetButton(1, PAD_BUTTON_CROSS, true);
|
||||
SetButton(2, PAD_BUTTON_CIRCLE, true);
|
||||
SetButton(5, PAD_BUTTON_TRIANGLE, true);
|
||||
Check(read(1).buttons == PAD_BUTTON_CROSS && read(2).buttons == PAD_BUTTON_CIRCLE &&
|
||||
read(3).buttons == 0 && read(4).buttons == 0,
|
||||
"physical pads did not route independently or overflow reached a player");
|
||||
|
||||
SetRumble(100, 50);
|
||||
CycleKeyboardPlayer(); // Dedicated player 1; physical pad 1 waits.
|
||||
Check(GetActiveControllerId() == HOST_INPUT_CONTROLLER_ID &&
|
||||
read(2).buttons == PAD_BUTTON_CIRCLE && pad_rumble[1].large == 0 &&
|
||||
pad_rumble[1].small == 0,
|
||||
"keyboard reassignment changed an unaffected player or left pad output "
|
||||
"running");
|
||||
for (int mode = 2; mode <= Config::MAX_PLAYERS; mode++) {
|
||||
CycleKeyboardPlayer();
|
||||
}
|
||||
CycleKeyboardPlayer(); // Shared player 1; all original pads must return.
|
||||
ResetInputState();
|
||||
for (int id = 1; id <= 5; id++) {
|
||||
SetButton(id, 1u << id, true);
|
||||
}
|
||||
for (int handle = 1; handle <= Config::MAX_PLAYERS; handle++) {
|
||||
Check(read(handle).buttons == (1u << handle),
|
||||
"cycling keyboard players lost or duplicated a connected pad");
|
||||
}
|
||||
|
||||
// Moving the keyboard clears its held input from the old player.
|
||||
SetButton(HOST_INPUT_CONTROLLER_ID, PAD_BUTTON_TRIANGLE, true);
|
||||
CycleKeyboardPlayer();
|
||||
CycleKeyboardPlayer();
|
||||
SetButton(HOST_INPUT_CONTROLLER_ID, PAD_BUTTON_TRIANGLE, true);
|
||||
CycleKeyboardPlayer(); // Keyboard now moves from player 2 to player 3.
|
||||
Check((read(2).buttons & PAD_BUTTON_TRIANGLE) == 0,
|
||||
"moving the keyboard retained a held button on its old player");
|
||||
CycleKeyboardPlayer();
|
||||
CycleKeyboardPlayer();
|
||||
Disconnect(2);
|
||||
SetButton(5, PAD_BUTTON_SQUARE, true);
|
||||
Check(read(2).buttons == PAD_BUTTON_SQUARE && GetActiveControllerId() == 1,
|
||||
"waiting pad did not fill a free slot or an unaffected pad moved");
|
||||
Disconnect(99);
|
||||
Check(PadCheckHandle(std::numeric_limits<int>::min()) == PAD_ERROR_INVALID_HANDLE &&
|
||||
PadClose(std::numeric_limits<int>::max()) == PAD_ERROR_INVALID_HANDLE,
|
||||
"an extreme handle aliased a player port");
|
||||
closed_pads.clear();
|
||||
EmergencyShutdown();
|
||||
std::sort(closed_pads.begin(), closed_pads.end());
|
||||
Check(closed_pads == std::vector<int> {1, 3, 4, 5},
|
||||
"shutdown did not close each registered host pad exactly once");
|
||||
}
|
||||
|
||||
void TestDedicatedKeyboardOutputs() {
|
||||
Config::ConfigOptions options;
|
||||
options.keyboard_player = 1;
|
||||
options.controller_color = Config::ControllerColor {30, 40, 50};
|
||||
Config::Load(options);
|
||||
Initialize();
|
||||
PadInit();
|
||||
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
|
||||
Check(PadOpen(Config::GetPlayerUserId(player), 0, 0, nullptr) == player + 1,
|
||||
"dedicated keyboard port did not open");
|
||||
}
|
||||
Connect(1);
|
||||
Connect(2);
|
||||
const PadVibrationParam request {100, 50};
|
||||
Check(PadSetVibration(3, &request) == OK, "player three vibration failed");
|
||||
SetButton(2, PAD_BUTTON_CROSS, true);
|
||||
CycleKeyboardPlayer(); // Pad 1 moves from player 2 to player 1.
|
||||
PadData data {};
|
||||
Check(GetActiveControllerId() == 1 && pad_colors[1] == *options.controller_color,
|
||||
"moving a pad to an earlier slot cleared its configured lightbar");
|
||||
Check(PadReadState(3, &data) == OK && data.buttons == PAD_BUTTON_CROSS &&
|
||||
pad_rumble[2].large == 100 * 257 && pad_rumble[2].small == 50 * 257,
|
||||
"keyboard reassignment reset an unaffected player's input or output");
|
||||
haptics_handles_rumble = true;
|
||||
SetRumble(200, 100);
|
||||
for (int mode = 0; mode < 4; mode++) {
|
||||
CycleKeyboardPlayer();
|
||||
}
|
||||
Check(GetActiveControllerId() == HOST_INPUT_CONTROLLER_ID && haptics.back().large == 0 &&
|
||||
haptics.back().small == 0 && haptics.back().duration == 0,
|
||||
"moving player one's pad retained its DualSense vibration request");
|
||||
Shutdown();
|
||||
Config::Load(Config::ConfigOptions {});
|
||||
}
|
||||
|
||||
void TestTriggerEffectState() {
|
||||
Controller controller;
|
||||
PadTriggerEffectStateInformation info {{-1, -1}};
|
||||
@@ -553,6 +685,7 @@ void TestTriggerEffectState() {
|
||||
|
||||
int main() {
|
||||
Config::Initialize();
|
||||
TestPlayerUserIds();
|
||||
TestPadPortOwnership();
|
||||
TestSettingCycles();
|
||||
TestGlobalControllerLevels();
|
||||
@@ -561,6 +694,8 @@ int main() {
|
||||
TestMaskedTriggersAndValidation();
|
||||
TestIndependentOutputsAndPadSwitch();
|
||||
TestTriggerEffectState();
|
||||
TestMultiplayerRouting();
|
||||
TestDedicatedKeyboardOutputs();
|
||||
Config::Shutdown();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1905,6 +1905,11 @@ void CheckSocketWakeup() {
|
||||
namespace Net = Libs::Network::Net;
|
||||
Loader::SymbolDatabase symbols;
|
||||
Libs::LibNet::InitNet_1_Net(&symbols);
|
||||
Libs::InitLibKernel_1(&symbols);
|
||||
const auto *shutdown_symbol = symbols.Find(
|
||||
{"TUuiYS2kE8s", "Posix", 1, "libkernel", 1, 1, Loader::SymbolType::Func});
|
||||
Check(shutdown_symbol != nullptr, "POSIX shutdown export resolves");
|
||||
const auto shutdown = reinterpret_cast<int (KYTY_SYSV_ABI *)(int, int)>(shutdown_symbol->vaddr);
|
||||
const auto *connect_symbol = symbols.Find(
|
||||
{"OXXX4mUk3uk", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||
const auto *getsockopt_symbol = symbols.Find(
|
||||
@@ -2145,6 +2150,76 @@ void CheckSocketWakeup() {
|
||||
CheckSocketReceiveBuffer(reader, writer);
|
||||
CheckSocketReceiveConcurrency();
|
||||
#endif
|
||||
const auto [shutdown_reader, shutdown_writer] = CreateTcpPair();
|
||||
const auto ready_to_read = [](int fd) {
|
||||
std::array<uint64_t, 16> readable {};
|
||||
readable[fd / 64] = uint64_t {1} << (fd % 64);
|
||||
const std::array<int64_t, 2> timeout {2, 0};
|
||||
return Net::Select(fd + 1, readable.data(), nullptr, nullptr, timeout.data()) == 1;
|
||||
};
|
||||
Check(shutdown(-1, 1) == -1 &&
|
||||
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EBADF &&
|
||||
shutdown(shutdown_writer, 3) == -1 &&
|
||||
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EINVAL,
|
||||
"shutdown rejects invalid descriptors and directions");
|
||||
Check(Net::Send(shutdown_writer, "x", 1, 0) == 1 && shutdown(shutdown_writer, 1) == 0 &&
|
||||
ready_to_read(shutdown_reader) &&
|
||||
Net::Recv(shutdown_reader, received.data(), 1, 0) == 1 && received[0] == 'x' &&
|
||||
ready_to_read(shutdown_reader) &&
|
||||
Net::Recv(shutdown_reader, received.data(), 1, 0) == 0 &&
|
||||
Net::Send(shutdown_reader, "y", 1, 0) == 1 &&
|
||||
ready_to_read(shutdown_writer) &&
|
||||
Net::Recv(shutdown_writer, received.data(), 1, 0) == 1 && received[0] == 'y',
|
||||
"write shutdown drains queued bytes, signals EOF, and preserves reverse traffic");
|
||||
Check(Net::SocketClose(shutdown_reader) == 0 && Net::SocketClose(shutdown_writer) == 0,
|
||||
"close half-shutdown sockets");
|
||||
const int encrypted = Net::Socket(2, 6, 0);
|
||||
int crypto = -1;
|
||||
uint32_t crypto_size = sizeof(crypto);
|
||||
Check(encrypted >= 0 &&
|
||||
Net::Getsockopt(encrypted, 0xffff, 0x20000, &crypto, &crypto_size) == 0 &&
|
||||
crypto == 0,
|
||||
"UDPP2P encryption defaults to disabled");
|
||||
for (const int setting : {7, 0}) {
|
||||
Check(Net::Setsockopt(encrypted, 0xffff, 0x20000, &setting, sizeof(setting)) == 0 &&
|
||||
Net::Getsockopt(encrypted, 0xffff, 0x20000, &crypto, &crypto_size) == 0 &&
|
||||
crypto == (setting != 0),
|
||||
"UDPP2P encryption configuration retains normalized enable and disable values");
|
||||
Check(Net::Send(encrypted, "x", 1, 0) == -1 &&
|
||||
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EOPNOTSUPP,
|
||||
"UDPP2P configuration never exposes unframed plaintext transport");
|
||||
}
|
||||
std::array<uint8_t, 16> p2p_address {16, 2, 0x0e, 0x4a, 127, 0, 0, 1, 0x1e, 0x61};
|
||||
Check(Net::Bind(encrypted, p2p_address.data(), p2p_address.size()) == 0,
|
||||
"bind UDPP2P physical and virtual ports");
|
||||
char untouched = '!';
|
||||
auto source = p2p_address;
|
||||
uint32_t source_size = source.size();
|
||||
Check(Net::Recvfrom(encrypted, &untouched, 1, 0x80, source.data(), &source_size) == -1 &&
|
||||
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EWOULDBLOCK,
|
||||
"per-call nonblocking UDPP2P receive reports an actually empty queue");
|
||||
Check(Net::Setsockopt(encrypted, 0xffff, 0x1200, &enabled, sizeof(enabled)) == 0 &&
|
||||
Net::Setsockopt(encrypted, 0xffff, 0x20000, &enabled, sizeof(enabled)) == 0 &&
|
||||
Net::Recvfrom(encrypted, &untouched, 1, 0, source.data(), &source_size) == -1 &&
|
||||
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EWOULDBLOCK &&
|
||||
untouched == '!' && source == p2p_address && source_size == source.size(),
|
||||
"empty encrypted nonblocking receive preserves all caller outputs");
|
||||
const int raw_sender = Net::Socket(2, 2, 0);
|
||||
Check(raw_sender >= 0 &&
|
||||
Net::Sendto(raw_sender, "pending", 7, 0, p2p_address.data(), p2p_address.size()) == 7,
|
||||
"queue a datagram requiring unsupported UDPP2P processing");
|
||||
const auto pending_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
||||
while (Net::Recv(encrypted, &untouched, 1, 0) == -1 &&
|
||||
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EWOULDBLOCK &&
|
||||
std::chrono::steady_clock::now() < pending_deadline) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
Check(*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EOPNOTSUPP &&
|
||||
Net::Recv(encrypted, &untouched, 1, 0) == -1 &&
|
||||
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EOPNOTSUPP && untouched == '!',
|
||||
"queued packets remain queued and unsupported instead of masquerading as empty");
|
||||
Check(Net::SocketClose(encrypted) == 0 && Net::SocketClose(raw_sender) == 0,
|
||||
"close UDPP2P readiness fixtures");
|
||||
Check(Net::SocketClose(writer) == 0, "close wake writer");
|
||||
*net_errno = Libs::Posix::POSIX_EINVAL;
|
||||
Check(net_recv(reader, received.data(), received.size(), 0) == 0 &&
|
||||
|
||||
@@ -874,7 +874,7 @@ void TestFiniteImageRefreshReusesScalarReads() {
|
||||
"finite image refresh grew reusable resource storage after warmup");
|
||||
}
|
||||
|
||||
void TestEmptyTraversalSkipsUnmappedDescriptor(bool independent_mask) {
|
||||
void TestEmptyTraversalSkipsUnmappedDescriptor(bool independent_mask, bool counted_loop = false) {
|
||||
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
||||
using Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind;
|
||||
Program program;
|
||||
@@ -898,54 +898,87 @@ void TestEmptyTraversalSkipsUnmappedDescriptor(bool independent_mask) {
|
||||
block.AddBranch(&target);
|
||||
if (other) block.AddBranch(other);
|
||||
};
|
||||
const auto lane = emit(entry, ValueOpcode::GetBuiltin,
|
||||
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)});
|
||||
const auto active = emit(entry, ValueOpcode::INotEqual32, {lane, Value(0u)});
|
||||
const auto initial_mask = independent_mask
|
||||
? emit(entry, ValueOpcode::IEqual32, {lane, Value(0u)}) : active;
|
||||
const auto initial = emit(entry, ValueOpcode::SelectU32,
|
||||
{initial_mask, Value(UINT32_MAX), Value(0u)});
|
||||
Value lane, active, initial;
|
||||
if (!counted_loop) {
|
||||
lane = emit(entry, ValueOpcode::GetBuiltin,
|
||||
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)});
|
||||
active = emit(entry, ValueOpcode::INotEqual32, {lane, Value(0u)});
|
||||
const auto initial_mask = independent_mask
|
||||
? emit(entry, ValueOpcode::IEqual32, {lane, Value(0u)}) : active;
|
||||
initial = emit(entry, ValueOpcode::SelectU32,
|
||||
{initial_mask, Value(UINT32_MAX), Value(0u)});
|
||||
}
|
||||
program.memory_info.push_back({.kind = ResourceKind::ScalarAddress, .planning_only = true});
|
||||
const auto address = emit(entry, ValueOpcode::GetAddressResource, {Value(0x1000u), Value(0u)});
|
||||
const auto count = emit(entry, ValueOpcode::LoadAddressU32,
|
||||
{address, Value(0u), Value(0u), Value(true)});
|
||||
count.Instruction()->SetFlags(MemoryFlags{.index = 0});
|
||||
const auto rejected = emit(entry, ValueOpcode::ULessThanEqual32, {count, lane});
|
||||
const auto accepted = emit(entry, ValueOpcode::LogicalNot, {rejected});
|
||||
branch(entry, loop, emit(entry, ValueOpcode::LogicalAnd, {active, accepted}), &merge);
|
||||
if (counted_loop) {
|
||||
branch(entry, loop);
|
||||
} else {
|
||||
const auto rejected = emit(entry, ValueOpcode::ULessThanEqual32, {count, lane});
|
||||
const auto accepted = emit(entry, ValueOpcode::LogicalNot, {rejected});
|
||||
branch(entry, loop, emit(entry, ValueOpcode::LogicalAnd, {active, accepted}), &merge);
|
||||
}
|
||||
|
||||
// A nonempty traversal contains a varying loop-carried value. Its cycle must
|
||||
// remain unknown; only the proven empty traversal can select the bypass arm.
|
||||
auto &carried = loop.AppendNewInst(ValueOpcode::Phi);
|
||||
carried.SetFlags(Type::U32);
|
||||
const auto next = emit(body, ValueOpcode::IAdd32, {Value(&carried), Value(1u)});
|
||||
carried.AddPhiOperand(&entry, lane);
|
||||
carried.AddPhiOperand(&entry, counted_loop ? Value(0u) : lane);
|
||||
carried.AddPhiOperand(&body, next);
|
||||
branch(loop, body, emit(loop, ValueOpcode::INotEqual32, {Value(&carried), Value(UINT32_MAX)}), &merge);
|
||||
const auto condition = counted_loop
|
||||
? emit(loop, ValueOpcode::ULessThan32, {Value(&carried), count})
|
||||
: emit(loop, ValueOpcode::INotEqual32, {Value(&carried), Value(UINT32_MAX)});
|
||||
branch(loop, counted_loop ? use : body, condition, &merge);
|
||||
loop.terminator.loop_header = true;
|
||||
loop.terminator.continue_block = &body;
|
||||
loop.terminator.merge_block = &merge;
|
||||
branch(body, loop);
|
||||
auto &selected = merge.AppendNewInst(ValueOpcode::Phi);
|
||||
selected.SetFlags(Type::U32);
|
||||
selected.AddPhiOperand(&entry, initial);
|
||||
selected.AddPhiOperand(&loop, Value(&carried));
|
||||
const auto valid = emit(merge, ValueOpcode::INotEqual32, {Value(&selected), Value(UINT32_MAX)});
|
||||
branch(merge, use, emit(merge, ValueOpcode::LogicalAnd, {active, valid}), &exit);
|
||||
branch(use, exit);
|
||||
if (counted_loop) {
|
||||
branch(merge, exit);
|
||||
branch(use, body);
|
||||
} else {
|
||||
auto &selected = merge.AppendNewInst(ValueOpcode::Phi);
|
||||
selected.SetFlags(Type::U32);
|
||||
selected.AddPhiOperand(&entry, initial);
|
||||
selected.AddPhiOperand(&loop, Value(&carried));
|
||||
const auto valid = emit(merge, ValueOpcode::INotEqual32, {Value(&selected), Value(UINT32_MAX)});
|
||||
branch(merge, use, emit(merge, ValueOpcode::LogicalAnd, {active, valid}), &exit);
|
||||
branch(use, exit);
|
||||
}
|
||||
DescriptorSource source;
|
||||
source.dwords = {Value(0x8000u), Value(1u << 16u), Value(16u), Value(3u << 28u)};
|
||||
const auto descriptor_address = emit(use, ValueOpcode::GetAddressResource,
|
||||
{Value(0x2000u), Value(0u)});
|
||||
for (uint32_t i = 0; i < 4; ++i) {
|
||||
const auto index = static_cast<uint32_t>(program.memory_info.size());
|
||||
program.memory_info.push_back({.kind = ResourceKind::ScalarAddress, .planning_only = true});
|
||||
source.dwords[i] = emit(use, ValueOpcode::LoadAddressU32,
|
||||
{descriptor_address, Value(i * 4u), Value(0u), Value(true)});
|
||||
source.dwords[i].Instruction()->SetFlags(MemoryFlags{.index = index});
|
||||
}
|
||||
source.dword_count = 4;
|
||||
program.descriptor_sources.push_back(source);
|
||||
program.info.buffers.push_back({.source = 0, .read = true});
|
||||
const auto buffer_index = static_cast<uint32_t>(program.memory_info.size());
|
||||
program.memory_info.push_back({.kind = ResourceKind::Buffer, .resource = 0});
|
||||
const auto buffer = emit(use, ValueOpcode::GetBufferResource,
|
||||
{source.dwords[0], source.dwords[1], source.dwords[2], source.dwords[3]});
|
||||
emit(use, ValueOpcode::LoadBufferU32, {buffer, Value(0u), Value(0u), Value(0u), Value(true)})
|
||||
.Instruction()->SetFlags(MemoryFlags{.index = 1});
|
||||
.Instruction()->SetFlags(MemoryFlags{.index = buffer_index});
|
||||
auto plan = ExtractResourcePlan(program);
|
||||
struct Reads { uint32_t count = 0; bool clean = true; } reads;
|
||||
struct Reads { uint32_t count = 0; bool clean = true; uint32_t descriptors = 0; } reads;
|
||||
const SrtRuntime runtime{
|
||||
.read_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
|
||||
constexpr std::array<uint32_t, 4> descriptor{0x8000u, 1u << 16u, 16u,
|
||||
(3u << 28u) | 0x4dfacu};
|
||||
if (address < 0x2000u || address >= 0x2010u || address % 4u || words.size() != 1)
|
||||
return false;
|
||||
++static_cast<Reads *>(data)->descriptors;
|
||||
words[0] = descriptor[(address - 0x2000u) / 4u];
|
||||
return true;
|
||||
},
|
||||
.userdata = &reads,
|
||||
.read_specialization_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
|
||||
const auto &reads = *static_cast<Reads *>(data);
|
||||
@@ -957,9 +990,11 @@ void TestEmptyTraversalSkipsUnmappedDescriptor(bool independent_mask) {
|
||||
ResourceSpecialization specialization;
|
||||
for (const auto state : {Reads{0u, true}, Reads{4u, true}, Reads{0u, false}, Reads{0u, true}}) {
|
||||
reads = state;
|
||||
uint32_t condition = 99;
|
||||
Check(!SrtWalker(plan, runtime).Evaluate(plan.control_flow[3].condition, condition),
|
||||
"direct merge evaluation used missing or previous runtime reachability");
|
||||
if (!counted_loop) {
|
||||
uint32_t condition = 99;
|
||||
Check(!SrtWalker(plan, runtime).Evaluate(plan.control_flow[3].condition, condition),
|
||||
"direct merge evaluation used missing or previous runtime reachability");
|
||||
}
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"partial traversal proof rejected a valid resource snapshot");
|
||||
const bool inactive = !independent_mask && reads.clean && reads.count == 0u;
|
||||
@@ -967,6 +1002,8 @@ void TestEmptyTraversalSkipsUnmappedDescriptor(bool independent_mask) {
|
||||
"empty traversal kept its descriptor or an uncertain traversal lost it");
|
||||
Check(specialization.buffers[0].zero_stride_oob == inactive,
|
||||
"inactive traversal did not expose dead vector reads to specialization");
|
||||
Check(reads.descriptors == (inactive ? 0u : 4u),
|
||||
"inactive traversal read its descriptor or an uncertain traversal skipped it");
|
||||
if (inactive) {
|
||||
Check(!snapshot.specialization_reads.empty(), "empty traversal lost its clean scalar read");
|
||||
for (const auto &read : snapshot.specialization_reads)
|
||||
@@ -1331,6 +1368,7 @@ int main() {
|
||||
TestFiniteImageRefreshReusesScalarReads();
|
||||
for (const bool independent_mask : {false, true})
|
||||
TestEmptyTraversalSkipsUnmappedDescriptor(independent_mask);
|
||||
TestEmptyTraversalSkipsUnmappedDescriptor(false, true);
|
||||
TestCyclicControlPredicatesRemainUnknown();
|
||||
TestEmptyScalarBufferSpecialization();
|
||||
TestMixedSamplerVariantsShareRuntimeDescriptor();
|
||||
|
||||
@@ -378,7 +378,7 @@ void TestClassicSavePaths() {
|
||||
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
|
||||
}
|
||||
Reset("CLASSIC");
|
||||
for (const char* text: {"", ".", "..", "../save1", "save/1", "save\\1"}) {
|
||||
for (const char* text: {"", ".", "..", "../save1", "save/1", "save\\1", "[users]"}) {
|
||||
auto name = DirName(text);
|
||||
struct SaveDataMount3 mount {};
|
||||
mount.user_id = 1;
|
||||
@@ -396,6 +396,57 @@ void TestClassicSavePaths() {
|
||||
CHECK(Search(1) == names);
|
||||
}
|
||||
|
||||
void TestGuestSaveIsolation() {
|
||||
Reset("MULTIUSER");
|
||||
const auto name = DirName("slot");
|
||||
const std::array<int32_t, 3> users {Config::GetUserId(), Config::GetPlayerUserId(1),
|
||||
Config::GetPlayerUserId(2)};
|
||||
std::array<fs::path, users.size()> paths;
|
||||
for (size_t i = 0; i < users.size(); ++i) {
|
||||
CHECK(Search(users[i]).empty());
|
||||
struct SaveDataMount3 mount {};
|
||||
mount.user_id = users[i];
|
||||
mount.dir_name = &name;
|
||||
mount.mount_mode = 4;
|
||||
mount.blocks = 48;
|
||||
SaveDataMountResult result {};
|
||||
CHECK(SaveDataMount3(&mount, &result) == OK);
|
||||
paths[i] = g_mounted_directory;
|
||||
std::ofstream(paths[i] / "progress") << users[i];
|
||||
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
|
||||
CHECK(Search(users[i]) == std::vector<std::string> {"slot"});
|
||||
}
|
||||
CHECK(paths[0] == fs::path("_SaveData") / "MULTIUSER" / "slot");
|
||||
CHECK(paths[0] != paths[1] && paths[1] != paths[2] && paths[0] != paths[2]);
|
||||
SceSaveDataTitleId title {};
|
||||
std::strcpy(title.data, "MULTIUSER");
|
||||
Reset("OTHER");
|
||||
for (size_t i = 0; i < users.size(); ++i) {
|
||||
CHECK(Search(users[i]).empty());
|
||||
CHECK(Search(users[i], &title) == std::vector<std::string> {"slot"});
|
||||
struct SaveDataTransferringMount transfer {};
|
||||
transfer.user_id = users[i];
|
||||
transfer.title_id = &title;
|
||||
transfer.dir_name = &name;
|
||||
SaveDataMountResult result {};
|
||||
CHECK(SaveDataTransferringMount(&transfer, &result) == OK);
|
||||
CHECK(g_mounted_directory == paths[i]);
|
||||
std::ifstream progress(paths[i] / "progress");
|
||||
int saved_user = -1;
|
||||
progress >> saved_user;
|
||||
CHECK(saved_user == users[i]);
|
||||
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
|
||||
}
|
||||
struct SaveDataDelete remove {};
|
||||
remove.user_id = users[1];
|
||||
remove.title_id = &title;
|
||||
remove.dir_name = &name;
|
||||
CHECK(SaveDataDelete(&remove) == OK);
|
||||
CHECK(Search(users[1], &title).empty());
|
||||
CHECK(Search(users[0], &title) == std::vector<std::string> {"slot"});
|
||||
CHECK(Search(users[2], &title) == std::vector<std::string> {"slot"});
|
||||
}
|
||||
|
||||
void TestSaveAllocations() {
|
||||
Reset("CAPACITY");
|
||||
std::array<SceSaveDataDirName, 2> names {DirName("Options"), DirName("Player")};
|
||||
@@ -596,6 +647,7 @@ void RunChild(const fs::path& executable, const char* mode) {
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
Config::Initialize();
|
||||
if (argc == 2) {
|
||||
if (std::string_view(argv[1]) == "write") {
|
||||
WriteRestartFixture();
|
||||
@@ -603,6 +655,7 @@ int main(int argc, char** argv) {
|
||||
CHECK(std::string_view(argv[1]) == "read");
|
||||
ReadRestartFixture();
|
||||
}
|
||||
Config::Shutdown();
|
||||
return 0;
|
||||
}
|
||||
const auto executable = fs::absolute(argv[0]);
|
||||
@@ -617,10 +670,12 @@ int main(int argc, char** argv) {
|
||||
TestIsolationAndSync();
|
||||
TestFailedWritePreservesSave();
|
||||
TestClassicSavePaths();
|
||||
TestGuestSaveIsolation();
|
||||
TestSaveAllocations();
|
||||
TestClassicSaveParams();
|
||||
CHECK(SaveDataTerminate() == OK);
|
||||
fs::current_path(previous);
|
||||
fs::remove_all(temp);
|
||||
Config::Shutdown();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -6572,6 +6572,11 @@ public:
|
||||
htile_depth.view_info.aspect = vk::ImageAspectFlagBits::eDepth;
|
||||
htile_depth.view_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment;
|
||||
const auto htile_depth_image = texture_cache.FindImage(htile_depth);
|
||||
(void)texture_cache.FindDepthTarget(htile_depth_image, htile_depth);
|
||||
Require(name, "HTile depth metadata registration",
|
||||
texture_cache.ClearMeta(htile_depth.info.metadata.range.address) &&
|
||||
texture_cache.TouchMeta(htile_depth.info.metadata.range.address, 0, false),
|
||||
"depth target did not register independent slice clear state");
|
||||
vk::ClearValue htile_clear{};
|
||||
htile_clear.depthStencil.depth = 0.25f;
|
||||
TextureCacheTestAccess::ClearImage(
|
||||
@@ -6605,6 +6610,11 @@ public:
|
||||
Require(name, "HTile depth storage prefix view",
|
||||
texture_cache.FindTexture(id, htile_storage) != nullptr,
|
||||
"converted depth prefix has no storage view");
|
||||
Require(name, "HTile metadata survives native depth replacement",
|
||||
texture_cache.IsMeta(htile_depth.info.metadata.range.address) &&
|
||||
!texture_cache.IsMetaCleared(htile_depth.info.metadata.range.address, 0) &&
|
||||
texture_cache.IsMetaCleared(htile_depth.info.metadata.range.address, 1),
|
||||
"storage conversion discarded guest metadata or its slice clear state");
|
||||
htile_storage_image = id;
|
||||
}
|
||||
auto htile_readback = CreateHostBuffer(
|
||||
@@ -6625,6 +6635,11 @@ public:
|
||||
std::vector<u32>(htile_layers, std::bit_cast<u32>(0.25f)),
|
||||
"partial storage conversion lost native depth data outside its view");
|
||||
DestroyBuffer(&htile_readback);
|
||||
texture_cache.UnmapMemory(htile_depth.info.metadata.range.address,
|
||||
htile_depth.info.metadata.range.size);
|
||||
Require(name, "HTile metadata allocation unmap",
|
||||
!texture_cache.IsMeta(htile_depth.info.metadata.range.address),
|
||||
"unmapped guest metadata retained its registration");
|
||||
|
||||
auto layered_stencil_desc = MakeLinearDesc(
|
||||
base + 0x27d4000, 6 * 4 * sizeof(float),
|
||||
@@ -7898,9 +7913,16 @@ public:
|
||||
"retiring the old DCC image erased the live HTile slice state");
|
||||
texture_cache.UnmapMemory(reused_depth.info.data.address,
|
||||
reused_depth.info.data.size);
|
||||
Require(name, "HTile allocation outlives native depth retirement",
|
||||
texture_cache.IsMeta(base + 0x28000) &&
|
||||
!texture_cache.IsMetaCleared(base + 0x28000, 0) &&
|
||||
texture_cache.IsMetaCleared(base + 0x28000, 1),
|
||||
"retiring depth data erased an independent metadata allocation");
|
||||
texture_cache.UnmapMemory(reused_depth.info.metadata.range.address,
|
||||
reused_depth.info.metadata.range.size);
|
||||
Require(name, "reused metadata final retirement",
|
||||
!texture_cache.IsMeta(base + 0x28000),
|
||||
"retiring the HTile image left its metadata registered");
|
||||
"unmapping the HTile allocation left its metadata registered");
|
||||
|
||||
constexpr uint64_t partial_unmap_image_offset = 0x2700000;
|
||||
auto partial_unmap_image =
|
||||
@@ -14672,7 +14694,7 @@ public:
|
||||
phased_rendering.num_color_attachments == 0 &&
|
||||
phased_rendering.depth_stencil_attachment.has_depth &&
|
||||
phased_rendering.depth_stencil_attachment.depth_clear &&
|
||||
phased_depth.depth_load_clear_enable &&
|
||||
static_cast<bool>(phased_depth.load_clear_aspects & vk::ImageAspectFlagBits::eDepth) &&
|
||||
texture_cache.IsMeta(phased_depth.desc.info.metadata.range.address) &&
|
||||
!texture_cache.IsMetaCleared(
|
||||
phased_depth.desc.info.metadata.range.address,
|
||||
@@ -14709,6 +14731,8 @@ public:
|
||||
RenderDepthInfo depth_only{};
|
||||
RenderExecutorTestAccess::ResolveRenderDepthTarget(
|
||||
executor, scheduler.Current(), depth_only);
|
||||
(void)RenderExecutorTestAccess::AcquireRenderTargets(
|
||||
executor, scheduler.Current(), &no_color, 0, depth_only);
|
||||
Require(
|
||||
name, "depth-only target with stale stencil state",
|
||||
depth_only.image_id && depth_only.desc.view_info.format == vk::Format::eD32Sfloat &&
|
||||
@@ -14853,7 +14877,7 @@ public:
|
||||
bounds_depth.image_id == depth_only.image_id &&
|
||||
bounds_depth.depth_bounds_test_enable &&
|
||||
!bounds_depth.depth_write_enable &&
|
||||
bounds_depth.depth_load_clear_enable == (pass == 0) &&
|
||||
static_cast<bool>(bounds_depth.load_clear_aspects & vk::ImageAspectFlagBits::eDepth) == (pass == 0) &&
|
||||
!texture_cache.IsMetaCleared(depth_only_htile_address, 0) &&
|
||||
bounds_feedback == (pass == 0
|
||||
? vk::ImageAspectFlagBits::eDepth
|
||||
@@ -14918,13 +14942,49 @@ public:
|
||||
texture_cache.ClearMeta(high_slice_target.htile_data_base_addr),
|
||||
"single high slice was rejected or changed the backing layer layout");
|
||||
auto high_slice_readback = CreateHostBuffer(
|
||||
name, 4 * sizeof(uint32_t), vk::BufferUsageFlagBits::eTransferDst, {});
|
||||
name, 7 * sizeof(uint32_t), vk::BufferUsageFlagBits::eTransferDst, {});
|
||||
auto high_slice_metadata = CreateHostBuffer(
|
||||
name, 6 * sizeof(uint32_t), vk::BufferUsageFlagBits::eTransferDst, {});
|
||||
const auto htile_slice_size = high_slice.desc.info.metadata.range.size / 87;
|
||||
const auto htile_slice_address =
|
||||
high_slice_target.htile_data_base_addr + 86 * htile_slice_size;
|
||||
Require(name, "ZMask support default", !Config::SupportZmaskEnabled(),
|
||||
"ZMask support should be disabled by default");
|
||||
const auto copy_htile = [&](uint32_t pass) {
|
||||
const auto program = make_buffer_program(
|
||||
ShaderType::Compute, {.read = true, .written = pass != 0});
|
||||
ShaderBufferResource descriptor{};
|
||||
descriptor.UpdateAddress48(high_slice_target.htile_data_base_addr);
|
||||
descriptor.fields[2] = high_slice.desc.info.metadata.range.size;
|
||||
ShaderRecompiler::IR::ResourceSnapshot snapshot;
|
||||
auto &value = snapshot.buffers.emplace_back();
|
||||
std::copy_n(descriptor.fields, 4, value.dwords.begin());
|
||||
value.dword_count = 4;
|
||||
ShaderStageRuntime runtime{&program, &snapshot};
|
||||
for (const bool support_zmask : {false, true}) {
|
||||
Config::Load({.support_zmask = support_zmask});
|
||||
PreparedBindings bindings;
|
||||
executor.PrepareBindings(runtime, bindings);
|
||||
std::array<PreparedBindings *, 1> stages{&bindings};
|
||||
executor.FindBuffers(stages);
|
||||
executor.RebindBuffers(bindings);
|
||||
const auto &source = bindings.buffers[0];
|
||||
const vk::BufferCopy copy{source.offset + 86 * htile_slice_size,
|
||||
(pass * 2 + support_zmask) * sizeof(uint32_t), sizeof(uint32_t)};
|
||||
scheduler.Current().Handle().copyBuffer(source.buffer,
|
||||
high_slice_metadata.buffer, 1, ©);
|
||||
}
|
||||
};
|
||||
for (uint32_t pass = 0; pass < 2; ++pass) {
|
||||
vk::ClearValue prior_depth{};
|
||||
prior_depth.depthStencil.depth = 0.625f;
|
||||
TextureCacheTestAccess::ClearImage(texture_cache, scheduler.Current(),
|
||||
high_slice.image_id, {vk::ImageAspectFlagBits::eDepth, 0, 1, 0, 87},
|
||||
prior_depth);
|
||||
if (pass == 1) {
|
||||
resources.GetBufferCache().FillBuffer(
|
||||
htile_slice_address, htile_slice_size, 0, false);
|
||||
}
|
||||
RenderDepthInfo acquired_high_slice{};
|
||||
RenderExecutorTestAccess::ResolveRenderDepthTarget(
|
||||
executor, scheduler.Current(), acquired_high_slice);
|
||||
@@ -14932,7 +14992,7 @@ public:
|
||||
executor, scheduler.Current(), &no_color, 0, acquired_high_slice);
|
||||
Require(name, "high HTile slice clear consumption",
|
||||
acquired_high_slice.image_id == high_slice.image_id &&
|
||||
acquired_high_slice.depth_load_clear_enable == (pass == 0) &&
|
||||
static_cast<bool>(acquired_high_slice.load_clear_aspects & vk::ImageAspectFlagBits::eDepth) == (pass == 0) &&
|
||||
rendering.depth_stencil_attachment.depth_clear == (pass == 0) &&
|
||||
!texture_cache.IsMetaCleared(high_slice_target.htile_data_base_addr, 86) &&
|
||||
texture_cache.IsMetaCleared(high_slice_target.htile_data_base_addr, 85),
|
||||
@@ -14944,15 +15004,67 @@ public:
|
||||
{vk::ImageAspectFlagBits::eDepth, 0, 85, 2}, {}, {1, 1, 1}};
|
||||
texture_cache.GetImage(high_slice.image_id).Download(
|
||||
std::span{©, 1}, high_slice_readback.buffer, 0, high_slice_readback.size);
|
||||
copy_htile(pass);
|
||||
}
|
||||
auto high_slice_storage = high_slice.desc;
|
||||
high_slice_storage.type = BindingType::Storage;
|
||||
high_slice_storage.info.pixel_format = vk::Format::eR32Sfloat;
|
||||
high_slice_storage.info.metadata = {};
|
||||
high_slice_storage.view_info.format = vk::Format::eR32Sfloat;
|
||||
high_slice_storage.view_info.aspect = vk::ImageAspectFlagBits::eColor;
|
||||
high_slice_storage.view_info.usage = vk::ImageUsageFlagBits::eStorage;
|
||||
const auto high_slice_storage_id = texture_cache.FindImage(high_slice_storage);
|
||||
Require(name, "HTile depth replacement before raw buffer read",
|
||||
high_slice_storage_id != high_slice.image_id,
|
||||
"R32 storage binding retained the native depth backing");
|
||||
resources.GetBufferCache().FillBuffer(
|
||||
htile_slice_address, htile_slice_size, 0, false);
|
||||
copy_htile(2);
|
||||
Config::Load({});
|
||||
for (uint32_t pass = 0; pass < 3; ++pass) {
|
||||
if (pass < 2) {
|
||||
Require(name, "HTile clear across copied depth recreation",
|
||||
texture_cache.ClearMeta(high_slice_target.htile_data_base_addr),
|
||||
"copied depth lost HTile registration");
|
||||
}
|
||||
RenderDepthInfo copied_depth{};
|
||||
RenderExecutorTestAccess::ResolveRenderDepthTarget(
|
||||
executor, scheduler.Current(), copied_depth);
|
||||
if (pass == 2) {
|
||||
vk::ClearValue prior_depth{};
|
||||
prior_depth.depthStencil.depth = 0.625f;
|
||||
TextureCacheTestAccess::ClearImage(texture_cache, scheduler.Current(),
|
||||
copied_depth.image_id, {vk::ImageAspectFlagBits::eDepth, 0, 1, 86, 1},
|
||||
prior_depth);
|
||||
}
|
||||
const auto rendering = RenderExecutorTestAccess::AcquireRenderTargets(
|
||||
executor, scheduler.Current(), &no_color, 0, copied_depth);
|
||||
Require(name, "copied depth consumes only subsequent HTile clears",
|
||||
copied_depth.image_id != high_slice_storage_id &&
|
||||
rendering.depth_stencil_attachment.depth_clear == (pass == 1),
|
||||
"copied depth was cleared again or ignored a subsequent metadata clear");
|
||||
scheduler.BeginRendering(rendering);
|
||||
scheduler.EndRendering();
|
||||
RenderExecutorTestAccess::ResetBindings(executor);
|
||||
const vk::BufferImageCopy copy{(4 + pass) * sizeof(uint32_t), 0, 0,
|
||||
{vk::ImageAspectFlagBits::eDepth, 0, 86, 1}, {}, {1, 1, 1}};
|
||||
texture_cache.GetImage(copied_depth.image_id).Download(
|
||||
std::span{©, 1}, high_slice_readback.buffer, 0, high_slice_readback.size);
|
||||
}
|
||||
scheduler.Current().Handle().pipelineBarrier2(bounds_dependency);
|
||||
scheduler.Finish();
|
||||
Require(name, "high HTile slice contents and neighbor",
|
||||
ReadBuffer(name, high_slice_readback, 4) == std::vector<uint32_t>{
|
||||
ReadBuffer(name, high_slice_readback, 7) == std::vector<uint32_t>{
|
||||
std::bit_cast<uint32_t>(0.625f), std::bit_cast<uint32_t>(0.375f),
|
||||
std::bit_cast<uint32_t>(0.625f), std::bit_cast<uint32_t>(0.625f)},
|
||||
"layer86 clear missed, repeated, or changed adjacent layer85");
|
||||
std::bit_cast<uint32_t>(0.625f), std::bit_cast<uint32_t>(0.625f),
|
||||
std::bit_cast<uint32_t>(0.625f), std::bit_cast<uint32_t>(0.375f),
|
||||
std::bit_cast<uint32_t>(0.625f)},
|
||||
"layer86 clear missed, repeated, or discarded copied depth");
|
||||
Require(name, "GPU-visible opt-in ZMask support",
|
||||
ReadBuffer(name, high_slice_metadata, 6) == std::vector<uint32_t>{0, 15, 0, 15, 0, 15},
|
||||
"raw HTile binding ignored the ZMask setting or lost it after depth replacement");
|
||||
DestroyBuffer(&high_slice_readback);
|
||||
DestroyBuffer(&high_slice_metadata);
|
||||
registers.SetDepthClearValue(0.0f);
|
||||
|
||||
// DB override copies run with depth and stencil tests disabled.
|
||||
@@ -17912,7 +18024,7 @@ public:
|
||||
registers.SetTargetOutputMode(1, 4);
|
||||
stencil_target.z_info.htile_acceleration = true;
|
||||
stencil_target.htile_data_base_addr = depth_address + 0x30000;
|
||||
registers.SetDepthRenderTarget(stencil_target);
|
||||
registers.SetStencilClearValue(5);
|
||||
registers.SetDepthClearValue(1);
|
||||
stencil_control = {};
|
||||
stencil_control.stencil_enable = true;
|
||||
@@ -17922,35 +18034,60 @@ public:
|
||||
registers.SetStencilMask({0x80, 0, 0x80, 1, 0x80, 0, 0x80, 1});
|
||||
user_config.SetPrimitiveType(Prospero::PrimitiveType::kTriList);
|
||||
shaders.SetEsShaderBase(vertex_address);
|
||||
for (uint32_t discard = 0; discard < null_pixels.size(); discard++) {
|
||||
const auto &code = null_pixels[discard];
|
||||
const auto address = reinterpret_cast<uint64_t>(code.data());
|
||||
ShaderMapUserData(address,
|
||||
{.type = Prospero::ShaderBinaryType::kPs,
|
||||
.code_size_bytes = static_cast<uint32_t>(code.size() * sizeof(u32))});
|
||||
shaders.SetPsShaderBase(address);
|
||||
RenderExecutorTestAccess::ResolveRenderDepthTarget(executor, scheduler.Current(), depth);
|
||||
(void)cache.FindDepthTarget(depth.image_id, depth.desc);
|
||||
vk::ClearValue zero{};
|
||||
TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), depth.image_id,
|
||||
{vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil,
|
||||
0, 1, 0, 1}, zero);
|
||||
Require(name, "pending stencil-pass HTile clear",
|
||||
cache.ClearMeta(stencil_target.htile_data_base_addr),
|
||||
"the stencil pass did not retain its HTile metadata");
|
||||
RenderExecutorTestAccess::DrawAuto(
|
||||
executor, scheduler.Current(), {.vertex_count = 3, .instance_count = 1});
|
||||
const auto depth_pixels = ReadCachedTexel(name, context, depth.image_id,
|
||||
{}, {extent, extent, 1});
|
||||
Require(name, "NULL-export full depth clear",
|
||||
std::ranges::all_of(depth_pixels, [](u32 v) { return v == 0x3f800000u; }),
|
||||
"an unexported color target clipped the pending depth clear");
|
||||
const auto stencil = read_stencil();
|
||||
const auto expected = discard == 0 ? 0x80808080u : 0u;
|
||||
Require(name, "NULL-export stencil coverage and discard",
|
||||
std::ranges::all_of(stencil, [=](u32 v) { return v == expected; }),
|
||||
"a stale color target clipped stencil coverage or the NULL-export shader was skipped");
|
||||
// Compressed metadata clears stencil; an uncompressed plane and an explicit
|
||||
// depth-only clear preserve its previous contents. Match Suikoden's stale mask 2.
|
||||
for (uint32_t clear_mode = 0; clear_mode < 3; clear_mode++) {
|
||||
stencil_target.stencil_info.htile_stencil_disabled = clear_mode == 0;
|
||||
registers.SetDepthRenderTarget(stencil_target);
|
||||
registers.SetRenderControl({.depth_clear_enable = clear_mode == 2});
|
||||
for (uint32_t discard = 0; discard < null_pixels.size(); discard++) {
|
||||
const auto &code = null_pixels[discard];
|
||||
const auto address = reinterpret_cast<uint64_t>(code.data());
|
||||
ShaderMapUserData(address,
|
||||
{.type = Prospero::ShaderBinaryType::kPs,
|
||||
.code_size_bytes = static_cast<uint32_t>(code.size() * sizeof(u32))});
|
||||
shaders.SetPsShaderBase(address);
|
||||
RenderExecutorTestAccess::ResolveRenderDepthTarget(executor, scheduler.Current(), depth);
|
||||
(void)cache.FindDepthTarget(depth.image_id, depth.desc);
|
||||
vk::ClearValue zero{};
|
||||
zero.depthStencil.stencil = 2;
|
||||
TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), depth.image_id,
|
||||
{vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil,
|
||||
0, 1, 0, 1}, zero);
|
||||
Require(name, "pending stencil-pass HTile clear",
|
||||
clear_mode == 2
|
||||
? cache.TouchMeta(stencil_target.htile_data_base_addr, 0, false)
|
||||
: cache.ClearMeta(stencil_target.htile_data_base_addr),
|
||||
"the stencil pass did not retain its HTile metadata");
|
||||
RenderExecutorTestAccess::DrawAuto(
|
||||
executor, scheduler.Current(), {.vertex_count = 3, .instance_count = 1});
|
||||
const auto depth_pixels = ReadCachedTexel(name, context, depth.image_id,
|
||||
{}, {extent, extent, 1});
|
||||
Require(name, "NULL-export full depth clear",
|
||||
std::ranges::all_of(depth_pixels, [](u32 v) { return v == 0x3f800000u; }),
|
||||
"an unexported color target clipped the pending depth clear");
|
||||
const auto stencil = read_stencil();
|
||||
const auto cleared = clear_mode == 1 ? 0x05050505u : 0x02020202u;
|
||||
const auto expected = cleared | (discard == 0 ? 0x80808080u : 0u);
|
||||
Require(name, "NULL-export stencil coverage and discard",
|
||||
std::ranges::all_of(stencil, [=](u32 v) { return v == expected; }),
|
||||
"a stale color target clipped stencil coverage or the NULL-export shader was skipped");
|
||||
Require(name, "HTile clear consumed once",
|
||||
!cache.IsMetaCleared(stencil_target.htile_data_base_addr, 0),
|
||||
"the draw retained a pending metadata clear");
|
||||
if (clear_mode == 1 && discard == 1) {
|
||||
zero.depthStencil.stencil = 9;
|
||||
TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), depth.image_id,
|
||||
{vk::ImageAspectFlagBits::eStencil, 0, 1, 0, 1}, zero);
|
||||
RenderExecutorTestAccess::DrawAuto(
|
||||
executor, scheduler.Current(), {.vertex_count = 3, .instance_count = 1});
|
||||
Require(name, "consumed HTile clear preserves later stencil",
|
||||
std::ranges::all_of(read_stencil(), [](u32 v) { return v == 0x09090909u; }),
|
||||
"a consumed metadata clear ran again on the next draw");
|
||||
}
|
||||
}
|
||||
}
|
||||
registers.SetRenderControl({});
|
||||
|
||||
// A depth-only draw retains the previous PS address and write masks, but
|
||||
// SPI_SHADER_COL_FORMAT disables its exports. Compiling this stale PS
|
||||
@@ -25850,6 +25987,49 @@ TestCase CvtPkrtzF16F32SubnormalRoundsTowardZero() {
|
||||
O::S_ENDPGM}};
|
||||
}
|
||||
|
||||
TestCase CvtPkrtzF16F32RuntimeEdges() {
|
||||
using O = ShaderOpcode;
|
||||
// Independent expected half encodings, including truncation boundaries and NaN payloads.
|
||||
constexpr std::array<std::array<u32, 2>, 31> inputs{{
|
||||
{0x00000000u, 0x0000u}, {0x80000000u, 0x8000u},
|
||||
{0x00000001u, 0x0000u}, {0x007fffffu, 0x0000u},
|
||||
{0x00800000u, 0x0000u}, {0x32ffffffu, 0x0000u},
|
||||
{0x33000000u, 0x0000u}, {0x337fffffu, 0x0000u},
|
||||
{0x33800000u, 0x0001u}, {0x33c00000u, 0x0001u},
|
||||
{0x34000000u, 0x0002u}, {0x387fc000u, 0x03ffu},
|
||||
{0x387fe000u, 0x03ffu}, {0x387fffffu, 0x03ffu},
|
||||
{0x38800000u, 0x0400u}, {0x38802000u, 0x0401u},
|
||||
{0x3e22f983u, 0x3117u}, {0x3f800000u, 0x3c00u},
|
||||
{0x3f801000u, 0x3c00u}, {0x3f801001u, 0x3c00u},
|
||||
{0x3f802000u, 0x3c01u}, {0x477fe000u, 0x7bffu},
|
||||
{0x477ff000u, 0x7bffu}, {0x477fffffu, 0x7bffu},
|
||||
{0x47800000u, 0x7bffu}, {0x7f7fffffu, 0x7bffu},
|
||||
{0x7f800000u, 0x7c00u}, {0x7f800001u, 0x7e00u},
|
||||
{0x7fa02000u, 0x7f01u}, {0x7fc12000u, 0x7e09u},
|
||||
{0x7fffffffu, 0x7fffu},
|
||||
}};
|
||||
TestCase test;
|
||||
test.name = "CvtPkrtzF16F32RuntimeEdges";
|
||||
for (const auto &input : inputs)
|
||||
test.initial.insert(test.initial.end(), {input[0], input[0] ^ 0x80000000u});
|
||||
test.expected = test.initial;
|
||||
for (u32 i = 0; i < inputs.size(); ++i) {
|
||||
for (u32 reg = 0; reg < 2; ++reg) {
|
||||
AppendVMovU32(&test.code, 30, (i * 2u + reg) * 4u);
|
||||
AppendBufferLoadDword(&test.code, reg, 30);
|
||||
}
|
||||
test.code.push_back(EncodeVop2(0x2f, 2, Vgpr(0), 1));
|
||||
AppendStoreVgpr(&test.code, 2, static_cast<u32>(test.expected.size()));
|
||||
const auto half = inputs[i][1];
|
||||
test.expected.push_back(half | ((half ^ 0x8000u) << 16));
|
||||
}
|
||||
AppendEnd(&test.code);
|
||||
test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_CVT_PKRTZ_F16_F32,
|
||||
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
test.forbidden_spirv = {"PackHalf2x16", "UnpackHalf2x16"};
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase CvtPkrtzF16F32SdwaAndOutputModifiers() {
|
||||
using O = ShaderOpcode;
|
||||
|
||||
@@ -31086,6 +31266,69 @@ TestCase BufferLoadDwordx2SnapshotsOverlappingAddress() {
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase CoherentBufferLoadDwordx2Bounds(u32 wave, u32 prefix) {
|
||||
using O = ShaderOpcode;
|
||||
TestCase test;
|
||||
test.name = wave == 64 ? (prefix ? "CoherentBufferPairBoundsOffsetWave64"
|
||||
: "CoherentBufferPairBoundsWave64")
|
||||
: (prefix ? "CoherentBufferPairBoundsOffsetWave32"
|
||||
: "CoherentBufferPairBoundsWave32");
|
||||
test.initial = {0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u};
|
||||
test.initial.resize(12);
|
||||
test.expected = test.initial;
|
||||
const u32 addresses[] = {0u, 4u, 8u, 0xfffffffcu};
|
||||
const u32 base = prefix / 4;
|
||||
const u32 values[][2] = {{test.initial[base], test.initial[base + 1]},
|
||||
{test.initial[base + 1], test.initial[base + 2]},
|
||||
{test.initial[base + 2], 0u},
|
||||
{0u, test.initial[base]}};
|
||||
for (u32 i = 0; i < std::size(addresses); ++i) {
|
||||
AppendVMovLiteral(&test.code, 20, addresses[i]);
|
||||
test.code.push_back(EncodeMubuf0(0x0d, 0, false, true, true) | (1u << 15u));
|
||||
test.code.push_back(EncodeMubuf1(2, 0, 20));
|
||||
for (u32 component = 0; component < 2; ++component) {
|
||||
AppendStoreVgpr(&test.code, 2 + component, 4 + i * 2 + component);
|
||||
test.expected[4 + i * 2 + component] = values[i][component];
|
||||
}
|
||||
}
|
||||
AppendEnd(&test.code);
|
||||
test.user_data = MakeStructuredStorageBufferData(0, 12);
|
||||
test.has_user_data = true;
|
||||
test.storage_buffer_offsets = {prefix};
|
||||
test.storage_buffer_ranges = {{0u, 12u + prefix}};
|
||||
test.required_spirv = {"OpAtomicLoad %ulong", "Int64Atomics",
|
||||
"ArrayStride 8"};
|
||||
test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORDX2, O::BUFFER_STORE_DWORD,
|
||||
O::S_ENDPGM};
|
||||
test.compute_info.wave_size = wave;
|
||||
test.has_compute_info = true;
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase CoherentBufferLoadDwordx2Swizzled() {
|
||||
using O = ShaderOpcode;
|
||||
TestCase test;
|
||||
test.name = "CoherentBufferLoadDwordx2Swizzled";
|
||||
AppendVMovU32(&test.code, 20, 0);
|
||||
test.code.push_back(EncodeMubuf0(0x0d, 0, true, false, true) | (1u << 15u));
|
||||
test.code.push_back(EncodeMubuf1(2, 0, 20));
|
||||
AppendStoreVgpr(&test.code, 2, 9);
|
||||
AppendStoreVgpr(&test.code, 3, 10);
|
||||
AppendEnd(&test.code);
|
||||
test.initial = {0x11111111u, 0x22222222u, 0, 0, 0, 0,
|
||||
0, 0, 0x33333333u, 0, 0};
|
||||
test.expected = test.initial;
|
||||
test.expected[9] = test.initial[0];
|
||||
test.expected[10] =
|
||||
test.initial[8]; // Four-byte elements span eight-index groups.
|
||||
test.user_data = MakeStructuredStorageBufferData(8, 8);
|
||||
test.user_data[1] |= 1u << 31u;
|
||||
test.has_user_data = true;
|
||||
test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORDX2, O::BUFFER_STORE_DWORD,
|
||||
O::S_ENDPGM};
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase BufferLoadDwordx3SnapshotsOverlappingAddress() {
|
||||
using O = ShaderOpcode;
|
||||
|
||||
@@ -35635,6 +35878,65 @@ TestCase Wave64AppendConsumeHighHalf() {
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase CoherentBufferPairContention(u32 wave_size) {
|
||||
using O = ShaderOpcode;
|
||||
TestCase test;
|
||||
test.name = wave_size == 64 ? "CoherentBufferPairContentionWave64"
|
||||
: "CoherentBufferPairContentionWave32";
|
||||
auto &code = test.code;
|
||||
AppendVMovU32(&code, 20, 0);
|
||||
AppendVMovU32(&code, 6, 0);
|
||||
AppendSMovLiteral(&code, 20, 128);
|
||||
const auto loop = code.size();
|
||||
code.push_back(EncodeVop2(0x25, 2, InlineU32(1), 0));
|
||||
code.push_back(
|
||||
EncodeVop1(0x37, 3, Vgpr(2))); // v_not_b32: correlated high word.
|
||||
AppendBufferStoreOpcode(&code, 0x50, 2, 20);
|
||||
code.push_back(EncodeMubuf0(0x0d, 0, false, true, true) | (1u << 15u));
|
||||
code.push_back(EncodeMubuf1(4, 12, 20));
|
||||
code.push_back(EncodeVop2(0x1d, 4, Vgpr(4), 5));
|
||||
code.push_back(EncodeVop1(0x37, 4, Vgpr(4)));
|
||||
code.push_back(EncodeVop2(0x1c, 6, Vgpr(4), 6));
|
||||
code.push_back(EncodeSop2(0x01, 20, 20, InlineU32(1)));
|
||||
code.push_back(EncodeSopc(0x07, 20, InlineU32(0)));
|
||||
code.push_back(EncodeSopp(0x05, static_cast<u32>(loop - code.size() - 1)));
|
||||
AppendStoreVgprAtLaneDwordOffset(&code, 6, 0, 2);
|
||||
code.push_back(EncodeSopp(0x0a, 0));
|
||||
// Restore a deterministic mailbox after all readers finish.
|
||||
code.push_back(EncodeSop1(0x04, 126, InlineU32(1)));
|
||||
AppendVMovU32(&code, 2, 0);
|
||||
AppendVMovLiteral(&code, 3, 0xffffffffu);
|
||||
AppendBufferStoreOpcode(&code, 0x50, 2, 20);
|
||||
AppendEnd(&code);
|
||||
test.initial.assign(wave_size * 2 + 2, 0);
|
||||
test.initial[1] = 0xffffffffu;
|
||||
test.expected = test.initial;
|
||||
test.required_spirv = {"OpAtomicExchange %ulong", "OpAtomicLoad %ulong",
|
||||
"Int64Atomics", "OpLoopMerge"};
|
||||
test.opcodes = {O::V_MOV_B32,
|
||||
O::S_MOV_B32,
|
||||
O::S_MOV_B64,
|
||||
O::V_ADD_NC_U32,
|
||||
O::V_NOT_B32,
|
||||
O::V_XOR_B32,
|
||||
O::V_OR_B32,
|
||||
O::BUFFER_ATOMIC_SWAP_X2,
|
||||
O::BUFFER_LOAD_DWORDX2,
|
||||
O::S_SUB_U32,
|
||||
O::S_CMP_LG_U32,
|
||||
O::S_CBRANCH_SCC1,
|
||||
O::V_LSHLREV_B32,
|
||||
O::BUFFER_STORE_DWORD,
|
||||
O::S_BARRIER,
|
||||
O::S_ENDPGM};
|
||||
test.compute_info.threads_num[0] = wave_size * 2;
|
||||
test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1;
|
||||
test.compute_info.thread_ids_num = 1;
|
||||
test.compute_info.wave_size = wave_size;
|
||||
test.has_compute_info = true;
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase BufferWorkgroupPublication(u32 wave_size, bool dlc_only = false,
|
||||
bool store_completion = false) {
|
||||
using O = ShaderOpcode;
|
||||
@@ -42064,6 +42366,7 @@ std::vector<TestCase> MakeCases() {
|
||||
AddCase(Vop3pInt16Ops);
|
||||
AddCase(CvtPkU8F32PacksSelectedByte);
|
||||
AddCase(CvtPkrtzF16F32SubnormalRoundsTowardZero);
|
||||
AddCase(CvtPkrtzF16F32RuntimeEdges);
|
||||
AddCase(CvtPkrtzF16F32SdwaAndOutputModifiers);
|
||||
AddCase(PackedMinMaxF16NanAndSignedZeroEdges);
|
||||
AddCase(VectorMinMaxF16Ops);
|
||||
@@ -42221,6 +42524,12 @@ std::vector<TestCase> MakeCases() {
|
||||
cases.push_back(BufferSubwordStoresAtHostOffset(2));
|
||||
cases.push_back(BufferSubwordStoresAtHostOffset(1));
|
||||
AddCase(BufferLoadDwordx2SnapshotsOverlappingAddress);
|
||||
for (const u32 wave : {32u, 64u}) {
|
||||
for (const u32 prefix : {0u, 4u})
|
||||
cases.push_back(CoherentBufferLoadDwordx2Bounds(wave, prefix));
|
||||
cases.push_back(CoherentBufferPairContention(wave));
|
||||
}
|
||||
AddCase(CoherentBufferLoadDwordx2Swizzled);
|
||||
AddCase(BufferLoadDwordx3SnapshotsOverlappingAddress);
|
||||
AddCase(BufferLoadDwordx4SnapshotsOverlappingAddress);
|
||||
AddCase(BufferLoadDwordx4ZeroesOnlyOutOfBoundsTail);
|
||||
@@ -45575,12 +45884,12 @@ void CheckDepthAttachmentWrites() {
|
||||
|
||||
target.depth_test_enable = false;
|
||||
target.depth_write_enable = false;
|
||||
target.depth_load_clear_enable = true;
|
||||
target.load_clear_aspects = vk::ImageAspectFlagBits::eDepth;
|
||||
Require("DepthAttachmentWrites", "depth clear",
|
||||
target.AttachmentWriteAspects() == vk::ImageAspectFlagBits::eDepth,
|
||||
"depth attachment clear did not claim depth");
|
||||
|
||||
target.depth_load_clear_enable = false;
|
||||
target.load_clear_aspects = {};
|
||||
target.stencil_test_enable = true;
|
||||
target.stencil_front = {vk::StencilOp::eKeep, vk::StencilOp::eKeep,
|
||||
vk::StencilOp::eKeep, vk::CompareOp::eAlways, 0xff, 0xff, 0};
|
||||
@@ -45606,7 +45915,7 @@ void CheckDepthAttachmentWrites() {
|
||||
"masked front write or KEEP-only back face claimed stencil");
|
||||
|
||||
target.stencil_test_enable = false;
|
||||
target.stencil_clear_enable = true;
|
||||
target.load_clear_aspects = vk::ImageAspectFlagBits::eStencil;
|
||||
Require("DepthAttachmentWrites", "stencil clear",
|
||||
target.AttachmentWriteAspects() == vk::ImageAspectFlagBits::eStencil,
|
||||
"stencil attachment clear did not claim stencil");
|
||||
@@ -45680,12 +45989,11 @@ void CheckDepthFeedbackAspects() {
|
||||
feedback(sampled) == vk::ImageAspectFlagBits::eDepth,
|
||||
"overlapping mip and layer boundaries missed depth feedback");
|
||||
target.depth_write_enable = false;
|
||||
target.depth_load_clear_enable = true;
|
||||
target.load_clear_aspects = vk::ImageAspectFlagBits::eDepth;
|
||||
Require("DepthFeedbackAspects", "depth clear only",
|
||||
feedback(sampled) == vk::ImageAspectFlagBits::eDepth,
|
||||
"a sampled depth attachment clear missed feedback");
|
||||
target.depth_load_clear_enable = false;
|
||||
target.stencil_clear_enable = true;
|
||||
target.load_clear_aspects = vk::ImageAspectFlagBits::eStencil;
|
||||
sampled.aspect = vk::ImageAspectFlagBits::eStencil;
|
||||
Require("DepthFeedbackAspects", "stencil clear only",
|
||||
feedback(sampled) == vk::ImageAspectFlagBits::eStencil,
|
||||
@@ -45739,7 +46047,7 @@ void CheckDynamicRenderingState() {
|
||||
depth_attachment_layout(attachment) ==
|
||||
vk::ImageLayout::eDepthStencilReadOnlyOptimal,
|
||||
"fully read-only depth/stencil used a writable layout");
|
||||
attachment.depth_load_clear_enable = true;
|
||||
attachment.load_clear_aspects = vk::ImageAspectFlagBits::eDepth;
|
||||
Require("DynamicRenderingState", "deferred depth clear layout",
|
||||
depth_attachment_layout(attachment) ==
|
||||
vk::ImageLayout::eDepthAttachmentStencilReadOnlyOptimal,
|
||||
@@ -45750,9 +46058,9 @@ void CheckDynamicRenderingState() {
|
||||
depth_attachment_layout(attachment) ==
|
||||
vk::ImageLayout::eDepthAttachmentStencilReadOnlyOptimal,
|
||||
"depth-only writes did not retain read-only stencil");
|
||||
attachment.depth_load_clear_enable = false;
|
||||
attachment.load_clear_aspects = {};
|
||||
attachment.depth_write_enable = false;
|
||||
attachment.stencil_clear_enable = true;
|
||||
attachment.load_clear_aspects = vk::ImageAspectFlagBits::eStencil;
|
||||
Require("DynamicRenderingState", "depth-read stencil-write layout",
|
||||
depth_attachment_layout(attachment) ==
|
||||
vk::ImageLayout::eDepthReadOnlyStencilAttachmentOptimal,
|
||||
@@ -47875,6 +48183,13 @@ int main(int argc, char **argv) {
|
||||
std::setvbuf(stdout, nullptr, _IONBF, 0);
|
||||
EnsureConfigInitialized();
|
||||
CheckLeastRecentlyUsedCacheOrdering();
|
||||
if (argc == 2 && std::strcmp(argv[1], "--cvt-pkrtz-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, CvtPkrtzF16F32SubnormalRoundsTowardZero());
|
||||
RunCase(&vulkan, CvtPkrtzF16F32RuntimeEdges());
|
||||
RunCase(&vulkan, CvtPkrtzF16F32SdwaAndOutputModifiers());
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--thread-dimensions-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
CheckComputeThreadDimensions(vulkan);
|
||||
@@ -48414,6 +48729,12 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--buffer-publication-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
for (const u32 wave : {32u, 64u}) {
|
||||
for (const u32 prefix : {0u, 4u})
|
||||
RunCase(&vulkan, CoherentBufferLoadDwordx2Bounds(wave, prefix));
|
||||
RunCase(&vulkan, CoherentBufferPairContention(wave));
|
||||
}
|
||||
RunCase(&vulkan, CoherentBufferLoadDwordx2Swizzled());
|
||||
RunCase(&vulkan, BufferWorkgroupPublication(32));
|
||||
RunCase(&vulkan, BufferWorkgroupPublication(64));
|
||||
RunCase(&vulkan, BufferWorkgroupPublication(32, true));
|
||||
@@ -48710,8 +49031,12 @@ int main(int argc, char **argv) {
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--htile-clear-only") == 0) {
|
||||
CheckNativeMsaaState();
|
||||
CheckPs5DepthRegisterDecoding();
|
||||
CheckDepthHtileStencilCompatibility();
|
||||
VulkanHarness vulkan;
|
||||
vulkan.CheckUnifiedTextureCacheFlow();
|
||||
vulkan.CheckRenderExecutorStencilBindingDiscovery();
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--dcc-clear-only") == 0) {
|
||||
|
||||
@@ -11108,12 +11108,13 @@ void TestMergedShaderUserDataSnapshot() {
|
||||
ShaderMapUserData(regs.gs_regs.data_addr, mapped);
|
||||
HW::Context context;
|
||||
context.SetShaderStages(0x20);
|
||||
context.SetMaxOutputPerSubgroup(252);
|
||||
context.SetMaxOutputPerSubgroup(192);
|
||||
context.SetGsMaxVertOut(18);
|
||||
context.SetNggSubgrpCntl(16);
|
||||
context.SetGsOutPrimType(2);
|
||||
HW::UserConfig user_config;
|
||||
user_config.SetPrimitiveType(Prospero::PrimitiveType::kTriList);
|
||||
user_config.SetGeControl({14, 14});
|
||||
user_config.SetGeControl({62, 192});
|
||||
ShaderVertexInputInfo first_input{};
|
||||
const auto first = PrepareProgram(regs, context, user_config, first_input);
|
||||
regs.gs_regs.user_data_addr = reinterpret_cast<uint64_t>(second_table.data());
|
||||
@@ -11126,8 +11127,10 @@ void TestMergedShaderUserDataSnapshot() {
|
||||
"merged shader parameters did not snapshot ordinary user SGPRs at s8");
|
||||
Check(first.hash == second.hash &&
|
||||
MakeStageStaticKey(first_input) == MakeStageStaticKey(second_input) &&
|
||||
first_input.mesh.scratch_size_dwords == 7,
|
||||
"merged shader identity or maximum scratch size changed with dynamic user data");
|
||||
first_input.mesh.scratch_size_dwords == 7 &&
|
||||
first_input.mesh.primitives_per_group == 10 &&
|
||||
first_input.mesh.max_primitives == 160 && first_input.mesh.threads_num[0] == 192,
|
||||
"merged shader lost its identity, scratch size or amplified subgroup bounds");
|
||||
CompileOptions options{};
|
||||
options.stage = ShaderType::Mesh;
|
||||
options.user_data_base = 0;
|
||||
@@ -11190,6 +11193,7 @@ void TestMergedShaderUserDataSnapshot() {
|
||||
regs.gs_regs.rsrc2.lds_size = 64;
|
||||
regs.gs_regs.rsrc1.gs_vgpr_component_count = 1;
|
||||
context.SetGsMaxVertOut(0);
|
||||
context.SetNggSubgrpCntl(1);
|
||||
for (const uint32_t wave_size : {32u, 64u}) {
|
||||
context.SetShaderStages(wave_size == 32 ? 0x00400000u : 0u);
|
||||
for (const uint16_t capacity : {64u, 128u, 256u}) {
|
||||
@@ -11215,18 +11219,31 @@ void TestMergedShaderUserDataSnapshot() {
|
||||
context.SetShaderStages(0x20);
|
||||
context.SetMaxOutputPerSubgroup(256);
|
||||
context.SetGsMaxVertOut(8);
|
||||
context.SetNggSubgrpCntl(6);
|
||||
user_config.SetPrimitiveType(Prospero::PrimitiveType::kTriFan);
|
||||
user_config.SetGeControl({32, 32});
|
||||
ShaderVertexInputInfo fan_input{};
|
||||
PrepareProgram(regs, context, user_config, fan_input);
|
||||
Check(fan_input.mesh.primitives_per_group == 30 && fan_input.mesh.vertices_per_group == 32 &&
|
||||
fan_input.mesh.max_vertices == 256 && fan_input.mesh.max_primitives == 192,
|
||||
fan_input.mesh.max_vertices == 256 && fan_input.mesh.max_primitives == 180,
|
||||
"captured triangle-fan GS configuration lost its subgroup assembly limits");
|
||||
context.SetShaderStages(0x00482030u);
|
||||
context.SetShaderStages(0x00402030u);
|
||||
context.SetMaxOutputPerSubgroup(32);
|
||||
context.SetGsMaxVertOut(32);
|
||||
user_config.SetPrimitiveType(Prospero::PrimitiveType::kPointList);
|
||||
user_config.SetGeControl({1, 1});
|
||||
for (const uint32_t amplification : {32u, 48u}) {
|
||||
context.SetNggSubgrpCntl(amplification);
|
||||
ShaderVertexInputInfo input{};
|
||||
PrepareProgram(regs, context, user_config, input);
|
||||
Check(!input.mesh.fast_launch && input.mesh.primitives_per_group == 1 &&
|
||||
input.mesh.vertices_per_group == 1 && input.mesh.max_vertices == 32 &&
|
||||
input.mesh.max_primitives == amplification &&
|
||||
input.mesh.threads_num[0] == (amplification == 32u ? 32u : 64u),
|
||||
"native NGG output primitives were derived from a vertex strip or omitted from subgroup size");
|
||||
}
|
||||
context.SetShaderStages(0x00482030u);
|
||||
context.SetNggSubgrpCntl(32);
|
||||
regs.gs_regs.rsrc1.gs_vgpr_component_count = 0;
|
||||
regs.gs_regs.rsrc2.es_vgpr_component_count = 1;
|
||||
ShaderVertexInputInfo fast_input{};
|
||||
@@ -14508,6 +14525,7 @@ void TestPatternFill() {
|
||||
const PatternCase cases[] = {
|
||||
{bounded_shader, 64, true, 10},
|
||||
{thread_dimension_shader, 32, false, 8},
|
||||
{thread_dimension_shader, 64, false, 8},
|
||||
{thread_dimension_wave64_shader, 64, false, 9},
|
||||
{thread_dimension_wave64_scalar_shader, 64, false, 8},
|
||||
{thread_dimension_wave64_vector_shader, 64, false, 8},
|
||||
@@ -14584,6 +14602,8 @@ void TestPatternFill() {
|
||||
Check(Build(changed).uniform_fill.fill.kind == UniformFillKind::None,
|
||||
"pattern recognition accepted modified executable instructions");
|
||||
for (const uint32_t invalid_input : {0u, 1u, 2u, 3u}) {
|
||||
// Both wave sizes are covered above for this executable.
|
||||
if (invalid_input == 1 && shader.data() == thread_dimension_shader) continue;
|
||||
const auto saved_compute = compute;
|
||||
const auto saved_wave = options.wave_size;
|
||||
if (invalid_input == 0) compute.workgroup_register = 11;
|
||||
|
||||
Reference in New Issue
Block a user