Compare commits

...

58 Commits

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

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

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-17 14:47:51 +02:00
Felipe 415387f00d Support multiple controllers for local multiplayer (#1583)
* Support multiple controllers for local co-op
2026-10-17 13:15:22 +02:00
Matthew Hurley 890393fbe6 vulkan: start without fragmentShaderBarycentric on GPUs that lack it (#1590)
* vulkan: start without fragmentShaderBarycentric on GPUs that lack it

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

* vulkan: fix macOS build and warn about unsupported GPUs

---------

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

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

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

Two more vectors in PackedMinMaxF16NanAndSignedZeroEdges cover it.

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

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

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

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

* shader: reject unsupported GDS compare-store variant

---------

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 00:05:08 +02:00
nmzik 5dec523143 Stub RazorCpu profiling markers in a dedicated library 2026-10-16 19:16:42 +02:00
nmzik d01633e18e Revert "Fix scalar buffer reads across sparse guest mappings"
This reverts commit d7974dc763.
2026-10-16 19:13:36 +02:00
nmzik e7ac242039 Load packaged system modules through the native module lifecycle 2026-10-16 19:13:36 +02:00
141 changed files with 6458 additions and 1431 deletions
+1 -1
View File
@@ -350,7 +350,7 @@ set(inc_headers
${KYTY_THIRD_PARTY_DIR}/SPIRV-Tools/include
${KYTY_THIRD_PARTY_DIR}/SPIRV-Headers/include
${KYTY_THIRD_PARTY_DIR}/fmt/include
${KYTY_THIRD_PARTY_DIR}/xxhash
${KYTY_THIRD_PARTY_DIR}/xxHash
${KYTY_THIRD_PARTY_DIR}/cpuinfo/include
${KYTY_THIRD_PARTY_DIR}/renderdoc
${KYTY_THIRD_PARTY_DIR}/stb
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "تم كسب الكأس!"
"trophy.toast.earned": "تم كسب الكأس!",
"user.guest": "ضيف"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofej získána!"
"trophy.toast.earned": "Trofej získána!",
"user.guest": "Host"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofæ optjent!"
"trophy.toast.earned": "Trofæ optjent!",
"user.guest": "Gæst"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trophäe erhalten!"
"trophy.toast.earned": "Trophäe erhalten!",
"user.guest": "Gast"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Κερδίσατε ένα τρόπαιο!"
"trophy.toast.earned": "Κερδίσατε ένα τρόπαιο!",
"user.guest": "Επισκ."
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trophy earned!"
"trophy.toast.earned": "Trophy earned!",
"user.guest": "Guest"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trophy earned!"
"trophy.toast.earned": "Trophy earned!",
"user.guest": "Guest"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "¡Ganaste un trofeo!"
"trophy.toast.earned": "¡Ganaste un trofeo!",
"user.guest": "Invitado"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "¡Ganaste un trofeo!"
"trophy.toast.earned": "¡Ganaste un trofeo!",
"user.guest": "Invitado"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Pokaali ansaittu!"
"trophy.toast.earned": "Pokaali ansaittu!",
"user.guest": "Vieras"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trophée obtenu !"
"trophy.toast.earned": "Trophée obtenu !",
"user.guest": "Invité"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trophée obtenu !"
"trophy.toast.earned": "Trophée obtenu !",
"user.guest": "Invité"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trófea megszerezve!"
"trophy.toast.earned": "Trófea megszerezve!",
"user.guest": "Vendég"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofi diperoleh!"
"trophy.toast.earned": "Trofi diperoleh!",
"user.guest": "Tamu"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofeo ottenuto!"
"trophy.toast.earned": "Trofeo ottenuto!",
"user.guest": "Ospite"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "トロフィーを獲得しました!"
"trophy.toast.earned": "トロフィーを獲得しました!",
"user.guest": "ゲスト"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "트로피를 획득했습니다!"
"trophy.toast.earned": "트로피를 획득했습니다!",
"user.guest": "게스트"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofee behaald!"
"trophy.toast.earned": "Trofee behaald!",
"user.guest": "Gast"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofé opptjent!"
"trophy.toast.earned": "Trofé opptjent!",
"user.guest": "Gjest"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Zdobyto trofeum!"
"trophy.toast.earned": "Zdobyto trofeum!",
"user.guest": "Gość"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Troféu conquistado!"
"trophy.toast.earned": "Troféu conquistado!",
"user.guest": "Convidado"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Troféu conquistado!"
"trophy.toast.earned": "Troféu conquistado!",
"user.guest": "Convidado"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofeu câștigat!"
"trophy.toast.earned": "Trofeu câștigat!",
"user.guest": "Invitat"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Трофей получен!"
"trophy.toast.earned": "Трофей получен!",
"user.guest": "Гость"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Trofé intjänad!"
"trophy.toast.earned": "Trofé intjänad!",
"user.guest": "Gäst"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "ได้รับถ้วยรางวัลแล้ว!"
"trophy.toast.earned": "ได้รับถ้วยรางวัลแล้ว!",
"user.guest": "แขก"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Kupa kazanıldı!"
"trophy.toast.earned": "Kupa kazanıldı!",
"user.guest": "Misafir"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "Đã nhận cúp!"
"trophy.toast.earned": "Đã nhận cúp!",
"user.guest": "Khách"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "已获得奖杯!"
"trophy.toast.earned": "已获得奖杯!",
"user.guest": "访客"
}
+2 -1
View File
@@ -1,3 +1,4 @@
{
"trophy.toast.earned": "已獲得獎盃!"
"trophy.toast.earned": "已獲得獎盃!",
"user.guest": "訪客"
}
+4
View File
@@ -43,6 +43,10 @@ target_link_libraries(common
ZArchive::zarchive
nlohmann_json::nlohmann_json
)
if(APPLE)
# hostStats.cpp reads GPU utilization from the IOAccelerator registry entries.
target_link_libraries(common PRIVATE "-framework CoreFoundation" "-framework IOKit")
endif()
get_property(inc_headers TARGET common PROPERTY INCLUDE_DIRECTORIES)
set(check_headers ${CMAKE_CURRENT_SOURCE_DIR})
+117
View File
@@ -0,0 +1,117 @@
#ifndef KYTY_COMMON_DEBUG_COUNTERS_H_
#define KYTY_COMMON_DEBUG_COUNTERS_H_
#include <atomic>
#include <chrono>
#include <cstdint>
namespace Common::DebugCounters {
static_assert(std::atomic<uint64_t>::is_always_lock_free && std::atomic<bool>::is_always_lock_free);
// Collect events only while the panel is visible. Relaxed, lock-free atomics keep updates
// cheap and safe in signal handlers. Rates come from comparing two snapshots.
enum class Counter {
// GPU work.
Draws,
DrawInstances,
IndirectDraws,
Dispatches,
QueueSubmits,
GpuWaits, // CPU blocked on a GPU timeline semaphore.
GpuWaitNs,
GpuFaults, // Guest writes to GPU-tracked memory.
ShadersCompiled,
ShaderCompileNs,
PipelinesCreated,
PipelineCreateNs,
// Caches.
TextureUploadBytes,
TextureDownloadBytes,
TextureEvictions,
BufferUploadBytes,
BufferDownloadBytes,
BufferEvictions,
// Presentation.
FlipsRequested,
FlipsRejected, // Guest flip queue was full.
LateVblanks, // The present thread fell behind the vblank rate.
SwapchainRecreations,
DroppedPresents,
// Emulator.
EmulatedInstructions, // Host CPU lacked an instruction the guest used.
UnresolvedImportCalls,
GuestFileReadBytes,
GuestFileWriteBytes,
AudioUnderruns,
Count
};
enum class Peak { ShaderCompileNs, PipelineCreateNs, Count };
namespace Detail {
inline std::atomic<bool> g_enabled {false};
inline std::atomic<uint64_t> g_counters[static_cast<int>(Counter::Count)] {};
inline std::atomic<uint64_t> g_peaks[static_cast<int>(Peak::Count)] {};
} // namespace Detail
[[nodiscard]] inline bool Enabled() noexcept {
return Detail::g_enabled.load(std::memory_order_relaxed);
}
inline void SetEnabled(bool enabled) noexcept {
Detail::g_enabled.store(enabled, std::memory_order_relaxed);
}
inline void Add(Counter counter, uint64_t value = 1) noexcept {
if (!Enabled()) {
return;
}
Detail::g_counters[static_cast<int>(counter)].fetch_add(value, std::memory_order_relaxed);
}
class ScopedTimer final {
public:
ScopedTimer(Counter count, Counter duration, Peak peak = Peak::Count)
: m_count(count), m_duration(duration), m_peak(peak),
m_start(Enabled() ? std::chrono::steady_clock::now()
: std::chrono::steady_clock::time_point {}) {}
~ScopedTimer() {
if (m_start == std::chrono::steady_clock::time_point {} || !Enabled()) {
return;
}
const auto elapsed =
static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - m_start)
.count());
Add(m_count);
Add(m_duration, elapsed);
if (m_peak != Peak::Count) {
auto& peak = Detail::g_peaks[static_cast<int>(m_peak)];
auto previous = peak.load(std::memory_order_relaxed);
while (previous < elapsed &&
!peak.compare_exchange_weak(previous, elapsed, std::memory_order_relaxed)) {
}
}
}
ScopedTimer(const ScopedTimer&) = delete;
ScopedTimer& operator=(const ScopedTimer&) = delete;
private:
Counter m_count;
Counter m_duration;
Peak m_peak;
std::chrono::steady_clock::time_point m_start;
};
[[nodiscard]] inline uint64_t Get(Counter counter) noexcept {
return Detail::g_counters[static_cast<int>(counter)].load(std::memory_order_relaxed);
}
[[nodiscard]] inline uint64_t Get(Peak peak) noexcept {
return Detail::g_peaks[static_cast<int>(peak)].load(std::memory_order_relaxed);
}
} // namespace Common::DebugCounters
#endif /* KYTY_COMMON_DEBUG_COUNTERS_H_ */
+25
View File
@@ -1,6 +1,7 @@
#include "common/emulatorConfig.h"
#include "common/assert.h"
#include "common/debugCounters.h"
#include <algorithm>
#include <memory>
@@ -16,6 +17,7 @@ void Initialize() {
}
void Shutdown() {
Common::DebugCounters::SetEnabled(false);
g_config.reset();
}
@@ -24,8 +26,10 @@ void Load(const ConfigOptions& cfg) {
EXIT_IF(cfg.user_name.empty() || cfg.user_name.size() > MAX_USER_NAME_LENGTH);
EXIT_IF(!IsConfiguredUserIdValid(cfg.user_id));
EXIT_IF(cfg.controller_speaker_volume > 100 || cfg.controller_vibration_intensity > 100);
EXIT_IF(cfg.keyboard_player > static_cast<uint32_t>(MAX_PLAYERS));
*g_config = cfg;
Common::DebugCounters::SetEnabled(cfg.perf_overlay_enabled);
}
uint32_t GetScreenWidth() {
@@ -60,6 +64,27 @@ uint32_t GetControllerVibrationIntensity() {
return g_config->controller_vibration_intensity;
}
uint32_t GetKeyboardPlayer() {
return g_config->keyboard_player;
}
int32_t GetPlayerUserId(int player) {
return g_config->user_id + player * PLAYER_USER_ID_STEP;
}
int GetPlayerFromUserId(int32_t user_id) {
for (int player = 0; player < MAX_PLAYERS; player++) {
if (user_id == GetPlayerUserId(player)) {
return player;
}
}
return -1;
}
bool IsGuestUserId(int32_t user_id) {
return GetPlayerFromUserId(user_id) > 0;
}
PresentMode GetPresentMode() {
return g_config->present_mode;
}
+18 -1
View File
@@ -6,6 +6,7 @@
#include <cstddef>
#include <array>
#include <filesystem>
#include <limits>
#include <optional>
#include <string>
#include <vector>
@@ -35,10 +36,16 @@ constexpr uint32_t MAX_CONSOLE_LANGUAGE = 29;
constexpr std::size_t MAX_USER_NAME_LENGTH = 16;
constexpr int32_t DEFAULT_USER_ID = 1000;
constexpr int MAX_PLAYERS = 4;
constexpr int32_t PLAYER_USER_ID_STEP = 0x100;
constexpr int32_t MAX_USER_ID =
std::numeric_limits<int32_t>::max() - (MAX_PLAYERS - 1) * PLAYER_USER_ID_STEP;
constexpr bool IsConfiguredUserIdValid(int32_t user_id) {
constexpr int32_t USER_ID_EVERYONE = 0xfe;
constexpr int32_t USER_ID_SYSTEM = 0xff;
return user_id >= 0 && user_id != USER_ID_EVERYONE && user_id != USER_ID_SYSTEM;
return user_id >= 0 && user_id <= MAX_USER_ID && user_id != USER_ID_EVERYONE &&
user_id != USER_ID_SYSTEM;
}
struct ConfigOptions {
@@ -50,6 +57,8 @@ struct ConfigOptions {
std::optional<ControllerColor> controller_color;
uint32_t controller_speaker_volume = 50;
uint32_t controller_vibration_intensity = 100;
// 0 shares player 1 with the first gamepad; 1..MAX_PLAYERS dedicates that player to the keyboard.
uint32_t keyboard_player = 0;
PresentMode present_mode = PresentMode::Mailbox;
int32_t gpu_index = -1;
bool fullscreen_enabled = false;
@@ -80,6 +89,7 @@ struct ConfigOptions {
bool tessellation_enabled = false;
bool trophy_enabled = true;
uint32_t trophy_duration_seconds = 6;
bool perf_overlay_enabled = false;
bool playgo_hack_enabled = false;
bool skip_notice_screen = false;
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
@@ -100,6 +110,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();
+580
View File
@@ -0,0 +1,580 @@
#include "common/hostStats.h"
#include "common/config.h"
#include <algorithm>
#include <chrono>
#include <thread>
#include <unordered_map>
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h> // IWYU pragma: keep
// clang-format off
#include <psapi.h>
// clang-format on
#elif defined(__APPLE__)
#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOKitLib.h>
#include <cstring>
#include <libproc.h>
#include <mach/mach.h>
#include <notify.h>
#include <sys/resource.h>
#include <sys/sysctl.h>
#include <unistd.h>
#else
#include <filesystem>
#include <fstream>
#include <sstream>
#include <sys/resource.h>
#include <unistd.h>
#endif
namespace Common {
namespace {
// Cumulative counters; rates come from the difference between two samples.
struct Counters {
struct Thread {
uint64_t cpu_ns;
std::string name;
};
std::optional<uint64_t> process_cpu_ns;
std::optional<uint64_t> system_busy;
std::optional<uint64_t> system_total;
std::optional<uint64_t> context_switches;
std::optional<uint64_t> page_faults;
std::optional<uint64_t> page_ins;
std::optional<uint64_t> disk_read;
std::optional<uint64_t> disk_write;
std::unordered_map<uint64_t, Thread> threads;
uint64_t wall_ns = 0;
};
uint64_t WallNanoseconds() {
return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count());
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
uint64_t FileTimeTo100Ns(const FILETIME& time) {
return (static_cast<uint64_t>(time.dwHighDateTime) << 32u) | time.dwLowDateTime;
}
struct PlatformState {};
void SamplePlatform(PlatformState& /*state*/, HostStats& stats, Counters& now) {
const auto process = GetCurrentProcess();
FILETIME creation {};
FILETIME exit {};
FILETIME kernel {};
FILETIME user {};
if (GetProcessTimes(process, &creation, &exit, &kernel, &user) != 0) {
now.process_cpu_ns = (FileTimeTo100Ns(kernel) + FileTimeTo100Ns(user)) * 100u;
}
FILETIME idle {};
if (GetSystemTimes(&idle, &kernel, &user) != 0) {
// Kernel time includes idle time.
now.system_total = FileTimeTo100Ns(kernel) + FileTimeTo100Ns(user);
now.system_busy = *now.system_total - FileTimeTo100Ns(idle);
}
PROCESS_MEMORY_COUNTERS memory {};
if (K32GetProcessMemoryInfo(process, &memory, sizeof(memory)) != 0) {
stats.process_memory = memory.WorkingSetSize;
stats.process_resident = memory.WorkingSetSize;
now.page_faults = memory.PageFaultCount;
}
IO_COUNTERS io {};
if (GetProcessIoCounters(process, &io) != 0) {
now.disk_read = io.ReadTransferCount;
now.disk_write = io.WriteTransferCount;
}
MEMORYSTATUSEX system {};
system.dwLength = sizeof(system);
if (GlobalMemoryStatusEx(&system) != 0) {
stats.system_memory_total = system.ullTotalPhys;
stats.system_memory_used = system.ullTotalPhys - system.ullAvailPhys;
stats.system_free = system.ullAvailPhys;
stats.memory_pressure = system.dwMemoryLoad >= 90 ? MemoryPressure::Critical
: system.dwMemoryLoad >= 80 ? MemoryPressure::Warning
: MemoryPressure::Normal;
}
}
#else
void SampleRusage(Counters& now) {
rusage usage {};
if (getrusage(RUSAGE_SELF, &usage) != 0) {
return;
}
const auto to_ns = [](const timeval& time) {
return static_cast<uint64_t>(time.tv_sec) * 1000000000u +
static_cast<uint64_t>(time.tv_usec) * 1000u;
};
now.process_cpu_ns = to_ns(usage.ru_utime) + to_ns(usage.ru_stime);
now.context_switches = static_cast<uint64_t>(usage.ru_nvcsw + usage.ru_nivcsw);
now.page_faults = static_cast<uint64_t>(usage.ru_minflt + usage.ru_majflt);
now.page_ins = static_cast<uint64_t>(usage.ru_majflt);
}
#if defined(__APPLE__)
template <typename T>
std::optional<T> SysctlValue(const char* name) {
T value {};
size_t size = sizeof(value);
if (sysctlbyname(name, &value, &size, nullptr, 0) != 0 || size != sizeof(value)) {
return std::nullopt;
}
return value;
}
struct PlatformState {
mach_port_t host = mach_host_self();
int thermal_token = -1;
bool thermal_ready = false;
std::string gpu_model;
std::optional<uint32_t> gpu_cores;
~PlatformState() {
if (thermal_ready) {
notify_cancel(thermal_token);
}
mach_port_deallocate(mach_task_self(), host);
}
};
std::optional<int64_t> DictionaryNumber(CFDictionaryRef dictionary, CFStringRef key) {
const auto* value = CFDictionaryGetValue(dictionary, key);
int64_t number = 0;
if (value == nullptr || CFGetTypeID(value) != CFNumberGetTypeID() ||
!CFNumberGetValue(static_cast<CFNumberRef>(value), kCFNumberSInt64Type, &number) ||
number < 0) {
return std::nullopt;
}
return number;
}
std::string RegistryString(io_object_t service, CFStringRef key) {
std::string result;
auto* value = IORegistryEntryCreateCFProperty(service, key, kCFAllocatorDefault, 0);
if (value == nullptr) {
return result;
}
if (CFGetTypeID(value) == CFStringGetTypeID()) {
char buffer[128] {};
if (CFStringGetCString(static_cast<CFStringRef>(value), buffer, sizeof(buffer),
kCFStringEncodingUTF8)) {
result = buffer;
}
} else if (CFGetTypeID(value) == CFDataGetTypeID()) {
// Intel Macs store the model as NUL-terminated data.
const auto* data = static_cast<CFDataRef>(value);
const auto* bytes = reinterpret_cast<const char*>(CFDataGetBytePtr(data));
const auto length = static_cast<size_t>(CFDataGetLength(data));
result.assign(bytes, strnlen(bytes, length));
}
CFRelease(value);
return result;
}
// IOAccelerator exposes whole-device statistics; there is no per-process figure.
void SampleGpu(PlatformState& state, HostStats& stats) {
io_iterator_t iterator = 0;
if (IOServiceGetMatchingServices(MACH_PORT_NULL, IOServiceMatching("IOAccelerator"),
&iterator) != KERN_SUCCESS) {
return;
}
const auto take_max = [](std::optional<double>& target, std::optional<int64_t> value) {
if (value) {
target = std::max(target.value_or(0.0), std::min(100.0, static_cast<double>(*value)));
}
};
const auto add = [](std::optional<uint64_t>& target, std::optional<int64_t> value) {
if (value) {
target = target.value_or(0) + static_cast<uint64_t>(*value);
}
};
for (io_object_t service = IOIteratorNext(iterator); service != 0;
service = IOIteratorNext(iterator)) {
if (state.gpu_model.empty()) {
state.gpu_model = RegistryString(service, CFSTR("model"));
auto* cores = IORegistryEntryCreateCFProperty(service, CFSTR("gpu-core-count"),
kCFAllocatorDefault, 0);
int64_t count = 0;
if (cores != nullptr) {
if (CFGetTypeID(cores) == CFNumberGetTypeID() &&
CFNumberGetValue(static_cast<CFNumberRef>(cores), kCFNumberSInt64Type,
&count) &&
count > 0) {
state.gpu_cores = static_cast<uint32_t>(count);
}
CFRelease(cores);
}
}
auto* statistics = IORegistryEntryCreateCFProperty(service, CFSTR("PerformanceStatistics"),
kCFAllocatorDefault, 0);
if (statistics != nullptr) {
if (CFGetTypeID(statistics) == CFDictionaryGetTypeID()) {
const auto* values = static_cast<CFDictionaryRef>(statistics);
take_max(stats.gpu_percent,
DictionaryNumber(values, CFSTR("Device Utilization %")));
take_max(stats.gpu_renderer_percent,
DictionaryNumber(values, CFSTR("Renderer Utilization %")));
take_max(stats.gpu_tiler_percent,
DictionaryNumber(values, CFSTR("Tiler Utilization %")));
add(stats.gpu_memory_in_use,
DictionaryNumber(values, CFSTR("In use system memory")));
add(stats.gpu_memory_allocated,
DictionaryNumber(values, CFSTR("Alloc system memory")));
add(stats.gpu_recoveries, DictionaryNumber(values, CFSTR("recoveryCount")));
}
CFRelease(statistics);
}
IOObjectRelease(service);
}
IOObjectRelease(iterator);
stats.gpu_model = state.gpu_model;
stats.gpu_cores = state.gpu_cores;
}
void SampleThreads(HostStats& stats, Counters& now) {
thread_act_array_t threads = nullptr;
mach_msg_type_number_t count = 0;
const auto task = mach_task_self();
if (task_threads(task, &threads, &count) != KERN_SUCCESS) {
return;
}
stats.thread_count = count;
for (mach_msg_type_number_t i = 0; i < count; i++) {
thread_identifier_info_data_t identifier {};
mach_msg_type_number_t size = THREAD_IDENTIFIER_INFO_COUNT;
thread_basic_info_data_t basic {};
mach_msg_type_number_t basic_size = THREAD_BASIC_INFO_COUNT;
if (thread_info(threads[i], THREAD_IDENTIFIER_INFO,
reinterpret_cast<thread_info_t>(&identifier), &size) == KERN_SUCCESS &&
thread_info(threads[i], THREAD_BASIC_INFO, reinterpret_cast<thread_info_t>(&basic),
&basic_size) == KERN_SUCCESS) {
const auto to_ns = [](const time_value_t& time) {
return static_cast<uint64_t>(time.seconds) * 1000000000u +
static_cast<uint64_t>(time.microseconds) * 1000u;
};
auto& thread = now.threads[identifier.thread_id];
thread.cpu_ns = to_ns(basic.user_time) + to_ns(basic.system_time);
thread_extended_info_data_t extended {};
mach_msg_type_number_t extended_size = THREAD_EXTENDED_INFO_COUNT;
if (thread_info(threads[i], THREAD_EXTENDED_INFO,
reinterpret_cast<thread_info_t>(&extended),
&extended_size) == KERN_SUCCESS) {
thread.name = std::string(extended.pth_name,
strnlen(extended.pth_name, sizeof(extended.pth_name)));
}
}
mach_port_deallocate(task, threads[i]);
}
vm_deallocate(task, reinterpret_cast<vm_address_t>(threads), count * sizeof(thread_act_t));
}
void SamplePlatform(PlatformState& state, HostStats& stats, Counters& now) {
SampleRusage(now);
host_cpu_load_info_data_t load {};
mach_msg_type_number_t load_count = HOST_CPU_LOAD_INFO_COUNT;
if (host_statistics(state.host, HOST_CPU_LOAD_INFO, reinterpret_cast<host_info_t>(&load),
&load_count) == KERN_SUCCESS) {
const uint64_t busy = static_cast<uint64_t>(load.cpu_ticks[CPU_STATE_USER]) +
load.cpu_ticks[CPU_STATE_SYSTEM] + load.cpu_ticks[CPU_STATE_NICE];
now.system_busy = busy;
now.system_total = busy + load.cpu_ticks[CPU_STATE_IDLE];
}
// Physical footprint is what Activity Monitor shows in its Memory column.
task_vm_info_data_t vm_info {};
mach_msg_type_number_t vm_info_count = TASK_VM_INFO_COUNT;
if (task_info(mach_task_self(), TASK_VM_INFO, reinterpret_cast<task_info_t>(&vm_info),
&vm_info_count) == KERN_SUCCESS) {
stats.process_memory = vm_info.phys_footprint;
stats.process_resident = vm_info.resident_size;
stats.process_compressed = vm_info.compressed;
stats.process_file_backed = vm_info.external;
stats.process_virtual = vm_info.virtual_size;
}
rusage_info_v2 usage {};
if (proc_pid_rusage(getpid(), RUSAGE_INFO_V2, reinterpret_cast<rusage_info_t*>(&usage)) == 0) {
now.disk_read = usage.ri_diskio_bytesread;
now.disk_write = usage.ri_diskio_byteswritten;
}
stats.system_memory_total = SysctlValue<uint64_t>("hw.memsize");
vm_size_t page = 0;
vm_statistics64_data_t vm {};
mach_msg_type_number_t count = HOST_VM_INFO64_COUNT;
if (host_page_size(state.host, &page) == KERN_SUCCESS &&
host_statistics64(state.host, HOST_VM_INFO64, reinterpret_cast<host_info64_t>(&vm),
&count) == KERN_SUCCESS) {
// Activity Monitor's "Memory Used": app memory + wired + compressed.
const uint64_t app = vm.internal_page_count > vm.purgeable_count
? vm.internal_page_count - vm.purgeable_count
: 0;
stats.system_memory_used = (app + vm.wire_count + vm.compressor_page_count) * page;
stats.system_wired = static_cast<uint64_t>(vm.wire_count) * page;
stats.system_compressed = static_cast<uint64_t>(vm.compressor_page_count) * page;
stats.system_cached =
(static_cast<uint64_t>(vm.external_page_count) + vm.purgeable_count) * page;
stats.system_free = static_cast<uint64_t>(vm.free_count) * page;
}
if (const auto swap = SysctlValue<xsw_usage>("vm.swapusage")) {
stats.swap_used = swap->xsu_used;
stats.swap_total = swap->xsu_total;
}
// 1 = normal, 2 = warning, 4 = critical, as reported to dispatch memory-pressure sources.
if (const auto level = SysctlValue<int>("kern.memorystatus_vm_pressure_level")) {
stats.memory_pressure = *level >= 4 ? MemoryPressure::Critical
: *level >= 2 ? MemoryPressure::Warning
: MemoryPressure::Normal;
}
// 0 nominal, 1 moderate, 2 heavy, 3 trapping, 4 sleeping.
if (!state.thermal_ready) {
state.thermal_ready = notify_register_check("com.apple.system.thermalpressurelevel",
&state.thermal_token) == NOTIFY_STATUS_OK;
}
uint64_t thermal = 0;
if (state.thermal_ready &&
notify_get_state(state.thermal_token, &thermal) == NOTIFY_STATUS_OK) {
stats.thermal_pressure = thermal >= 3 ? ThermalPressure::Critical
: thermal == 2 ? ThermalPressure::Heavy
: thermal == 1 ? ThermalPressure::Moderate
: ThermalPressure::Nominal;
}
SampleThreads(stats, now);
SampleGpu(state, stats);
}
#else
using ProcValues = std::unordered_map<std::string, uint64_t>;
// Reads "<key>: <number> [kB]" lines; kB values are returned in bytes.
ProcValues ReadProcValues(const char* path) {
ProcValues values;
std::ifstream file(path);
std::string line;
while (std::getline(file, line)) {
const auto colon = line.find(':');
if (colon == std::string::npos) {
continue;
}
std::istringstream stream(line.substr(colon + 1));
uint64_t number = 0;
std::string unit;
if (stream >> number) {
stream >> unit;
values[line.substr(0, colon)] = unit == "kB" ? number * 1024u : number;
}
}
return values;
}
std::optional<uint64_t> Find(const ProcValues& values, const char* key) {
const auto it = values.find(key);
return it != values.end() ? std::optional(it->second) : std::nullopt;
}
struct PlatformState {};
void SampleThreads(HostStats& stats, Counters& now) {
static const double ns_per_tick = 1e9 / static_cast<double>(std::max(sysconf(_SC_CLK_TCK), 1L));
std::error_code error;
uint32_t count = 0;
for (std::filesystem::directory_iterator it("/proc/self/task", error), end; !error && it != end;
it.increment(error)) {
count++;
std::ifstream stat_file(it->path() / "stat");
std::string stat;
uint64_t id = 0;
if (!(stat_file >> id)) {
continue; // The thread may have exited during enumeration.
}
std::getline(stat_file, stat);
const auto close = stat.rfind(')');
if (close == std::string::npos) {
continue;
}
// Fields after the command name start at field 3 (state); utime and stime are 14 and 15.
std::istringstream fields(stat.substr(close + 2));
std::string field;
for (int index = 3; index < 14 && fields >> field; index++) {
}
uint64_t user = 0;
uint64_t system = 0;
if (!(fields >> user >> system)) {
continue;
}
auto& thread = now.threads[id];
thread.cpu_ns = static_cast<uint64_t>(static_cast<double>(user + system) * ns_per_tick);
std::ifstream name_file(it->path() / "comm");
std::getline(name_file, thread.name);
}
if (!error) {
stats.thread_count = count;
}
}
void SamplePlatform(PlatformState& /*state*/, HostStats& stats, Counters& now) {
SampleRusage(now);
std::ifstream cpu_file("/proc/stat");
std::string label;
if (cpu_file >> label && label == "cpu") {
uint64_t total = 0;
uint64_t idle = 0;
uint64_t value = 0;
for (int index = 0; index < 8 && cpu_file >> value; index++) {
total += value;
if (index == 3 || index == 4) { // idle, iowait
idle += value;
}
}
now.system_total = total;
now.system_busy = total - idle;
}
const auto status = ReadProcValues("/proc/self/status");
stats.process_memory = Find(status, "VmRSS");
stats.process_resident = Find(status, "VmRSS");
stats.process_file_backed = Find(status, "RssFile");
stats.process_compressed = Find(status, "VmSwap");
stats.process_virtual = Find(status, "VmSize");
const auto io = ReadProcValues("/proc/self/io");
now.disk_read = Find(io, "read_bytes");
now.disk_write = Find(io, "write_bytes");
const auto memory = ReadProcValues("/proc/meminfo");
const auto total = Find(memory, "MemTotal");
const auto available = Find(memory, "MemAvailable");
if (total && available && *available <= *total) {
stats.system_memory_total = total;
stats.system_memory_used = *total - *available;
}
stats.system_free = Find(memory, "MemFree");
if (const auto cached = Find(memory, "Cached")) {
stats.system_cached = *cached + Find(memory, "Buffers").value_or(0);
}
const auto swap_total = Find(memory, "SwapTotal");
const auto swap_free = Find(memory, "SwapFree");
if (swap_total && swap_free && *swap_free <= *swap_total) {
stats.swap_total = swap_total;
stats.swap_used = *swap_total - *swap_free;
}
// Pressure stall information: share of the last 10 s some task waited on memory.
std::ifstream pressure("/proc/pressure/memory");
std::string kind;
std::string average;
double stalled = 0.0;
if (pressure >> kind >> average && kind == "some" && average.rfind("avg10=", 0) == 0 &&
std::istringstream(average.substr(6)) >> stalled) {
stats.memory_pressure = stalled >= 20.0 ? MemoryPressure::Critical
: stalled >= 5.0 ? MemoryPressure::Warning
: MemoryPressure::Normal;
} else if (total && available && *total != 0) {
const double free_share = static_cast<double>(*available) / static_cast<double>(*total);
stats.memory_pressure = free_share < 0.10 ? MemoryPressure::Critical
: free_share < 0.25 ? MemoryPressure::Warning
: MemoryPressure::Normal;
}
SampleThreads(stats, now);
}
#endif
#endif
constexpr size_t BUSIEST_THREADS = 6;
} // namespace
struct HostStatsSampler::Impl {
PlatformState state;
Counters last;
uint32_t logical_cores = std::max(std::thread::hardware_concurrency(), 1u);
bool has_last = false;
};
HostStatsSampler::HostStatsSampler(): m_impl(std::make_unique<Impl>()) {}
HostStatsSampler::~HostStatsSampler() = default;
HostStats HostStatsSampler::Sample() {
HostStats stats;
stats.logical_cores = m_impl->logical_cores;
Counters now;
now.wall_ns = WallNanoseconds();
SamplePlatform(m_impl->state, stats, now);
const auto& last = m_impl->last;
const auto elapsed = static_cast<double>(now.wall_ns - last.wall_ns) / 1e9;
if (m_impl->has_last && elapsed > 0.0) {
const auto rate = [&](std::optional<uint64_t> current,
std::optional<uint64_t> previous) -> std::optional<double> {
if (!current || !previous || *current < *previous) {
return std::nullopt;
}
return static_cast<double>(*current - *previous) / elapsed;
};
if (const auto cpu = rate(now.process_cpu_ns, last.process_cpu_ns)) {
stats.process_cpu_percent =
std::clamp(*cpu / 1e9 * 100.0 / stats.logical_cores, 0.0, 100.0);
}
if (now.system_total && last.system_total && now.system_busy && last.system_busy &&
*now.system_total > *last.system_total && *now.system_busy >= *last.system_busy) {
stats.system_cpu_percent =
std::clamp(static_cast<double>(*now.system_busy - *last.system_busy) /
static_cast<double>(*now.system_total - *last.system_total) * 100.0,
0.0, 100.0);
}
stats.context_switches_per_s = rate(now.context_switches, last.context_switches);
stats.page_faults_per_s = rate(now.page_faults, last.page_faults);
stats.page_ins_per_s = rate(now.page_ins, last.page_ins);
stats.disk_read_per_s = rate(now.disk_read, last.disk_read);
stats.disk_write_per_s = rate(now.disk_write, last.disk_write);
for (const auto& [id, thread]: now.threads) {
// A thread missing from the last sample started during this interval.
const auto previous = last.threads.find(id);
const uint64_t before = previous != last.threads.end() ? previous->second.cpu_ns : 0;
if (thread.cpu_ns < before) {
continue;
}
const double percent =
static_cast<double>(thread.cpu_ns - before) / 1e9 / elapsed * 100.0;
stats.busiest_threads.push_back(
{thread.name.empty() ? "thread " + std::to_string(id) : thread.name, percent});
}
std::sort(stats.busiest_threads.begin(), stats.busiest_threads.end(),
[](const auto& a, const auto& b) { return a.cpu_percent > b.cpu_percent; });
if (stats.busiest_threads.size() > BUSIEST_THREADS) {
stats.busiest_threads.resize(BUSIEST_THREADS);
}
}
m_impl->last = std::move(now);
m_impl->has_last = true;
return stats;
}
} // namespace Common
+86
View File
@@ -0,0 +1,86 @@
#ifndef KYTY_COMMON_HOST_STATS_H_
#define KYTY_COMMON_HOST_STATS_H_
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <vector>
namespace Common {
enum class MemoryPressure { Unknown, Normal, Warning, Critical };
enum class ThermalPressure { Unknown, Nominal, Moderate, Heavy, Critical };
struct ThreadUsage {
std::string name;
double cpu_percent = 0.0; // Of one core.
};
// Host-wide and process-wide resource usage. Missing values are std::nullopt; rates are per
// second since the previous sample.
struct HostStats {
uint32_t logical_cores = 0;
// Processing.
std::optional<double> process_cpu_percent; // Share of all logical cores, 0-100.
std::optional<double> system_cpu_percent; // Whole machine, 0-100.
std::optional<uint32_t> thread_count;
std::vector<ThreadUsage> busiest_threads; // Busiest first.
std::optional<double> context_switches_per_s;
std::optional<double> page_faults_per_s;
std::optional<double> page_ins_per_s; // Faults that read from disk.
ThermalPressure thermal_pressure = ThermalPressure::Unknown;
// Emulator process memory.
std::optional<uint64_t> process_memory; // Physical footprint / resident set.
std::optional<uint64_t> process_resident;
std::optional<uint64_t> process_compressed; // Compressed or swapped out.
std::optional<uint64_t> process_file_backed;
std::optional<uint64_t> process_virtual; // Reserved address space.
// System memory.
std::optional<uint64_t> system_memory_used;
std::optional<uint64_t> system_memory_total;
std::optional<uint64_t> system_wired;
std::optional<uint64_t> system_compressed;
std::optional<uint64_t> system_cached; // File cache, reclaimable.
std::optional<uint64_t> system_free;
std::optional<uint64_t> swap_used;
std::optional<uint64_t> swap_total;
MemoryPressure memory_pressure = MemoryPressure::Unknown;
// Disk I/O of the emulator process.
std::optional<double> disk_read_per_s;
std::optional<double> disk_write_per_s;
// GPU, whole device (macOS IOAccelerator).
std::string gpu_model;
std::optional<uint32_t> gpu_cores;
std::optional<double> gpu_percent;
std::optional<double> gpu_renderer_percent;
std::optional<double> gpu_tiler_percent;
std::optional<uint64_t> gpu_memory_in_use; // System memory the GPU driver has in use.
std::optional<uint64_t> gpu_memory_allocated;
std::optional<uint64_t> gpu_recoveries; // GPU resets since boot.
};
// May block for a few milliseconds (thread enumeration, OS registry reads), so call it off the
// render thread. The first sample has no rates.
class HostStatsSampler final {
public:
HostStatsSampler();
~HostStatsSampler();
HostStatsSampler(const HostStatsSampler&) = delete;
HostStatsSampler& operator=(const HostStatsSampler&) = delete;
[[nodiscard]] HostStats Sample();
private:
struct Impl;
std::unique_ptr<Impl> m_impl;
};
} // namespace Common
#endif /* KYTY_COMMON_HOST_STATS_H_ */
+1 -1
View File
@@ -367,7 +367,7 @@ bool SysFileDeleteFile(const std::filesystem::path& name) {
bool SysFileFlush(sys_file_t& f) {
if (f.type == SYS_FILE_FILE && f.f != nullptr) {
return (fflush(f.f) == 0);
return fflush(f.f) == 0 && fsync(fileno(f.f)) == 0;
}
return false;
-38
View File
@@ -89,42 +89,6 @@ static void MountSandboxDirs() {
MountOrCreateDir("_TempData/" + title_id, "/temp");
}
static bool ClearDirectoryContents(const std::filesystem::path& dir) {
bool ok = true;
for (const auto& entry: Common::File::GetDirEntries(dir)) {
if (entry.name == "." || entry.name == "..") {
continue;
}
auto path = dir / entry.name;
if (entry.is_file) {
Common::File::RemoveReadonly(path);
ok = Common::File::DeleteFile(path) && ok;
} else {
ok = ClearDirectoryContents(path) && ok;
ok = Common::File::DeleteDirectory(path) && ok;
}
}
return ok;
}
static void ClearDebugTextureFolder() {
const std::string debug_texture_folder = "_Textures";
if (!Common::File::IsDirectoryExisting(debug_texture_folder)) {
Common::File::CreateDirectories(debug_texture_folder);
return;
}
if (!ClearDirectoryContents(debug_texture_folder)) {
LOGF_COLOR(Log::Color::BrightYellow, "TextureDump: failed to completely clear %s\n",
debug_texture_folder.c_str());
}
}
static void Init(const Config::ConfigOptions& cfg, const std::filesystem::path& param_json,
Common::Subsystems& subsystems) {
EXIT_IF(!Common::Thread::IsMainThread());
@@ -198,8 +162,6 @@ void Run(const RunOptions& options) {
Common::Subsystems subsystems(true);
Init(options.config, param_json, subsystems);
ClearDebugTextureFolder();
PrintSystemInfo();
std::string title_id;
Loader::SystemContentParamSfoGetString("TITLE_ID", &title_id);
+7 -6
View File
@@ -1,6 +1,7 @@
#include "graphics/guest_gpu/graphicsRun.h"
#include "common/assert.h"
#include "common/debugCounters.h"
#include "common/emulatorConfig.h"
#include "common/logging/log.h"
#include "common/profiler.h"
@@ -342,12 +343,11 @@ void CommandProcessor::WriteData(uint32_t* dst, const uint32_t* src, uint32_t dw
return;
}
if (write_one_address) {
for (uint32_t i = 0; i < dw_num; i++) {
dst[0] = src[i];
}
} else {
memcpy(dst, src, static_cast<size_t>(dw_num) * sizeof(uint32_t));
// Completion labels can be reused immediately; memcpy may write a destination twice.
auto* output = static_cast<volatile uint32_t*>(dst);
for (uint32_t i = 0; i < dw_num; i++) {
*output = src[i];
output += !write_one_address;
}
}
@@ -906,6 +906,7 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
if (draw_count == 0) {
return;
}
Common::DebugCounters::Add(Common::DebugCounters::Counter::IndirectDraws, draw_count);
const auto args_size = indexed ? sizeof(DrawIndexedIndirectArgs) : sizeof(DrawIndirectArgs);
EXIT_NOT_IMPLEMENTED(stride_in_bytes < args_size);
+9
View File
@@ -8,6 +8,7 @@
#include <map>
#include <mutex>
#include <span>
#include <tuple>
#include <vector>
#include <vk_mem_alloc.h>
@@ -99,6 +100,14 @@ struct GraphicContext {
[[nodiscard]] bool CanReportMemoryUsage() const noexcept { return memory_budget_ext_enabled; }
[[nodiscard]] uint64_t GetDeviceMemoryUsage() const;
[[nodiscard]] uint64_t GetTotalMemoryBudget() const;
struct HeapUsage {
uint64_t usage = 0; // Whole process, as reported by VK_EXT_memory_budget.
uint64_t budget = 0;
uint32_t allocations = 0;
bool device_local = false;
};
[[nodiscard]] uint32_t GetHeapUsage(std::span<HeapUsage, VK_MAX_MEMORY_HEAPS> heaps) const;
[[nodiscard]] bool CreateImage(const vk::ImageCreateInfo& info, VulkanImage& image);
void DeleteImage(VulkanImage& image);
+4
View File
@@ -2,6 +2,7 @@
#include "common/alignment.h"
#include "common/assert.h"
#include "common/debugCounters.h"
#include "common/logging/log.h"
#include "common/profiler.h"
#include "graphics/guest_gpu/graphicsRun.h"
@@ -177,6 +178,7 @@ bool BufferCache::DownloadBufferMemory(Buffer& buffer, uint64_t vaddr, uint64_t
if (copies.empty()) {
return false;
}
Common::DebugCounters::Add(Common::DebugCounters::Counter::BufferDownloadBytes, total_size);
auto [mapped, offset] = m_download_buffer.Map(total_size, 64);
std::unique_ptr<Buffer> temporary;
@@ -467,6 +469,7 @@ vk::Buffer BufferCache::UploadCopies(Buffer& buffer, std::span<vk::BufferCopy> c
if (copies.empty()) {
return nullptr;
}
Common::DebugCounters::Add(Common::DebugCounters::Counter::BufferUploadBytes, total_size);
const auto source = UploadMemory(m_graphics, m_scheduler, m_staging_buffer, total_size, 4,
vk::BufferUsageFlagBits::eTransferSrc, [&](uint8_t* mapped, uint64_t base_offset) {
@@ -658,6 +661,7 @@ void BufferCache::RunGarbageCollector() {
} else {
m_memory_tracker.UntrackMemory(buffer.CpuAddress(), buffer.Size());
DeleteBuffer(id);
Common::DebugCounters::Add(Common::DebugCounters::Counter::BufferEvictions);
}
return ++retire_count == limit;
});
+10 -14
View File
@@ -2,6 +2,7 @@
#include "common/alignment.h"
#include "common/assert.h"
#include "common/debugCounters.h"
#include "common/emulatorConfig.h"
#include "common/logging/log.h"
#include "common/profiler.h"
@@ -262,14 +263,8 @@ void TextureCache::DeleteImage(ImageId id) {
EXIT("TextureCache: deleting a GPU-modified image without resolving its contents\n");
}
m_download_images.erase(id);
if (image->info.HasMetadata()) {
const auto metadata = m_surface_metas.find(image->info.metadata.range.address);
if (metadata != m_surface_metas.end() &&
image->info.metadata.kind == ImageMetadataKind::Htile &&
metadata->second.type == MetaDataInfo::Type::HTile) {
// A later binding may have reused this address for another metadata type.
m_surface_metas.erase(metadata);
}
if (image->info.metadata.kind == ImageMetadataKind::Htile) {
m_surface_metas.erase(image->info.metadata.range.address);
}
UnregisterImage(id);
if (m_scheduler.Active()) {
@@ -954,6 +949,7 @@ void TextureCache::UploadImage(Image& image, Buffer& source, uint64_t source_off
const auto linear = m_tiler.Detile(source.Handle(), source_offset, info);
const auto copies = info.BufferCopies(linear.offset);
destination.Upload(copies, linear.buffer, linear.offset, linear.size);
Common::DebugCounters::Add(Common::DebugCounters::Counter::TextureUploadBytes, linear.size);
return;
}
@@ -999,6 +995,7 @@ void TextureCache::UploadImage(Image& image, Buffer& source, uint64_t source_off
copies[0].imageExtent = info.extent;
}
destination.Upload(copies, linear.buffer, linear.offset, linear.size);
Common::DebugCounters::Add(Common::DebugCounters::Counter::TextureUploadBytes, linear.size);
}
void TextureCache::InitializeImage(ImageId id) {
@@ -1039,7 +1036,7 @@ void TextureCache::MaterializeColorClear(ImageId id, const ImageDesc& desc,
std::scoped_lock lock {m_lock};
auto& image = m_slot_images[id];
image.info.metadata = desc.info.metadata;
// Native color metadata must not retain a reused HTile/CMask/FMask clear flag.
// Native color metadata must not retain a reused HTile clear flag.
m_surface_metas.erase(range.address);
if (range.size == 0 || desc.info.resources.levels != 1) {
return;
@@ -1417,11 +1414,8 @@ vk::ImageView TextureCache::FindDepthTarget(ImageId id, const ImageDesc& desc) {
image.info.metadata = desc.info.metadata;
if (desc.info.HasMetadata()) {
const auto [metadata, inserted] = m_surface_metas.try_emplace(desc.info.metadata.range.address);
if (inserted) {
metadata->second.type = MetaDataInfo::Type::HTile;
if (image.info.htile_initial_clear) {
metadata->second.clear_mask.set();
}
if (inserted && image.info.htile_initial_clear) {
metadata->second.clear_mask.set();
}
}
RefreshImage(id);
@@ -1774,6 +1768,7 @@ bool TextureCache::DownloadImageMemory(ImageId id) {
download.Flush(offset, range.size);
DownloadImage(image, download, offset, range.size, image.info.resources.levels);
Common::DebugCounters::Add(Common::DebugCounters::Counter::TextureDownloadBytes, range.size);
vk::BufferMemoryBarrier barrier {};
barrier.srcAccessMask = vk::AccessFlagBits::eMemoryWrite | vk::AccessFlagBits::eTransferWrite |
vk::AccessFlagBits::eShaderWrite;
@@ -1943,6 +1938,7 @@ void TextureCache::RunGarbageCollector() {
}
}
FreeImage(id);
Common::DebugCounters::Add(Common::DebugCounters::Counter::TextureEvictions);
if (m_total_used_memory < m_critical_gc_memory && aggressive) {
deletions >>= 2;
aggressive = false;
-3
View File
@@ -81,9 +81,6 @@ public:
private:
struct MetaDataInfo {
enum class Type : uint8_t { CMask, FMask, HTile };
Type type;
// PS5 DB_DEPTH_VIEW encodes 13-bit base and last array-slice indices.
std::bitset<8192> clear_mask;
};
@@ -72,7 +72,16 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
return;
}
const auto samples = render_sample_count(rt.attrib.num_fragments);
if (samples == 0 || rt.attrib.num_samples != rt.attrib.num_fragments) {
// TODO(PPSA24156): Replace this 2x MSAA approximation with proper 4-coverage/2-fragment EQAA.
const bool approximate_eqaa = rt.attrib.num_samples == 2 && rt.attrib.num_fragments == 1;
if (approximate_eqaa) {
static std::atomic_bool logged = false;
if (!logged.exchange(true, std::memory_order_relaxed)) {
Log::WriteToConsoleAndLog(
"Warning: approximating 4-coverage/2-fragment EQAA with 2x MSAA.\n");
}
}
if (samples == 0 || (rt.attrib.num_samples != rt.attrib.num_fragments && !approximate_eqaa)) {
EXIT("unsupported render-target sample configuration: samples=%u fragments=%u\n",
rt.attrib.num_samples, rt.attrib.num_fragments);
}
@@ -220,8 +229,7 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
}
const bool has_dcc = rt.info.dcc_compression_enable && rt.dcc_addr.addr != 0;
const bool has_cmask = !rt.info.dcc_compression_enable && rt.info.cmask_fast_clear_enable &&
rt.cmask.addr != 0 && samples == 1 &&
!rt.info.fmask_compression_enable &&
rt.cmask.addr != 0 &&
!rt.attrib3.write_vrs_rate_hint_to_cmask;
if (has_dcc || has_cmask) {
TileSizeAlign metadata_size {};
@@ -233,7 +241,7 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
} else {
(void)TileGetCmaskSize(width, height, layers, levels, metadata_size);
}
// DCC owns the clear when both planes are enabled; single-sample CMASK stays expanded.
// DCC owns the clear when both planes are enabled.
desc.info.metadata.kind = has_dcc ? ImageMetadataKind::Dcc : ImageMetadataKind::Cmask;
desc.info.metadata.range = {has_dcc ? rt.dcc_addr.addr : rt.cmask.addr, metadata_size.size};
desc.info.metadata.clear_word =
@@ -1,6 +1,7 @@
#include "graphics/host_gpu/renderer/commandScheduler.h"
#include "common/assert.h"
#include "common/debugCounters.h"
#include "common/logging/log.h"
#include "graphics/host_gpu/graphicContext.h"
@@ -373,6 +374,7 @@ uint64_t CommandScheduler::Submit(SubmitInfo submit) {
result = graphics.queue.submit(1, &submit_info, nullptr);
}
Common::DebugCounters::Add(Common::DebugCounters::Counter::QueueSubmits);
if (result != vk::Result::eSuccess) {
ReportVulkanFatal("vkQueueSubmit", result, tick, m_command.m_debug_op,
-10
View File
@@ -294,16 +294,6 @@ static void RtCheck(const HW::RenderTarget& rt) {
EXIT_NOT_IMPLEMENTED(rt.dcc.overwrite_combiner_disable != false);
// EXIT_NOT_IMPLEMENTED(rt.dcc.data_write_on_dcc_clear_to_reg != false);
EXIT_NOT_IMPLEMENTED(rt.dcc.dcc_clear_key_enable != false);
if (rt.cmask.addr != 0x0000000000000000 || rt.fmask.addr != 0x0000000000000000 ||
rt.dcc_addr.addr != 0x0000000000000000) {
static bool logged = false;
if (!logged) {
LOGF("RenderTarget: temporary: ignoring PS5 metadata addresses cmask=0x%016" PRIx64
" fmask=0x%016" PRIx64 " dcc=0x%016" PRIx64 "\n",
rt.cmask.addr, rt.fmask.addr, rt.dcc_addr.addr);
logged = true;
}
}
}
}
@@ -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.
@@ -353,8 +353,12 @@ void Image::CopyImage(Image& source) {
std::vector<vk::ImageCopy> copies;
copies.reserve(levels);
for (uint32_t level = 0; level < levels; level++) {
const auto width = std::max(source.backing.extent.width >> level, 1u);
const auto height = std::max(source.backing.extent.height >> level, 1u);
// Overlapping cache images can differ in size; copying the source extent into a smaller
// destination writes out of bounds and loses the device.
const auto width = std::min(std::max(source.backing.extent.width >> level, 1u),
std::max(backing.extent.width >> level, 1u));
const auto height = std::min(std::max(source.backing.extent.height >> level, 1u),
std::max(backing.extent.height >> level, 1u));
const auto depth = std::max(base_depth >> level, 1u);
const auto [source_layers, destination_layers] = SanitizeCopyLayers(source, *this, depth);
vk::ImageCopy copy {};
@@ -1,6 +1,7 @@
#include "graphics/host_gpu/renderer/masterSemaphore.h"
#include "common/assert.h"
#include "common/debugCounters.h"
#include "graphics/host_gpu/graphicContext.h"
namespace Libs::Graphics {
@@ -49,6 +50,8 @@ void MasterSemaphore::Wait(uint64_t tick) {
wait_info.pSemaphores = &m_semaphore;
wait_info.pValues = &tick;
Common::DebugCounters::ScopedTimer timer(Common::DebugCounters::Counter::GpuWaits,
Common::DebugCounters::Counter::GpuWaitNs);
const auto result = m_graphics.device.waitSemaphores(&wait_info, UINT64_MAX);
EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess);
Refresh();
@@ -764,7 +764,10 @@ void RenderExecutor::FindBuffers(std::span<PreparedBindings* const> stages) {
size = std::min({size, uint64_t {256} * 1024 * 1024, limit});
}
}
size = Libs::LibKernel::Memory::ClampRangeSize(address, size);
// Finite GPU descriptors can span sparse PRT holes; clamp only oversized/sentinel ranges.
if (size >= (uint64_t {1} << 30u) || descriptor.NumRecords() == UINT32_MAX) {
size = Libs::LibKernel::Memory::ClampRangeSize(address, size);
}
source = {address, size, cache.FindBuffer(address, size)};
}
}
@@ -1,6 +1,7 @@
#include "graphics/host_gpu/renderer/pipeline/pipelineCache.h"
#include "common/assert.h"
#include "common/debugCounters.h"
#include "common/emulatorConfig.h"
#include "common/file.h"
#include "common/logging/log.h"
@@ -24,6 +25,7 @@
#include <array>
#include <atomic>
#include <cctype>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <fmt/format.h>
@@ -273,6 +275,7 @@ struct PipelineCache::ProgramCache {
ShaderRecompiler::IR::ResourceSnapshot resources;
ShaderRecompiler::IR::ResourceSpecialization specialization;
std::vector<Permutation> permutations;
bool skip_dispatch = false;
};
struct ProgramKeyHash {
@@ -299,6 +302,9 @@ struct PipelineCache::ProgramCache {
ShaderRecompiler::IR::ResourceSpecialization specialization,
uint32_t push_data_start_dword,
std::span<const uint32_t> function_code) {
Common::DebugCounters::ScopedTimer timer(Common::DebugCounters::Counter::ShadersCompiled,
Common::DebugCounters::Counter::ShaderCompileNs,
Common::DebugCounters::Peak::ShaderCompileNs);
auto result = ShaderRecompiler::CompileProgram(std::move(translated), options,
specialization, push_data_start_dword);
if (!ValidateShaderSpirv(options.dump_label, options.shader_hash, result.spirv,
@@ -355,6 +361,7 @@ struct PipelineCache::ProgramCache {
bool source_changed = false;
if (entry != programs.end()) {
auto& cached = entry->second;
if (cached.skip_dispatch) return {};
Permutation* permutation = nullptr;
if (cached.call_source) {
source_changed = cached.RefreshCallSource(runtime);
@@ -414,10 +421,18 @@ struct PipelineCache::ProgramCache {
DumpShaderOriginal(stage_name, options.shader_hash, params.code);
ShaderRecompiler::ShaderSource source;
if (entry == programs.end()) source = ShaderRecompiler::PrepareShaderSource(params.code, options);
if (source.unsupported_call_pc != UINT32_MAX) {
PipelineCacheLog("!!! TEMPORARY COMPUTE DISPATCH SKIP !!! shader=0x{:016x} pc=0x{:08x} "
"reason=unsupported GPU-selected callable target; rendering is incomplete",
params.hash, source.unsupported_call_pc);
programs.try_emplace(lookup_key, ShaderRecompiler::IR::ResourcePlan{}, nullptr)
.first->second.skip_dispatch = true;
return {};
}
auto* current = entry == programs.end() ? &source : entry->second.call_source.get();
ShaderRecompiler::TranslateResult translated;
if (current != nullptr) {
if (current->call) {
if (!current->calls.empty()) {
if (entry == programs.end()) ShaderRecompiler::RefreshShaderSource(*current, runtime);
translated = ShaderRecompiler::TranslateProgram(current->linked->decoded, options);
translated.program.source_reads = current->reads;
@@ -428,7 +443,7 @@ struct PipelineCache::ProgramCache {
translated = ShaderRecompiler::TranslateProgram(params.code, options);
}
if (entry == programs.end()) {
auto retained = source.call ? std::make_unique<ShaderRecompiler::ShaderSource>(std::move(source)) : nullptr;
auto retained = !source.calls.empty() ? std::make_unique<ShaderRecompiler::ShaderSource>(std::move(source)) : nullptr;
entry = programs.try_emplace(lookup_key,
ShaderRecompiler::IR::ExtractResourcePlan(translated.program), std::move(retained)).first;
EXIT_IF(!ShaderRecompiler::IR::MaterializeResources(
@@ -575,8 +590,8 @@ void PipelineCache::InitializeDriverCache() {
return;
}
if (!initial_data.empty()) {
PipelineCacheLog("Vulkan pipeline cache: loaded {} bytes from {}", initial_data.size(),
path);
PipelineCacheLog("Vulkan pipeline cache: loaded {:.2f} MB from {}",
static_cast<double>(initial_data.size()) / 1'000'000.0, path);
} else {
PipelineCacheLog("Vulkan pipeline cache: initialized empty");
}
@@ -616,8 +631,8 @@ void PipelineCache::Save() {
}
if (result != vk::Result::eSuccess || size == 0 ||
size > std::numeric_limits<uint32_t>::max()) {
PipelineCacheLog("Vulkan pipeline cache: save failed ({}, {} bytes)",
vk::to_string(result), size);
PipelineCacheLog("Vulkan pipeline cache: save failed ({}, {:.2f} MB)",
vk::to_string(result), static_cast<double>(size) / 1'000'000.0);
return;
}
payload.resize(size);
@@ -649,7 +664,8 @@ void PipelineCache::Save() {
Common::PathToString(m_driver_cache_path));
return;
}
PipelineCacheLog("Vulkan pipeline cache: saved {} bytes to {}", payload.size(),
PipelineCacheLog("Vulkan pipeline cache: saved {:.2f} MB to {}",
static_cast<double>(payload.size()) / 1'000'000.0,
Common::PathToString(m_driver_cache_path));
m_driver_cache_dirty = false;
}
@@ -993,8 +1009,13 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(
auto cached = std::make_unique<Pipeline>();
LogPipelineTrace("CreatePipelineInternal begin", vs_id, ps_id);
CreatePipelineInternal(m_graphics, *cached, rendering, key.vertex_input, vertex_info,
ps_input_info, programs, static_params, m_driver_cache);
{
Common::DebugCounters::ScopedTimer timer(Common::DebugCounters::Counter::PipelinesCreated,
Common::DebugCounters::Counter::PipelineCreateNs,
Common::DebugCounters::Peak::PipelineCreateNs);
CreatePipelineInternal(m_graphics, *cached, rendering, key.vertex_input, vertex_info,
ps_input_info, programs, static_params, m_driver_cache);
}
LogPipelineTrace("CreatePipelineInternal done", vs_id, ps_id);
EXIT_NOT_IMPLEMENTED(cached->pipeline == nullptr);
@@ -1024,7 +1045,13 @@ PipelineCache::GetComputePipeline(const ShaderComputeInputInfo& input_info,
}
auto cached = std::make_unique<Pipeline>();
CreatePipelineInternal(m_graphics, *cached, input_info, compute_program.module, m_driver_cache);
{
Common::DebugCounters::ScopedTimer timer(Common::DebugCounters::Counter::PipelinesCreated,
Common::DebugCounters::Counter::PipelineCreateNs,
Common::DebugCounters::Peak::PipelineCreateNs);
CreatePipelineInternal(m_graphics, *cached, input_info, compute_program.module,
m_driver_cache);
}
EXIT_NOT_IMPLEMENTED(cached->pipeline == nullptr);
EXIT_NOT_IMPLEMENTED(cached->pipeline_layout == nullptr);
@@ -196,21 +196,27 @@ static void CreateDescriptorLayout(GraphicContext& graphics, PipelineCache::Pipe
const auto& limits = graphics.GetPhysicalDeviceProperties().limits;
uint64_t descriptor_count = 0;
uint64_t storage_buffers = 0;
std::array<uint64_t, 32> stage_buffers {}, stage_resources {};
uint64_t samplers = 0;
std::array<uint64_t, 32> stage_buffers {}, stage_resources {}, stage_samplers {};
for (const auto& binding: bindings) {
descriptor_count += binding.descriptorCount;
const bool storage = binding.descriptorType == vk::DescriptorType::eStorageBuffer;
const bool sampler = binding.descriptorType == vk::DescriptorType::eSampler;
if (storage) storage_buffers += binding.descriptorCount;
if (sampler) samplers += binding.descriptorCount;
for (auto stages = static_cast<uint32_t>(binding.stageFlags); stages != 0; stages &= stages - 1u) {
const auto stage = std::countr_zero(stages);
if (storage) stage_buffers[stage] += binding.descriptorCount;
if (binding.descriptorType != vk::DescriptorType::eSampler)
if (sampler) stage_samplers[stage] += binding.descriptorCount;
else
stage_resources[stage] += binding.descriptorCount;
if (stage_buffers[stage] > limits.maxPerStageDescriptorStorageBuffers ||
stage_resources[stage] > limits.maxPerStageResources) {
stage_resources[stage] > limits.maxPerStageResources ||
stage_samplers[stage] > limits.maxPerStageDescriptorSamplers) {
EXIT("native descriptor stage %u exceeds device limits: buffers=%" PRIu64
"/%u resources=%" PRIu64 "/%u\n", stage, stage_buffers[stage],
limits.maxPerStageDescriptorStorageBuffers, stage_resources[stage], limits.maxPerStageResources);
"/%u resources=%" PRIu64 "/%u samplers=%" PRIu64 "/%u\n", stage, stage_buffers[stage],
limits.maxPerStageDescriptorStorageBuffers, stage_resources[stage], limits.maxPerStageResources,
stage_samplers[stage], limits.maxPerStageDescriptorSamplers);
}
}
}
@@ -218,6 +224,10 @@ static void CreateDescriptorLayout(GraphicContext& graphics, PipelineCache::Pipe
EXIT("native storage-buffer descriptors exceed device limit: %" PRIu64 "/%u\n",
storage_buffers, limits.maxDescriptorSetStorageBuffers);
}
if (samplers > limits.maxDescriptorSetSamplers) {
EXIT("native sampler descriptors exceed device limit: %" PRIu64 "/%u\n",
samplers, limits.maxDescriptorSetSamplers);
}
pipeline.uses_push_descriptors = descriptor_count <= graphics.max_push_descriptors;
vk::DescriptorSetLayoutCreateInfo create {};
+5 -5
View File
@@ -201,11 +201,11 @@ private:
void BindImage(ImageId id, bool storage);
void BindRenderTarget(ImageId id);
void ResetBindings();
void UpdateComputeMetaClears(const ShaderComputeInputInfo& input,
const CommandBuffer& buffer);
[[nodiscard]] bool TryConsumeComputeImageClear(const ShaderComputeInputInfo& input,
CommandBuffer& command, uint32_t group_x,
uint32_t group_y, uint32_t group_z, uint32_t mode);
[[nodiscard]] bool TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
const CommandBuffer& buffer);
[[nodiscard]] bool TryConsumeComputeFill(const ShaderComputeInputInfo& input,
CommandBuffer& command, uint32_t group_x,
uint32_t group_y, uint32_t group_z, uint32_t mode);
RenderContext& m_context;
GraphicsBindings m_graphics_bindings;
@@ -1,5 +1,6 @@
#include "common/assert.h"
#include "common/common.h"
#include "common/debugCounters.h"
#include "common/emulatorConfig.h"
#include "common/file.h"
#include "common/logging/log.h"
@@ -50,8 +51,8 @@ static bool FillSourcesDisjoint(std::span<const ShaderRecompiler::IR::Descriptor
return true;
}
void RenderExecutor::UpdateComputeMetaClears(const ShaderComputeInputInfo& input,
const CommandBuffer& buffer) {
bool RenderExecutor::TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
const CommandBuffer& buffer) {
const auto& program = *input.stage.program;
const auto& resources = *input.stage.resources;
if (resources.buffers.size() != program.info.buffers.size()) {
@@ -61,9 +62,10 @@ void RenderExecutor::UpdateComputeMetaClears(const ShaderComputeInputInfo& input
for (uint32_t i = 0; i < program.info.buffers.size(); i++) {
const auto& resource = program.info.buffers[i];
const auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(resources.buffers[i]);
// Reads and transforms of metadata do not establish a clear state.
// A metadata resource that is also read is not proven to be a full overwrite. Execute it
// conservatively instead of replacing the dispatch with a coarse full-surface clear.
if ((!resource.written || resource.read) && cache.IsMeta(descriptor.Base48())) {
return;
return false;
}
}
@@ -73,10 +75,13 @@ void RenderExecutor::UpdateComputeMetaClears(const ShaderComputeInputInfo& input
if (resource.written) {
const auto descriptor =
DecodeNativeDescriptor<ShaderBufferResource>(resources.buffers[i]);
(void)cache.ClearMeta(descriptor.Base48());
if (cache.ClearMeta(descriptor.Base48())) {
return true;
}
}
}
}
return false;
}
bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t group_x,
@@ -94,9 +99,10 @@ bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t grou
constexpr std::array formats {
Prospero::BufferFormat::k32UInt, Prospero::BufferFormat::k32_32UInt,
Prospero::BufferFormat::k32_32_32UInt, Prospero::BufferFormat::k32_32_32_32UInt};
// Offset-or-swizzle bounds do not describe the element range proved by the shader.
if (descriptor.Stride() != element_size || descriptor.Format() != formats[fill.words - 1] ||
descriptor.SwizzleEnabled() || descriptor.IndexStride() != 0 || descriptor.AddTid() ||
descriptor.Base48() == 0) {
descriptor.OutOfBounds() == 3 || descriptor.Base48() == 0) {
return false;
}
if (input.threads_num[0] == 0 || input.threads_num[0] != fill.group_stride[0] ||
@@ -108,7 +114,11 @@ bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t grou
? group_x
: static_cast<uint64_t>(group_x) * input.threads_num[0];
const auto size = descriptor.GetSize();
if (invocations != descriptor.NumRecords() || size == 0 || size > UINT32_MAX ||
const auto elements = descriptor.NumRecords();
const bool covered = fill.bounded_elements != 0
? fill.bounded_elements == elements && invocations >= elements
: invocations == elements;
if (!covered || invocations > uint64_t {UINT32_MAX} + 1 || size == 0 || size > UINT32_MAX ||
(input.dispatch_thread_dimensions &&
(group_x % input.threads_num[0] != 0 || input.dispatch_threads_num[0] != group_x ||
input.dispatch_threads_num[1] != 1 || input.dispatch_threads_num[2] != 1))) {
@@ -122,9 +132,9 @@ bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t grou
return true;
}
bool RenderExecutor::TryConsumeComputeImageClear(const ShaderComputeInputInfo& input,
CommandBuffer& command, uint32_t group_x,
uint32_t group_y, uint32_t group_z, uint32_t mode) {
bool RenderExecutor::TryConsumeComputeFill(const ShaderComputeInputInfo& input,
CommandBuffer& command, uint32_t group_x,
uint32_t group_y, uint32_t group_z, uint32_t mode) {
const auto& program = *input.stage.program;
const auto& resources = *input.stage.resources;
const auto& fill = resources.uniform_fill;
@@ -180,14 +190,26 @@ bool RenderExecutor::TryConsumeComputeImageClear(const ShaderComputeInputInfo& i
size)) {
return false;
}
if (!cache.ClearImageFromBuffer(command, descriptor.Base48(), size, packed_clear)) {
return false;
const bool image_clear =
cache.ClearImageFromBuffer(command, descriptor.Base48(), size, packed_clear);
if (!image_clear) {
// CPU fills invalidate overlapping cached images. Preserve the native dispatch for
// partial or ambiguous image backings, including pooled metadata aliases.
if ((descriptor.Base48() & 3u) != 0 ||
!m_context.IsMapped(descriptor.Base48(), size) ||
cache.IsRegionRegistered(descriptor.Base48(), size)) {
return false;
}
// Metadata need not have an image backing yet. Keep proven fills in guest memory
// when CPU-owned, so color-clear discovery can read them without a GPU download.
m_context.GetBufferCache().FillBuffer(descriptor.Base48(), size, packed_clear, false);
}
static std::atomic<uint32_t> logged_clears {0};
if (logged_clears.fetch_add(1, std::memory_order_relaxed) < 32) {
LOGF("GraphicsRenderDispatchDirect: compute image clear shader=0x%016" PRIx64
LOGF("GraphicsRenderDispatchDirect: compute %s shader=0x%016" PRIx64
" addr=0x%016" PRIx64 " size=0x%016" PRIx64 " value=0x%08" PRIx32 "\n",
input.stage.program->shader_hash, descriptor.Base48(), size, packed_clear);
image_clear ? "image clear" : "buffer fill", input.stage.program->shader_hash,
descriptor.Base48(), size, packed_clear);
}
return true;
}
@@ -252,10 +274,6 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
return;
}
if (sh_ctx.GetCs().cs_regs.data_addr == 0) {
return;
}
constexpr uint32_t DISPATCH_INITIATOR_USE_THREAD_DIMENSIONS = 1u << 5u;
constexpr uint32_t DISPATCH_INITIATOR_BASE_BITS = 0x41u;
constexpr uint32_t DISPATCH_INITIATOR_MODIFIER_BITS = 0xa038u;
@@ -295,6 +313,7 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
}
const auto compute_program =
m_context.GetPipelineCache().GetComputeProgram(cs_regs, sh_regs, input_info);
if (!compute_program) return;
if (use_thread_dimensions) {
input_info.dispatch_threads_num[0] = thread_group_x;
input_info.dispatch_threads_num[1] = thread_group_y;
@@ -303,14 +322,12 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
const auto& program = *input_info.stage.program;
const auto& resources = *input_info.stage.resources;
if (resources.specialization_reads.empty()) {
// Cached metadata addresses can be reused as ordinary buffers; preserve their guest writes.
UpdateComputeMetaClears(input_info, buffer);
if (TryConsumeComputeImageClear(input_info, buffer, thread_group_x, thread_group_y,
thread_group_z, mode)) {
ResetBindings();
return;
}
if (resources.specialization_reads.empty() &&
(TryConsumeComputeMetaClear(input_info, buffer) ||
TryConsumeComputeFill(input_info, buffer, thread_group_x, thread_group_y,
thread_group_z, mode))) {
ResetBindings();
return;
}
if (use_thread_dimensions) {
@@ -431,6 +448,7 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
vk_buffer.bindPipeline(vk::PipelineBindPoint::eCompute, pipeline.pipeline);
vk_buffer.dispatch(thread_group_x, thread_group_y, thread_group_z);
Common::DebugCounters::Add(Common::DebugCounters::Counter::Dispatches);
// The removed host fence also ordered read-only dispatches before later writers.
ShaderAccessBarrier(vk_buffer, vk::PipelineStageFlagBits::eComputeShader);
@@ -461,6 +479,7 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
input_info.indirect_args_address = args_addr;
const auto compute_program = m_context.GetPipelineCache().GetComputeProgram(
cs_regs, buffer.GetRegisters().GetShaderRegisters(), input_info);
if (!compute_program) return;
buffer.EndRendering();
auto& pipeline = m_context.GetPipelineCache().GetComputePipeline(input_info, compute_program);
auto& bindings = m_compute_bindings;
@@ -500,6 +519,7 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
1, &barrier, 0, nullptr, 0, nullptr);
vk_buffer.bindPipeline(vk::PipelineBindPoint::eCompute, pipeline.pipeline);
vk_buffer.dispatchIndirect(args_buffer->Handle(), args_offset);
Common::DebugCounters::Add(Common::DebugCounters::Counter::Dispatches);
ShaderAccessBarrier(vk_buffer, vk::PipelineStageFlagBits::eComputeShader);
ResetBindings();
}
+14 -8
View File
@@ -2,6 +2,7 @@
#include "common/assert.h"
#include "common/common.h"
#include "common/debugCounters.h"
#include "common/emulatorConfig.h"
#include "common/file.h"
#include "common/logging/log.h"
@@ -507,14 +508,15 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
}
const auto image_view = cache.FindDepthTarget(depth.image_id, depth.desc);
const auto& metadata = depth.desc.info.metadata;
if (metadata.kind == ImageMetadataKind::Htile && depth.depth_clear_enable &&
!cache.ClearMeta(metadata.range.address)) {
EXIT("failed to acquire HTile metadata for a depth clear\n");
}
const bool meta_clear =
metadata.kind == ImageMetadataKind::Htile &&
cache.IsMetaCleared(metadata.range.address, depth.desc.view_info.base_layer);
depth.depth_load_clear_enable = depth.depth_clear_enable || meta_clear;
depth.load_clear_aspects = {};
if (depth.depth_clear_enable || meta_clear)
depth.load_clear_aspects |= vk::ImageAspectFlagBits::eDepth;
// Apply pending clears to compressed stencil as well as depth.
if (depth.stencil_clear_enable || (meta_clear && metadata.stencil_compressed))
depth.load_clear_aspects |= vk::ImageAspectFlagBits::eStencil;
if (meta_clear &&
!cache.TouchMeta(metadata.range.address, depth.desc.view_info.base_layer, false)) {
EXIT("failed to consume HTile clear state\n");
@@ -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);
+18
View File
@@ -97,6 +97,24 @@ uint64_t GraphicContext::GetDeviceMemoryUsage() const {
return usage;
}
uint32_t GraphicContext::GetHeapUsage(std::span<HeapUsage, VK_MAX_MEMORY_HEAPS> heaps) const {
if (allocator == nullptr) {
return 0;
}
VmaBudget budgets[VK_MAX_MEMORY_HEAPS] {};
vmaGetHeapBudgets(allocator, budgets);
const auto count = physical_device_memory_properties.memoryHeapCount;
for (uint32_t heap = 0; heap < count; heap++) {
const auto& properties = physical_device_memory_properties.memoryHeaps[heap];
heaps[heap] = {.usage = budgets[heap].usage,
.budget = budgets[heap].budget,
.allocations = budgets[heap].statistics.allocationCount,
.device_local = static_cast<bool>(properties.flags &
vk::MemoryHeapFlagBits::eDeviceLocal)};
}
return count;
}
uint64_t GraphicContext::GetTotalMemoryBudget() const {
if (allocator == nullptr) {
return 0;
@@ -0,0 +1,747 @@
#include "graphics/presentation/performanceOverlay.h"
#include "common/debugCounters.h"
#include "common/emulatorConfig.h"
#include "common/hostStats.h"
#include "graphics/host_gpu/graphicContext.h"
#include "imgui.h"
#include "kernel/memory.h"
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdarg>
#include <cstdio>
#include <mutex>
#include <optional>
#include <stop_token>
#include <thread>
namespace Libs::Graphics {
namespace {
using Clock = std::chrono::steady_clock;
using Counter = Common::DebugCounters::Counter;
using Peak = Common::DebugCounters::Peak;
constexpr auto REFRESH_INTERVAL = std::chrono::seconds(1);
constexpr size_t FRAME_HISTORY = 120;
constexpr float FRAME_GRAPH_MS = 50.0f;
constexpr float FRAME_TIME_GOOD = 17.5f;
constexpr float FRAME_TIME_MEDIUM = 34.0f;
constexpr size_t COUNTER_COUNT = static_cast<size_t>(Counter::Count);
// Tables as wide as their columns; the default fills the window, which an auto-resizing
// window then never shrinks.
constexpr ImGuiTableFlags TABLE_FLAGS =
ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoHostExtendX;
constexpr ImU32 GOOD = IM_COL32(96, 220, 120, 255);
constexpr ImU32 MEDIUM = IM_COL32(230, 195, 60, 255);
constexpr ImU32 BAD = IM_COL32(235, 80, 70, 255);
constexpr ImU32 NEUTRAL = IM_COL32(160, 160, 160, 255);
struct FrameWindow {
std::array<float, FRAME_HISTORY> intervals_ms {};
size_t next = 0;
size_t count = 0;
uint32_t flips = 0;
float max_ms = 0.0f;
uint32_t guest_width = 0;
uint32_t guest_height = 0;
Clock::time_point last_flip = {};
Clock::time_point window_start = {};
};
// Shown values, recomputed once per refresh interval.
struct Snapshot {
double fps = 0.0;
double frame_ms = 0.0;
double max_frame_ms = 0.0;
double low_fps = 0.0; // From the 99th percentile.
double stalled_s = 0.0;
uint32_t guest_width = 0;
uint32_t guest_height = 0;
std::array<double, COUNTER_COUNT> rates {};
std::array<uint64_t, COUNTER_COUNT> totals {};
uint64_t shader_compile_peak_ns = 0;
uint64_t pipeline_create_peak_ns = 0;
Common::HostStats host;
std::optional<uint64_t> device_memory;
uint64_t device_budget = 0;
std::array<GraphicContext::HeapUsage, VK_MAX_MEMORY_HEAPS> heaps {};
uint32_t heap_count = 0;
std::array<float, FRAME_HISTORY> history {};
size_t history_count = 0;
Clock::time_point sample_time {};
LibKernel::Memory::DebugStats guest_memory;
};
std::mutex g_frame_mutex;
FrameWindow g_frames;
std::atomic<uint64_t> g_generation {0};
std::atomic<bool> g_details {false};
std::atomic<uint64_t> g_revision {0};
std::mutex g_host_mutex;
Common::HostStats g_host_stats;
uint64_t g_host_generation = 0;
std::condition_variable_any g_sampler_wait;
std::jthread g_sampler;
void SetOverlayEnabled(bool enabled) {
{
std::scoped_lock lock(g_host_mutex);
if (enabled) {
std::scoped_lock frame_lock(g_frame_mutex);
g_frames = {};
g_generation.fetch_add(1, std::memory_order_release);
}
Common::DebugCounters::SetEnabled(enabled);
}
g_sampler_wait.notify_all();
}
bool GuestStalled(Clock::time_point now) {
std::scoped_lock lock(g_frame_mutex);
return now - g_frames.last_flip >= REFRESH_INTERVAL;
}
// Host statistics can block for milliseconds (IOKit, thread enumeration), so sample them off
// the present path. While the guest stops flipping (loading screens), ask for one repaint per
// interval.
void SamplerThread(const std::stop_token& stop) {
std::optional<Common::HostStatsSampler> sampler;
uint64_t sampled_generation = 0;
std::unique_lock lock(g_host_mutex);
while (!stop.stop_requested()) {
g_sampler_wait.wait(lock, stop, [] { return Common::DebugCounters::Enabled(); });
if (stop.stop_requested()) {
break;
}
const auto generation = g_generation.load(std::memory_order_acquire);
lock.unlock();
if (!sampler || generation != sampled_generation) {
sampler.emplace();
sampled_generation = generation;
}
auto stats = sampler->Sample();
lock.lock();
g_host_stats = std::move(stats);
g_host_generation = generation;
if (Common::DebugCounters::Enabled() && GuestStalled(Clock::now())) {
g_revision.fetch_add(1, std::memory_order_acq_rel);
}
g_sampler_wait.wait_for(lock, stop, REFRESH_INTERVAL, [generation] {
return !Common::DebugCounters::Enabled() ||
generation != g_generation.load(std::memory_order_acquire);
});
}
}
void EnsureSampler() {
static std::once_flag once;
std::call_once(once, [] { g_sampler = std::jthread(SamplerThread); });
}
ImU32 FrameTimeColor(double ms) {
return ms <= FRAME_TIME_GOOD ? GOOD : ms <= FRAME_TIME_MEDIUM ? MEDIUM : BAD;
}
// Green below `warn`, yellow below `bad`, red above.
ImU32 LevelColor(double value, double warn, double bad) {
return value < warn ? GOOD : value < bad ? MEDIUM : BAD;
}
ImVec4 Color(ImU32 color) {
return ImGui::ColorConvertU32ToFloat4(color);
}
std::string Bytes(uint64_t bytes) {
constexpr double GIB = 1024.0 * 1024.0 * 1024.0;
constexpr double MIB = 1024.0 * 1024.0;
char buffer[32];
if (static_cast<double>(bytes) >= GIB) {
std::snprintf(buffer, sizeof(buffer), "%.2f GB", static_cast<double>(bytes) / GIB);
} else {
std::snprintf(buffer, sizeof(buffer), "%.0f MB", static_cast<double>(bytes) / MIB);
}
return buffer;
}
std::string Bytes(std::optional<uint64_t> bytes) {
return bytes ? Bytes(*bytes) : "n/a";
}
std::string Throughput(double bytes_per_second) {
char buffer[32];
std::snprintf(buffer, sizeof(buffer), "%.1f MB/s", bytes_per_second / (1024.0 * 1024.0));
return buffer;
}
std::string Percent(std::optional<double> value) {
if (!value) {
return "n/a";
}
char buffer[16];
std::snprintf(buffer, sizeof(buffer), "%.0f%%", *value);
return buffer;
}
void Row(const char* label, const std::string& value, std::optional<ImU32> color = std::nullopt) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::TextDisabled("%s", label);
ImGui::TableNextColumn();
if (color) {
ImGui::TextColored(Color(*color), "%s", value.c_str());
} else {
ImGui::TextUnformatted(value.c_str());
}
}
[[gnu::format(printf, 1, 2)]] std::string Fmt(const char* format, ...) {
char buffer[128];
va_list args;
va_start(args, format);
std::vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
return buffer;
}
bool BeginSection(const char* title) {
ImGui::SeparatorText(title);
return ImGui::BeginTable(title, 2, TABLE_FLAGS);
}
void DrawFrameGraph(const std::array<float, FRAME_HISTORY>& history, size_t count, float width,
float height) {
const ImVec2 origin = ImGui::GetCursorScreenPos();
auto* draw = ImGui::GetWindowDrawList();
draw->AddRectFilled(origin, {origin.x + width, origin.y + height}, IM_COL32(0, 0, 0, 90));
const auto y_of = [&](float ms) {
return origin.y + height - std::min(ms, FRAME_GRAPH_MS) / FRAME_GRAPH_MS * height;
};
for (const float reference: {1000.0f / 60.0f, 1000.0f / 30.0f}) {
draw->AddLine({origin.x, y_of(reference)}, {origin.x + width, y_of(reference)},
IM_COL32(255, 255, 255, 45));
}
const float bar = width / static_cast<float>(FRAME_HISTORY);
for (size_t i = 0; i < count; i++) {
const float ms = history[i];
const float x = origin.x + width - static_cast<float>(count - i) * bar;
draw->AddRectFilled({x, y_of(ms)}, {x + std::max(bar - 1.0f, 1.0f), origin.y + height},
FrameTimeColor(ms));
}
ImGui::Dummy({width, height});
}
const char* PressureName(Common::MemoryPressure pressure) {
switch (pressure) {
case Common::MemoryPressure::Normal: return "Normal";
case Common::MemoryPressure::Warning: return "Warning";
case Common::MemoryPressure::Critical: return "Critical";
case Common::MemoryPressure::Unknown: break;
}
return "n/a";
}
ImU32 PressureColor(Common::MemoryPressure pressure) {
switch (pressure) {
case Common::MemoryPressure::Normal: return GOOD;
case Common::MemoryPressure::Warning: return MEDIUM;
case Common::MemoryPressure::Critical: return BAD;
case Common::MemoryPressure::Unknown: break;
}
return NEUTRAL;
}
const char* ThermalName(Common::ThermalPressure thermal) {
switch (thermal) {
case Common::ThermalPressure::Nominal: return "Nominal";
case Common::ThermalPressure::Moderate: return "Moderate";
case Common::ThermalPressure::Heavy: return "Heavy (throttling)";
case Common::ThermalPressure::Critical: return "Critical (throttling)";
case Common::ThermalPressure::Unknown: break;
}
return "n/a";
}
ImU32 ThermalColor(Common::ThermalPressure thermal) {
switch (thermal) {
case Common::ThermalPressure::Nominal: return GOOD;
case Common::ThermalPressure::Moderate: return MEDIUM;
case Common::ThermalPressure::Heavy:
case Common::ThermalPressure::Critical: return BAD;
case Common::ThermalPressure::Unknown: break;
}
return NEUTRAL;
}
const char* PresentModeName(Config::PresentMode mode) {
switch (mode) {
case Config::PresentMode::Fifo: return "FIFO";
case Config::PresentMode::Mailbox: return "Mailbox";
case Config::PresentMode::Immediate: return "Immediate";
}
return "?";
}
double Rate(const Snapshot& shown, Counter counter) {
return shown.rates[static_cast<size_t>(counter)];
}
uint64_t Total(const Snapshot& shown, Counter counter) {
return shown.totals[static_cast<size_t>(counter)];
}
void TransferRow(const Snapshot& shown, const char* label, Counter upload, Counter download) {
Row(label,
Throughput(Rate(shown, upload)) + " up / " + Throughput(Rate(shown, download)) + " down");
}
void GpuActivityRows(const Snapshot& shown) {
const auto draws = Rate(shown, Counter::Draws);
Row("Draw calls", Fmt("%.0f/frame %.0f/s", draws / std::max(shown.fps, 1.0), draws));
// Includes present pacing waits, so a high value alone is not a stall.
Row("CPU waiting on GPU",
Fmt("%.0f ms/s (%.0f waits/s)", Rate(shown, Counter::GpuWaitNs) / 1e6,
Rate(shown, Counter::GpuWaits)));
}
void CompilationRows(const Snapshot& shown) {
const auto row = [&](const char* label, Counter count_counter, Counter duration_counter,
uint64_t peak_ns) {
const auto count = Total(shown, count_counter);
Row(label,
Fmt("%llu (avg %.1f ms, peak %.1f ms)", static_cast<unsigned long long>(count),
count != 0 ? static_cast<double>(Total(shown, duration_counter)) / 1e6 /
static_cast<double>(count)
: 0.0,
static_cast<double>(peak_ns) / 1e6),
Rate(shown, count_counter) > 0.0 ? MEDIUM : GOOD);
};
row("Shader recompiler", Counter::ShadersCompiled, Counter::ShaderCompileNs,
shown.shader_compile_peak_ns);
row("Pipeline compile", Counter::PipelinesCreated, Counter::PipelineCreateNs,
shown.pipeline_create_peak_ns);
}
void Refresh(Snapshot& shown, const GraphicContext& graphics, Clock::time_point now,
uint64_t generation) {
const double seconds = shown.sample_time == Clock::time_point {}
? 0.0
: std::chrono::duration<double>(now - shown.sample_time).count();
for (size_t i = 0; i < COUNTER_COUNT; i++) {
const auto value = Common::DebugCounters::Get(static_cast<Counter>(i));
shown.rates[i] =
seconds > 0.0 ? static_cast<double>(value - shown.totals[i]) / seconds : 0.0;
shown.totals[i] = value;
}
shown.shader_compile_peak_ns = Common::DebugCounters::Get(Peak::ShaderCompileNs);
shown.pipeline_create_peak_ns = Common::DebugCounters::Get(Peak::PipelineCreateNs);
shown.sample_time = now;
{
std::scoped_lock lock(g_host_mutex);
if (g_host_generation == generation) {
shown.host = g_host_stats;
}
}
shown.heap_count = graphics.GetHeapUsage(shown.heaps);
shown.device_memory =
graphics.CanReportMemoryUsage() ? std::optional<uint64_t>(0) : std::nullopt;
shown.device_budget = 0;
const bool discrete =
graphics.GetPhysicalDeviceProperties().deviceType == vk::PhysicalDeviceType::eDiscreteGpu;
for (uint32_t i = 0; i < shown.heap_count; i++) {
const auto& heap = shown.heaps[i];
if (shown.device_memory && (!discrete || heap.device_local)) {
*shown.device_memory += heap.usage;
shown.device_budget += heap.budget;
}
}
if (g_details.load(std::memory_order_acquire)) {
shown.guest_memory = LibKernel::Memory::GetDebugStats();
}
}
void DrawCompact(const Snapshot& shown) {
if (!ImGui::BeginTable("##performance", 2, TABLE_FLAGS)) {
return;
}
const auto& host = shown.host;
if (shown.max_frame_ms > 0.0) {
Row("Worst frame", Fmt("%.1f ms", shown.max_frame_ms), FrameTimeColor(shown.max_frame_ms));
}
Row("CPU", host.process_cpu_percent
? Fmt("%.0f%% of %u threads", *host.process_cpu_percent, host.logical_cores)
: std::string("n/a"));
Row("GPU", Percent(host.gpu_percent));
GpuActivityRows(shown);
CompilationRows(shown);
if (shown.device_memory) {
Row("GPU memory", Bytes(*shown.device_memory) + " / " + Bytes(shown.device_budget));
}
TransferRow(shown, "Texture transfers", Counter::TextureUploadBytes,
Counter::TextureDownloadBytes);
TransferRow(shown, "Buffer transfers", Counter::BufferUploadBytes,
Counter::BufferDownloadBytes);
Row("Emulator RAM", Bytes(host.process_memory));
if (host.system_memory_used && host.system_memory_total) {
Row("System RAM",
Bytes(*host.system_memory_used) + " / " + Bytes(*host.system_memory_total));
}
ImGui::EndTable();
}
void DrawFramesAndCpu(const Snapshot& shown, vk::Extent2D extent) {
const auto& host = shown.host;
if (BeginSection("Frames")) {
if (shown.low_fps > 0.0) {
Row("1% low", Fmt("%.1f FPS", shown.low_fps), FrameTimeColor(1000.0 / shown.low_fps));
}
Row("Worst frame", Fmt("%.1f ms", shown.max_frame_ms), FrameTimeColor(shown.max_frame_ms));
Row("Guest output", shown.guest_width != 0
? Fmt("%ux%u", shown.guest_width, shown.guest_height)
: std::string("n/a"));
Row("Window", Fmt("%ux%u %s %u Hz", extent.width, extent.height,
PresentModeName(Config::GetPresentMode()), Config::GetVblankFrequency()));
Row("Flips", Fmt("%.0f/s requested", Rate(shown, Counter::FlipsRequested)));
const auto rejected = Rate(shown, Counter::FlipsRejected);
Row("Flip queue full", Fmt("%.0f/s", rejected), rejected > 0.0 ? BAD : GOOD);
const auto late = Rate(shown, Counter::LateVblanks);
Row("Late vblanks", Fmt("%.0f/s", late), LevelColor(late, 1.0, 10.0));
Row("Swapchain rebuilds",
Fmt("%llu (dropped %llu)",
static_cast<unsigned long long>(Total(shown, Counter::SwapchainRecreations)),
static_cast<unsigned long long>(Total(shown, Counter::DroppedPresents))));
ImGui::EndTable();
}
if (BeginSection("CPU")) {
Row("Emulator",
host.process_cpu_percent
? Fmt("%.0f%% of %u threads (%.1f cores)", *host.process_cpu_percent,
host.logical_cores, *host.process_cpu_percent * host.logical_cores / 100.0)
: std::string("n/a"));
Row("Machine", Percent(host.system_cpu_percent),
host.system_cpu_percent ? std::optional(LevelColor(*host.system_cpu_percent, 70, 90))
: std::nullopt);
if (host.thread_count) {
Row("Host threads", Fmt("%u", *host.thread_count));
}
for (const auto& thread: host.busiest_threads) {
Row(Fmt(" %.24s", thread.name.c_str()).c_str(), Fmt("%.0f%%", thread.cpu_percent),
LevelColor(thread.cpu_percent, 70, 95));
}
if (host.context_switches_per_s) {
Row("Context switches", Fmt("%.0f/s", *host.context_switches_per_s));
}
if (host.page_faults_per_s) {
Row("Page faults", Fmt("%.0f/s (%.0f from disk)", *host.page_faults_per_s,
host.page_ins_per_s.value_or(0.0)));
}
Row("Thermal", ThermalName(host.thermal_pressure), ThermalColor(host.thermal_pressure));
const auto emulated = Rate(shown, Counter::EmulatedInstructions);
Row("Emulated instrs", Fmt("%.0f/s", emulated), LevelColor(emulated, 1000, 100000));
const auto unresolved = Rate(shown, Counter::UnresolvedImportCalls);
Row("Missing HLE calls",
Fmt("%.0f/s (%llu total)", unresolved,
static_cast<unsigned long long>(Total(shown, Counter::UnresolvedImportCalls))),
unresolved > 0.0 ? MEDIUM : GOOD);
ImGui::EndTable();
}
}
void DrawGpu(const Snapshot& shown, const GraphicContext& graphics) {
const auto& host = shown.host;
if (BeginSection("GPU")) {
Row("Device", host.gpu_model.empty()
? std::string(graphics.GetPhysicalDeviceProperties().deviceName.data())
: host.gpu_cores
? Fmt("%s (%u cores)", host.gpu_model.c_str(), *host.gpu_cores)
: host.gpu_model);
Row("Utilization", Percent(host.gpu_percent),
host.gpu_percent ? std::optional(LevelColor(*host.gpu_percent, 80, 95)) : std::nullopt);
if (host.gpu_renderer_percent || host.gpu_tiler_percent) {
Row("Renderer / tiler",
Percent(host.gpu_renderer_percent) + " / " + Percent(host.gpu_tiler_percent));
}
if (host.gpu_memory_in_use) {
Row("Driver memory", Bytes(*host.gpu_memory_in_use) + " in use / " +
Bytes(host.gpu_memory_allocated) + " allocated");
}
if (host.gpu_recoveries) {
Row("GPU resets", Fmt("%llu", static_cast<unsigned long long>(*host.gpu_recoveries)),
*host.gpu_recoveries > 0 ? BAD : GOOD);
}
const double fps = std::max(shown.fps, 1.0);
GpuActivityRows(shown);
Row("Instances", Fmt("%.0f/frame", Rate(shown, Counter::DrawInstances) / fps));
Row("Indirect draws", Fmt("%.0f/s", Rate(shown, Counter::IndirectDraws)));
Row("Dispatches", Fmt("%.0f/frame %.0f/s", Rate(shown, Counter::Dispatches) / fps,
Rate(shown, Counter::Dispatches)));
Row("Queue submits", Fmt("%.0f/s", Rate(shown, Counter::QueueSubmits)));
const auto faults = Rate(shown, Counter::GpuFaults);
Row("Write-tracking faults", Fmt("%.0f/s", faults), LevelColor(faults, 20000, 100000));
CompilationRows(shown);
ImGui::EndTable();
}
if (BeginSection("GPU memory")) {
if (shown.device_memory) {
const double share = shown.device_budget != 0
? static_cast<double>(*shown.device_memory) /
static_cast<double>(shown.device_budget) * 100.0
: 0.0;
Row("Vulkan budget", Bytes(*shown.device_memory) + " / " + Bytes(shown.device_budget),
LevelColor(share, 80, 95));
}
for (size_t heap = 0; heap < shown.heap_count; heap++) {
const auto& usage = shown.heaps[heap];
Row(Fmt(" Heap %zu%s", heap, usage.device_local ? " (device)" : "").c_str(),
Bytes(usage.usage) + " / " + Bytes(usage.budget) +
Fmt(" %u allocs", usage.allocations));
}
TransferRow(shown, "Texture transfers", Counter::TextureUploadBytes,
Counter::TextureDownloadBytes);
Row("Texture evictions", Fmt("%.0f/s", Rate(shown, Counter::TextureEvictions)));
TransferRow(shown, "Buffer transfers", Counter::BufferUploadBytes,
Counter::BufferDownloadBytes);
Row("Buffer evictions", Fmt("%.0f/s", Rate(shown, Counter::BufferEvictions)));
ImGui::EndTable();
}
}
void DrawMemoryAndIo(const Snapshot& shown) {
const auto& host = shown.host;
const auto& guest = shown.guest_memory;
if (BeginSection("PS5 memory")) {
const auto direct_used = guest.direct_allocated + guest.pooled_allocated;
Row("Direct", Bytes(direct_used) + " / " + Bytes(guest.direct_total),
LevelColor(guest.direct_total != 0 ? static_cast<double>(direct_used) /
static_cast<double>(guest.direct_total) * 100.0
: 0.0,
85, 97));
Row(" Mapped", Bytes(guest.direct_mapped));
Row(" Pool",
Bytes(guest.pooled_allocated) + " (" + Bytes(guest.pool_committed) + " committed)");
Row("Flexible", Bytes(guest.flexible_used) + " / " + Bytes(guest.flexible_total));
Row("Extended", Bytes(guest.automatic_allocated));
// Past the pool size the guest sees no page-table entries available.
const auto entries = std::max(guest.cpu_page_entries, guest.gpu_page_entries);
Row("Page table (2 MB)",
Fmt("CPU %llu GPU %llu / %llu",
static_cast<unsigned long long>(guest.cpu_page_entries),
static_cast<unsigned long long>(guest.gpu_page_entries),
static_cast<unsigned long long>(guest.page_entries_total)),
entries > guest.page_entries_total ? BAD
: entries * 10 > guest.page_entries_total * 9 ? MEDIUM
: GOOD);
ImGui::EndTable();
}
if (BeginSection("Emulator process")) {
Row("Footprint", Bytes(host.process_memory));
Row("Resident", Bytes(host.process_resident));
if (host.process_compressed) {
Row("Compressed / swapped", Bytes(*host.process_compressed),
*host.process_compressed > (1ull << 30u) ? MEDIUM : GOOD);
}
Row("File-backed", Bytes(host.process_file_backed));
Row("Address space", Bytes(host.process_virtual));
ImGui::EndTable();
}
if (BeginSection("System memory")) {
if (host.system_memory_used && host.system_memory_total) {
Row("Used", Bytes(*host.system_memory_used) + " / " + Bytes(*host.system_memory_total));
}
if (host.system_wired) {
Row("Wired", Bytes(*host.system_wired));
}
if (host.system_compressed) {
Row("Compressed", Bytes(*host.system_compressed));
}
Row("Cached files", Bytes(host.system_cached));
Row("Free", Bytes(host.system_free));
if (host.swap_used) {
Row("Swap", Bytes(*host.swap_used) + " / " + Bytes(host.swap_total),
*host.swap_used > (4ull << 30u) ? MEDIUM : GOOD);
}
Row("Pressure", PressureName(host.memory_pressure), PressureColor(host.memory_pressure));
ImGui::EndTable();
}
if (BeginSection("I/O & audio")) {
Row("Disk read / write", Throughput(host.disk_read_per_s.value_or(0.0)) + " / " +
Throughput(host.disk_write_per_s.value_or(0.0)));
Row("Guest file read / write", Throughput(Rate(shown, Counter::GuestFileReadBytes)) +
" / " +
Throughput(Rate(shown, Counter::GuestFileWriteBytes)));
const auto underruns = Rate(shown, Counter::AudioUnderruns);
Row("Audio underruns",
Fmt("%.0f/s (%llu total)", underruns,
static_cast<unsigned long long>(Total(shown, Counter::AudioUnderruns))),
underruns > 0.0 ? BAD : GOOD);
ImGui::EndTable();
}
}
} // namespace
void PerformanceOverlayRecordFlip(uint32_t guest_width, uint32_t guest_height) noexcept {
if (!Common::DebugCounters::Enabled()) {
return;
}
std::scoped_lock lock(g_frame_mutex);
const auto now = Clock::now();
if (g_frames.window_start == Clock::time_point {}) {
g_frames.window_start = now;
}
auto& frames = g_frames;
if (frames.last_flip != Clock::time_point {}) {
const float ms = std::chrono::duration<float, std::milli>(now - frames.last_flip).count();
frames.intervals_ms[frames.next] = ms;
frames.next = (frames.next + 1) % FRAME_HISTORY;
frames.count = std::min(frames.count + 1, FRAME_HISTORY);
frames.max_ms = std::max(frames.max_ms, ms);
}
frames.last_flip = now;
frames.guest_width = guest_width;
frames.guest_height = guest_height;
frames.flips++;
}
bool PerformanceOverlayEnabled() noexcept {
return Common::DebugCounters::Enabled();
}
void TogglePerformanceOverlay() noexcept {
SetOverlayEnabled(!Common::DebugCounters::Enabled());
g_revision.fetch_add(1, std::memory_order_acq_rel);
}
void TogglePerformanceOverlayDetails() noexcept {
if (Common::DebugCounters::Enabled()) {
g_details.store(!g_details.load(std::memory_order_acquire), std::memory_order_release);
} else {
g_details.store(true, std::memory_order_release);
SetOverlayEnabled(true);
}
g_revision.fetch_add(1, std::memory_order_acq_rel);
}
uint64_t PerformanceOverlayRevision() noexcept {
return g_revision.load(std::memory_order_acquire);
}
float DrawPerformanceOverlay(const GraphicContext& graphics, vk::Extent2D extent) {
if (!PerformanceOverlayEnabled()) {
return 0.0f;
}
EnsureSampler();
static Snapshot shown;
static uint64_t shown_generation = 0;
static bool shown_details = false;
const auto generation = g_generation.load(std::memory_order_acquire);
const bool details = g_details.load(std::memory_order_acquire);
const auto now = Clock::now();
if (generation != shown_generation) {
shown = {};
shown_generation = generation;
}
if (shown.sample_time == Clock::time_point {} || now - shown.sample_time >= REFRESH_INTERVAL ||
details != shown_details) {
{
std::scoped_lock lock(g_frame_mutex);
auto& frames = g_frames;
shown.history_count = frames.count;
for (size_t i = 0; i < frames.count; i++) {
shown.history[i] =
frames.intervals_ms[(frames.next + FRAME_HISTORY - frames.count + i) %
FRAME_HISTORY];
}
if (frames.window_start == Clock::time_point {}) {
frames.window_start = now;
}
const double seconds = std::chrono::duration<double>(now - frames.window_start).count();
shown.fps = seconds > 0.0 ? frames.flips / seconds : 0.0;
shown.frame_ms = frames.flips > 0 ? seconds * 1000.0 / frames.flips : 0.0;
shown.max_frame_ms = frames.max_ms;
shown.stalled_s = frames.last_flip == Clock::time_point {}
? 0.0
: std::chrono::duration<double>(now - frames.last_flip).count();
shown.guest_width = frames.guest_width;
shown.guest_height = frames.guest_height;
frames.flips = 0;
frames.max_ms = 0.0f;
frames.window_start = now;
}
shown_details = details;
if (shown.history_count > 0) {
auto sorted = shown.history;
const auto end = sorted.begin() + shown.history_count;
const auto percentile = sorted.begin() + shown.history_count * 99 / 100;
std::nth_element(sorted.begin(), percentile, end);
shown.low_fps = *percentile > 0.0f ? 1000.0 / *percentile : 0.0;
}
Refresh(shown, graphics, now, generation);
}
const float scale = std::clamp(std::min(static_cast<float>(extent.width) / 1920.0f,
static_cast<float>(extent.height) / 1080.0f),
0.75f, 1.5f);
ImGui::SetNextWindowPos({static_cast<float>(extent.width) - 12.0f * scale, 12.0f * scale},
ImGuiCond_Always, {1.0f, 0.0f});
ImGui::SetNextWindowBgAlpha(details ? 0.82f : 0.72f);
constexpr ImGuiWindowFlags FLAGS =
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_AlwaysAutoResize;
ImGui::PushFont(nullptr, (details ? 13.0f : 15.0f) * scale);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f * scale);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {10.0f * scale, 8.0f * scale});
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {8.0f * scale, 2.0f * scale});
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, {4.0f * scale, 1.0f * scale});
if (ImGui::Begin("##kyty_performance_overlay", nullptr, FLAGS)) {
if (shown.fps > 0.0) {
ImGui::TextColored(Color(FrameTimeColor(shown.frame_ms)), "%.1f FPS %.1f ms",
shown.fps, shown.frame_ms);
} else if (shown.stalled_s >= 1.0) {
ImGui::TextColored(Color(BAD), "No frames for %.0f s", shown.stalled_s);
} else {
ImGui::TextDisabled("Waiting for frames");
}
DrawFrameGraph(shown.history, shown.history_count, (details ? 330.0f : 260.0f) * scale,
46.0f * scale);
if (details) {
// Three columns keep the detailed panel within a 720p window.
if (ImGui::BeginTable("##details", 3, TABLE_FLAGS)) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
DrawFramesAndCpu(shown, extent);
ImGui::TableNextColumn();
DrawGpu(shown, graphics);
ImGui::TableNextColumn();
DrawMemoryAndIo(shown);
ImGui::EndTable();
}
} else {
DrawCompact(shown);
}
#if defined(__APPLE__)
ImGui::TextDisabled(details ? "Cmd+Shift+P compact Cmd+P hide"
: "Cmd+Shift+P details Cmd+P hide");
#else
ImGui::TextDisabled(details ? "Shift+F2 compact F2 hide" : "Shift+F2 details F2 hide");
#endif
}
const float bottom = ImGui::GetWindowPos().y + ImGui::GetWindowSize().y;
ImGui::End();
ImGui::PopStyleVar(4);
ImGui::PopFont();
return bottom;
}
} // namespace Libs::Graphics
@@ -0,0 +1,30 @@
#ifndef EMULATOR_SRC_GRAPHICS_PRESENTATION_PERFORMANCEOVERLAY_H_
#define EMULATOR_SRC_GRAPHICS_PRESENTATION_PERFORMANCEOVERLAY_H_
#include "graphics/host_gpu/vulkanCommon.h"
#include <cstdint>
namespace Libs::Graphics {
struct GraphicContext;
// Called for each main-bus guest flip, with the guest's video-out size.
void PerformanceOverlayRecordFlip(uint32_t guest_width, uint32_t guest_height) noexcept;
[[nodiscard]] bool PerformanceOverlayEnabled() noexcept;
void TogglePerformanceOverlay() noexcept;
// Switches between the compact and the detailed panel, showing the panel if it was hidden.
void TogglePerformanceOverlayDetails() noexcept;
// Changes when the panel needs a repaint without a guest flip (toggled, or the guest stalled).
[[nodiscard]] uint64_t PerformanceOverlayRevision() noexcept;
// Must run between ImGui::NewFrame() and ImGui::Render(), on the present thread while the
// graphics subsystem is alive. Draws in the top-right corner and returns the panel's bottom
// edge, or 0 when hidden.
float DrawPerformanceOverlay(const GraphicContext& graphics, vk::Extent2D extent);
} // namespace Libs::Graphics
#endif // EMULATOR_SRC_GRAPHICS_PRESENTATION_PERFORMANCEOVERLAY_H_
+2 -1
View File
@@ -162,7 +162,8 @@ void RenderDocOnGuestFlip(RenderContext& renderer) {
return;
}
if (state == RenderDocState::Capturing) {
LOGF("RenderDoc: captured guest flip %u/2\n", ++g_captured_flips);
++g_captured_flips;
LOGF("RenderDoc: captured guest flip %u/2\n", g_captured_flips);
if (g_captured_flips < 2) {
return;
}
+10 -9
View File
@@ -1,7 +1,5 @@
#include "graphics/presentation/systemOverlay.h"
#include <SDL3/SDL.h>
#include "common/assert.h"
#include "common/emulatorConfig.h"
#include "common/file.h"
@@ -11,10 +9,11 @@
#include "gpu_blit_shaders/gpu_imgui_hdr_spv.h"
#include "graphics/host_gpu/graphicContext.h"
#include "graphics/presentation/ddsImage.h"
#include "graphics/presentation/performanceOverlay.h"
#include "graphics/presentation/window/hostInput.h"
#include "imgui.h"
#include "imgui_internal.h"
#include "imgui_impl_vulkan.h"
#include "imgui_internal.h"
#include "libs/controller.h"
#include "libs/dialog.h"
#include "libs/ime.h"
@@ -22,6 +21,7 @@
#include "loader/systemContent.h"
#include "stb_image.h"
#include <SDL3/SDL.h>
#include <algorithm>
#include <array>
#include <atomic>
@@ -712,7 +712,8 @@ SystemOverlayVisualState GetSystemOverlayVisualState() noexcept {
return {core.active || dialog.active || system.active || trophy_active || splash ||
(cursor.enabled && cursor.cursor_visible),
core.revision + dialog.revision + system.revision + (trophy_active ? 1u : 0u) +
(splash ? 2u : 0u) + static_cast<decltype(core.revision)>(cursor.revision)};
(splash ? 2u : 0u) + static_cast<decltype(core.revision)>(cursor.revision) +
PerformanceOverlayRevision()};
}
bool ProcessSystemOverlayInput(const SDL_Event& event) {
@@ -1662,8 +1663,7 @@ struct SystemOverlay::Impl {
}
}
void DrawTrophyNotification(vk::Extent2D frame_extent, Clock::time_point now) {
void DrawTrophyNotification(vk::Extent2D frame_extent, Clock::time_point now, float min_top) {
std::erase_if(retired_trophy_images, [](const auto& texture) {
if (texture->Status != ImTextureStatus_Destroyed) {
return false;
@@ -1731,7 +1731,7 @@ struct SystemOverlay::Impl {
const float alpha = std::min(fade_in, fade_out);
const float slide = (1.0f - fade_in) * (width + 24.0f);
const ImVec2 top_left {static_cast<float>(frame_extent.width) - 32.0f - width + slide,
32.0f};
std::max(32.0f, min_top + 12.0f)};
const ImVec2 bottom_right {top_left.x + width, top_left.y + height};
auto* draw = ImGui::GetForegroundDrawList();
const auto fade = [alpha](int r, int g, int b, int a) {
@@ -1799,7 +1799,7 @@ struct SystemOverlay::Impl {
const auto now = Clock::now();
if (!has_overlay && !g_trophy_notification_active.load(std::memory_order_acquire) &&
!g_splash_active.load(std::memory_order_acquire) &&
!GetHostInputTouchPadVisualState().enabled) {
!GetHostInputTouchPadVisualState().enabled && !PerformanceOverlayEnabled()) {
return false;
}
const auto prepared_session = snapshot.session;
@@ -1830,6 +1830,7 @@ struct SystemOverlay::Impl {
ImGui_ImplVulkan_NewFrame();
ImGui::NewFrame();
DrawSplash(frame_extent);
const float performance_bottom = DrawPerformanceOverlay(graphics, frame_extent);
GetOverlaySnapshot(&snapshot);
if (has_overlay && snapshot.session == prepared_session) {
if (snapshot.session.kind == OverlayKind::Dialog) {
@@ -1838,7 +1839,7 @@ struct SystemOverlay::Impl {
DrawIme(snapshot.ime, frame_extent);
}
}
DrawTrophyNotification(frame_extent, now);
DrawTrophyNotification(frame_extent, now, performance_bottom);
DrawTouchPadCursor(GetHostInputTouchPadVisualState());
ImGui::Render();
extent = frame_extent;
+10
View File
@@ -3,6 +3,7 @@
#include "common/abi.h"
#include "common/assert.h"
#include "common/common.h"
#include "common/debugCounters.h"
#include "common/emulatorConfig.h"
#include "common/logging/log.h"
#include "common/profiler.h"
@@ -15,6 +16,7 @@
#include "graphics/host_gpu/renderer/image/imageInfo.h"
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/presentation/performanceOverlay.h"
#include "graphics/presentation/presenter.h"
#include "graphics/presentation/renderDoc.h"
#include "kernel/pthread.h"
@@ -909,6 +911,9 @@ void VideoOutDriver::Impl::PresentThread(std::stop_token token) {
const auto frame_end = Common::Timer::QueryPerformanceCounter();
total_wait += static_cast<int64_t>(period) - static_cast<int64_t>(frame_end - frame_begin);
if (total_wait < 0) {
Common::DebugCounters::Add(Common::DebugCounters::Counter::LateVblanks);
}
}
}
@@ -938,6 +943,7 @@ bool FlipQueue::Reserve(VideoOutConfig& cfg, int index, int64_t flip_arg, FlipRe
m_done_cond_var.SignalAll();
}
if (cfg.flip_status.flipPendingNum >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) {
Common::DebugCounters::Add(Common::DebugCounters::Counter::FlipsRejected);
return false;
}
auto& pending = source == FlipRequestSource::GpuEop ? m_requests : m_cpu_requests;
@@ -965,6 +971,7 @@ bool FlipQueue::Reserve(VideoOutConfig& cfg, int index, int64_t flip_arg, FlipRe
pending.push_back(r);
request_id = r.id;
Common::DebugCounters::Add(Common::DebugCounters::Counter::FlipsRequested);
cfg.flip_status.flipPendingNum++;
cfg.flip_status.submitProcessTimeCounter = r.submit_ptc;
@@ -1243,6 +1250,9 @@ bool FlipQueue::Flip(uint32_t micros) {
}
if (due) {
m_presenter.Present(std::span(layers.data(), count));
if (requests[0].cfg->bus == 0) {
Graphics::PerformanceOverlayRecordFlip(requests[0].cfg->width, requests[0].cfg->height);
}
}
m_mutex.Lock();
@@ -254,7 +254,8 @@ public:
}
const bool reserved = binding.key == SDLK_ESCAPE || binding.key == SDLK_F1 ||
binding.key == SDLK_F7 || binding.key == SDLK_F11;
binding.key == SDLK_F2 || binding.key == SDLK_F7 ||
binding.key == SDLK_F8 || binding.key == SDLK_F11;
if (binding.control == INVALID_CONTROL || reserved ||
(binding.key == SDLK_UNKNOWN && binding.mouse_button == 0)) {
EXIT("Invalid input mapping: %s\n", value.c_str());
@@ -1,5 +1,6 @@
#include "common/assert.h"
#include "common/common.h"
#include "common/debugCounters.h"
#include "common/emulatorConfig.h"
#include "common/logging/log.h"
#include "common/profiler.h"
@@ -10,6 +11,7 @@
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/presentation/performanceOverlay.h"
#include "graphics/presentation/presenter.h"
#include "graphics/presentation/systemOverlay.h"
#include "graphics/presentation/videoOut.h"
@@ -324,6 +326,7 @@ struct Presenter::Impl {
void RecoverSwapchain(Swapchain::Status status) {
LOGF("Recovering Vulkan swapchain%s\n",
status == Swapchain::Status::SurfaceLost ? " and surface" : "");
Common::DebugCounters::Add(Common::DebugCounters::Counter::SwapchainRecreations);
swapchain.Recreate(status == Swapchain::Status::SurfaceLost);
frames.SetFormat(swapchain.Format());
}
@@ -1039,8 +1042,8 @@ void Presenter::Impl::Present() {
// present_mutex owns the prepared frames and this scheduler. Only queue submission
// is shared with rendering, and it has its own queue_mutex.
auto& command = present_scheduler.BeginCommand();
const bool draw_system_overlay =
overlay_visual.active && swapchain.PrepareSystemOverlay();
const bool draw_system_overlay = (overlay_visual.active || PerformanceOverlayEnabled()) &&
swapchain.PrepareSystemOverlay();
swapchain.RecordPresentCommands(command, layers[0].frame, layers[1],
draw_system_overlay);
const auto tick = swapchain.Submit(present_scheduler);
@@ -1060,6 +1063,7 @@ void Presenter::Impl::Present() {
return;
}
LOGF("Vulkan presentation retry exhausted; dropping frame\n");
Common::DebugCounters::Add(Common::DebugCounters::Counter::DroppedPresents);
}
void Presenter::Discard(Frame& frame) {
@@ -266,7 +266,6 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
#endif
#if !defined(__APPLE__)
check_feature(device_features2.features.depthClamp, "depthClamp");
check_feature(fragment_barycentric.fragmentShaderBarycentric, "fragmentShaderBarycentric");
#endif
check_feature(features12.samplerMirrorClampToEdge, "samplerMirrorClampToEdge",
@@ -498,7 +497,21 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
image_atomic_int64.pNext = supported_features2.pNext;
supported_features2.pNext = &image_atomic_int64;
}
const bool barycentric_extension =
HasExtension(device_extensions, VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME);
vk::PhysicalDeviceFragmentShaderBarycentricFeaturesKHR supported_barycentric {};
if (barycentric_extension) {
supported_barycentric.pNext = supported_features2.pNext;
supported_features2.pNext = &supported_barycentric;
}
physical_device.getFeatures2(&supported_features2);
const bool barycentric_supported =
barycentric_extension && supported_barycentric.fragmentShaderBarycentric == VK_TRUE;
if (!barycentric_supported) {
Log::WriteToConsoleAndLog(
"WARNING: Your GPU is not fully supported (fragmentShaderBarycentric is unavailable). "
"Some games may render incorrectly or crash.\n");
}
graphics.shader_image_int64_atomics_enabled = image_atomic_int64.shaderImageInt64Atomics;
auto features12 = WindowContext::RequiredVulkan12Features();
@@ -610,9 +623,13 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
robustness2.pNext = &features12;
#else
vk::PhysicalDeviceFragmentShaderBarycentricFeaturesKHR fragment_barycentric {};
fragment_barycentric.pNext = &features12;
fragment_barycentric.fragmentShaderBarycentric = VK_TRUE;
robustness2.pNext = &fragment_barycentric;
if (barycentric_supported) {
fragment_barycentric.pNext = &features12;
fragment_barycentric.fragmentShaderBarycentric = VK_TRUE;
robustness2.pNext = &fragment_barycentric;
} else {
robustness2.pNext = &features12;
}
#endif
robustness2.robustBufferAccess2 = supported_robustness2.robustBufferAccess2;
robustness2.robustImageAccess2 = supported_robustness2.robustImageAccess2;
@@ -966,7 +983,6 @@ void WindowContext::CreateVulkan() {
#else
device_extensions.push_back(VK_EXT_DEPTH_CLIP_ENABLE_EXTENSION_NAME);
device_extensions.push_back(VK_EXT_COLOR_WRITE_ENABLE_EXTENSION_NAME);
device_extensions.push_back(VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME);
#endif
#ifdef KYTY_ENABLE_DEBUG_PRINTF
@@ -1024,7 +1040,8 @@ void WindowContext::CreateVulkan() {
VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME,
VK_EXT_MESH_SHADER_EXTENSION_NAME,
VK_KHR_WORKGROUP_MEMORY_EXPLICIT_LAYOUT_EXTENSION_NAME,
VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME}) {
VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME,
VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME}) {
if (HasExtension(available_extensions, extension)) {
device_extensions.push_back(extension);
}
+34 -9
View File
@@ -1,21 +1,20 @@
#include "graphics/presentation/window.h"
#include <SDL3/SDL.h>
#include "common/assert.h"
#include "common/common.h"
#include "common/emulatorConfig.h"
#include "common/file.h"
#include "common/logging/log.h"
#include "common/profiler.h"
#include "common/stringUtils.h"
#include "common/systemInfo.h"
#include "common/threads.h"
#include "common/timer.h"
#include "common/stringUtils.h"
#include "graphics/host_gpu/graphicContext.h"
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/presentation/performanceOverlay.h"
#include "graphics/presentation/renderDoc.h"
#include "graphics/presentation/systemOverlay.h"
#include "graphics/presentation/window/hostInput.h"
@@ -24,13 +23,14 @@
#include "libs/controller.h"
#include "loader/systemContent.h"
#include <SDL3/SDL.h>
#include <cstdlib>
#include <filesystem>
#include <fmt/format.h>
#include <memory>
#include <string>
#include <vector>
#include <vulkan/vk_platform.h>
#include <filesystem>
#define STB_IMAGE_IMPLEMENTATION
#define STBI_NO_SIMD
@@ -459,12 +459,37 @@ void WindowContext::ProcessDisplayEvent(const SDL_DisplayEvent& display) {
void WindowContext::ProcessEvent(double time_s) {
auto& game = loop;
auto* event = &game.event;
if ((event->type == SDL_EVENT_KEY_DOWN || event->type == SDL_EVENT_KEY_UP) &&
event->key.key == SDLK_F7) {
if (event->type == SDL_EVENT_KEY_DOWN && event->key.repeat == 0) {
HostInputToggleMouseToJoystick();
if (event->type == SDL_EVENT_KEY_DOWN || event->type == SDL_EVENT_KEY_UP) {
const bool pressed = event->type == SDL_EVENT_KEY_DOWN && event->key.repeat == 0;
switch (event->key.key) {
case SDLK_F7:
if (pressed) {
HostInputToggleMouseToJoystick();
}
return;
case SDLK_F8:
if (pressed) {
Controller::CycleKeyboardPlayer();
}
return;
#if defined(__APPLE__)
case SDLK_P:
if ((event->key.mod & SDL_KMOD_GUI) == 0) {
break;
}
[[fallthrough]];
#endif
case SDLK_F2:
if (pressed) {
if ((event->key.mod & SDL_KMOD_SHIFT) != 0) {
TogglePerformanceOverlayDetails();
} else {
TogglePerformanceOverlay();
}
}
return;
default: break;
}
return;
}
if (ProcessSystemOverlayInput(*event)) {
return;
@@ -481,18 +481,14 @@ Decoder::Program DecodeFusedProgram(std::span<const uint32_t> front, std::span<c
return result;
}
void PrepareCallTarget(ShaderSource& source, const CompileOptions& options) {
void PrepareCallTargets(ShaderSource& source, const CompileOptions& options) {
using namespace IR;
EXIT_NOT_IMPLEMENTED(options.stage != ShaderType::Compute);
for (uint32_t i = 0; i < source.decoded.instructions.size(); ++i) {
if (source.decoded.instructions[i].opcode != Decoder::Opcode::S_SWAPPC_B64) continue;
EXIT_NOT_IMPLEMENTED(source.call.has_value() || options.stage != ShaderType::Compute);
source.call = ShaderSource::Call {.instruction = i};
source.calls.push_back({.instruction = i});
}
EXIT_IF(options.input_info.compute == nullptr);
const auto& call = source.decoded.instructions[source.call->instruction];
EXIT_NOT_IMPLEMENTED(call.src0.kind != Decoder::OperandKind::Sgpr ||
call.dst.kind != Decoder::OperandKind::Sgpr ||
call.src0.reg != call.dst.reg || call.dst.reg >= 105u);
const auto graph = CFG::BuildGraph(source.decoded);
Program query;
query.stage = options.stage;
@@ -524,7 +520,11 @@ void PrepareCallTarget(ShaderSource& source, const CompileOptions& options) {
return value;
};
Register = [&](uint32_t before, uint32_t code) -> Value {
const auto index = CFG::FindScalarDefinition(source.decoded, graph, before, code);
std::vector<uint32_t> crossed_calls;
const auto index = CFG::FindScalarDefinition(source.decoded, graph, before, code, crossed_calls);
for (const auto crossed: crossed_calls) {
std::ranges::find(source.calls, crossed, &ShaderSource::Call::instruction)->preserved_registers.set(code);
}
if (index == UINT32_MAX) {
EXIT_IF(code < query.user_data_base || code - query.user_data_base >= query.user_data_count);
return IREmitter(&block).GetUserData(static_cast<ScalarReg>(code));
@@ -557,18 +557,30 @@ void PrepareCallTarget(ShaderSource& source, const CompileOptions& options) {
EXIT_IF(!producer.ready || !producer.values.contains(code));
return producer.values.at(code).Resolve();
};
for (uint32_t i = 0; i < 2u; ++i) {
const auto value = Register(source.call->instruction, call.src0.reg + i);
EXIT_IF(!ValidateRuntimeValue(query, value, RuntimeValueType::Integer));
auto& root = query.value_storage.emplace_back(ValueOpcode::ReferenceU32);
root.SetArg(0, value);
source.call->target[i] = Value(&root);
for (auto& site: source.calls) {
const auto& call = source.decoded.instructions[site.instruction];
EXIT_NOT_IMPLEMENTED(call.src0.kind != Decoder::OperandKind::Sgpr ||
call.dst.kind != Decoder::OperandKind::Sgpr ||
call.src0.reg != call.dst.reg || call.dst.reg >= 105u);
for (uint32_t i = 0; i < 2u; ++i) {
const auto value = Register(site.instruction, call.src0.reg + i);
if (!ValidateRuntimeValue(query, value, RuntimeValueType::Integer)) {
EXIT_IF(value.Instruction()->GetOpcode() != ValueOpcode::ReadConstBuffer);
source.unsupported_call_pc = call.pc;
source.calls.clear();
return;
}
auto& root = query.value_storage.emplace_back(ValueOpcode::ReferenceU32);
root.SetArg(0, value);
site.target[i] = Value(&root);
}
}
RewriteToSsa(query.blocks);
ConstantPropagationPass(query.blocks, query.wave_size);
RemoveIdentities(query);
EliminateDeadCode(query.blocks);
for (auto& value: source.call->target) value = value.Instruction()->Arg(0).Resolve();
for (auto& site: source.calls)
for (auto& value: site.target) value = value.Instruction()->Arg(0).Resolve();
query.value_storage.clear();
for (auto& inst: block) {
inst.SetParent(nullptr);
@@ -595,7 +607,7 @@ ShaderSource PrepareShaderSource(std::span<const uint32_t> code, const CompileOp
Decoder::DecodeProgram(code, source.decoded);
}
if (source.decoded.has_swap_pc) {
PrepareCallTarget(source, options);
PrepareCallTargets(source, options);
source.code.assign(code.begin(), code.end());
source.decoded.code = source.code;
}
@@ -603,88 +615,122 @@ ShaderSource PrepareShaderSource(std::span<const uint32_t> code, const CompileOp
}
const Decoder::Program& RefreshShaderSource(ShaderSource& source, const IR::SrtRuntime& runtime) {
EXIT_IF(source.unsupported_call_pc != UINT32_MAX);
auto& reads = source.reads;
reads.clear();
IR::SrtReadCapture capture(runtime, reads);
const auto clean = IR::CleanRuntime(capture.ObservedRuntime());
IR::SrtWalker walker(source.call_targets, clean);
uint32_t low = 0, high = 0;
const auto& call = source.decoded.instructions[source.call->instruction];
if (!walker.Evaluate(source.call->target[0], low) || !walker.Evaluate(source.call->target[1], high))
EXIT("shader call at pc 0x%08x has an unavailable scalar target", call.pc);
// SWAPPC reads the old aliased pair and ignores the target's low two bits.
const auto address = ((uint64_t {high} << 32u) | low) & ~uint64_t {3};
EXIT_IF(address == 0u);
for (auto& site: source.calls) {
uint32_t low = 0, high = 0;
if (!walker.Evaluate(site.target[0], low) || !walker.Evaluate(site.target[1], high))
EXIT("shader call at pc 0x%08x has an unavailable scalar target",
source.decoded.instructions[site.instruction].pc);
// SWAPPC reads the old aliased pair and ignores the target's low two bits.
const auto address = ((uint64_t {high} << 32u) | low) & ~uint64_t {3};
EXIT_IF(address == 0u);
site.address = address;
}
const auto query_reads = reads.size();
if (source.linked) {
auto& linked = *source.linked;
const auto function = std::span(linked.code).subspan(source.code.size());
if (clean.read_memory(clean.userdata, address, linked.observed_function) &&
std::ranges::equal(function, linked.observed_function)) {
return linked.decoded;
bool unchanged = true;
for (size_t i = 0; i < source.calls.size(); ++i) {
const auto& function = linked.functions[i];
const auto expected = std::span(linked.code).subspan(function.offset, function.size);
const auto preceding = std::span(source.calls).first(i);
const auto previous = std::ranges::find(preceding, source.calls[i].address, &ShaderSource::Call::address);
if (previous != preceding.end()) {
const auto& observed = linked.functions[previous - preceding.begin()];
if (std::ranges::equal(expected, std::span(linked.code).subspan(observed.offset, observed.size))) continue;
unchanged = false;
break;
}
linked.observed_function.resize(function.size);
if (!clean.read_memory(clean.userdata, source.calls[i].address, linked.observed_function) ||
!std::ranges::equal(expected, linked.observed_function)) {
unchanged = false;
break;
}
}
if (unchanged) return linked.decoded;
reads.resize(query_reads);
}
ShaderSource::Linked linked {.code = source.code,
.decoded = source.decoded};
ShaderSource::Linked linked {.code = source.code, .decoded = source.decoded};
const auto& last = source.decoded.instructions.back();
EXIT_NOT_IMPLEMENTED(last.opcode != Decoder::Opcode::S_ENDPGM);
// Keep the caller's data/footer bytes at their native offsets. The leaf's virtual CFG
// PCs follow decoded instructions; its physical code is independently read and owned.
const auto function_pc = last.pc + last.word_count * sizeof(uint32_t);
for (uint32_t index = 0;;) {
// The observed queue helper is a scalar leaf. Scalar encodings need at most one literal.
EXIT_IF(index >= 64u || address > UINT64_MAX - (uint64_t {index} + 2u) * sizeof(uint32_t));
std::array<uint32_t, 2> words {};
if (!clean.read_memory(clean.userdata, address + uint64_t {index} * sizeof(uint32_t),
std::span(words).first(1)))
EXIT("shader call at pc 0x%08x cannot read leaf code", call.pc);
const auto family = Decoder::GetInstructionFamily(words[0]);
EXIT_NOT_IMPLEMENTED(family != Decoder::Family::SOP1 && family != Decoder::Family::SOP2 &&
family != Decoder::Family::SOPC && family != Decoder::Family::SOPK &&
family != Decoder::Family::SOPP);
Decoder::Instruction inst;
Decoder::DecodeInstruction(words, 0u, inst);
if (inst.word_count == 2u) {
if (!clean.read_memory(clean.userdata, address + uint64_t {index + 1u} * sizeof(uint32_t),
std::span(words).last(1)))
EXIT("shader call at pc 0x%08x cannot read leaf literal", call.pc);
inst = {};
Decoder::DecodeInstruction(words, 0u, inst);
// Keep the caller's data/footer at native offsets. Expand leaf CFGs per call site
// before SSA, so return edges neither require a dispatcher nor merge resource roots.
uint32_t function_pc = last.pc + last.word_count * sizeof(uint32_t);
std::map<uint64_t, std::vector<Decoder::Instruction>> functions;
for (size_t i = 0; i < source.calls.size(); ++i) {
const auto& site = source.calls[i];
const auto& call = source.decoded.instructions[site.instruction];
const auto address = source.calls[i].address;
auto [entry, inserted] = functions.try_emplace(address);
auto& function = entry->second;
if (inserted) {
for (uint32_t index = 0;;) {
std::array<uint32_t, Decoder::MaxInstructionRawWords> words {};
EXIT_IF(index > UINT32_MAX / sizeof(uint32_t) - words.size() ||
address > UINT64_MAX - (uint64_t {index} + words.size()) * sizeof(uint32_t));
uint32_t count = 1;
Decoder::Instruction inst;
for (;;) {
if (!clean.read_memory(clean.userdata, address + uint64_t {index} * sizeof(uint32_t),
std::span(words).first(count)))
EXIT("shader call at pc 0x%08x cannot read leaf code", call.pc);
inst = {};
Decoder::DecodeInstruction(words, 0u, inst);
if (inst.word_count <= count) break;
count = inst.word_count;
}
inst.pc = index * sizeof(uint32_t);
if (Decoder::IsDirectBranch(inst.opcode)) inst.branch_target += inst.pc;
index += inst.word_count;
EXIT_NOT_IMPLEMENTED(inst.opcode == Decoder::Opcode::S_SWAPPC_B64 ||
inst.opcode == Decoder::Opcode::S_GETPC_B64 ||
inst.opcode == Decoder::Opcode::S_ENDPGM);
function.push_back(inst);
if (inst.opcode == Decoder::Opcode::S_SETPC_B64) break;
}
}
inst.pc = index * sizeof(uint32_t);
if (Decoder::IsDirectBranch(inst.opcode)) inst.branch_target += inst.pc;
linked.code.insert(linked.code.end(), inst.raw, inst.raw + inst.word_count);
index += inst.word_count;
EXIT_NOT_IMPLEMENTED(inst.opcode == Decoder::Opcode::S_SWAPPC_B64 ||
inst.opcode == Decoder::Opcode::S_GETPC_B64 ||
inst.opcode == Decoder::Opcode::S_ENDPGM);
EXIT_IF(CFG::MayWriteScalarRegister(inst, call.dst.reg) || CFG::MayWriteScalarRegister(inst, call.dst.reg + 1u));
const bool returns = inst.opcode == Decoder::Opcode::S_SETPC_B64;
if (returns) {
EXIT_NOT_IMPLEMENTED(inst.src0.kind != Decoder::OperandKind::Sgpr || inst.src0.reg != call.dst.reg);
inst.opcode = Decoder::Opcode::S_BRANCH;
inst.branch_target = call.pc + call.word_count * sizeof(uint32_t);
} else if (Decoder::IsDirectBranch(inst.opcode)) {
EXIT_IF(inst.branch_target > UINT32_MAX - function_pc);
inst.branch_target += function_pc;
const uint32_t function_size = function.back().pc + function.back().word_count * sizeof(uint32_t);
EXIT_IF(function_pc > UINT32_MAX - function_size);
const auto function_offset = static_cast<uint32_t>(linked.code.size());
for (auto inst: function) {
EXIT_IF(CFG::MayWriteScalarRegister(inst, call.dst.reg) ||
CFG::MayWriteScalarRegister(inst, call.dst.reg + 1u));
for (uint32_t code = 0; code < site.preserved_registers.size(); ++code) {
if (site.preserved_registers[code] && CFG::MayWriteScalarRegister(inst, code))
EXIT("shader call at pc 0x%08x clobbers scalar target dependency s%u", call.pc, code);
}
linked.code.insert(linked.code.end(), inst.raw, inst.raw + inst.word_count);
if (inst.opcode == Decoder::Opcode::S_SETPC_B64) {
EXIT_NOT_IMPLEMENTED(inst.src0.kind != Decoder::OperandKind::Sgpr || inst.src0.reg != call.dst.reg);
inst.opcode = Decoder::Opcode::S_BRANCH;
inst.branch_target = call.pc + call.word_count * sizeof(uint32_t);
} else if (Decoder::IsDirectBranch(inst.opcode)) {
EXIT_IF(inst.branch_target >= function_size);
inst.branch_target += function_pc;
}
inst.pc += function_pc;
linked.decoded.instructions.push_back(std::move(inst));
}
inst.pc += function_pc;
linked.decoded.instructions.push_back(std::move(inst));
if (returns) break;
}
const auto function_end = linked.decoded.instructions.back().pc +
linked.decoded.instructions.back().word_count * sizeof(uint32_t);
for (size_t i = source.decoded.instructions.size(); i + 1u < linked.decoded.instructions.size(); ++i) {
const auto& inst = linked.decoded.instructions[i];
if (Decoder::IsDirectBranch(inst.opcode))
EXIT_IF(inst.branch_target < function_pc || inst.branch_target >= function_end);
linked.functions.push_back({function_offset, function_size / 4u});
linked.decoded.instructions[site.instruction].branch_target = function_pc;
function_pc += function_size;
}
reads.resize(query_reads);
reads.emplace_back(address, (linked.code.size() - source.code.size()) * sizeof(uint32_t));
linked.decoded.instructions[source.call->instruction].branch_target = function_pc;
for (size_t i = 0; i < source.calls.size(); ++i) {
const auto preceding = std::span(source.calls).first(i);
if (std::ranges::find(preceding, source.calls[i].address, &ShaderSource::Call::address) == preceding.end())
reads.emplace_back(source.calls[i].address, linked.functions[i].size * sizeof(uint32_t));
}
linked.decoded.code = linked.code;
linked.observed_function.resize(linked.code.size() - source.code.size());
linked.observed_function.resize(std::ranges::max_element(linked.functions, {},
&ShaderSource::Linked::Function::size)->size);
source.linked = std::move(linked);
++source.revision;
return source.linked->decoded;
@@ -8,6 +8,7 @@
#include "graphics/shader/shader.h"
#include <array>
#include <bitset>
#include <optional>
#include <span>
#include <vector>
@@ -47,16 +48,21 @@ struct ShaderSource {
struct Call {
uint32_t instruction;
std::array<IR::Value, 2> target;
std::bitset<128> preserved_registers;
uint64_t address = 0;
};
struct Linked {
struct Function { uint32_t offset; uint32_t size; };
std::vector<uint32_t> code;
Decoder::Program decoded;
std::vector<Function> functions;
std::vector<uint32_t> observed_function;
};
std::vector<uint32_t> code;
Decoder::Program decoded;
IR::ResourcePlan call_targets;
std::optional<Call> call;
std::vector<Call> calls;
uint32_t unsupported_call_pc = UINT32_MAX;
std::optional<Linked> linked;
uint64_t revision = 0;
std::vector<std::pair<uint64_t, uint64_t>> reads;
@@ -81,7 +81,8 @@ void ValidateNativeProgram(const IR::Program& program, bool lds_storage) {
}
uses_gds |= memory.kind == IR::ResourceKind::Gds;
uses_lds |= memory.kind == IR::ResourceKind::Lds;
} else if (memory.kind == IR::ResourceKind::Buffer) {
} else if (memory.kind == IR::ResourceKind::Buffer ||
memory.kind == IR::ResourceKind::ScalarBuffer) {
if (memory.resource >= program.info.buffers.size()) {
Fail(program, "typed shader contains an invalid buffer resource");
}
@@ -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();
@@ -122,19 +160,6 @@ uint32_t EmitFPMad32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
return sum;
}
uint32_t EmitFPMedTri32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
const auto min_ab = EmitMinMaxF32Value(state, a, b, false);
const auto min3 = EmitMinMaxF32Value(state, min_ab, c, false);
const auto max_ab = EmitMinMaxF32Value(state, a, b, true);
const auto high_min = EmitMinMaxF32Value(state, max_ab, c, false);
const auto median = EmitMinMaxF32Value(state, min_ab, high_min, true);
const auto nan_ab = Binary(state, spv::OpLogicalOr, TypeBool(state),
EmitClassifyF32(state, a).nan, EmitClassifyF32(state, b).nan);
const auto any_nan =
Binary(state, spv::OpLogicalOr, TypeBool(state), nan_ab, EmitClassifyF32(state, c).nan);
return Select(state, TypeF32(state), any_nan, min3, median);
}
uint32_t EmitFindUMsb64(EmitterState& state, uint32_t value) {
// GLSL FindUMsb is restricted to 32-bit components.
const auto unpacked = Unary(state, spv::OpBitcast, TypeU32Vector(state, 2), value);
@@ -210,28 +235,18 @@ uint32_t EmitCompositeExtractU32x2(EmitterState& state, uint32_t arg0, IR::Value
}
uint32_t EmitPackFloat2x16Rtz(EmitterState& state, uint32_t arg0, uint32_t arg1) {
const auto pair = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 2), pair, arg0, arg1);
const auto packed = EmitPackHalf2x16(state, pair);
const auto back = EmitExt(state, TypeF32Vector(state, 2), GLSLstd450UnpackHalf2x16, {packed});
auto result = packed;
for (uint32_t i = 0; i < 2; i++) {
const auto rounded = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeExtract, TypeF32(state), rounded, back, i);
const auto above =
Binary(state, spv::OpFOrdGreaterThan, TypeBool(state),
EmitExt(state, TypeF32(state), GLSLstd450FAbs, {rounded}),
EmitExt(state, TypeF32(state), GLSLstd450FAbs, {i == 0 ? arg0 : arg1}));
result = Binary(state, spv::OpISub, TypeU32(state), result,
Select(state, TypeU32(state), above,
ConstantU32(state, i == 0 ? 1u : 0x10000u), ConstantU32(state, 0)));
}
return result;
const auto low = EmitF32ToF16RtzBits(state, arg0);
const auto high = Binary(state, spv::OpShiftLeftLogical, TypeU32(state),
EmitF32ToF16RtzBits(state, arg1), ConstantU32(state, 16));
return EmitOrU32(state, low, high);
}
uint32_t EmitFPSaturate32(EmitterState& state, uint32_t arg0) {
return EmitExt(state, TypeF32(state), GLSLstd450FClamp,
{arg0, ConstantF32(state, 0), ConstantF32(state, 0x3f800000u)});
// The clamp modifier runs in DX10 clamp mode: NaN becomes 0. FClamp leaves NaN undefined.
const auto clamped = EmitExt(state, TypeF32(state), GLSLstd450FClamp,
{arg0, ConstantF32(state, 0), ConstantF32(state, 0x3f800000u)});
return Select(state, TypeF32(state), EmitClassifyF32(state, arg0).nan, ConstantF32(state, 0),
clamped);
}
uint32_t EmitSMulHi(EmitterState& state, uint32_t arg0, uint32_t arg1) {
@@ -100,10 +100,9 @@ uint32_t EmitClassMaskF16(EmitterState& state, uint32_t bits, uint32_t mask) {
return EmitClassMaskBits(state, bits, mask, true);
}
uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value) {
const auto lhs_class = EmitClassifyF32(state, lhs);
const auto rhs_class = EmitClassifyF32(state, rhs);
static uint32_t EmitMinMaxF32(EmitterState& state, uint32_t lhs, uint32_t rhs,
const F32Class& lhs_class, const F32Class& rhs_class,
bool max_value) {
const auto numeric_cond = state.builder.AllocateId();
state.builder.AddFunction(max_value ? spv::OpFOrdGreaterThanEqual : spv::OpFOrdLessThan,
TypeBool(state), numeric_cond, lhs, rhs);
@@ -121,6 +120,34 @@ uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, boo
return EmitBitcastU32ToF32(state, result_bits);
}
uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value) {
const auto lhs_class = EmitClassifyF32(state, lhs);
const auto rhs_class = EmitClassifyF32(state, rhs);
return EmitMinMaxF32(state, lhs, rhs, lhs_class, rhs_class, max_value);
}
uint32_t EmitFPMedTri32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
const auto a_class = EmitClassifyF32(state, a);
const auto b_class = EmitClassifyF32(state, b);
const auto c_class = EmitClassifyF32(state, c);
const auto min_ab = EmitMinMaxF32(state, a, b, a_class, b_class, false);
const auto min_ab_class = EmitClassifyF32(state, min_ab);
const auto min3 = EmitMinMaxF32(state, min_ab, c, min_ab_class, c_class, false);
const auto max_ab = EmitMinMaxF32(state, a, b, a_class, b_class, true);
const auto max_ab_class = EmitClassifyF32(state, max_ab);
const auto max3 = EmitMinMaxF32(state, max_ab, c, max_ab_class, c_class, true);
const auto max_bc = EmitMinMaxF32(state, b, c, b_class, c_class, true);
const auto max_ac = EmitMinMaxF32(state, a, c, a_class, c_class, true);
// RDNA2 compares numerically against MAX3, including ties between signed zeros.
const auto a_is_max = Binary(state, spv::OpFOrdEqual, TypeBool(state), max3, a);
const auto b_is_max = Binary(state, spv::OpFOrdEqual, TypeBool(state), max3, b);
const auto median = Select(state, TypeF32(state), a_is_max, max_bc,
Select(state, TypeF32(state), b_is_max, max_ac, max_ab));
const auto nan_ab = EmitLogicalOrBool(state, a_class.nan, b_class.nan);
const auto any_nan = EmitLogicalOrBool(state, nan_ab, c_class.nan);
return Select(state, TypeF32(state), any_nan, min3, median);
}
uint32_t EmitFlushF32DenormToSignedZero(EmitterState& state, uint32_t value) {
const auto bits = state.builder.AllocateId();
const auto abs_bits = state.builder.AllocateId();
@@ -655,6 +655,14 @@ void EmitStoreCompletion(EmitterState& state) {
spv::MemorySemanticsImageMemoryMask));
}
void EmitDataCacheInvalidate(EmitterState& state) {
// RDNA2 GL0/GL1 invalidation makes vector memory visible without a workgroup rendezvous.
state.builder.AddFunction(
spv::OpMemoryBarrier, ConstantU32(state, spv::ScopeDevice),
ConstantU32(state, spv::MemorySemanticsAcquireMask | spv::MemorySemanticsUniformMemoryMask |
spv::MemorySemanticsImageMemoryMask));
}
void EmitBarrier(EmitterState& state) {
const auto tessellation = state.program.stage == ShaderType::TessellationControl;
if (!tessellation && ShaderWorkgroupInput(state.program.stage, state.input_info) == nullptr) {
@@ -97,10 +97,14 @@ uint32_t GatherMip(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Memory
ConstantF32Value(state, 1.0f / 256.0f));
};
const auto sampler_control = ctx.Arg(inst, 6);
const auto mip_filter = Extract(sampler_control, 26, 2);
const auto preclamp = Binary(state, spv::OpINotEqual, TypeBool(state),
Extract(sampler_control, 28, 1), ConstantU32(state, 0));
const auto half = ConstantF32Value(state, 0.5f);
const auto zero = ZeroF32(state);
// Point selects the nearest mip; linear gathers return the lower, primary mip only.
const auto rounding = Select(state, TypeF32(state),
Binary(state, spv::OpIEqual, TypeBool(state), mip_filter, ConstantU32(state, 1)),
ConstantF32Value(state, 0.5f), zero);
const auto base = Unary(state, spv::OpConvertUToF, TypeF32(state),
Extract(ctx.Arg(inst, 4), 12, 4));
const auto minimum = Binary(state, spv::OpFSub, TypeF32(state),
@@ -108,19 +112,19 @@ uint32_t GatherMip(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Memory
const auto last = ConstantF32Value(state, static_cast<float>(mip_count - 1u));
auto lod = Binary(state, spv::OpFAdd, TypeF32(state),
AddressF32(ctx, mem, address, layout.lod),
Select(state, TypeF32(state), preclamp, half, zero));
Select(state, TypeF32(state), preclamp, rounding, zero));
lod = EmitGlsl<GLSLstd450FClamp, IR::Type::F32>(
state, lod, LodFixed(ctx.Arg(inst, 5), 0), LodFixed(ctx.Arg(inst, 5), 12));
// T# minimum LOD is in physical mip space and applies after the S# view-relative clamp.
lod = EmitGlsl<GLSLstd450FMax, IR::Type::F32>(state, lod, minimum);
lod = EmitGlsl<GLSLstd450FClamp, IR::Type::F32>(state, lod, zero, last);
lod = Binary(state, spv::OpFAdd, TypeF32(state), lod,
Select(state, TypeF32(state), preclamp, zero, half));
Select(state, TypeF32(state), preclamp, zero, rounding));
// The clamped value is nonnegative, so conversion performs the integer mip rounding.
const auto mip = EmitUMin32(state, Unary(state, spv::OpConvertFToU, TypeU32(state), lod),
ConstantU32(state, mip_count - 1u));
const auto mip_none = Binary(state, spv::OpIEqual, TypeBool(state),
Extract(sampler_control, 26, 2), ConstantU32(state, 0));
mip_filter, ConstantU32(state, 0));
return Select(state, TypeU32(state), mip_none, ConstantU32(state, 0), mip);
}
@@ -284,10 +288,10 @@ uint32_t ResultVector(ValueEmitContext& ctx, uint32_t value,
}
uint32_t QueryDimensions(ValueEmitContext& ctx, const IR::MemoryInfo& mem,
const IR::Inst& address) {
const auto dimension = ctx.state.program.info.images.at(mem.resource).dimension;
const IR::Inst& address, uint32_t resource, uint32_t array_index) {
const auto dimension = ctx.state.program.info.images.at(resource).dimension;
const auto& info = ImageDimensionInfoFor(dimension);
const auto image = LoadImageDescriptor(ctx.state, mem.resource);
const auto image = LoadImageDescriptor(ctx.state, resource, 0u, array_index);
const auto size = ctx.state.builder.AllocateId();
if (info.multisampled != 0u) {
ctx.state.builder.AddFunction(spv::OpImageQuerySize,
@@ -689,68 +693,58 @@ auto EmitImageAccess(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t resul
}
const auto selected = EmitIndirectResourceIndex(
state, key, image.indirect_mapping_offset, image.indirect_search_iterations, 0u);
struct ImageRun {
uint32_t first;
uint32_t count;
uint32_t resource;
uint32_t slot_bias;
const auto SameType = [&](uint32_t lhs, uint32_t rhs) {
const auto& first = state.program.info.images[lhs];
const auto& second = state.program.info.images[rhs];
return first.dimension == second.dimension && first.cube == second.cube &&
IR::DescriptorBindingForImage(first) == IR::DescriptorBindingForImage(second) &&
(inst.GetOpcode() != IR::ValueOpcode::ImageWrite ||
first.shader_swizzle == second.shader_swizzle) &&
first.mip_count == 1u && second.mip_count == 1u;
};
std::vector<ImageRun> runs;
for (uint32_t ordinal = 0; ordinal < image.indirect_resources.size(); ++ordinal) {
const auto resource = image.indirect_resources[ordinal];
const auto& candidate = state.program.info.images[resource];
const auto kind = *IR::DescriptorBindingForImage(candidate);
if (!runs.empty()) {
auto& run = runs.back();
const auto& first = state.program.info.images[run.resource];
const auto next_slot = run.slot_bias + ordinal;
if (candidate.dimension == first.dimension && candidate.cube == first.cube &&
IR::DescriptorBindingForImage(first) == kind &&
(inst.GetOpcode() != IR::ValueOpcode::ImageWrite ||
candidate.shader_swizzle == first.shader_swizzle) &&
candidate.mip_count == 1u && first.mip_count == 1u &&
(ordinal == 1u || candidate.descriptor_index == next_slot)) {
// Native roots precede appended children; only the root may have a slot gap.
if (ordinal == 1u)
run.slot_bias = candidate.descriptor_index - ordinal;
++run.count;
continue;
}
}
runs.push_back({ordinal, 1u, resource,
candidate.descriptor_index - ordinal});
// Descriptor order is unrelated to image type. Share each typed access and
// select its descriptor slot independently, including gaps between native roots.
struct ImageGroup { uint32_t resource; uint32_t count; };
std::vector<ImageGroup> groups;
std::vector<uint32_t> ordinal_groups;
for (const auto resource: image.indirect_resources) {
const auto found = std::ranges::find_if(groups, [&](const ImageGroup& group) {
return SameType(resource, group.resource);
});
ordinal_groups.push_back(static_cast<uint32_t>(found - groups.begin()));
if (found == groups.end()) groups.push_back({resource, 1u});
else ++found->count;
}
const auto EmitRun = [&](uint32_t index) {
const auto& run = runs[index];
if (run.count == 1u) return emit(run.resource, 0u);
auto slot = Binary(state, spv::OpIAdd, TypeU32(state), selected,
ConstantU32(state, run.slot_bias));
if (run.first == 0u) {
const auto root_slot = image.descriptor_index;
if (root_slot != run.slot_bias) {
const auto is_root = Binary(state, spv::OpIEqual, TypeBool(state), selected,
ConstantU32(state, 0u));
const auto root_or_child = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelect, TypeU32(state), root_or_child,
is_root, ConstantU32(state, root_slot), slot);
slot = root_or_child;
}
}
return emit(run.resource, slot);
};
auto run_index = ConstantU32(state, 0u);
for (uint32_t index = 1; index < runs.size(); ++index) {
const bool dynamic_slot = std::ranges::any_of(groups, [](const ImageGroup& group) {
return group.count > 1u;
});
auto group_index = ConstantU32(state, 0u);
auto slot_bias = ConstantU32(state, image.descriptor_index);
auto previous_bias = image.descriptor_index;
for (uint32_t ordinal = 1; ordinal < image.indirect_resources.size(); ++ordinal) {
const auto& candidate = state.program.info.images[image.indirect_resources[ordinal]];
const auto bias = candidate.descriptor_index - ordinal;
const bool select_group = ordinal_groups[ordinal] != ordinal_groups[ordinal - 1u];
const bool select_slot = dynamic_slot && bias != previous_bias;
previous_bias = bias;
if (!select_group && !select_slot) continue;
const auto at_or_after = Binary(state, spv::OpUGreaterThanEqual, TypeBool(state),
selected, ConstantU32(state, runs[index].first));
const auto next = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelect, TypeU32(state), next, at_or_after,
ConstantU32(state, index), run_index);
run_index = next;
selected, ConstantU32(state, ordinal));
if (select_group) group_index = Select(state, TypeU32(state), at_or_after,
ConstantU32(state, ordinal_groups[ordinal]), group_index);
if (select_slot) slot_bias = Select(state, TypeU32(state), at_or_after,
ConstantU32(state, bias), slot_bias);
}
return runs.size() == 1u
? EmitRun(0u)
: EmitIndexSwitch(state, run_index, static_cast<uint32_t>(runs.size()),
result_type, EmitRun);
const auto slot = dynamic_slot
? Binary(state, spv::OpIAdd, TypeU32(state), selected, slot_bias) : 0u;
const auto EmitGroup = [&](uint32_t index) {
const auto& group = groups[index];
return emit(group.resource, group.count == 1u ? 0u : slot);
};
return groups.size() == 1u
? EmitGroup(0u)
: EmitIndexSwitch(state, group_index, static_cast<uint32_t>(groups.size()),
result_type, EmitGroup);
}
template <typename Emit>
@@ -852,7 +846,10 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto* address = ctx.ImageAddress(inst.Arg(image_info.needs_sampler ? 2 : 1));
if (op == IR::ValueOpcode::ImageQueryDimensions) {
state.builder.RequireCapability(spv::CapabilityImageQuery);
ctx.Define(inst, QueryDimensions(ctx, mem, *address));
ctx.Define(inst, EmitImageAccess(ctx, inst, TypeU32Vector(state, 4),
[&](uint32_t resource, uint32_t array_index) {
return QueryDimensions(ctx, mem, *address, resource, array_index);
}));
return;
}
if (image_info.needs_sampler &&
@@ -218,6 +218,7 @@ inline constexpr auto EmitTtraceData = EmitVoid;
inline constexpr auto EmitInstPrefetch = EmitVoid;
void EmitBarrier(EmitterState& state);
void EmitStoreCompletion(EmitterState& state);
void EmitDataCacheInvalidate(EmitterState& state);
void EmitMeshAllocate(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitMeshDrawParameter(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitGetTessellationAttribute(ValueEmitContext& ctx, const IR::Inst& inst);
@@ -296,6 +297,7 @@ inline constexpr auto EmitBufferAtomicSMax32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicUMax32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicAnd32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicAnd64 = EmitBufferAtomic64;
inline constexpr auto EmitBufferAtomicIAdd64 = EmitBufferAtomic64;
inline constexpr auto EmitBufferAtomicOr32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicOr64 = EmitBufferAtomic64;
inline constexpr auto EmitBufferAtomicXor32 = EmitAtomic32;
@@ -325,6 +327,7 @@ inline constexpr auto EmitSharedAtomicSMin32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicUMin32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicSMax32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicUMax64 = EmitSharedAtomic64;
inline constexpr auto EmitSharedAtomicCmpSwap32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicCmpSwap64 = EmitSharedAtomic64;
inline constexpr auto EmitSharedAtomicUMax32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicAnd32 = EmitAtomic32;
@@ -420,6 +420,7 @@ void EnsureLdsStorage(EmitterState& state);
struct MemoryResourceAccess {
IR::ResourceKind kind = IR::ResourceKind::None;
uint32_t object_pointer = 0;
uint32_t descriptor_index = 0;
uint32_t length = 0;
uint32_t byte_offset = 0;
uint32_t element_bits = 32;
@@ -867,7 +867,7 @@ void StoreFormattedPrepared(ValueEmitContext& ctx, const IR::Inst& inst,
});
}
uint32_t LoadScalarBuffer(ValueEmitContext& ctx, const IR::Inst& inst) {
uint32_t LoadIndirectScalarBuffer(ValueEmitContext& ctx, const IR::Inst& inst) {
auto& state = ctx.state;
const auto& handle = *inst.Arg(0).ResolveInstruction();
const auto word1 = ctx.Arg(handle, 1);
@@ -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);
});
});
}
@@ -1545,8 +1597,29 @@ uint32_t EmitReadConst(ValueEmitContext& ctx, const IR::Inst& inst) {
}
void EmitReadConstBuffer(ValueEmitContext& ctx, const IR::Inst& inst) {
if (ctx.Memory(inst).planning_only) return;
ctx.Define(inst, LoadScalarBuffer(ctx, inst));
auto mem = ctx.Memory(inst);
if (mem.planning_only) return;
if (mem.kind == IR::ResourceKind::IndirectBuffer) {
ctx.Define(inst, LoadIndirectScalarBuffer(ctx, inst));
return;
}
auto& state = ctx.state;
mem.kind = IR::ResourceKind::ScalarBuffer;
auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), ctx.Arg(inst, 1),
ConstantU32(state, 2));
if (mem.offset >= 4u) {
index = Binary(state, spv::OpIAdd, TypeU32(state), index, ConstantU32(state, mem.offset >> 2u));
}
const auto access = PrepareMemoryResourceAccess(state, mem);
// Scalar buffer addressing aligns the base and each offset independently.
const auto element = Binary(
state, spv::OpIAdd, TypeU32(state), index,
Binary(state, spv::OpShiftRightLogical, TypeU32(state), access.byte_offset, ConstantU32(state, 2)));
const auto condition = EmitMemoryElementInBounds(state, access, element);
ctx.Define(inst, EmitValueOrZeroIfCondition(state, condition, [&]() {
return EmitNative<spv::OpLoad, IR::Type::U32>(
state, EmitMemoryElementPointer(state, access, element));
}));
}
uint32_t EmitGetBufferResourceIndex(ValueEmitContext& ctx, const IR::Inst& inst) {
@@ -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);
@@ -269,8 +270,10 @@ MemoryResourceAccess PrepareMemoryResourceAccess(EmitterState& state, const IR::
case IR::ResourceKind::Flat:
case IR::ResourceKind::Global:
EXIT("physical address memory must use the BDA emitter\n");
case IR::ResourceKind::ScalarBuffer:
case IR::ResourceKind::Buffer: {
const auto bits = IR::StorageBufferElementBits(state.program, mem);
const auto bits = mem.kind == IR::ResourceKind::Buffer
? IR::StorageBufferElementBits(state.program, mem) : 32u;
auto* buffer = &state.storage_buffers[2];
if (bits == 8u) {
buffer = &state.storage_buffers[0];
@@ -502,6 +505,7 @@ uint32_t EncodePackedColorComponent(EmitterState& state, const Format::BufferFor
spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
switch (opcode) {
case IR::ValueOpcode::SharedAtomicCmpSwap32:
case IR::ValueOpcode::SharedAtomicCmpSwap64:
case IR::ValueOpcode::ImageAtomicCompareSwap32:
case IR::ValueOpcode::BufferAtomicCmpSwap32: return spv::OpAtomicCompareExchange;
@@ -514,6 +518,7 @@ spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
case IR::ValueOpcode::ImageAtomicIAdd64:
case IR::ValueOpcode::BufferAtomicIAdd32:
case IR::ValueOpcode::SharedAtomicIAdd64:
case IR::ValueOpcode::BufferAtomicIAdd64:
case IR::ValueOpcode::SharedAtomicIAdd32: return spv::OpAtomicIAdd;
case IR::ValueOpcode::BufferAtomicISub32:
case IR::ValueOpcode::SharedAtomicISub32: return spv::OpAtomicISub;
@@ -558,8 +563,12 @@ uint32_t EmitAtomicOperation(ValueEmitContext& ctx, const IR::Inst& inst, uint32
const bool wide = inst.GetType() == IR::Type::U64;
const auto type = wide ? TypeU64(ctx.state) : TypeU32(ctx.state);
if (opcode == spv::OpAtomicCompareExchange) {
const auto desired = ctx.Arg(inst, inst.NumArgs() - 3);
const auto comparator = ctx.Arg(inst, inst.NumArgs() - 2);
auto desired = ctx.Arg(inst, inst.NumArgs() - 3);
auto comparator = ctx.Arg(inst, inst.NumArgs() - 2);
if (inst.GetOpcode() == IR::ValueOpcode::SharedAtomicCmpSwap32) {
// RDNA2 DS compares DATA and stores DATA2, opposite to BUFFER atomics.
std::swap(desired, comparator);
}
ctx.state.builder.AddFunction(
spv::OpAtomicCompareExchange, TypeU32(ctx.state), old, pointer,
ConstantU32(ctx.state, scope), ConstantU32(ctx.state, spv::MemorySemanticsMaskNone),
@@ -307,6 +307,7 @@ void EmitBlock(ValueEmitContext& ctx, const IR::Block* block, EmitInstruction&&
ctx.state.lane_half = half;
if (half == 0 || (inst.GetOpcode() != IR::ValueOpcode::Barrier &&
inst.GetOpcode() != IR::ValueOpcode::StoreCompletion &&
inst.GetOpcode() != IR::ValueOpcode::DataCacheInvalidate &&
inst.GetOpcode() != IR::ValueOpcode::MeshAllocate)) {
emit_instruction(lane, inst);
}
@@ -8,6 +8,8 @@
#include <deque>
#include <list>
#include <map>
#include <optional>
#include <set>
#include <span>
namespace Libs::Graphics::ShaderRecompiler::CFG {
@@ -1650,6 +1652,32 @@ Graph BuildGraph(const Decoder::Program& program) {
}
SortUnique(graph.code_table_load_pcs);
if (!indirect_setpc && std::ranges::any_of(program.instructions, [](const auto& inst) {
return inst.opcode == Opcode::S_SWAPPC_B64 && inst.branch_target != UINT32_MAX;
})) {
// Linked leaf bodies follow the caller in native PC order. Visit them in
// execution order so returning to the caller does not manufacture goto loops.
std::vector<bool> visited(graph.blocks.size(), false);
std::vector<std::pair<uint32_t, bool>> pending {{graph.entry_block, false}};
std::vector<BasicBlock> ordered;
ordered.reserve(graph.blocks.size());
while (!pending.empty()) {
const auto [id, finished] = pending.back();
pending.pop_back();
if (finished) {
ordered.push_back(std::move(graph.blocks[id]));
continue;
}
if (visited[id]) continue;
visited[id] = true;
pending.emplace_back(id, true);
for (const auto successor: graph.blocks[id].successors)
pending.emplace_back(successor, false);
}
std::ranges::reverse(ordered);
ApplyBlockOrder(graph, std::move(ordered));
}
RecomputeAnalyses(graph);
if (indirect_setpc) {
@@ -1681,33 +1709,56 @@ Graph Structurize(const Graph& graph) {
}
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code) {
uint32_t destination = UINT32_MAX;
// Scalar ALU pair widths are not all decoded. The typed producer must confirm this word.
return InstructionWritesScalarCode(inst, code) ||
(ScalarOperandCode(inst.dst, destination) && code == destination + 1u);
// Vector comparisons and carry results can write a scalar pair in wave64.
if (inst.family == Decoder::Family::VOP1 || inst.family == Decoder::Family::VOP2 ||
inst.family == Decoder::Family::VOPC || inst.family == Decoder::Family::VOP3 ||
inst.family == Decoder::Family::VOP3P) {
uint32_t destination = 0;
for (const auto* operand: {&inst.dst, &inst.dst2})
if (ScalarOperandCode(*operand, destination) && code >= destination && code <= destination + 1u)
return true;
}
return InstructionWritesScalarCode(inst, code);
}
uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& graph,
uint32_t before, uint32_t code) {
uint32_t before, uint32_t code, std::vector<uint32_t>& crossed_calls) {
EXIT_IF(before >= program.instructions.size());
const auto found = std::ranges::find_if(graph.blocks, [before](const auto& block) {
return block.inst_begin <= before && before < block.inst_end;
});
EXIT_IF(found == graph.blocks.end());
const auto* block = &*found;
for (size_t depth = 0; depth < graph.blocks.size(); ++depth) {
for (uint32_t i = before; i > block->inst_begin;) {
const auto& inst = program.instructions[--i];
EXIT_IF(inst.opcode == Opcode::S_SWAPPC_B64);
if (MayWriteScalarRegister(inst, code)) return i;
}
if (block->id == graph.entry_block) return UINT32_MAX;
EXIT_IF(block->predecessors.size() != 1u);
block = graph.FindBlock(block->predecessors[0]);
std::vector<std::pair<uint32_t, uint32_t>> pending {{found->id, before}};
std::set<std::pair<uint32_t, uint32_t>> visited;
std::optional<uint32_t> definition;
while (!pending.empty()) {
const auto point = pending.back();
pending.pop_back();
if (!visited.insert(point).second) continue;
const auto* block = graph.FindBlock(point.first);
EXIT_IF(block == nullptr);
before = block->inst_end;
uint32_t index = UINT32_MAX;
for (uint32_t i = point.second; i > block->inst_begin;) {
const auto& inst = program.instructions[--i];
if (inst.opcode == Opcode::S_SWAPPC_B64) {
EXIT_NOT_IMPLEMENTED(MayWriteScalarRegister(inst, code));
crossed_calls.push_back(i);
continue;
}
if (MayWriteScalarRegister(inst, code)) { index = i; break; }
}
if (index != UINT32_MAX || block->id == graph.entry_block) {
EXIT_IF(definition && *definition != index);
definition = index;
continue;
}
EXIT_IF(block->predecessors.empty());
for (const auto predecessor: block->predecessors) {
pending.emplace_back(predecessor, graph.blocks[predecessor].inst_end);
}
}
EXIT("scalar shader call source has cyclic reaching definitions");
EXIT_IF(!definition);
return *definition;
}
std::string BranchConditionToString(BranchCondition condition) {
@@ -115,7 +115,7 @@ struct Graph {
Graph BuildGraph(const Decoder::Program& program);
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code);
uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& graph,
uint32_t before, uint32_t code);
uint32_t before, uint32_t code, std::vector<uint32_t>& crossed_calls);
// Returns structured control flow or failure diagnostics without changing the native graph.
// On failure, failure_block is an original block ID or UINT32_MAX.
Graph Structurize(const Graph& graph);
@@ -67,8 +67,11 @@ constexpr MemoryOpcodeInfo MUBUF_OPCODE_LIST[] = {
{0x3fu, Opcode::BUFFER_ATOMIC_FMIN, 1, 32},
{0x40u, Opcode::BUFFER_ATOMIC_FMAX, 1, 32},
{0x50u, Opcode::BUFFER_ATOMIC_SWAP_X2, 2, 32},
{0x52u, Opcode::BUFFER_ATOMIC_ADD_X2, 2, 32},
{0x59u, Opcode::BUFFER_ATOMIC_AND_X2, 2, 32},
{0x5au, Opcode::BUFFER_ATOMIC_OR_X2, 2, 32},
{0x71u, Opcode::BUFFER_GL0_INV, 0, 0},
{0x72u, Opcode::BUFFER_GL1_INV, 0, 0},
{0x80u, Opcode::BUFFER_LOAD_FORMAT_D16_X, 1, 16, false, false, true},
{0x83u, Opcode::BUFFER_LOAD_FORMAT_D16_XYZW, 2, 16, false, false, true},
{0x84u, Opcode::BUFFER_STORE_FORMAT_D16_X, 1, 16, false, false, true},
@@ -114,6 +117,7 @@ constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = {
{0x27u, Opcode::DS_MIN_RTN_U32, 1, 32}, {0x28u, Opcode::DS_MAX_RTN_U32, 1, 32},
{0x29u, Opcode::DS_AND_RTN_B32, 1, 32}, {0x2au, Opcode::DS_OR_RTN_B32, 1, 32},
{0x2bu, Opcode::DS_XOR_RTN_B32, 1, 32}, {0x2du, Opcode::DS_WRXCHG_RTN_B32, 1, 32},
{0x30u, Opcode::DS_CMPST_RTN_B32, 1, 32},
{0x35u, Opcode::DS_SWIZZLE_B32, 1, 32}, {0x36u, Opcode::DS_READ_B32, 1, 32},
{0x37u, Opcode::DS_READ2_B32, 2, 32}, {0x38u, Opcode::DS_READ2ST64_B32, 2, 32},
{0x39u, Opcode::DS_READ_I8, 1, 8, true}, {0x3au, Opcode::DS_READ_U8, 1, 8},
@@ -200,6 +204,7 @@ bool IsDsAtomicOpcode(Opcode opcode) {
case Opcode::DS_MAX_U32:
case Opcode::DS_MAX_RTN_U32:
case Opcode::DS_MAX_RTN_U64:
case Opcode::DS_CMPST_RTN_B32:
case Opcode::DS_CMPST_RTN_B64:
case Opcode::DS_AND_B32:
case Opcode::DS_AND_RTN_B32:
@@ -220,6 +225,7 @@ uint32_t DsSourceCount(Opcode opcode) {
case Opcode::DS_WRITE2ST64_B32:
case Opcode::DS_WRITE2_B64:
case Opcode::DS_WRITE2ST64_B64:
case Opcode::DS_CMPST_RTN_B32:
case Opcode::DS_CMPST_RTN_B64:
case Opcode::DS_MSKOR_B32: return 3u;
case Opcode::DS_MIN_F32:
@@ -308,6 +314,9 @@ void DecodeMubuf(uint32_t pc, std::span<const uint32_t> code, uint32_t word_inde
if (inst.opcode == Opcode::UNSUPPORTED) {
SetUnsupported(inst, Family::MUBUF, opcode, "MUBUF opcode is not implemented");
}
if (inst.opcode == Opcode::BUFFER_GL0_INV || inst.opcode == Opcode::BUFFER_GL1_INV) {
return;
}
DecodeVectorGpr(vdata, inst.dst);
if (inst.opcode == Opcode::BUFFER_LOAD_FORMAT_D16_XYZW ||
@@ -454,6 +463,9 @@ void DecodeDs(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
if (inst.opcode == Opcode::DS_SWIZZLE_B32 && inst.offset >= 0xe000u) {
SetUnsupported(inst, Family::DS, opcode, "DS swizzle FFT mode is not implemented");
}
if (inst.gds && inst.opcode == Opcode::DS_CMPST_RTN_B32) {
SetUnsupported(inst, Family::DS, opcode, "DS compare-store return B32 GDS variant is not implemented");
}
if (inst.gds &&
(inst.opcode == Opcode::DS_SWIZZLE_B32 || inst.opcode == Opcode::DS_PERMUTE_B32 ||
inst.opcode == Opcode::DS_BPERMUTE_B32 ||
@@ -164,9 +164,23 @@ void DecodeSop1(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
switch (inst.opcode) {
case Opcode::S_SWAPPC_B64:
inst.branch_target = UINT32_MAX;
inst.data_dwords = 2;
[[fallthrough]];
case Opcode::S_MOV_B64:
case Opcode::S_CMOV_B64:
case Opcode::S_NOT_B64:
case Opcode::S_WQM_B64:
case Opcode::S_BREV_B64:
case Opcode::S_BITSET0_B64:
case Opcode::S_BITSET1_B64:
case Opcode::S_AND_SAVEEXEC_B64:
case Opcode::S_ORN2_SAVEEXEC_B64:
case Opcode::S_QUADMASK_B64:
case Opcode::S_ANDN1_SAVEEXEC_B64:
case Opcode::S_BITREPLICATE_B64_B32:
inst.data_dwords = 2u;
break;
case Opcode::S_GETPC_B64:
inst.data_dwords = 2u;
inst.src_count = 0;
DecodeScalarDestination(sdst, pc, inst.dst);
return;
@@ -206,6 +220,8 @@ void DecodeSop2(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
DecodeScalarDestination(sdst, pc, inst.dst);
DecodeBinarySources(pc, code, word_index, inst, ssrc0, ssrc1);
if (opcode == 0x0bu || (opcode >= 0x0fu && opcode <= 0x25u && (opcode & 1u)) || opcode == 0x29u)
inst.data_dwords = 2u;
}
void DecodeSopk(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
@@ -566,6 +566,9 @@ std::string InstructionToString(const Instruction& inst) {
OperandToString(inst.src0).c_str()));
case Opcode::S_BARRIER:
return WithUnsupportedReason(inst, fmt::format("0x{:08x}: s_barrier", inst.pc));
case Opcode::BUFFER_GL0_INV:
case Opcode::BUFFER_GL1_INV:
return fmt::format("0x{:08x}: {}", inst.pc, magic_enum::enum_name(inst.opcode));
case Opcode::V_NOP:
return WithUnsupportedReason(inst, fmt::format("0x{:08x}: v_nop", inst.pc));
case Opcode::S_ENDPGM:
@@ -670,6 +673,7 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::BUFFER_ATOMIC_CMPSWAP:
case Opcode::BUFFER_ATOMIC_SWAP_X2:
case Opcode::BUFFER_ATOMIC_ADD:
case Opcode::BUFFER_ATOMIC_ADD_X2:
case Opcode::BUFFER_ATOMIC_SUB:
case Opcode::BUFFER_ATOMIC_SMIN:
case Opcode::BUFFER_ATOMIC_UMIN:
@@ -717,6 +721,7 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::DS_MAX_U32:
case Opcode::DS_MAX_RTN_U32:
case Opcode::DS_MAX_RTN_U64:
case Opcode::DS_CMPST_RTN_B32:
case Opcode::DS_CMPST_RTN_B64:
case Opcode::DS_AND_B32:
case Opcode::DS_AND_RTN_B32:
@@ -586,6 +586,7 @@ enum class Opcode {
BUFFER_ATOMIC_CMPSWAP,
BUFFER_ATOMIC_SWAP_X2,
BUFFER_ATOMIC_ADD,
BUFFER_ATOMIC_ADD_X2,
BUFFER_ATOMIC_SUB,
BUFFER_ATOMIC_SMIN,
BUFFER_ATOMIC_UMIN,
@@ -598,6 +599,8 @@ enum class Opcode {
BUFFER_ATOMIC_XOR,
BUFFER_ATOMIC_FMIN,
BUFFER_ATOMIC_FMAX,
BUFFER_GL0_INV,
BUFFER_GL1_INV,
FLAT_LOAD_UBYTE,
FLAT_LOAD_SBYTE,
FLAT_LOAD_USHORT,
@@ -631,6 +634,7 @@ enum class Opcode {
DS_MAX_U32,
DS_MAX_RTN_U32,
DS_MAX_RTN_U64,
DS_CMPST_RTN_B32,
DS_CMPST_RTN_B64,
DS_AND_B32,
DS_AND_RTN_B32,
@@ -27,7 +27,7 @@ void Translator::PackedFloat16(const Decoder::Instruction& inst, IR::ValueOpcode
if (quiet_snan) {
const auto quiet_snan_lane = [&](const Decoder::Operand& operand, bool high) {
const auto bits = ReadU16LaneAsU32(operand, high, false);
const auto exponent = ir.BitwiseAnd(bits, IR::U32(IR::Value(0x7c00u)));
const auto exponent = ir.BitwiseAnd(bits, IR::U32(IR::Value(0x7e00u)));
const auto payload = ir.BitwiseAnd(bits, IR::U32(IR::Value(0x01ffu)));
const auto snan = ir.LogicalAnd(ir.IEqual(exponent, IR::U32(IR::Value(0x7c00u))),
ir.INotEqual(payload, IR::U32(IR::Value(0u))));
@@ -940,6 +940,10 @@ void Translator::DS_PERMUTE(const Decoder::Instruction& inst, bool backward) {
void Translator::EmitMemory(const Decoder::Instruction& inst) {
switch (inst.opcode) {
case Decoder::Opcode::BUFFER_GL0_INV:
case Decoder::Opcode::BUFFER_GL1_INV:
ir.Emit(IR::ValueOpcode::DataCacheInvalidate);
return;
case Decoder::Opcode::IMAGE_BVH_INTERSECT_RAY: {
const auto result = ir.Emit(IR::ValueOpcode::BvhIntersect,
{ConstructU32x4(inst.src1, 4), MakeImageAddress(inst, inst.src0), ir.GetExec()},
@@ -1046,6 +1050,8 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicFMin32);
case Decoder::Opcode::BUFFER_ATOMIC_FMAX:
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicFMax32);
case Decoder::Opcode::BUFFER_ATOMIC_ADD_X2:
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicIAdd64);
case Decoder::Opcode::DS_ADD_U32:
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicIAdd32, false);
@@ -1083,6 +1089,8 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicUMax32, true);
case Decoder::Opcode::DS_MAX_RTN_U64:
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicUMax64, true);
case Decoder::Opcode::DS_CMPST_RTN_B32:
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicCmpSwap32, true);
case Decoder::Opcode::DS_CMPST_RTN_B64:
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicCmpSwap64, true);
case Decoder::Opcode::DS_AND_B32:
@@ -233,13 +233,6 @@ IR::Value Translator::ReadOperand(const Decoder::Operand& operand, IR::Type type
if (type == IR::Type::F64) {
if (operand.kind == Decoder::OperandKind::LiteralConstant) {
pair = {IR::U32(IR::Value(0u)), IR::U32(IR::Value(operand.value))};
} else if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
const auto bits = operand.value == 0x3e22f983u
? 0x3fc45f306dc9c882ull
: std::bit_cast<uint64_t>(static_cast<double>(
std::bit_cast<float>(operand.value)));
pair = {IR::U32(IR::Value(static_cast<uint32_t>(bits))),
IR::U32(IR::Value(static_cast<uint32_t>(bits >> 32u)))};
}
if (operand.absolute) {
pair[1] = ir.BitwiseAnd(pair[1], IR::U32(IR::Value(0x7fffffffu)));
@@ -488,6 +481,14 @@ std::array<IR::U32, 2> Translator::ReadU32Pair(const Decoder::Operand& operand)
if (operand.kind == Decoder::OperandKind::VccLo) {
return {ir.GetVccLo(), ir.GetVccHi()};
}
if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
const auto bits =
operand.value == 0x3e22f983u
? 0x3fc45f306dc9c882ull
: std::bit_cast<uint64_t>(static_cast<double>(std::bit_cast<float>(operand.value)));
return {IR::U32(IR::Value(static_cast<uint32_t>(bits))),
IR::U32(IR::Value(static_cast<uint32_t>(bits >> 32u)))};
}
const auto low = ApplyBitSourceModifiers(operand, ReadRawU32(operand));
IR::U32 high(IR::Value(0u));
if (operand.kind == Decoder::OperandKind::Sgpr || operand.kind == Decoder::OperandKind::Vgpr) {
@@ -546,9 +547,11 @@ IR::U32 Translator::Read16LaneBits(const Decoder::Operand& operand, bool high_la
auto bits = ReadRawU32(operand);
if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
// Inline floats encode the operand's width before SDWA or OP_SEL selects bits.
const auto half = IR::F16(ir.Emit(IR::ValueOpcode::ConvertF16F32, {ir.BitCastF32(bits)}));
const auto half_bits = IR::U16(ir.Emit(IR::ValueOpcode::BitCastU16F16, {half}));
bits = IR::U32(ir.Emit(IR::ValueOpcode::ConvertU32U16, {half_bits}));
const auto value = operand.value;
bits = IR::U32(IR::Value(value == 0x3e22f983u
? 0x3118u
: ((value >> 16u) & 0x8000u) |
((((value >> 23u) & 0xffu) - 112u) << 10u)));
}
bits = ApplyBitSourceModifiers(raw_operand, bits);
uint32_t offset = (high_lane ? operand.op_sel_hi : operand.op_sel) ? 16u : 0u;
@@ -25,6 +25,7 @@ struct UniformFill {
std::array<uint32_t, 3> group_stride {};
uint32_t words = 0;
uint32_t value = 0;
uint32_t bounded_elements = 0;
bool operator==(const UniformFill&) const = default;
};
+8 -1
View File
@@ -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;
@@ -128,6 +133,7 @@ struct BufferResource {
bool written = false;
bool atomic = false;
bool formatted = false;
bool scalar = false;
bool live = false;
uint32_t indirect_root = NoIndirectBuffer;
uint32_t indirect_mapping_offset = 0;
@@ -489,7 +495,6 @@ struct ShaderInfo {
static constexpr uint32_t MaxBufferRoots = 64;
static constexpr uint32_t MaxImages = 64;
static constexpr uint32_t MaxSamplers = 32;
static constexpr uint32_t MaxSampledPairs = 64;
std::vector<BufferResource> buffers;
std::vector<ImageResource> images;
@@ -621,6 +626,8 @@ struct CompiledShaderInfo {
struct UniformFillPlan {
UniformFill fill;
std::array<Value, 4> values;
Value pattern_size;
Value element_count;
};
// Resource analysis retained by the shader cache. It owns immutable descriptor/SRT,
@@ -94,6 +94,7 @@ bool HasSideEffects(ValueOpcode opcode) {
case ValueOpcode::SetTessellationAttribute:
case ValueOpcode::MeshAllocate:
case ValueOpcode::StoreCompletion:
case ValueOpcode::DataCacheInvalidate:
case ValueOpcode::Barrier: return true;
default: return false;
}
@@ -118,6 +119,7 @@ BufferAccess BufferAccessOf(ValueOpcode opcode) {
case ValueOpcode::BufferAtomicCmpSwap32:
case ValueOpcode::BufferAtomicSwap64:
case ValueOpcode::BufferAtomicIAdd32:
case ValueOpcode::BufferAtomicIAdd64:
case ValueOpcode::BufferAtomicISub32:
case ValueOpcode::BufferAtomicSMin32:
case ValueOpcode::BufferAtomicUMin32:
@@ -137,6 +139,7 @@ BufferAccess BufferAccessOf(ValueOpcode opcode) {
uint32_t BufferComponentCount(ValueOpcode opcode) {
switch (opcode) {
case ValueOpcode::BufferAtomicSwap64:
case ValueOpcode::BufferAtomicIAdd64:
case ValueOpcode::BufferAtomicAnd64:
case ValueOpcode::BufferAtomicOr64:
case ValueOpcode::LoadBufferU32x2:
@@ -176,6 +179,7 @@ SharedAccess SharedAccessOf(ValueOpcode opcode) {
case ValueOpcode::SharedAtomicSMax32:
case ValueOpcode::SharedAtomicUMax32:
case ValueOpcode::SharedAtomicUMax64:
case ValueOpcode::SharedAtomicCmpSwap32:
case ValueOpcode::SharedAtomicCmpSwap64:
case ValueOpcode::SharedAtomicAnd32:
case ValueOpcode::SharedAtomicOr32:
@@ -268,6 +268,7 @@ VALUE_OPCODE(BufferAtomicSwap32, U32, BufferResource, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicCmpSwap32, U32, BufferResource, U32, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicSwap64, U64, BufferResource, U32, U32, U32, U64, U1)
VALUE_OPCODE(BufferAtomicIAdd32, U32, BufferResource, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicIAdd64, U64, BufferResource, U32, U32, U32, U64, U1)
VALUE_OPCODE(BufferAtomicISub32, U32, BufferResource, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicSMin32, U32, BufferResource, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicUMin32, U32, BufferResource, U32, U32, U32, U32, U1)
@@ -306,6 +307,7 @@ VALUE_OPCODE(SharedAtomicUMin32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicSMax32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicUMax32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicUMax64, U64, U32, U64, U1)
VALUE_OPCODE(SharedAtomicCmpSwap32, U32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicCmpSwap64, U64, U32, U64, U64, U1)
VALUE_OPCODE(SharedAtomicAnd32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicOr32, U32, U32, U32, U1)
@@ -348,6 +350,7 @@ VALUE_OPCODE(SetAttribute, Void, U32x4, U1)
VALUE_OPCODE(ControlNop, Void)
VALUE_OPCODE(StoreCompletion, Void)
VALUE_OPCODE(DataCacheInvalidate, Void)
VALUE_OPCODE(Barrier, Void)
VALUE_OPCODE(Sendmsg, Void)
VALUE_OPCODE(TtraceData, Void)
@@ -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
@@ -1023,6 +1029,19 @@ void ConstantPropagationPass(const BlockList& blocks, uint32_t wave_size) {
}
drain();
} while (normalized);
// Repeated register reads share a bitcast only within the same SSA block.
std::unordered_map<const Inst*, Inst*> bitcasts;
for (auto* block: blocks) {
bitcasts.clear();
for (auto& inst: *block) {
if (inst.GetOpcode() != ValueOpcode::BitCastF32U32 &&
inst.GetOpcode() != ValueOpcode::BitCastU32F32) continue;
const auto* source = Arg(inst, 0).TryInstruction();
if (source == nullptr) continue;
const auto [entry, inserted] = bitcasts.try_emplace(source, &inst);
if (!inserted) Replace(inst, Value(entry->second));
}
}
}
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -30,32 +30,62 @@ void RemoveIdentities(Program& program) {
}
void EliminateDeadCode(const BlockList& blocks) {
std::vector<Inst*> pending;
for (auto* block: blocks) {
for (auto& inst: *block) {
// Retained resource-planning instructions live outside the block list.
inst.live = inst.MayHaveSideEffects() || std::ranges::any_of(inst.Uses(),
[](const Use& use) { return use.user->Parent() == nullptr; });
if (inst.live) pending.push_back(&inst);
}
}
while (!pending.empty()) {
const auto* inst = pending.back();
pending.pop_back();
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) {
auto* input = inst->Arg(arg).TryInstruction();
if (input != nullptr && input->Parent() != nullptr && !input->live) {
input->live = true;
pending.push_back(input);
bool changed;
do {
std::vector<Inst*> pending;
for (auto* block: blocks) {
for (auto& inst: *block) {
// Retained resource-planning instructions live outside the block list.
inst.live = inst.MayHaveSideEffects() || std::ranges::any_of(inst.Uses(),
[](const Use& use) { return use.user->Parent() == nullptr; });
if (inst.live) pending.push_back(&inst);
}
}
}
// Unlink every dead operand before freeing nodes in a dead Phi cycle.
for (auto* block: blocks)
for (auto& inst: *block)
if (!inst.live) inst.Invalidate();
for (auto* block: blocks)
std::erase_if(block->Instructions(), [](const Inst& inst) { return !inst.live; });
while (!pending.empty()) {
const auto* inst = pending.back();
pending.pop_back();
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) {
auto* input = inst->Arg(arg).TryInstruction();
if (input != nullptr && input->Parent() != nullptr && !input->live) {
input->live = true;
pending.push_back(input);
}
}
}
// Unlink every dead operand before freeing nodes in a dead Phi cycle.
for (auto* block: blocks)
for (auto& inst: *block)
if (!inst.live) inst.Invalidate();
for (auto* block: blocks)
std::erase_if(block->Instructions(), [](const Inst& inst) { return !inst.live; });
changed = false;
for (auto* block: blocks) {
const auto& term = block->terminator;
if (term.kind != CFG::TerminatorKind::ConditionalBranch || term.loop_header ||
block->condition.IsImmediate()) continue;
for (const bool take_true: {false, true}) {
const auto* empty = take_true ? term.false_block : term.true_block;
const auto* merge = take_true ? term.true_block : term.false_block;
if (empty == nullptr || merge == nullptr || !empty->empty() ||
empty->terminator.loop_header || merge->terminator.loop_header ||
empty->terminator.kind != CFG::TerminatorKind::Branch ||
empty->terminator.true_block != merge ||
std::ranges::any_of(*merge, [](const Inst& inst) {
return inst.GetOpcode() == ValueOpcode::Phi;
})) continue;
// Keep the CFG and SSA edges intact; only the empty detour's predicate is dead.
for (auto& inst: *block) {
if (inst.GetOpcode() == ValueOpcode::Reference &&
inst.Arg(0).Resolve() == block->condition.Resolve())
inst.SetArg(0, Value(take_true));
}
block->condition = Value(take_true);
changed = true;
break;
}
}
} while (changed);
}
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -292,11 +292,23 @@ bool NormalizeIndirectBuffer(DescriptorValue& value, bool indexed, const BufferR
bool IsBoundedDescriptorTable(const ResourcePlan& program,
const DescriptorSource::IndirectDescriptor& indirect) {
const auto* table = Source(program, indirect.table_source);
return indirect.sources.empty() && !indirect.selector &&
return indirect.sources.empty() && (!indirect.selector || indirect.table_scalar) &&
(indirect.key_count.IsEmpty() || indirect.table_maximum) && indirect.workgroup_axis == UINT32_MAX &&
table != nullptr && table->dword_count == 4u;
}
bool CanReadIndexedSelector(const ShaderBufferResource& material, uint32_t offset) {
if (material.Type() != 0u || material.SwizzleEnabled() || material.AddTid() ||
material.OutOfBounds() != 0u) return false;
if (material.Format() == Prospero::BufferFormat::kInvalid || material.NumRecords() == 0u ||
offset >= material.Stride()) return true;
return material.Base48() != 0u &&
((material.Base48() | material.Stride() | offset) & 3u) == 0u &&
material.NumRecords() <= MaxIndirectDescriptorProbes &&
uint64_t {material.NumRecords() - 1u} * material.Stride() + offset + 4u <=
uint64_t {UINT32_MAX} + 1u;
}
bool EvaluateIndirectCount(const DescriptorSource::IndirectDescriptor& indirect,
const SrtRuntime& runtime, SrtWalker& clean, uint32_t& count) {
if (indirect.workgroup_axis != UINT32_MAX) {
@@ -490,6 +502,15 @@ bool ResolveSelectorKeys(const DescriptorSource::IndirectDescriptor& indirect,
keys.push_back(key);
mask &= mask - 1u;
}
} else if (selector->stride == 0u) {
ShaderBufferResource material;
if (!DecodeBufferDescriptor(material_value, material) ||
!CanReadIndexedSelector(material, selector->offset)) return false;
if (material.Format() != Prospero::BufferFormat::kInvalid && material.NumRecords() != 0u &&
selector->offset < material.Stride() &&
!read_keys(material.Base48(), material.GetSize(), selector->offset,
material.Stride(), material.NumRecords())) return false;
keys.push_back(0u); // Every out-of-bounds indexed load returns zero.
} else {
ShaderBufferResource material;
if (!DecodeBufferDescriptor(material_value, material) || selector->stride < 4u ||
@@ -524,29 +545,29 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
ResourceSnapshot& snapshot,
std::vector<DescriptorValue>& descriptors,
std::vector<Specialization>& specializations,
uint32_t maximum_resources, Normalize&& normalize) {
Normalize&& normalize) {
const auto& indirect = *program.descriptor_sources[source_index].indirect_descriptor;
const auto descriptor_bytes = read_dwords * sizeof(uint32_t);
const auto& sources = indirect.sources;
const auto* selector = indirect.selector ? &*indirect.selector : nullptr;
const bool byte_keys = IsBoundedDescriptorTable(program, indirect);
auto* selector_keys = selector != nullptr
? &program.selector_keys[program.selector_groups[source_index]] : nullptr;
auto& keys = selector_keys != nullptr ? selector_keys->values : program.material_keys;
auto& byte_offsets = program.material_keys;
const auto children_begin = descriptors.size();
const auto root_resource = specializations[resource_index];
const auto intern_candidate = [&](const DescriptorValue& candidate, uint32_t& ordinal) {
ordinal = 0u;
if (candidate == descriptors[resource_index]) return true;
const auto intern_candidate = [&](const DescriptorValue& candidate) {
if (candidate == descriptors[resource_index]) return 0u;
const auto found = std::find(descriptors.begin() + children_begin, descriptors.end(), candidate);
ordinal = static_cast<uint32_t>(found - descriptors.begin() - children_begin + 1u);
const auto ordinal = static_cast<uint32_t>(found - descriptors.begin() - children_begin + 1u);
if (found == descriptors.end()) {
if (descriptors.size() >= maximum_resources) return false;
descriptors.push_back(candidate);
auto child = root_resource;
child.indirect_root = resource_index;
specializations.push_back(child);
}
return true;
return ordinal;
};
uint64_t table_base = 0;
uint64_t table_size = UINT64_MAX; // Scalar addresses have no buffer descriptor bounds.
@@ -558,7 +579,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
"indirect {} resource={} source={} table_source={} words={:08x},{:08x},{:08x},{:08x} "
"base={:#x} size={} type={} runtime_stride={} records={} swizzle={} addtid={} oob={} "
"maximum={} offset={} immediate={} record_bytes={} child_count={} child_stride={} "
"child_offset={} indexed_pointer={} candidates={} capacity={} {}",
"child_offset={} indexed_pointer={} candidates={} {}",
reason, resource_index, source_index, indirect.table_source,
table_value.dwords[0], table_value.dwords[1], table_value.dwords[2], table_value.dwords[3],
table_base, table_size, table.Type(), table.Stride(), table.NumRecords(),
@@ -567,7 +588,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
indirect.table_offset, indirect.table_immediate, indirect.table_record_bytes,
pointer != nullptr ? pointer->count : 1u, pointer != nullptr ? pointer->stride : 0u,
pointer != nullptr ? pointer->offset : 0u, pointer != nullptr && pointer->indexed,
descriptors.size(), maximum_resources, details));
descriptors.size(), details));
};
const auto read_failed = [&](std::string_view reason, uint32_t row, uint32_t child,
uint64_t base, uint64_t offset, size_t bytes) {
@@ -583,7 +604,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
if (sources.empty()) {
if (selector == nullptr) keys.clear();
DescriptorValue material_value;
if ((selector != nullptr && !selector_keys->resolved &&
if ((selector != nullptr && (byte_keys || !selector_keys->resolved) &&
!clean.EvaluateDescriptor(selector->source, material_value)) ||
!clean.EvaluateDescriptor(indirect.table_source, table_value)) {
return false;
@@ -596,16 +617,40 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
} else {
return false;
}
if (IsBoundedDescriptorTable(program, indirect)) {
if (byte_keys && table_size > UINT32_MAX) return failed("table geometry");
bool use_selector = selector != nullptr;
if (byte_keys && selector != nullptr) {
ShaderBufferResource material;
if (!DecodeBufferDescriptor(material_value, material)) return false;
use_selector = CanReadIndexedSelector(material, selector->offset);
if (use_selector && material.Format() != Prospero::BufferFormat::kInvalid &&
material.NumRecords() != 0u && selector->offset < material.Stride()) {
const auto begin = material.Base48() + selector->offset;
const auto end = begin + uint64_t {material.NumRecords() - 1u} * material.Stride() + 4u;
for (uint32_t i = 0; i < program.info.buffers.size(); ++i) {
const auto& buffer = program.info.buffers[i];
if (!buffer.written) continue;
ShaderBufferResource written;
if (program.descriptor_sources[buffer.source].indirect_descriptor ||
!DecodeBufferDescriptor(snapshot.buffers[i], written) ||
(written.Base48() != 0u && written.GetSize() != 0u &&
begin < written.Base48() + written.GetSize() &&
written.Base48() < end)) {
use_selector = false;
break;
}
}
}
}
if (byte_keys && !use_selector) {
if (!indirect.table_scalar && table.Format() == Prospero::BufferFormat::kInvalid) {
descriptors[resource_index] = {.dword_count = dword_count};
return true;
}
if (table_size > UINT32_MAX ||
(indirect.table_record_bytes != 0u &&
if (indirect.table_record_bytes != 0u &&
(table.Type() != 0u || table.SwizzleEnabled() || table.AddTid() ||
table.OutOfBounds() != 0u || (table_base & 3u) != 0u ||
(table.Stride() & 3u) != 0u || table.Stride() < indirect.table_record_bytes)))
(table.Stride() & 3u) != 0u || table.Stride() < indirect.table_record_bytes))
return failed("table geometry");
const auto* pointer = indirect.pointer_table ? &*indirect.pointer_table : nullptr;
const bool indexed_pointer = pointer != nullptr && pointer->indexed;
@@ -640,10 +685,11 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
return failed("candidate count", fmt::format("rows={} limit={}", count, MaxIndirectDescriptorProbes));
// Affine SMEM offsets wrap in U32 before adding the aligned immediate.
// Image heaps retain one bulk snapshot and their absolute byte-offset mapping.
auto& heap_words = program.material_keys;
if (image_heap) {
keys.resize(table_size / 4u);
if (!ReadScalarTable(table_base, table_size, 0u, runtime, keys))
return read_failed("image heap read", 0u, 0u, table_base, 0u, keys.size() * sizeof(uint32_t));
heap_words.resize(table_size / 4u);
if (!ReadScalarTable(table_base, table_size, 0u, runtime, heap_words))
return read_failed("image heap read", 0u, 0u, table_base, 0u, heap_words.size() * sizeof(uint32_t));
}
const auto mapping_offset = snapshot.flattened_srt.size();
descriptors[resource_index] = {.dword_count = dword_count};
@@ -670,15 +716,14 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
std::span(candidate.dwords).first(read_dwords))) return false;
} else {
const auto table_word = table_offset / 4u;
std::copy_n(keys.begin() + table_word,
std::min<size_t>(read_dwords, keys.size() - table_word),
std::copy_n(heap_words.begin() + table_word,
std::min<size_t>(read_dwords, heap_words.size() - table_word),
candidate.dwords.begin());
}
if (!normalize(candidate))
return normalization_failed(table_offset, word, step,
indirect.table_maximum ? "key" : "probe");
uint32_t ordinal;
if (!intern_candidate(candidate, ordinal)) return false;
const auto ordinal = intern_candidate(candidate);
if (ordinal == 0u) continue;
const auto mapping_word = image_heap ? table_offset / 4u : word * child_count + child;
snapshot.flattened_srt.resize(mapping_offset + 2u + mapping_word, 0u);
@@ -722,21 +767,38 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
selector_keys->resolved = true;
}
}
if (byte_keys) {
// Keep cached selector values independent of each destination table's
// U32 arithmetic, and use the same byte keys as the full-heap GPU path.
byte_offsets.clear();
byte_offsets.reserve(keys.size());
for (const auto key: keys) {
const auto offset = uint64_t {(key * indirect.table_stride + indirect.table_offset) & ~3u} +
(indirect.table_immediate & ~3u);
byte_offsets.push_back(offset <= UINT32_MAX ? static_cast<uint32_t>(offset) : UINT32_MAX);
}
std::ranges::sort(byte_offsets);
byte_offsets.erase(std::unique(byte_offsets.begin(), byte_offsets.end()), byte_offsets.end());
}
}
const auto mapping_offset = snapshot.flattened_srt.size();
const auto key_count = sources.empty() ? keys.size() : sources.size();
const auto selected_keys = byte_keys ? std::span<const uint32_t>(byte_offsets)
: std::span<const uint32_t>(keys);
const auto key_count = sources.empty() ? selected_keys.size() : sources.size();
snapshot.flattened_srt.resize(mapping_offset + 1u + key_count * 2u);
snapshot.flattened_srt[mapping_offset] = static_cast<uint32_t>(key_count);
for (uint32_t entry = 0; entry < key_count; ++entry) {
const auto key = sources.empty() ? keys[entry] : entry;
const auto key = sources.empty() ? selected_keys[entry] : entry;
uint64_t table_offset = 0;
DescriptorValue candidate;
candidate.dword_count = dword_count;
if (sources.empty()) {
table_offset = uint64_t {(key * indirect.table_stride + indirect.table_offset) & ~3u} +
(indirect.table_immediate & ~3u);
if (!ReadScalarTable(table_base, table_size, table_offset, runtime,
table_offset = byte_keys ? key
: uint64_t {(key * indirect.table_stride + indirect.table_offset) & ~3u} +
(indirect.table_immediate & ~3u);
if (!(byte_keys && key == UINT32_MAX) &&
!ReadScalarTable(table_base, table_size, table_offset, runtime,
std::span(candidate.dwords).first(read_dwords))) {
return read_failed("table descriptor read", key, 0u, table_base, table_offset, descriptor_bytes);
}
@@ -746,10 +808,10 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
if (!normalize(candidate))
return normalization_failed(table_offset, key, indirect.table_stride, "key");
uint32_t ordinal = 0;
if (entry == 0) {
if (entry == 0 && !byte_keys) {
descriptors[resource_index] = candidate;
} else if (!intern_candidate(candidate, ordinal)) {
return false;
} else {
ordinal = intern_candidate(candidate);
}
snapshot.flattened_srt[mapping_offset + 1u + entry * 2u] = key;
snapshot.flattened_srt[mapping_offset + 2u + entry * 2u] = ordinal;
@@ -772,9 +834,8 @@ struct SamplerPlan {
uint32_t source;
SamplerClass type;
};
std::array<std::array<uint32_t, 2>, ShaderInfo::MaxSamplers> mapping;
std::array<Binding, ShaderInfo::MaxSamplers> bindings;
uint32_t sampler_count = 0;
std::vector<std::array<uint32_t, 2>> mapping;
std::vector<Binding> bindings;
};
template <typename T, typename Keep>
@@ -913,6 +974,7 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
if (i >= program.info.images.size() && source != nullptr &&
source->indirect_descriptor &&
IsBoundedDescriptorTable(program, *source->indirect_descriptor) &&
specialization.images[base_index].indirect_search_iterations == 0u &&
!source->indirect_descriptor->pointer_table) {
const auto& indirect = *source->indirect_descriptor;
const size_t mapping = specialization.images[base_index].indirect_mapping_offset;
@@ -997,7 +1059,6 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
is_1d_or_2d(image_class.dimension);
if ((!same_coordinates && !(is_2d(image.dimension) && is_2d(image_class.dimension)) &&
!unfiltered_coordinates) ||
image.mip_count != image_class.mip_count ||
image.conversion_format != image_class.conversion_format ||
(!storage_swizzle && image.shader_swizzle != image_class.shader_swizzle)) {
return SpecializationFail(
@@ -1011,11 +1072,9 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
}
bool BuildSamplerPlan(const ShaderInfo& base, SamplerPlan& plan) {
if (base.samplers.size() > plan.mapping.size()) {
return false;
}
std::array<uint8_t, ShaderInfo::MaxSamplers> usage {};
plan.sampler_count = static_cast<uint32_t>(base.samplers.size());
plan.mapping.resize(base.samplers.size());
plan.bindings.resize(base.samplers.size());
std::vector<uint8_t> usage(base.samplers.size());
for (const auto& pair: base.sampled_pairs) {
if (pair.image >= base.images.size() || pair.sampler >= base.samplers.size()) {
return false;
@@ -1037,8 +1096,8 @@ bool BuildSamplerPlan(const ShaderInfo& base, SamplerPlan& plan) {
bool first = true;
for (uint32_t type = 0; type < mapping.size(); type++) {
if ((classes & (1u << type)) == 0u) continue;
const auto target = first ? index : plan.sampler_count++;
if (target >= ShaderInfo::MaxSamplers) return false;
const auto target = first ? index : static_cast<uint32_t>(plan.bindings.size());
if (!first) plan.bindings.emplace_back();
mapping[type] = target;
plan.bindings[target] = {index, static_cast<SamplerClass>(type)};
first = false;
@@ -1178,7 +1237,7 @@ static UniformFillPlan AnalyzeUniformFill(const Program& program) {
}
if (store == nullptr || visited.size() != program.blocks.size()) return {};
for (const auto& buffer: program.info.buffers) {
if (buffer.read) return {};
if (buffer.read && (!buffer.scalar || buffer.written)) return {};
}
const auto& memory = program.memory_info.at(store->Flags<MemoryFlags>().index);
UniformFillPlan result;
@@ -1409,7 +1468,8 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
}
if (op != ValueOpcode::LogicalAnd && op != ValueOpcode::LogicalOr &&
op != ValueOpcode::LogicalNot && op != ValueOpcode::IEqual32 &&
op != ValueOpcode::INotEqual32 && op != ValueOpcode::ULessThanEqual32 &&
op != ValueOpcode::INotEqual32 && op != ValueOpcode::ULessThan32 &&
op != ValueOpcode::ULessThanEqual32 &&
op != ValueOpcode::SelectU1 && op != ValueOpcode::SelectU32) return finish(opaque());
auto& node = plan.value_storage.emplace_back(op);
for (size_t index = 0; index < inst->NumArgs(); ++index)
@@ -1424,10 +1484,11 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
for (auto it = first; it != plan.value_storage.end(); ++it) it->Invalidate();
plan.value_storage.erase(first, plan.value_storage.end());
}
plan.uniform_fill = AnalyzeUniformFill(program);
for (uint32_t i = 0; i < plan.uniform_fill.fill.words; ++i) {
plan.uniform_fill.values[i] = Clone(plan.uniform_fill.values[i]);
}
plan.uniform_fill = program.uniform_fill.fill.kind != UniformFillKind::None
? program.uniform_fill : AnalyzeUniformFill(program);
for (auto& value: plan.uniform_fill.values) value = Clone(value);
plan.uniform_fill.pattern_size = Clone(plan.uniform_fill.pattern_size);
plan.uniform_fill.element_count = Clone(plan.uniform_fill.element_count);
plan.capture_specialization_reads |=
capture_indirect_reads || capture_control_reads || !plan.source_reads.empty();
if (plan.capture_specialization_reads) {
@@ -1513,15 +1574,21 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
}
snapshot.uniform_fill = {};
const auto& fill = program.uniform_fill;
const auto words = fill.fill.words;
auto words = fill.fill.words;
std::array<uint32_t, 4> stored {};
bool uniform_fill = words != 0;
if (uniform_fill && !fill.pattern_size.IsEmpty())
uniform_fill = clean.Evaluate(fill.pattern_size, words) && words >= 1 && words <= 4;
uint32_t elements = 0;
if (uniform_fill && !fill.element_count.IsEmpty())
uniform_fill = clean.Evaluate(fill.element_count, elements) && elements != 0;
for (uint32_t i = 0; i < words && uniform_fill; ++i) {
uniform_fill = clean.Evaluate(fill.values[i], stored[i]) && stored[i] == stored[0];
}
if (uniform_fill) {
snapshot.uniform_fill = fill.fill;
snapshot.uniform_fill.value = stored[0];
snapshot.uniform_fill.bounded_elements = elements;
}
const auto evaluate = [&](uint32_t source, DescriptorValue& value, bool written = false) {
if (source >= program.descriptor_sources.size()) {
@@ -1536,9 +1603,9 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
return true;
};
const auto resource_failed = [&](std::string_view kind, uint32_t resource, uint32_t source,
size_t candidates, uint32_t capacity) {
return SpecializationFail(fmt::format("indirect {} resource={} source={} candidates={} capacity={}",
kind, resource, source, candidates, capacity));
size_t candidates) {
return SpecializationFail(fmt::format("indirect {} resource={} source={} candidates={}",
kind, resource, source, candidates));
};
snapshot.buffers.resize(program.info.buffers.size());
specialization.buffers.assign(program.info.buffers.size(), {});
@@ -1555,12 +1622,12 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
if (active.empty() || active[base.source]) {
if (!MaterializeIndirectDescriptor(
program, base.source, i, 4u, 4u, observed, clean, snapshot,
snapshot.buffers, specialization.buffers, UINT32_MAX,
snapshot.buffers, specialization.buffers,
[&](DescriptorValue& value) {
return NormalizeIndirectBuffer(
value, source->indirect_descriptor->indexed, base);
}))
return resource_failed("buffer", i, base.source, snapshot.buffers.size(), UINT32_MAX);
return resource_failed("buffer", i, base.source, snapshot.buffers.size());
}
} else if (!evaluate(base.source, snapshot.buffers[i], base.written)) {
return false;
@@ -1589,6 +1656,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
base.formatted ? descriptor.Format() : Prospero::BufferFormat::kInvalid;
buffer.descriptor_swizzle = base.formatted ? descriptor.DstSelXYZW() : DstSel(4, 5, 6, 7);
buffer.zero_stride_oob = descriptor.OutOfBounds() == 0u && stride == 0u;
buffer.scalar_empty = base.scalar && descriptor.GetSize() == 0u;
}
snapshot.images.resize(program.info.images.size());
specialization.images.resize(program.info.images.size());
@@ -1618,14 +1686,14 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
const bool bounded_table = IsBoundedDescriptorTable(program, indirect);
if (!MaterializeIndirectDescriptor(
program, image.source, i, 8u, image.r128 ? 4u : 8u, observed, clean, snapshot, snapshot.images,
specialization.images, UINT32_MAX, [&](DescriptorValue& value) {
specialization.images, [&](DescriptorValue& value) {
// A broad heap also contains resources for other typed image operations.
if (NullImageDescriptor(value) || !ValidImageDescriptor(value, image.r128) ||
(bounded_table && DescriptorDimension(value, image.dimension) != image.dimension))
value.dwords.fill(0u);
return true;
})) {
return resource_failed("image", i, image.source, snapshot.images.size(), UINT32_MAX);
return resource_failed("image", i, image.source, snapshot.images.size());
}
} else {
if (!evaluate(image.source, snapshot.images[i], image.written)) {
@@ -1651,9 +1719,9 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
snapshot.samplers[i] = {.dword_count = 4u};
if ((active.empty() || active[source]) && !MaterializeIndirectDescriptor(
program, source, i, 4u, 4u, observed, clean, snapshot, snapshot.samplers,
specialization.samplers, ShaderInfo::MaxSamplers,
specialization.samplers,
[](DescriptorValue&) { return true; }))
return resource_failed("sampler", i, source, snapshot.samplers.size(), ShaderInfo::MaxSamplers);
return resource_failed("sampler", i, source, snapshot.samplers.size());
} else {
if (indirect != nullptr) source = indirect->sources[0];
if (!evaluate(source, snapshot.samplers[i])) return false;
@@ -1662,11 +1730,13 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
if (program.info.samplers[base_index].gather_lod) {
const auto control = snapshot.samplers[i].dwords[2];
const auto filter = (control >> 26u) & 3u;
// MipNone always selects the base level. Explicit point gathers currently require
// encoded-zero primary and secondary bias; linear primary-mip selection is unsupported.
if (filter > 1u || (filter == 1u && (control & 0xfffffu) != 0u)) {
const auto perf_mip = (snapshot.samplers[i].dwords[1] >> 24u) & 15u;
// MipNone always selects the base level. Point/linear selection requires zero
// primary/secondary bias; linear mip snapping is not implemented.
if (filter > 2u || (filter != 0u && (control & 0xfffffu) != 0u) ||
(filter == 2u && perf_mip != 0u)) {
return SpecializationFail(
"explicit-LOD gather requires mip filtering None or Point with zero LOD biases");
"explicit-LOD gather requires None, unbiased Point, or unbiased Linear without mip snapping");
}
}
}
@@ -1759,8 +1829,8 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
SamplerPlan sampler_plan;
EXIT_IF(!BuildSamplerPlan(program.info, sampler_plan));
const auto original_sampler_count = samplers.size();
samplers.reserve(sampler_plan.sampler_count);
for (uint32_t index = 0; index < sampler_plan.sampler_count; index++) {
samplers.reserve(sampler_plan.bindings.size());
for (uint32_t index = 0; index < sampler_plan.bindings.size(); index++) {
const auto& binding = sampler_plan.bindings[index];
if (index >= original_sampler_count) {
samplers.push_back(samplers[binding.source]);
@@ -1769,7 +1839,7 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
samplers[index].force_point_filtering = binding.type == SamplerClass::Integer;
samplers[index].integer_border = binding.type != SamplerClass::Float;
}
for (uint32_t index = 0; index < sampler_plan.sampler_count; index++) {
for (uint32_t index = 0; index < sampler_plan.bindings.size(); index++) {
const auto& binding = sampler_plan.bindings[index];
for (auto& candidate: samplers[index].indirect_resources) {
candidate = sampler_plan.mapping[candidate][static_cast<uint32_t>(binding.type)];
@@ -1786,6 +1856,17 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
}
auto& memory_info = program.memory_info;
// Resolve empty scalar-table words before folding the indirect descriptors that use them.
for (auto* block: program.blocks) {
for (auto& inst: *block) {
if (inst.GetOpcode() != ValueOpcode::ReadConstBuffer) continue;
const auto& memory = memory_info[inst.Flags<MemoryFlags>().index];
if (memory.kind == ResourceKind::ScalarBuffer &&
specialization.buffers[memory.resource].scalar_empty &&
buffers[memory.resource].indirect_root == BufferResource::NoIndirectBuffer)
inst.ReplaceUsesWith(Value(0u));
}
}
const ImageRemap image_remap(specialization);
for (auto* block: program.blocks) {
for (auto it = block->begin(); it != block->end(); ++it) {
@@ -1885,6 +1966,7 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
EXIT_IF(image.indirect_root == memory.resource &&
inst.GetOpcode() != ValueOpcode::ImageSampleRaw &&
inst.GetOpcode() != ValueOpcode::ImageGatherRaw &&
inst.GetOpcode() != ValueOpcode::ImageQueryDimensions &&
inst.GetOpcode() != ValueOpcode::ImageRead &&
inst.GetOpcode() != ValueOpcode::ImageWrite);
}
@@ -13,6 +13,7 @@ struct ResourceSpecialization {
Prospero::BufferFormat descriptor_format = Prospero::BufferFormat::kInvalid;
uint32_t descriptor_swizzle = DstSel(4, 5, 6, 7);
bool zero_stride_oob = false;
bool scalar_empty = false;
uint32_t indirect_root = BufferResource::NoIndirectBuffer;
uint32_t indirect_mapping_offset = 0;
uint32_t indirect_search_iterations = 0;
@@ -15,6 +15,8 @@
#include <tuple>
#include <unordered_map>
#include <utility>
#define XXH_INLINE_ALL
#include <xxhash.h>
namespace Libs::Graphics::ShaderRecompiler::IR {
namespace {
@@ -1556,14 +1558,49 @@ private:
return read;
}
const Inst* IndexedMaterialRead(const Inst* read, Value active,
DescriptorSource::IndirectDescriptor::SelectorRead& selector) const {
uint32_t lane = 0;
if (read != nullptr && read->GetOpcode() == ValueOpcode::CompositeExtractU32x2) {
if (!ImmediateU32(read->Arg(1), lane) || lane >= 2u) return nullptr;
read = read->Arg(0).Resolve().TryInstruction();
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32x2) return nullptr;
} else if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32) {
return nullptr;
}
const auto& memory = m_program.memory_info[read->Flags<MemoryFlags>().index];
if (memory.kind != ResourceKind::Buffer || memory.typed || memory.formatted ||
!memory.idxen || memory.offen || memory.offset > UINT32_MAX - lane * 4u ||
read->Arg(2).Resolve() != Value(0u) || read->Arg(3).Resolve() != Value(0u) ||
!EquivalentValue(m_program, active, read->Arg(4))) return nullptr;
selector.offset = memory.offset + lane * 4u;
return read;
}
bool MatchUniformizedMaterialKey(Value key, const Inst& image,
DescriptorSource::IndirectDescriptor& indirect,
DescriptorSource& material_source) {
DescriptorSource& material_source, bool indexed) {
const auto* selected = UniformizedMaterialValue(key, image);
if (selected == nullptr) return false;
const auto active = selected->Arg(0).Resolve();
DescriptorSource::IndirectDescriptor::SelectorRead selector;
const auto* read = SelectorReadValue(selected->Arg(1), active, selector);
if (indexed) {
read = IndexedMaterialRead(read, active, selector);
// This optional domain is checked against the dispatch's writable buffers.
// Other stages and unbounded/image writes need the original heap domain.
if (read == nullptr || m_program.stage != ShaderType::Compute ||
std::ranges::any_of(m_program.blocks, [](const Block* block) {
return std::ranges::any_of(*block, [](const Inst& inst) {
const auto op = inst.GetOpcode();
return ImageOpcodeInfoOf(op).resource_class == ImageResourceClass::Storage ||
AddressOpcodeInfoOf(op).access == AddressAccess::Write;
});
}) || !MakeRuntimeTableSource(*read, material_source)) return false;
selector.source = InternSource(material_source);
indirect.selector = std::move(selector);
return true;
}
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadAddressU32 ||
read->NumArgs() != 4u || !Implies(active, read->Arg(3)))
return false;
@@ -2813,7 +2850,7 @@ private:
} else result.maximum = std::max(result.maximum, SelectorMaximum(key, {}, 0u, active));
}
}
if (lane != nullptr) result = SelectedFiniteDomain(*lane, handle != nullptr ? lane->Arg(1) : Value {});
if (lane != nullptr) result = SelectedSelectorDomain(*lane, handle != nullptr ? lane->Arg(1) : Value {});
else if (handle == nullptr) result.maximum = SelectorMaximum(key);
if (result.maximum > UINT32_MAX / 4u ||
uint64_t {result.maximum} * indirect.table_stride + indirect.table_offset > UINT32_MAX)
@@ -2969,6 +3006,13 @@ private:
// Keep live descriptor payloads, but project descriptor-only SMEM onto
// the GPU key using the same plan as other indirect image reads.
plan.retain_reads |= table_source.dword_count == 4u;
} else if (!sampler && !plan.table_indexed && table_source.dword_count == 4u &&
MatchUniformizedMaterialKey(key, handle, indirect, material_source, true)) {
// Preserve the heap's byte-offset key so runtime geometry can choose the
// tighter selector domain without changing the GPU's descriptor reads.
indirect.table_scalar = true;
plan.table_read = plan.reads[0];
plan.retain_reads = true;
} else if (sampler && table_source.dword_count == 4u && !plan.table_indexed &&
SetExactTableMaximum(key, indirect, &handle)) {
indirect.table_scalar = true;
@@ -3018,7 +3062,7 @@ private:
}
}
if (indirect.key_count.IsEmpty() &&
!MatchUniformizedMaterialKey(key, handle, indirect, material_source) &&
!MatchUniformizedMaterialKey(key, handle, indirect, material_source, false) &&
!MatchReversedImageRange(key, indirect)) {
const auto maximum = SelectorMaximum(key);
if (maximum >= MaxIndirectDescriptorProbes) return false;
@@ -3062,6 +3106,10 @@ private:
indirect.selector_mask = selected.selector_mask;
indirect.signed_count = selected.signed_count;
indirect.workgroup_axis = selected.workgroup_axis;
if (selected.table_scalar) {
indirect.table_scalar = true;
plan.table_read = plan.reads[0];
}
material_source = m_sources[selected.selector->source];
shared_selector = true;
break;
@@ -3262,26 +3310,13 @@ private:
if (value == nullptr || (selected != nullptr && selected != value)) return false;
selected = value;
}
const auto* read = selected->Arg(1).Resolve().TryInstruction();
uint32_t lane = 0;
if (read != nullptr && read->GetOpcode() == ValueOpcode::CompositeExtractU32x2) {
if (!ImmediateU32(read->Arg(1), lane) || lane >= 2u) return false;
read = read->Arg(0).Resolve().TryInstruction();
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32x2) return false;
} else if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32) {
return false;
}
const auto& memory = m_program.memory_info[read->Flags<MemoryFlags>().index];
uint32_t offset = 1u, scalar = 1u;
if (memory.kind != ResourceKind::Buffer || memory.typed || memory.formatted ||
!memory.idxen || memory.offen || memory.offset > UINT32_MAX - lane * 4u ||
!ImmediateU32(read->Arg(2), offset) || offset != 0u ||
!ImmediateU32(read->Arg(3), scalar) || scalar != 0u ||
!EquivalentValue(m_program, selected->Arg(0), read->Arg(4))) return false;
if (!MakeRuntimeTableSource(*read, material_source) ||
DescriptorSource::IndirectDescriptor::SelectorRead selector;
const auto* read = IndexedMaterialRead(selected->Arg(1).Resolve().TryInstruction(),
selected->Arg(0), selector);
if (read == nullptr || !MakeRuntimeTableSource(*read, material_source) ||
!BoundedSelectedIndex(read->Arg(1), read->Arg(4), indirect)) return false;
indirect.selector.emplace(DescriptorSource::IndirectDescriptor::SelectorRead{.source = InternSource(material_source),
.offset = memory.offset + lane * 4u});
selector.source = InternSource(material_source);
indirect.selector = std::move(selector);
return finish();
}
@@ -3370,10 +3405,12 @@ private:
return found ? SelectorDomain {maximum, counted ? count : Value {}} : SelectorDomain {};
}
SelectorDomain SelectedFiniteDomain(const Inst& lane, Value guard) const {
SelectorDomain SelectedSelectorDomain(const Inst& lane, Value guard) const {
if (guard.IsEmpty() ? !HasActiveLane(lane.Arg(1), lane.Parent())
: !Implies(guard, lane.Arg(1))) return {};
return FiniteSelectorDomain(lane.Arg(0), lane.Arg(1));
auto result = FiniteSelectorDomain(lane.Arg(0), lane.Arg(1));
if (result.maximum == UINT32_MAX) result.maximum = PossibleU32Bits(lane.Arg(0));
return result;
}
uint32_t SelectorMaximum(Value value, SelectorBounds bounds = {}, uint32_t depth = 0,
@@ -3390,7 +3427,7 @@ private:
};
switch (inst->GetOpcode()) {
case ValueOpcode::ReadFirstLane:
return SelectedFiniteDomain(*inst, guard).maximum;
return SelectedSelectorDomain(*inst, guard).maximum;
case ValueOpcode::ReadLane:
// ReadLane ignores EXEC; only context-free bounds on every source lane apply.
return PossibleU32Bits(inst->Arg(0));
@@ -3735,6 +3772,8 @@ private:
resource.written = resource.written || write;
resource.atomic = resource.atomic || atomic;
resource.formatted = resource.formatted || memory.formatted;
resource.scalar = resource.scalar || op == ValueOpcode::ReadConstBuffer ||
memory.kind == ResourceKind::ScalarBuffer;
}
uint32_t AddImage(uint32_t source, const MemoryInfo& memory, ValueOpcode op, uint32_t pc) {
@@ -3819,9 +3858,6 @@ private:
return;
}
}
if (m_info.sampled_pairs.size() >= ShaderInfo::MaxSampledPairs) {
Fail(pc, "sampled image/sampler pair limit exceeded");
}
m_info.sampled_pairs.push_back({image, sampler, pc});
}
@@ -3881,24 +3917,6 @@ private:
if (memory.planning_only || IsIndirectPlanningMemory(flags.index)) {
return;
}
if (op == ValueOpcode::ReadConstBuffer) {
const auto* handle = inst.Arg(0).Resolve().TryInstruction();
if (memory.kind != ResourceKind::ScalarBuffer || handle == nullptr ||
handle->GetOpcode() != ValueOpcode::GetBufferResource || handle->NumArgs() != 4u) {
Fail(flags.pc, "scalar buffer read requires a four-DWORD buffer descriptor");
}
// Scalar descriptors can span disjoint guest mappings. Keep their raw GPU operands
// separate from handles that vector-resource planning may rewrite.
auto* block = inst.Parent();
const auto where = std::ranges::find_if(block->Instructions(),
[&](const Inst& candidate) { return &candidate == &inst; });
auto& scalar = *block->PrependNewInst(where, ValueOpcode::GetBufferResource,
{handle->Arg(0), handle->Arg(1), handle->Arg(2), handle->Arg(3)});
inst.SetArg(0, Value(&scalar));
m_program.memory_info[flags.index].kind = ResourceKind::IndirectBuffer;
m_info.uses_dma = true;
return;
}
Inst* handle = nullptr;
uint32_t source = 0;
uint32_t resource = 0;
@@ -3910,11 +3928,12 @@ private:
source = indirect->source;
} else if (!GetHandle(inst.Arg(0), ValueOpcode::GetBufferResource, 4, flags.pc,
memory.resource * 4u, handle, source)) {
if (memory.kind != ResourceKind::Buffer ||
if (memory.kind != (op == ValueOpcode::ReadConstBuffer ? ResourceKind::ScalarBuffer
: ResourceKind::Buffer) ||
!memory.SupportsIndirectBufferLoad(op)) {
Fail(flags.pc,
"buffer descriptor is not a valid runtime value; GPU-selected access "
"requires a raw 8/16-bit, raw DWORD x1/x2/x3/x4, or formatted X load");
"requires a scalar, raw 8/16-bit, raw DWORD x1/x2/x3/x4, or formatted X load");
}
m_program.memory_info[flags.index].kind = ResourceKind::IndirectBuffer;
m_info.uses_dma = true;
@@ -4055,11 +4074,71 @@ private:
bool m_shader_writes = false;
};
UniformFillPlan RecognizePatternFill(Program& program, const Decoder::Program& decoded,
ShaderStageInputInfo input) {
struct Pattern {
size_t code_words;
uint64_t hash;
uint32_t wave_size_mask;
bool bounded;
};
constexpr std::array patterns {
// Bounded 64-lane variant.
Pattern {.code_words = 69, .hash = 0x0704b9818e3cba06ull, .wave_size_mask = 64, .bounded = true},
// Dispatch thread dimensions, 64-lane variants.
Pattern {.code_words = 42, .hash = 0x92ce57f21310ef02ull, .wave_size_mask = 64, .bounded = false},
Pattern {.code_words = 40, .hash = 0x374161027f58bee7ull, .wave_size_mask = 64, .bounded = false},
Pattern {.code_words = 40, .hash = 0x7d7172c56c2e296dull, .wave_size_mask = 64, .bounded = false},
// Dispatch thread dimensions, valid in both wave sizes.
Pattern {.code_words = 38, .hash = 0xacbda1a1ccdd4198ull, .wave_size_mask = 32 | 64, .bounded = false},
};
if (program.stage != ShaderType::Compute || input.compute == nullptr ||
decoded.instructions.empty() || program.user_data_base != 0 || program.user_data_count < 10 ||
program.info.buffers.size() != 1 || !program.info.images.empty()) return {};
const auto& cs = *input.compute;
const bool matching_inputs = cs.workgroup_register == 10 && cs.thread_ids_num == 1 &&
cs.group_id[0] && !cs.group_id[1] && !cs.group_id[2] && cs.threads_num[0] == 64 &&
cs.threads_num[1] == 1 && cs.threads_num[2] == 1 && cs.float_mode == 0xc0;
if (!matching_inputs) return {};
const auto& last = decoded.instructions.back();
const size_t words = last.pc / sizeof(uint32_t) + last.word_count;
if (words > decoded.code.size() ||
std::ranges::find(patterns, words, &Pattern::code_words) == patterns.end()) return {};
// Hash executable instructions through S_ENDPGM, excluding binary metadata.
// Verified variants implement Destination[id] = value[id % pattern_size].
const auto hash = XXH3_64bits(decoded.code.data(), words * sizeof(uint32_t));
const auto pattern = std::ranges::find_if(patterns, [&](const Pattern& candidate) {
return candidate.code_words == words && candidate.hash == hash;
});
if (pattern == patterns.end() || (program.wave_size & pattern->wave_size_mask) == 0 ||
cs.dispatch_thread_dimensions != !pattern->bounded) return {};
std::array<Value, 10> userdata;
for (auto* block: program.blocks) {
for (auto& inst: *block) {
if (inst.GetOpcode() != ValueOpcode::GetUserData) continue;
const auto reg = RegIndex(inst.Arg(0).ScalarRegister());
if (reg < userdata.size()) userdata[reg] = Value(&inst);
}
}
for (uint32_t reg = 4; reg < 10; ++reg)
if ((reg != 8 || pattern->bounded) && userdata[reg].IsEmpty()) return {};
UniformFillPlan result;
result.fill.kind = UniformFillKind::Buffer;
result.fill.words = 1;
result.fill.group_stride[0] = 64;
std::copy_n(userdata.begin() + 4, 4, result.values.begin());
result.pattern_size = userdata[9];
// Only the bounded variant reads an explicit count; the other uses thread dimensions.
if (pattern->bounded) result.element_count = userdata[8];
return result;
}
} // namespace
void TrackResources(Program& program, const Decoder::Program& decoded, const CFG::Graph& native_cfg,
ShaderStageInputInfo input) {
Tracker(program, decoded, native_cfg, input).Run();
program.uniform_fill = RecognizePatternFill(program, decoded, input);
}
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -307,7 +307,7 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs
if (shared_access != SharedAccess::None) {
info.uses_lds |= kind == ResourceKind::Lds;
info.uses_gds |= kind == ResourceKind::Gds;
} else if (kind == ResourceKind::Buffer) {
} else if (kind == ResourceKind::Buffer || kind == ResourceKind::ScalarBuffer) {
if (memory.resource >= info.buffers.size()) {
return Fail("buffer operation has invalid resource metadata");
}
@@ -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");
}
@@ -508,6 +509,7 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs
case ValueOpcode::ReadConst: info.uses_flattened_srt = true; break;
case ValueOpcode::BitwiseXor32: info.has_bitwise_xor = true; break;
case ValueOpcode::StoreCompletion: info.subgroup_barrier = true; break;
case ValueOpcode::DataCacheInvalidate: info.coherent_buffers = true; break;
case ValueOpcode::BvhIntersect: info.bvh = true; break;
case ValueOpcode::ConditionRef:
info.subgroup_vote |=
@@ -89,8 +89,7 @@ bool IsRawRead(const ResourcePlan& values, const Inst& inst) {
const auto& memory = values.memory_info[index];
if (op != ValueOpcode::LoadBufferU32) {
return (op == ValueOpcode::LoadAddressU32 && memory.kind == ResourceKind::ScalarAddress) ||
(op == ValueOpcode::ReadConstBuffer &&
(memory.kind == ResourceKind::ScalarBuffer || memory.kind == ResourceKind::IndirectBuffer));
(op == ValueOpcode::ReadConstBuffer && memory.kind == ResourceKind::ScalarBuffer);
}
if (inst.NumArgs() != 5u || memory.kind != ResourceKind::Buffer || memory.typed ||
memory.formatted || memory.coherent || memory.data_bits != 32u ||
@@ -160,9 +159,11 @@ bool IsRuntimeUniformOp(ValueOpcode op) {
case ValueOpcode::ULessThan32:
case ValueOpcode::ULessThanEqual32:
case ValueOpcode::IEqual32:
case ValueOpcode::IEqual64:
case ValueOpcode::UGreaterThan32:
case ValueOpcode::SGreaterThanEqual32:
case ValueOpcode::INotEqual32:
case ValueOpcode::INotEqual64:
case ValueOpcode::LogicalOr:
case ValueOpcode::LogicalAnd:
case ValueOpcode::LogicalXor:
@@ -1054,8 +1055,19 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
return true;
}
return false;
case ValueOpcode::IEqual64:
case ValueOpcode::INotEqual64:
if (!binary()) return false;
result = inst.GetOpcode() == ValueOpcode::IEqual64 ? a == b : a != b;
return true;
case ValueOpcode::ULessThan32:
if (binary()) {
// No U32 is below zero, including an unknown loop-carried value.
if (!Arg(inst, 1, b)) return false;
if (static_cast<uint32_t>(b) == 0u) {
result = 0u;
return true;
}
if (Arg(inst, 0, a)) {
result = static_cast<uint32_t>(a) < static_cast<uint32_t>(b);
return true;
}
+5 -5
View File
@@ -583,16 +583,16 @@ ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context&
mesh.input_primitive, sh.m_vgtGsOutPrimType, sh.m_vgtGsMaxVertOut,
group.primitive_group_size, group.vertex_group_size, mesh.max_vertices);
}
mesh.max_primitives = mesh.fast_launch ? mesh.max_vertices :
native_ngg ? mesh.InputPrimitiveCount(mesh.max_vertices) :
group.primitive_group_size * (sh.m_vgtGsMaxVertOut - 2u);
mesh.primitives_per_group = std::min({static_cast<uint32_t>(group.primitive_group_size),
mesh.InputPrimitiveCount(group.vertex_group_size),
native_ngg ? mesh.max_primitives :
native_ngg ? mesh.InputPrimitiveCount(mesh.max_vertices) :
mesh.max_vertices / sh.m_vgtGsMaxVertOut});
EXIT_IF(mesh.primitives_per_group == 0u);
mesh.vertices_per_group = mesh.InputVertexCount(mesh.primitives_per_group);
const auto threads = native_ngg && !mesh.fast_launch ? mesh.vertices_per_group : mesh.max_vertices;
mesh.max_primitives = mesh.primitives_per_group * (sh.m_geNggSubgrpCntl & 0x1ffu);
const auto threads = std::max(native_ngg && !mesh.fast_launch ? mesh.vertices_per_group
: mesh.max_vertices,
mesh.max_primitives);
mesh.threads_num[0] = ((threads + mesh.wave_size - 1u) / mesh.wave_size) * mesh.wave_size;
mesh.threads_num[1] = mesh.threads_num[2] = 1u;
return params;

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