mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-21 11:26:56 +02:00
Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4644c52927 | |||
| 4bb4be50b9 | |||
| be9e13915d | |||
| 68d0ba5a81 | |||
| 70668fbd98 | |||
| ea1b032c59 | |||
| 3c4124dafa | |||
| 8cc5f5364a | |||
| 1ce663b5a9 | |||
| 66f488a17f | |||
| 7b7f9a70c6 | |||
| 6ad393775f | |||
| f492027be2 | |||
| 8843291506 | |||
| 63ca9c0b80 | |||
| 0caf4a6aa0 | |||
| c715891242 | |||
| f3e01aaf1e | |||
| 0957f088ad | |||
| 4a2961cdf9 | |||
| f0e462a8a2 | |||
| feed075a9f | |||
| 18e6747dbf | |||
| 6b6274fa5a | |||
| 1a053f9e13 | |||
| 12ca1599cf | |||
| f186b35706 | |||
| 992a83003d | |||
| fa8216c4d9 | |||
| 742fe17173 | |||
| 0cad4672e9 | |||
| cd4eab9d65 | |||
| fde0b98e69 | |||
| d4465faa79 | |||
| e5f0ee7fc2 | |||
| 5f29374d5b | |||
| 2c730e58ea | |||
| e87fb8c859 | |||
| 440245f616 | |||
| 05d25ef28c | |||
| cad6aafdc5 | |||
| defb2f11d3 | |||
| 75d7a4311d | |||
| bb505a3ae8 | |||
| ab8566591f | |||
| b2d3ce6dfc | |||
| 09c993dfe0 | |||
| de81300b28 | |||
| ac3711ce7c | |||
| 8b51370ca1 | |||
| e9bcc77e0b | |||
| 8639e0de81 | |||
| 7675579e79 | |||
| 831862a50b | |||
| 099de8dea6 | |||
| f4bdb52922 | |||
| 22ef16a044 | |||
| 37df7f3278 | |||
| 33208a545a | |||
| 4059179e9d | |||
| fe3857a171 | |||
| c3e7ec8285 | |||
| 763d3be3a9 | |||
| d8c4a9af38 | |||
| 6aa3846fb2 | |||
| 415387f00d | |||
| 890393fbe6 | |||
| 9f88043ebe |
Vendored
+1
-1
@@ -27,7 +27,7 @@
|
||||
"args": [
|
||||
"--game", "D:/gamesps5/PPSA07429",
|
||||
"--printf-direction", "File",
|
||||
"--graphics-debug-dump", "True"
|
||||
"--graphics-debug-dump"
|
||||
],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}\\_Build\\vscode-clang",
|
||||
|
||||
Vendored
+17
@@ -25,6 +25,23 @@ FetchContent_MakeAvailable(opus)
|
||||
set(BUILD_TESTING "${kyty_build_testing_saved}")
|
||||
install(FILES "${opus_SOURCE_DIR}/COPYING" DESTINATION "licenses/opus")
|
||||
|
||||
# Headsets are driven through OpenXR. Only the static loader is built; the runtime that serves it
|
||||
# (for example SteamVR, Monado or Virtual Desktop) comes from the user's system.
|
||||
set(DYNAMIC_LOADER OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_WITH_SYSTEM_JSONCPP OFF CACHE BOOL "" FORCE)
|
||||
FetchContent_Declare(openxr
|
||||
GIT_REPOSITORY https://github.com/KhronosGroup/OpenXR-SDK.git
|
||||
GIT_TAG f2448a8797c85814aa892efc1ab8707900fbcc78 # release-1.1.63
|
||||
GIT_SHALLOW FALSE)
|
||||
FetchContent_MakeAvailable(openxr)
|
||||
# Keep the loader's headers and library out of the installed package.
|
||||
set_property(DIRECTORY "${openxr_SOURCE_DIR}" PROPERTY EXCLUDE_FROM_ALL TRUE)
|
||||
# The loader uses exceptions internally.
|
||||
if(NOT KYTY_CLANG_CL)
|
||||
target_compile_options(openxr_loader PRIVATE -fexceptions)
|
||||
endif()
|
||||
install(FILES "${openxr_SOURCE_DIR}/LICENSE" DESTINATION "licenses/openxr")
|
||||
|
||||
# The guest instruction patcher needs a decoder/encoder and a small
|
||||
# trampoline assembler. Pin the same revisions used by shadPS4's implementation.
|
||||
set(XBYAK_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
|
||||
+3
-1
@@ -336,7 +336,7 @@ if (KYTY_CLANG_CL)
|
||||
set_source_files_properties(${kyty_emulator_src} PROPERTIES COMPILE_FLAGS "-Wno-pragma-pack -Wno-deprecated-declarations -D_TIMESPEC_DEFINED")
|
||||
endif()
|
||||
|
||||
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL3::SDL3-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version Zydis::Zydis xbyak::xbyak opus)
|
||||
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL3::SDL3-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version Zydis::Zydis xbyak::xbyak opus OpenXR::openxr_loader)
|
||||
|
||||
# Linux system libraries.
|
||||
if(LINUX)
|
||||
@@ -655,6 +655,8 @@ if(BUILD_TESTING)
|
||||
add_test(NAME save_data_dialog COMMAND $<TARGET_FILE:save_data_dialog_tests>)
|
||||
add_test(NAME http_uri_parse COMMAND $<TARGET_FILE:http_uri_parse_tests>)
|
||||
add_test(NAME shader_recompiler_compute COMMAND $<TARGET_FILE:shader_recompiler_compute_tests>)
|
||||
add_test(NAME shader_recompiler_tessellation
|
||||
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --tessellation-only)
|
||||
add_test(NAME shader_recompiler_alignbyte
|
||||
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --alignbyte-only)
|
||||
add_test(NAME graphics_pipeline_rasterization
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "تم كسب الكأس!"
|
||||
"trophy.toast.earned": "تم كسب الكأس!",
|
||||
"user.guest": "ضيف"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofej získána!"
|
||||
"trophy.toast.earned": "Trofej získána!",
|
||||
"user.guest": "Host"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofæ optjent!"
|
||||
"trophy.toast.earned": "Trofæ optjent!",
|
||||
"user.guest": "Gæst"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophäe erhalten!"
|
||||
"trophy.toast.earned": "Trophäe erhalten!",
|
||||
"user.guest": "Gast"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Κερδίσατε ένα τρόπαιο!"
|
||||
"trophy.toast.earned": "Κερδίσατε ένα τρόπαιο!",
|
||||
"user.guest": "Επισκ."
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophy earned!"
|
||||
"trophy.toast.earned": "Trophy earned!",
|
||||
"user.guest": "Guest"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophy earned!"
|
||||
"trophy.toast.earned": "Trophy earned!",
|
||||
"user.guest": "Guest"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "¡Ganaste un trofeo!"
|
||||
"trophy.toast.earned": "¡Ganaste un trofeo!",
|
||||
"user.guest": "Invitado"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "¡Ganaste un trofeo!"
|
||||
"trophy.toast.earned": "¡Ganaste un trofeo!",
|
||||
"user.guest": "Invitado"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Pokaali ansaittu!"
|
||||
"trophy.toast.earned": "Pokaali ansaittu!",
|
||||
"user.guest": "Vieras"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophée obtenu !"
|
||||
"trophy.toast.earned": "Trophée obtenu !",
|
||||
"user.guest": "Invité"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophée obtenu !"
|
||||
"trophy.toast.earned": "Trophée obtenu !",
|
||||
"user.guest": "Invité"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trófea megszerezve!"
|
||||
"trophy.toast.earned": "Trófea megszerezve!",
|
||||
"user.guest": "Vendég"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofi diperoleh!"
|
||||
"trophy.toast.earned": "Trofi diperoleh!",
|
||||
"user.guest": "Tamu"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofeo ottenuto!"
|
||||
"trophy.toast.earned": "Trofeo ottenuto!",
|
||||
"user.guest": "Ospite"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "トロフィーを獲得しました!"
|
||||
"trophy.toast.earned": "トロフィーを獲得しました!",
|
||||
"user.guest": "ゲスト"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "트로피를 획득했습니다!"
|
||||
"trophy.toast.earned": "트로피를 획득했습니다!",
|
||||
"user.guest": "게스트"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofee behaald!"
|
||||
"trophy.toast.earned": "Trofee behaald!",
|
||||
"user.guest": "Gast"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofé opptjent!"
|
||||
"trophy.toast.earned": "Trofé opptjent!",
|
||||
"user.guest": "Gjest"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Zdobyto trofeum!"
|
||||
"trophy.toast.earned": "Zdobyto trofeum!",
|
||||
"user.guest": "Gość"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Troféu conquistado!"
|
||||
"trophy.toast.earned": "Troféu conquistado!",
|
||||
"user.guest": "Convidado"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Troféu conquistado!"
|
||||
"trophy.toast.earned": "Troféu conquistado!",
|
||||
"user.guest": "Convidado"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofeu câștigat!"
|
||||
"trophy.toast.earned": "Trofeu câștigat!",
|
||||
"user.guest": "Invitat"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Трофей получен!"
|
||||
"trophy.toast.earned": "Трофей получен!",
|
||||
"user.guest": "Гость"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofé intjänad!"
|
||||
"trophy.toast.earned": "Trofé intjänad!",
|
||||
"user.guest": "Gäst"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "ได้รับถ้วยรางวัลแล้ว!"
|
||||
"trophy.toast.earned": "ได้รับถ้วยรางวัลแล้ว!",
|
||||
"user.guest": "แขก"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Kupa kazanıldı!"
|
||||
"trophy.toast.earned": "Kupa kazanıldı!",
|
||||
"user.guest": "Misafir"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Đã nhận cúp!"
|
||||
"trophy.toast.earned": "Đã nhận cúp!",
|
||||
"user.guest": "Khách"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "已获得奖杯!"
|
||||
"trophy.toast.earned": "已获得奖杯!",
|
||||
"user.guest": "访客"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "已獲得獎盃!"
|
||||
"trophy.toast.earned": "已獲得獎盃!",
|
||||
"user.guest": "訪客"
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@ target_link_libraries(common
|
||||
ZArchive::zarchive
|
||||
nlohmann_json::nlohmann_json
|
||||
)
|
||||
if(APPLE)
|
||||
# hostStats.cpp reads GPU utilization from the IOAccelerator registry entries.
|
||||
target_link_libraries(common PRIVATE "-framework CoreFoundation" "-framework IOKit")
|
||||
endif()
|
||||
|
||||
get_property(inc_headers TARGET common PROPERTY INCLUDE_DIRECTORIES)
|
||||
set(check_headers ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#ifndef KYTY_COMMON_DEBUG_COUNTERS_H_
|
||||
#define KYTY_COMMON_DEBUG_COUNTERS_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Common::DebugCounters {
|
||||
|
||||
static_assert(std::atomic<uint64_t>::is_always_lock_free && std::atomic<bool>::is_always_lock_free);
|
||||
|
||||
// Collect events only while the panel is visible. Relaxed, lock-free atomics keep updates
|
||||
// cheap and safe in signal handlers. Rates come from comparing two snapshots.
|
||||
enum class Counter {
|
||||
// GPU work.
|
||||
Draws,
|
||||
DrawInstances,
|
||||
IndirectDraws,
|
||||
Dispatches,
|
||||
QueueSubmits,
|
||||
GpuWaits, // CPU blocked on a GPU timeline semaphore.
|
||||
GpuWaitNs,
|
||||
GpuFaults, // Guest writes to GPU-tracked memory.
|
||||
ShadersCompiled,
|
||||
ShaderCompileNs,
|
||||
PipelinesCreated,
|
||||
PipelineCreateNs,
|
||||
// Caches.
|
||||
TextureUploadBytes,
|
||||
TextureDownloadBytes,
|
||||
TextureEvictions,
|
||||
BufferUploadBytes,
|
||||
BufferDownloadBytes,
|
||||
BufferEvictions,
|
||||
// Presentation.
|
||||
FlipsRequested,
|
||||
FlipsRejected, // Guest flip queue was full.
|
||||
LateVblanks, // The present thread fell behind the vblank rate.
|
||||
SwapchainRecreations,
|
||||
DroppedPresents,
|
||||
// Emulator.
|
||||
EmulatedInstructions, // Host CPU lacked an instruction the guest used.
|
||||
UnresolvedImportCalls,
|
||||
GuestFileReadBytes,
|
||||
GuestFileWriteBytes,
|
||||
AudioUnderruns,
|
||||
Count
|
||||
};
|
||||
|
||||
enum class Peak { ShaderCompileNs, PipelineCreateNs, Count };
|
||||
|
||||
namespace Detail {
|
||||
inline std::atomic<bool> g_enabled {false};
|
||||
inline std::atomic<uint64_t> g_counters[static_cast<int>(Counter::Count)] {};
|
||||
inline std::atomic<uint64_t> g_peaks[static_cast<int>(Peak::Count)] {};
|
||||
} // namespace Detail
|
||||
|
||||
[[nodiscard]] inline bool Enabled() noexcept {
|
||||
return Detail::g_enabled.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
inline void SetEnabled(bool enabled) noexcept {
|
||||
Detail::g_enabled.store(enabled, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
inline void Add(Counter counter, uint64_t value = 1) noexcept {
|
||||
if (!Enabled()) {
|
||||
return;
|
||||
}
|
||||
Detail::g_counters[static_cast<int>(counter)].fetch_add(value, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
class ScopedTimer final {
|
||||
public:
|
||||
ScopedTimer(Counter count, Counter duration, Peak peak = Peak::Count)
|
||||
: m_count(count), m_duration(duration), m_peak(peak),
|
||||
m_start(Enabled() ? std::chrono::steady_clock::now()
|
||||
: std::chrono::steady_clock::time_point {}) {}
|
||||
~ScopedTimer() {
|
||||
if (m_start == std::chrono::steady_clock::time_point {} || !Enabled()) {
|
||||
return;
|
||||
}
|
||||
const auto elapsed =
|
||||
static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::steady_clock::now() - m_start)
|
||||
.count());
|
||||
Add(m_count);
|
||||
Add(m_duration, elapsed);
|
||||
if (m_peak != Peak::Count) {
|
||||
auto& peak = Detail::g_peaks[static_cast<int>(m_peak)];
|
||||
auto previous = peak.load(std::memory_order_relaxed);
|
||||
while (previous < elapsed &&
|
||||
!peak.compare_exchange_weak(previous, elapsed, std::memory_order_relaxed)) {
|
||||
}
|
||||
}
|
||||
}
|
||||
ScopedTimer(const ScopedTimer&) = delete;
|
||||
ScopedTimer& operator=(const ScopedTimer&) = delete;
|
||||
|
||||
private:
|
||||
Counter m_count;
|
||||
Counter m_duration;
|
||||
Peak m_peak;
|
||||
std::chrono::steady_clock::time_point m_start;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline uint64_t Get(Counter counter) noexcept {
|
||||
return Detail::g_counters[static_cast<int>(counter)].load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline uint64_t Get(Peak peak) noexcept {
|
||||
return Detail::g_peaks[static_cast<int>(peak)].load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
} // namespace Common::DebugCounters
|
||||
|
||||
#endif /* KYTY_COMMON_DEBUG_COUNTERS_H_ */
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "common/emulatorConfig.h"
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/debugCounters.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
@@ -16,6 +17,7 @@ void Initialize() {
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
Common::DebugCounters::SetEnabled(false);
|
||||
g_config.reset();
|
||||
}
|
||||
|
||||
@@ -24,8 +26,10 @@ void Load(const ConfigOptions& cfg) {
|
||||
EXIT_IF(cfg.user_name.empty() || cfg.user_name.size() > MAX_USER_NAME_LENGTH);
|
||||
EXIT_IF(!IsConfiguredUserIdValid(cfg.user_id));
|
||||
EXIT_IF(cfg.controller_speaker_volume > 100 || cfg.controller_vibration_intensity > 100);
|
||||
EXIT_IF(cfg.keyboard_player > static_cast<uint32_t>(MAX_PLAYERS));
|
||||
|
||||
*g_config = cfg;
|
||||
Common::DebugCounters::SetEnabled(cfg.perf_overlay_enabled);
|
||||
}
|
||||
|
||||
uint32_t GetScreenWidth() {
|
||||
@@ -60,6 +64,27 @@ uint32_t GetControllerVibrationIntensity() {
|
||||
return g_config->controller_vibration_intensity;
|
||||
}
|
||||
|
||||
uint32_t GetKeyboardPlayer() {
|
||||
return g_config->keyboard_player;
|
||||
}
|
||||
|
||||
int32_t GetPlayerUserId(int player) {
|
||||
return g_config->user_id + player * PLAYER_USER_ID_STEP;
|
||||
}
|
||||
|
||||
int GetPlayerFromUserId(int32_t user_id) {
|
||||
for (int player = 0; player < MAX_PLAYERS; player++) {
|
||||
if (user_id == GetPlayerUserId(player)) {
|
||||
return player;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool IsGuestUserId(int32_t user_id) {
|
||||
return GetPlayerFromUserId(user_id) > 0;
|
||||
}
|
||||
|
||||
PresentMode GetPresentMode() {
|
||||
return g_config->present_mode;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,580 @@
|
||||
#include "common/hostStats.h"
|
||||
|
||||
#include "common/config.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h> // IWYU pragma: keep
|
||||
// clang-format off
|
||||
#include <psapi.h>
|
||||
// clang-format on
|
||||
#elif defined(__APPLE__)
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <IOKit/IOKitLib.h>
|
||||
#include <cstring>
|
||||
#include <libproc.h>
|
||||
#include <mach/mach.h>
|
||||
#include <notify.h>
|
||||
#include <sys/resource.h>
|
||||
#include <sys/sysctl.h>
|
||||
#include <unistd.h>
|
||||
#else
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <sys/resource.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace Common {
|
||||
|
||||
namespace {
|
||||
|
||||
// Cumulative counters; rates come from the difference between two samples.
|
||||
struct Counters {
|
||||
struct Thread {
|
||||
uint64_t cpu_ns;
|
||||
std::string name;
|
||||
};
|
||||
std::optional<uint64_t> process_cpu_ns;
|
||||
std::optional<uint64_t> system_busy;
|
||||
std::optional<uint64_t> system_total;
|
||||
std::optional<uint64_t> context_switches;
|
||||
std::optional<uint64_t> page_faults;
|
||||
std::optional<uint64_t> page_ins;
|
||||
std::optional<uint64_t> disk_read;
|
||||
std::optional<uint64_t> disk_write;
|
||||
std::unordered_map<uint64_t, Thread> threads;
|
||||
uint64_t wall_ns = 0;
|
||||
};
|
||||
|
||||
uint64_t WallNanoseconds() {
|
||||
return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count());
|
||||
}
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
|
||||
uint64_t FileTimeTo100Ns(const FILETIME& time) {
|
||||
return (static_cast<uint64_t>(time.dwHighDateTime) << 32u) | time.dwLowDateTime;
|
||||
}
|
||||
|
||||
struct PlatformState {};
|
||||
|
||||
void SamplePlatform(PlatformState& /*state*/, HostStats& stats, Counters& now) {
|
||||
const auto process = GetCurrentProcess();
|
||||
FILETIME creation {};
|
||||
FILETIME exit {};
|
||||
FILETIME kernel {};
|
||||
FILETIME user {};
|
||||
if (GetProcessTimes(process, &creation, &exit, &kernel, &user) != 0) {
|
||||
now.process_cpu_ns = (FileTimeTo100Ns(kernel) + FileTimeTo100Ns(user)) * 100u;
|
||||
}
|
||||
FILETIME idle {};
|
||||
if (GetSystemTimes(&idle, &kernel, &user) != 0) {
|
||||
// Kernel time includes idle time.
|
||||
now.system_total = FileTimeTo100Ns(kernel) + FileTimeTo100Ns(user);
|
||||
now.system_busy = *now.system_total - FileTimeTo100Ns(idle);
|
||||
}
|
||||
|
||||
PROCESS_MEMORY_COUNTERS memory {};
|
||||
if (K32GetProcessMemoryInfo(process, &memory, sizeof(memory)) != 0) {
|
||||
stats.process_memory = memory.WorkingSetSize;
|
||||
stats.process_resident = memory.WorkingSetSize;
|
||||
now.page_faults = memory.PageFaultCount;
|
||||
}
|
||||
IO_COUNTERS io {};
|
||||
if (GetProcessIoCounters(process, &io) != 0) {
|
||||
now.disk_read = io.ReadTransferCount;
|
||||
now.disk_write = io.WriteTransferCount;
|
||||
}
|
||||
|
||||
MEMORYSTATUSEX system {};
|
||||
system.dwLength = sizeof(system);
|
||||
if (GlobalMemoryStatusEx(&system) != 0) {
|
||||
stats.system_memory_total = system.ullTotalPhys;
|
||||
stats.system_memory_used = system.ullTotalPhys - system.ullAvailPhys;
|
||||
stats.system_free = system.ullAvailPhys;
|
||||
stats.memory_pressure = system.dwMemoryLoad >= 90 ? MemoryPressure::Critical
|
||||
: system.dwMemoryLoad >= 80 ? MemoryPressure::Warning
|
||||
: MemoryPressure::Normal;
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
void SampleRusage(Counters& now) {
|
||||
rusage usage {};
|
||||
if (getrusage(RUSAGE_SELF, &usage) != 0) {
|
||||
return;
|
||||
}
|
||||
const auto to_ns = [](const timeval& time) {
|
||||
return static_cast<uint64_t>(time.tv_sec) * 1000000000u +
|
||||
static_cast<uint64_t>(time.tv_usec) * 1000u;
|
||||
};
|
||||
now.process_cpu_ns = to_ns(usage.ru_utime) + to_ns(usage.ru_stime);
|
||||
now.context_switches = static_cast<uint64_t>(usage.ru_nvcsw + usage.ru_nivcsw);
|
||||
now.page_faults = static_cast<uint64_t>(usage.ru_minflt + usage.ru_majflt);
|
||||
now.page_ins = static_cast<uint64_t>(usage.ru_majflt);
|
||||
}
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
||||
template <typename T>
|
||||
std::optional<T> SysctlValue(const char* name) {
|
||||
T value {};
|
||||
size_t size = sizeof(value);
|
||||
if (sysctlbyname(name, &value, &size, nullptr, 0) != 0 || size != sizeof(value)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
struct PlatformState {
|
||||
mach_port_t host = mach_host_self();
|
||||
int thermal_token = -1;
|
||||
bool thermal_ready = false;
|
||||
std::string gpu_model;
|
||||
std::optional<uint32_t> gpu_cores;
|
||||
|
||||
~PlatformState() {
|
||||
if (thermal_ready) {
|
||||
notify_cancel(thermal_token);
|
||||
}
|
||||
mach_port_deallocate(mach_task_self(), host);
|
||||
}
|
||||
};
|
||||
|
||||
std::optional<int64_t> DictionaryNumber(CFDictionaryRef dictionary, CFStringRef key) {
|
||||
const auto* value = CFDictionaryGetValue(dictionary, key);
|
||||
int64_t number = 0;
|
||||
if (value == nullptr || CFGetTypeID(value) != CFNumberGetTypeID() ||
|
||||
!CFNumberGetValue(static_cast<CFNumberRef>(value), kCFNumberSInt64Type, &number) ||
|
||||
number < 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return number;
|
||||
}
|
||||
|
||||
std::string RegistryString(io_object_t service, CFStringRef key) {
|
||||
std::string result;
|
||||
auto* value = IORegistryEntryCreateCFProperty(service, key, kCFAllocatorDefault, 0);
|
||||
if (value == nullptr) {
|
||||
return result;
|
||||
}
|
||||
if (CFGetTypeID(value) == CFStringGetTypeID()) {
|
||||
char buffer[128] {};
|
||||
if (CFStringGetCString(static_cast<CFStringRef>(value), buffer, sizeof(buffer),
|
||||
kCFStringEncodingUTF8)) {
|
||||
result = buffer;
|
||||
}
|
||||
} else if (CFGetTypeID(value) == CFDataGetTypeID()) {
|
||||
// Intel Macs store the model as NUL-terminated data.
|
||||
const auto* data = static_cast<CFDataRef>(value);
|
||||
const auto* bytes = reinterpret_cast<const char*>(CFDataGetBytePtr(data));
|
||||
const auto length = static_cast<size_t>(CFDataGetLength(data));
|
||||
result.assign(bytes, strnlen(bytes, length));
|
||||
}
|
||||
CFRelease(value);
|
||||
return result;
|
||||
}
|
||||
|
||||
// IOAccelerator exposes whole-device statistics; there is no per-process figure.
|
||||
void SampleGpu(PlatformState& state, HostStats& stats) {
|
||||
io_iterator_t iterator = 0;
|
||||
if (IOServiceGetMatchingServices(MACH_PORT_NULL, IOServiceMatching("IOAccelerator"),
|
||||
&iterator) != KERN_SUCCESS) {
|
||||
return;
|
||||
}
|
||||
const auto take_max = [](std::optional<double>& target, std::optional<int64_t> value) {
|
||||
if (value) {
|
||||
target = std::max(target.value_or(0.0), std::min(100.0, static_cast<double>(*value)));
|
||||
}
|
||||
};
|
||||
const auto add = [](std::optional<uint64_t>& target, std::optional<int64_t> value) {
|
||||
if (value) {
|
||||
target = target.value_or(0) + static_cast<uint64_t>(*value);
|
||||
}
|
||||
};
|
||||
for (io_object_t service = IOIteratorNext(iterator); service != 0;
|
||||
service = IOIteratorNext(iterator)) {
|
||||
if (state.gpu_model.empty()) {
|
||||
state.gpu_model = RegistryString(service, CFSTR("model"));
|
||||
auto* cores = IORegistryEntryCreateCFProperty(service, CFSTR("gpu-core-count"),
|
||||
kCFAllocatorDefault, 0);
|
||||
int64_t count = 0;
|
||||
if (cores != nullptr) {
|
||||
if (CFGetTypeID(cores) == CFNumberGetTypeID() &&
|
||||
CFNumberGetValue(static_cast<CFNumberRef>(cores), kCFNumberSInt64Type,
|
||||
&count) &&
|
||||
count > 0) {
|
||||
state.gpu_cores = static_cast<uint32_t>(count);
|
||||
}
|
||||
CFRelease(cores);
|
||||
}
|
||||
}
|
||||
auto* statistics = IORegistryEntryCreateCFProperty(service, CFSTR("PerformanceStatistics"),
|
||||
kCFAllocatorDefault, 0);
|
||||
if (statistics != nullptr) {
|
||||
if (CFGetTypeID(statistics) == CFDictionaryGetTypeID()) {
|
||||
const auto* values = static_cast<CFDictionaryRef>(statistics);
|
||||
take_max(stats.gpu_percent,
|
||||
DictionaryNumber(values, CFSTR("Device Utilization %")));
|
||||
take_max(stats.gpu_renderer_percent,
|
||||
DictionaryNumber(values, CFSTR("Renderer Utilization %")));
|
||||
take_max(stats.gpu_tiler_percent,
|
||||
DictionaryNumber(values, CFSTR("Tiler Utilization %")));
|
||||
add(stats.gpu_memory_in_use,
|
||||
DictionaryNumber(values, CFSTR("In use system memory")));
|
||||
add(stats.gpu_memory_allocated,
|
||||
DictionaryNumber(values, CFSTR("Alloc system memory")));
|
||||
add(stats.gpu_recoveries, DictionaryNumber(values, CFSTR("recoveryCount")));
|
||||
}
|
||||
CFRelease(statistics);
|
||||
}
|
||||
IOObjectRelease(service);
|
||||
}
|
||||
IOObjectRelease(iterator);
|
||||
stats.gpu_model = state.gpu_model;
|
||||
stats.gpu_cores = state.gpu_cores;
|
||||
}
|
||||
|
||||
void SampleThreads(HostStats& stats, Counters& now) {
|
||||
thread_act_array_t threads = nullptr;
|
||||
mach_msg_type_number_t count = 0;
|
||||
const auto task = mach_task_self();
|
||||
if (task_threads(task, &threads, &count) != KERN_SUCCESS) {
|
||||
return;
|
||||
}
|
||||
stats.thread_count = count;
|
||||
for (mach_msg_type_number_t i = 0; i < count; i++) {
|
||||
thread_identifier_info_data_t identifier {};
|
||||
mach_msg_type_number_t size = THREAD_IDENTIFIER_INFO_COUNT;
|
||||
thread_basic_info_data_t basic {};
|
||||
mach_msg_type_number_t basic_size = THREAD_BASIC_INFO_COUNT;
|
||||
if (thread_info(threads[i], THREAD_IDENTIFIER_INFO,
|
||||
reinterpret_cast<thread_info_t>(&identifier), &size) == KERN_SUCCESS &&
|
||||
thread_info(threads[i], THREAD_BASIC_INFO, reinterpret_cast<thread_info_t>(&basic),
|
||||
&basic_size) == KERN_SUCCESS) {
|
||||
const auto to_ns = [](const time_value_t& time) {
|
||||
return static_cast<uint64_t>(time.seconds) * 1000000000u +
|
||||
static_cast<uint64_t>(time.microseconds) * 1000u;
|
||||
};
|
||||
auto& thread = now.threads[identifier.thread_id];
|
||||
thread.cpu_ns = to_ns(basic.user_time) + to_ns(basic.system_time);
|
||||
thread_extended_info_data_t extended {};
|
||||
mach_msg_type_number_t extended_size = THREAD_EXTENDED_INFO_COUNT;
|
||||
if (thread_info(threads[i], THREAD_EXTENDED_INFO,
|
||||
reinterpret_cast<thread_info_t>(&extended),
|
||||
&extended_size) == KERN_SUCCESS) {
|
||||
thread.name = std::string(extended.pth_name,
|
||||
strnlen(extended.pth_name, sizeof(extended.pth_name)));
|
||||
}
|
||||
}
|
||||
mach_port_deallocate(task, threads[i]);
|
||||
}
|
||||
vm_deallocate(task, reinterpret_cast<vm_address_t>(threads), count * sizeof(thread_act_t));
|
||||
}
|
||||
|
||||
void SamplePlatform(PlatformState& state, HostStats& stats, Counters& now) {
|
||||
SampleRusage(now);
|
||||
|
||||
host_cpu_load_info_data_t load {};
|
||||
mach_msg_type_number_t load_count = HOST_CPU_LOAD_INFO_COUNT;
|
||||
if (host_statistics(state.host, HOST_CPU_LOAD_INFO, reinterpret_cast<host_info_t>(&load),
|
||||
&load_count) == KERN_SUCCESS) {
|
||||
const uint64_t busy = static_cast<uint64_t>(load.cpu_ticks[CPU_STATE_USER]) +
|
||||
load.cpu_ticks[CPU_STATE_SYSTEM] + load.cpu_ticks[CPU_STATE_NICE];
|
||||
now.system_busy = busy;
|
||||
now.system_total = busy + load.cpu_ticks[CPU_STATE_IDLE];
|
||||
}
|
||||
|
||||
// Physical footprint is what Activity Monitor shows in its Memory column.
|
||||
task_vm_info_data_t vm_info {};
|
||||
mach_msg_type_number_t vm_info_count = TASK_VM_INFO_COUNT;
|
||||
if (task_info(mach_task_self(), TASK_VM_INFO, reinterpret_cast<task_info_t>(&vm_info),
|
||||
&vm_info_count) == KERN_SUCCESS) {
|
||||
stats.process_memory = vm_info.phys_footprint;
|
||||
stats.process_resident = vm_info.resident_size;
|
||||
stats.process_compressed = vm_info.compressed;
|
||||
stats.process_file_backed = vm_info.external;
|
||||
stats.process_virtual = vm_info.virtual_size;
|
||||
}
|
||||
rusage_info_v2 usage {};
|
||||
if (proc_pid_rusage(getpid(), RUSAGE_INFO_V2, reinterpret_cast<rusage_info_t*>(&usage)) == 0) {
|
||||
now.disk_read = usage.ri_diskio_bytesread;
|
||||
now.disk_write = usage.ri_diskio_byteswritten;
|
||||
}
|
||||
|
||||
stats.system_memory_total = SysctlValue<uint64_t>("hw.memsize");
|
||||
vm_size_t page = 0;
|
||||
vm_statistics64_data_t vm {};
|
||||
mach_msg_type_number_t count = HOST_VM_INFO64_COUNT;
|
||||
if (host_page_size(state.host, &page) == KERN_SUCCESS &&
|
||||
host_statistics64(state.host, HOST_VM_INFO64, reinterpret_cast<host_info64_t>(&vm),
|
||||
&count) == KERN_SUCCESS) {
|
||||
// Activity Monitor's "Memory Used": app memory + wired + compressed.
|
||||
const uint64_t app = vm.internal_page_count > vm.purgeable_count
|
||||
? vm.internal_page_count - vm.purgeable_count
|
||||
: 0;
|
||||
stats.system_memory_used = (app + vm.wire_count + vm.compressor_page_count) * page;
|
||||
stats.system_wired = static_cast<uint64_t>(vm.wire_count) * page;
|
||||
stats.system_compressed = static_cast<uint64_t>(vm.compressor_page_count) * page;
|
||||
stats.system_cached =
|
||||
(static_cast<uint64_t>(vm.external_page_count) + vm.purgeable_count) * page;
|
||||
stats.system_free = static_cast<uint64_t>(vm.free_count) * page;
|
||||
}
|
||||
|
||||
if (const auto swap = SysctlValue<xsw_usage>("vm.swapusage")) {
|
||||
stats.swap_used = swap->xsu_used;
|
||||
stats.swap_total = swap->xsu_total;
|
||||
}
|
||||
|
||||
// 1 = normal, 2 = warning, 4 = critical, as reported to dispatch memory-pressure sources.
|
||||
if (const auto level = SysctlValue<int>("kern.memorystatus_vm_pressure_level")) {
|
||||
stats.memory_pressure = *level >= 4 ? MemoryPressure::Critical
|
||||
: *level >= 2 ? MemoryPressure::Warning
|
||||
: MemoryPressure::Normal;
|
||||
}
|
||||
|
||||
// 0 nominal, 1 moderate, 2 heavy, 3 trapping, 4 sleeping.
|
||||
if (!state.thermal_ready) {
|
||||
state.thermal_ready = notify_register_check("com.apple.system.thermalpressurelevel",
|
||||
&state.thermal_token) == NOTIFY_STATUS_OK;
|
||||
}
|
||||
uint64_t thermal = 0;
|
||||
if (state.thermal_ready &&
|
||||
notify_get_state(state.thermal_token, &thermal) == NOTIFY_STATUS_OK) {
|
||||
stats.thermal_pressure = thermal >= 3 ? ThermalPressure::Critical
|
||||
: thermal == 2 ? ThermalPressure::Heavy
|
||||
: thermal == 1 ? ThermalPressure::Moderate
|
||||
: ThermalPressure::Nominal;
|
||||
}
|
||||
|
||||
SampleThreads(stats, now);
|
||||
SampleGpu(state, stats);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
using ProcValues = std::unordered_map<std::string, uint64_t>;
|
||||
|
||||
// Reads "<key>: <number> [kB]" lines; kB values are returned in bytes.
|
||||
ProcValues ReadProcValues(const char* path) {
|
||||
ProcValues values;
|
||||
std::ifstream file(path);
|
||||
std::string line;
|
||||
while (std::getline(file, line)) {
|
||||
const auto colon = line.find(':');
|
||||
if (colon == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
std::istringstream stream(line.substr(colon + 1));
|
||||
uint64_t number = 0;
|
||||
std::string unit;
|
||||
if (stream >> number) {
|
||||
stream >> unit;
|
||||
values[line.substr(0, colon)] = unit == "kB" ? number * 1024u : number;
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
std::optional<uint64_t> Find(const ProcValues& values, const char* key) {
|
||||
const auto it = values.find(key);
|
||||
return it != values.end() ? std::optional(it->second) : std::nullopt;
|
||||
}
|
||||
|
||||
struct PlatformState {};
|
||||
|
||||
void SampleThreads(HostStats& stats, Counters& now) {
|
||||
static const double ns_per_tick = 1e9 / static_cast<double>(std::max(sysconf(_SC_CLK_TCK), 1L));
|
||||
std::error_code error;
|
||||
uint32_t count = 0;
|
||||
for (std::filesystem::directory_iterator it("/proc/self/task", error), end; !error && it != end;
|
||||
it.increment(error)) {
|
||||
count++;
|
||||
std::ifstream stat_file(it->path() / "stat");
|
||||
std::string stat;
|
||||
uint64_t id = 0;
|
||||
if (!(stat_file >> id)) {
|
||||
continue; // The thread may have exited during enumeration.
|
||||
}
|
||||
std::getline(stat_file, stat);
|
||||
const auto close = stat.rfind(')');
|
||||
if (close == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
// Fields after the command name start at field 3 (state); utime and stime are 14 and 15.
|
||||
std::istringstream fields(stat.substr(close + 2));
|
||||
std::string field;
|
||||
for (int index = 3; index < 14 && fields >> field; index++) {
|
||||
}
|
||||
uint64_t user = 0;
|
||||
uint64_t system = 0;
|
||||
if (!(fields >> user >> system)) {
|
||||
continue;
|
||||
}
|
||||
auto& thread = now.threads[id];
|
||||
thread.cpu_ns = static_cast<uint64_t>(static_cast<double>(user + system) * ns_per_tick);
|
||||
std::ifstream name_file(it->path() / "comm");
|
||||
std::getline(name_file, thread.name);
|
||||
}
|
||||
if (!error) {
|
||||
stats.thread_count = count;
|
||||
}
|
||||
}
|
||||
|
||||
void SamplePlatform(PlatformState& /*state*/, HostStats& stats, Counters& now) {
|
||||
SampleRusage(now);
|
||||
|
||||
std::ifstream cpu_file("/proc/stat");
|
||||
std::string label;
|
||||
if (cpu_file >> label && label == "cpu") {
|
||||
uint64_t total = 0;
|
||||
uint64_t idle = 0;
|
||||
uint64_t value = 0;
|
||||
for (int index = 0; index < 8 && cpu_file >> value; index++) {
|
||||
total += value;
|
||||
if (index == 3 || index == 4) { // idle, iowait
|
||||
idle += value;
|
||||
}
|
||||
}
|
||||
now.system_total = total;
|
||||
now.system_busy = total - idle;
|
||||
}
|
||||
|
||||
const auto status = ReadProcValues("/proc/self/status");
|
||||
stats.process_memory = Find(status, "VmRSS");
|
||||
stats.process_resident = Find(status, "VmRSS");
|
||||
stats.process_file_backed = Find(status, "RssFile");
|
||||
stats.process_compressed = Find(status, "VmSwap");
|
||||
stats.process_virtual = Find(status, "VmSize");
|
||||
|
||||
const auto io = ReadProcValues("/proc/self/io");
|
||||
now.disk_read = Find(io, "read_bytes");
|
||||
now.disk_write = Find(io, "write_bytes");
|
||||
|
||||
const auto memory = ReadProcValues("/proc/meminfo");
|
||||
const auto total = Find(memory, "MemTotal");
|
||||
const auto available = Find(memory, "MemAvailable");
|
||||
if (total && available && *available <= *total) {
|
||||
stats.system_memory_total = total;
|
||||
stats.system_memory_used = *total - *available;
|
||||
}
|
||||
stats.system_free = Find(memory, "MemFree");
|
||||
if (const auto cached = Find(memory, "Cached")) {
|
||||
stats.system_cached = *cached + Find(memory, "Buffers").value_or(0);
|
||||
}
|
||||
const auto swap_total = Find(memory, "SwapTotal");
|
||||
const auto swap_free = Find(memory, "SwapFree");
|
||||
if (swap_total && swap_free && *swap_free <= *swap_total) {
|
||||
stats.swap_total = swap_total;
|
||||
stats.swap_used = *swap_total - *swap_free;
|
||||
}
|
||||
|
||||
// Pressure stall information: share of the last 10 s some task waited on memory.
|
||||
std::ifstream pressure("/proc/pressure/memory");
|
||||
std::string kind;
|
||||
std::string average;
|
||||
double stalled = 0.0;
|
||||
if (pressure >> kind >> average && kind == "some" && average.rfind("avg10=", 0) == 0 &&
|
||||
std::istringstream(average.substr(6)) >> stalled) {
|
||||
stats.memory_pressure = stalled >= 20.0 ? MemoryPressure::Critical
|
||||
: stalled >= 5.0 ? MemoryPressure::Warning
|
||||
: MemoryPressure::Normal;
|
||||
} else if (total && available && *total != 0) {
|
||||
const double free_share = static_cast<double>(*available) / static_cast<double>(*total);
|
||||
stats.memory_pressure = free_share < 0.10 ? MemoryPressure::Critical
|
||||
: free_share < 0.25 ? MemoryPressure::Warning
|
||||
: MemoryPressure::Normal;
|
||||
}
|
||||
|
||||
SampleThreads(stats, now);
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
constexpr size_t BUSIEST_THREADS = 6;
|
||||
|
||||
} // namespace
|
||||
|
||||
struct HostStatsSampler::Impl {
|
||||
PlatformState state;
|
||||
Counters last;
|
||||
uint32_t logical_cores = std::max(std::thread::hardware_concurrency(), 1u);
|
||||
bool has_last = false;
|
||||
};
|
||||
|
||||
HostStatsSampler::HostStatsSampler(): m_impl(std::make_unique<Impl>()) {}
|
||||
|
||||
HostStatsSampler::~HostStatsSampler() = default;
|
||||
|
||||
HostStats HostStatsSampler::Sample() {
|
||||
HostStats stats;
|
||||
stats.logical_cores = m_impl->logical_cores;
|
||||
|
||||
Counters now;
|
||||
now.wall_ns = WallNanoseconds();
|
||||
SamplePlatform(m_impl->state, stats, now);
|
||||
|
||||
const auto& last = m_impl->last;
|
||||
const auto elapsed = static_cast<double>(now.wall_ns - last.wall_ns) / 1e9;
|
||||
if (m_impl->has_last && elapsed > 0.0) {
|
||||
const auto rate = [&](std::optional<uint64_t> current,
|
||||
std::optional<uint64_t> previous) -> std::optional<double> {
|
||||
if (!current || !previous || *current < *previous) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return static_cast<double>(*current - *previous) / elapsed;
|
||||
};
|
||||
if (const auto cpu = rate(now.process_cpu_ns, last.process_cpu_ns)) {
|
||||
stats.process_cpu_percent =
|
||||
std::clamp(*cpu / 1e9 * 100.0 / stats.logical_cores, 0.0, 100.0);
|
||||
}
|
||||
if (now.system_total && last.system_total && now.system_busy && last.system_busy &&
|
||||
*now.system_total > *last.system_total && *now.system_busy >= *last.system_busy) {
|
||||
stats.system_cpu_percent =
|
||||
std::clamp(static_cast<double>(*now.system_busy - *last.system_busy) /
|
||||
static_cast<double>(*now.system_total - *last.system_total) * 100.0,
|
||||
0.0, 100.0);
|
||||
}
|
||||
stats.context_switches_per_s = rate(now.context_switches, last.context_switches);
|
||||
stats.page_faults_per_s = rate(now.page_faults, last.page_faults);
|
||||
stats.page_ins_per_s = rate(now.page_ins, last.page_ins);
|
||||
stats.disk_read_per_s = rate(now.disk_read, last.disk_read);
|
||||
stats.disk_write_per_s = rate(now.disk_write, last.disk_write);
|
||||
|
||||
for (const auto& [id, thread]: now.threads) {
|
||||
// A thread missing from the last sample started during this interval.
|
||||
const auto previous = last.threads.find(id);
|
||||
const uint64_t before = previous != last.threads.end() ? previous->second.cpu_ns : 0;
|
||||
if (thread.cpu_ns < before) {
|
||||
continue;
|
||||
}
|
||||
const double percent =
|
||||
static_cast<double>(thread.cpu_ns - before) / 1e9 / elapsed * 100.0;
|
||||
stats.busiest_threads.push_back(
|
||||
{thread.name.empty() ? "thread " + std::to_string(id) : thread.name, percent});
|
||||
}
|
||||
std::sort(stats.busiest_threads.begin(), stats.busiest_threads.end(),
|
||||
[](const auto& a, const auto& b) { return a.cpu_percent > b.cpu_percent; });
|
||||
if (stats.busiest_threads.size() > BUSIEST_THREADS) {
|
||||
stats.busiest_threads.resize(BUSIEST_THREADS);
|
||||
}
|
||||
}
|
||||
m_impl->last = std::move(now);
|
||||
m_impl->has_last = true;
|
||||
return stats;
|
||||
}
|
||||
|
||||
} // namespace Common
|
||||
@@ -0,0 +1,86 @@
|
||||
#ifndef KYTY_COMMON_HOST_STATS_H_
|
||||
#define KYTY_COMMON_HOST_STATS_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Common {
|
||||
|
||||
enum class MemoryPressure { Unknown, Normal, Warning, Critical };
|
||||
enum class ThermalPressure { Unknown, Nominal, Moderate, Heavy, Critical };
|
||||
|
||||
struct ThreadUsage {
|
||||
std::string name;
|
||||
double cpu_percent = 0.0; // Of one core.
|
||||
};
|
||||
|
||||
// Host-wide and process-wide resource usage. Missing values are std::nullopt; rates are per
|
||||
// second since the previous sample.
|
||||
struct HostStats {
|
||||
uint32_t logical_cores = 0;
|
||||
|
||||
// Processing.
|
||||
std::optional<double> process_cpu_percent; // Share of all logical cores, 0-100.
|
||||
std::optional<double> system_cpu_percent; // Whole machine, 0-100.
|
||||
std::optional<uint32_t> thread_count;
|
||||
std::vector<ThreadUsage> busiest_threads; // Busiest first.
|
||||
std::optional<double> context_switches_per_s;
|
||||
std::optional<double> page_faults_per_s;
|
||||
std::optional<double> page_ins_per_s; // Faults that read from disk.
|
||||
ThermalPressure thermal_pressure = ThermalPressure::Unknown;
|
||||
|
||||
// Emulator process memory.
|
||||
std::optional<uint64_t> process_memory; // Physical footprint / resident set.
|
||||
std::optional<uint64_t> process_resident;
|
||||
std::optional<uint64_t> process_compressed; // Compressed or swapped out.
|
||||
std::optional<uint64_t> process_file_backed;
|
||||
std::optional<uint64_t> process_virtual; // Reserved address space.
|
||||
|
||||
// System memory.
|
||||
std::optional<uint64_t> system_memory_used;
|
||||
std::optional<uint64_t> system_memory_total;
|
||||
std::optional<uint64_t> system_wired;
|
||||
std::optional<uint64_t> system_compressed;
|
||||
std::optional<uint64_t> system_cached; // File cache, reclaimable.
|
||||
std::optional<uint64_t> system_free;
|
||||
std::optional<uint64_t> swap_used;
|
||||
std::optional<uint64_t> swap_total;
|
||||
MemoryPressure memory_pressure = MemoryPressure::Unknown;
|
||||
|
||||
// Disk I/O of the emulator process.
|
||||
std::optional<double> disk_read_per_s;
|
||||
std::optional<double> disk_write_per_s;
|
||||
|
||||
// GPU, whole device (macOS IOAccelerator).
|
||||
std::string gpu_model;
|
||||
std::optional<uint32_t> gpu_cores;
|
||||
std::optional<double> gpu_percent;
|
||||
std::optional<double> gpu_renderer_percent;
|
||||
std::optional<double> gpu_tiler_percent;
|
||||
std::optional<uint64_t> gpu_memory_in_use; // System memory the GPU driver has in use.
|
||||
std::optional<uint64_t> gpu_memory_allocated;
|
||||
std::optional<uint64_t> gpu_recoveries; // GPU resets since boot.
|
||||
};
|
||||
|
||||
// May block for a few milliseconds (thread enumeration, OS registry reads), so call it off the
|
||||
// render thread. The first sample has no rates.
|
||||
class HostStatsSampler final {
|
||||
public:
|
||||
HostStatsSampler();
|
||||
~HostStatsSampler();
|
||||
HostStatsSampler(const HostStatsSampler&) = delete;
|
||||
HostStatsSampler& operator=(const HostStatsSampler&) = delete;
|
||||
|
||||
[[nodiscard]] HostStats Sample();
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> m_impl;
|
||||
};
|
||||
|
||||
} // namespace Common
|
||||
|
||||
#endif /* KYTY_COMMON_HOST_STATS_H_ */
|
||||
@@ -51,20 +51,26 @@ inline constexpr auto BrightWhite = fmt::fg(fmt::terminal_color::bright_white)
|
||||
|
||||
} // namespace Color
|
||||
|
||||
// Out of line, so callers on small guest stacks (fibers) do not reserve the formatting frame.
|
||||
template <typename... Args>
|
||||
[[gnu::noinline]] void Printf(fmt::text_style style, const char* format, const Args&... args) {
|
||||
Write(style, fmt::sprintf(format, args...));
|
||||
}
|
||||
|
||||
} // namespace Log
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||
#define LOGF(...) \
|
||||
do { \
|
||||
if (!::Log::IsSilent()) { \
|
||||
::Log::Write(::fmt::sprintf(__VA_ARGS__)); \
|
||||
::Log::Printf({}, __VA_ARGS__); \
|
||||
} \
|
||||
} while (false)
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||
#define LOGF_COLOR(style, ...) \
|
||||
do { \
|
||||
if (!::Log::IsSilent()) { \
|
||||
::Log::Write((style), ::fmt::sprintf(__VA_ARGS__)); \
|
||||
::Log::Printf((style), __VA_ARGS__); \
|
||||
} \
|
||||
} while (false)
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -104,7 +104,6 @@ public:
|
||||
uint32_t value);
|
||||
void PrepareCpuFlip(uint64_t request_id);
|
||||
void SynchronizeGpu();
|
||||
void EmitGlobalBarrier();
|
||||
void TriggerEopEventAtEndOfPipe(uint32_t interrupt_context_id);
|
||||
void DispatchDirect(uint32_t thread_group_x, uint32_t thread_group_y, uint32_t thread_group_z,
|
||||
uint32_t mode);
|
||||
|
||||
@@ -71,12 +71,6 @@ constexpr uint32_t NormalizeRegisterOffset(uint32_t raw_offset) {
|
||||
return raw_offset & ~RegisterSelectorMask;
|
||||
}
|
||||
|
||||
bool ReleaseMemGcrNeedsBarrier(uint32_t eop_event_type, uint32_t gcr_cntl) {
|
||||
return eop_event_type != 0x28u ||
|
||||
(gcr_cntl & (GcrGl2MetadataInvalidate | GcrGl0VectorInvalidate | GcrGl1Invalidate |
|
||||
GcrGl2Invalidate | GcrGl2Writeback)) != 0;
|
||||
}
|
||||
|
||||
uint32_t ReleaseMemCacheActionFromGcr(uint32_t gcr_cntl) {
|
||||
return ((gcr_cntl & GcrGl2Writeback) != 0 ? 0x38u : 0x00u);
|
||||
}
|
||||
@@ -291,10 +285,6 @@ static void HwCtxIgnoreAaMaskRegister([[maybe_unused]] uint32_t cmd_offset,
|
||||
|
||||
static void HwCtxIgnoreAlphaToMaskRegister([[maybe_unused]] uint32_t value) {}
|
||||
|
||||
static void HwCtxIgnoreDisabledUserClipPlane(CommandProcessor& cp, uint32_t value) {
|
||||
EXIT_NOT_IMPLEMENTED(value != 0 || cp.GetCtx().GetClipControl().user_clip_planes != 0);
|
||||
}
|
||||
|
||||
static void HwCtxIgnorePrimitiveIdReset([[maybe_unused]] uint32_t value) {}
|
||||
|
||||
static void HwCtxIgnoreVtxControl([[maybe_unused]] uint32_t value) {}
|
||||
@@ -2263,30 +2253,14 @@ KYTY_CP_OP_PARSER(CpOpReleaseMem) {
|
||||
}
|
||||
};
|
||||
|
||||
if (data_sel == 0 || interrupt_selector == 4) {
|
||||
if (eop_event_type != 0x28 || gcr_cntl != 0) {
|
||||
cp.EmitGlobalBarrier();
|
||||
}
|
||||
|
||||
// Cache actions only order GPU work, which the renderer's resource barriers already order.
|
||||
if (data_sel == 0 || interrupt_selector == 4 ||
|
||||
(release_dst == ReleaseMemDstMemory && dst_gpu_addr == nullptr)) {
|
||||
trigger_interrupt();
|
||||
|
||||
return 7;
|
||||
}
|
||||
|
||||
if (release_dst == ReleaseMemDstMemory && dst_gpu_addr == nullptr) {
|
||||
if (eop_event_type != 0x28 || gcr_cntl != 0) {
|
||||
cp.EmitGlobalBarrier();
|
||||
}
|
||||
|
||||
trigger_interrupt();
|
||||
|
||||
return 7;
|
||||
}
|
||||
|
||||
if (ReleaseMemGcrNeedsBarrier(eop_event_type, gcr_cntl)) {
|
||||
cp.EmitGlobalBarrier();
|
||||
}
|
||||
|
||||
auto cache_action = ReleaseMemCacheActionFromGcr(gcr_cntl);
|
||||
cache_policy = 0;
|
||||
|
||||
@@ -2899,7 +2873,7 @@ void GraphicsInitJmpTablesCxIndirect() {
|
||||
|
||||
for (auto cmd_offset = Pm4::PA_CL_UCP_0_X; cmd_offset <= Pm4::PA_CL_UCP_5_W; cmd_offset++) {
|
||||
g_hw_ctx_indirect_func[cmd_offset] = [](KYTY_HW_CTX_INDIRECT_ARGS) {
|
||||
HwCtxIgnoreDisabledUserClipPlane(cp, value);
|
||||
cp.GetCtx().SetUserClipPlane(cmd_offset - Pm4::PA_CL_UCP_0_X, value);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -607,7 +607,6 @@ bool GuestGpu::Process(Submission& submission) {
|
||||
case SubmissionType::SuspendPoint:
|
||||
// Age caches at the frame boundary, not once per command list.
|
||||
m_renderer.RunGarbageCollector();
|
||||
cp.EmitGlobalBarrier();
|
||||
m_renderer.GetCommandScheduler().DeferPriorityOperation(
|
||||
[ready = m_suspend_point_ready] { ready->release(); });
|
||||
cp.BufferFlush();
|
||||
@@ -906,6 +905,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);
|
||||
@@ -1221,22 +1221,6 @@ void CommandProcessor::WriteAtEndOfPipe64(uint32_t cache_policy, uint32_t event_
|
||||
interrupt_context_id);
|
||||
}
|
||||
|
||||
void CommandProcessor::EmitGlobalBarrier() {
|
||||
Common::LockGuard lock(m_renderer.GetMutex());
|
||||
|
||||
vk::MemoryBarrier2 barrier {};
|
||||
barrier.srcStageMask = vk::PipelineStageFlagBits2::eAllCommands;
|
||||
barrier.srcAccessMask = vk::AccessFlagBits2::eMemoryWrite;
|
||||
barrier.dstStageMask = vk::PipelineStageFlagBits2::eAllCommands;
|
||||
barrier.dstAccessMask = vk::AccessFlagBits2::eMemoryRead | vk::AccessFlagBits2::eMemoryWrite;
|
||||
|
||||
vk::DependencyInfo dependency {};
|
||||
dependency.memoryBarrierCount = 1;
|
||||
dependency.pMemoryBarriers = &barrier;
|
||||
GetScheduler().EndRendering();
|
||||
CurrentBuffer().Handle().pipelineBarrier2(dependency);
|
||||
}
|
||||
|
||||
void CommandProcessor::TriggerEopEventAtEndOfPipe(uint32_t interrupt_context_id) {
|
||||
(void)CurrentBuffer().Handle();
|
||||
GetScheduler().DeferPriorityOperation(
|
||||
@@ -1256,21 +1240,17 @@ void CommandProcessor::TriggerEvent(uint32_t event_type, uint32_t event_index,
|
||||
}
|
||||
|
||||
const auto valid_cache_event_index = event_index == 0x00000000 || event_index == 0x00000007;
|
||||
// Partial flushes and cache writebacks only order GPU work, which the renderer's resource
|
||||
// barriers already order.
|
||||
switch (event_type) {
|
||||
// CsPartialFlush, GsPartialFlush, PsPartialFlush.
|
||||
case 0x00000007:
|
||||
case 0x0000000f:
|
||||
case 0x00000010: EmitGlobalBarrier(); break;
|
||||
// CbDbDataWritebackInvalidate, CbDataWritebackInvalidate.
|
||||
case 0x00000010: break;
|
||||
// CbDbDataWritebackInvalidate, CbDataWritebackInvalidate, DbDataWritebackInvalidate,
|
||||
// DbMetadataWritebackInvalidate, CbMetadataWritebackInvalidate.
|
||||
case 0x00000016:
|
||||
case 0x00000031:
|
||||
if (!valid_cache_event_index) {
|
||||
EXIT("unknown event type: 0x%08" PRIx32 ", 0x%08" PRIx32 "\n", event_type,
|
||||
event_index);
|
||||
}
|
||||
EmitGlobalBarrier();
|
||||
break;
|
||||
// DbDataWritebackInvalidate, DbMetadataWritebackInvalidate, CbMetadataWritebackInvalidate.
|
||||
case 0x0000002a:
|
||||
case 0x0000002c:
|
||||
case 0x0000002e:
|
||||
@@ -1278,7 +1258,6 @@ void CommandProcessor::TriggerEvent(uint32_t event_type, uint32_t event_index,
|
||||
EXIT("unknown event type: 0x%08" PRIx32 ", 0x%08" PRIx32 "\n", event_type,
|
||||
event_index);
|
||||
}
|
||||
EmitGlobalBarrier();
|
||||
break;
|
||||
case 0x0000000d:
|
||||
case 0x0000000e:
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include "common/common.h"
|
||||
#include "graphics/guest_gpu/gpu_defs.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace Libs::Graphics::HW {
|
||||
|
||||
struct ColorBase {
|
||||
@@ -824,6 +826,11 @@ public:
|
||||
void SetBlendColor(const BlendColor& color) { m_blend_color = color; }
|
||||
[[nodiscard]] const ClipControl& GetClipControl() const { return m_clip_control; }
|
||||
void SetClipControl(const ClipControl& control) { m_clip_control = control; }
|
||||
// PA_CL_UCP_0_X..PA_CL_UCP_5_W as raw float bits, four per plane.
|
||||
[[nodiscard]] const std::array<uint32_t, 24>& GetUserClipPlanes() const {
|
||||
return m_user_clip_planes;
|
||||
}
|
||||
void SetUserClipPlane(uint32_t index, uint32_t value) { m_user_clip_planes[index] = value; }
|
||||
[[nodiscard]] const RenderControl& GetRenderControl() const { return m_render_control; }
|
||||
void SetRenderControl(const RenderControl& control) { m_render_control = control; }
|
||||
[[nodiscard]] const DepthRenderOverride& GetDepthRenderOverride() const {
|
||||
@@ -924,6 +931,8 @@ private:
|
||||
ColorControl m_color_control;
|
||||
ScanModeControl m_scan_mode_control;
|
||||
|
||||
std::array<uint32_t, 24> m_user_clip_planes {};
|
||||
|
||||
AaSampleControl m_aa_sample_control;
|
||||
AaConfig m_aa_config;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <span>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
#include <vk_mem_alloc.h>
|
||||
@@ -99,6 +100,14 @@ struct GraphicContext {
|
||||
[[nodiscard]] bool CanReportMemoryUsage() const noexcept { return memory_budget_ext_enabled; }
|
||||
[[nodiscard]] uint64_t GetDeviceMemoryUsage() const;
|
||||
[[nodiscard]] uint64_t GetTotalMemoryBudget() const;
|
||||
|
||||
struct HeapUsage {
|
||||
uint64_t usage = 0; // Whole process, as reported by VK_EXT_memory_budget.
|
||||
uint64_t budget = 0;
|
||||
uint32_t allocations = 0;
|
||||
bool device_local = false;
|
||||
};
|
||||
[[nodiscard]] uint32_t GetHeapUsage(std::span<HeapUsage, VK_MAX_MEMORY_HEAPS> heaps) const;
|
||||
[[nodiscard]] bool CreateImage(const vk::ImageCreateInfo& info, VulkanImage& image);
|
||||
void DeleteImage(VulkanImage& image);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "common/alignment.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/debugCounters.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/profiler.h"
|
||||
#include "graphics/guest_gpu/graphicsRun.h"
|
||||
@@ -177,6 +178,7 @@ bool BufferCache::DownloadBufferMemory(Buffer& buffer, uint64_t vaddr, uint64_t
|
||||
if (copies.empty()) {
|
||||
return false;
|
||||
}
|
||||
Common::DebugCounters::Add(Common::DebugCounters::Counter::BufferDownloadBytes, total_size);
|
||||
|
||||
auto [mapped, offset] = m_download_buffer.Map(total_size, 64);
|
||||
std::unique_ptr<Buffer> temporary;
|
||||
@@ -254,6 +256,10 @@ BufferCache::BufferCache(GraphicContext& graphics, CommandScheduler& scheduler,
|
||||
m_gds_buffer.Flush(0, m_gds_buffer.Size());
|
||||
SetVulkanObjectNameF(m_graphics.device, m_bda_pagetable_buffer.Handle(),
|
||||
"BDA Page Table Buffer");
|
||||
m_scheduler.BeginCommand();
|
||||
for (auto* buffer: {&m_bda_pagetable_buffer, m_fault_manager.GetFaultBuffer()}) {
|
||||
buffer->Fill(0, buffer->Size(), 0);
|
||||
}
|
||||
const auto null_id =
|
||||
m_slot_buffers.insert(m_graphics, m_scheduler, MemoryUsage::DeviceLocal, 0, AllFlags, 16);
|
||||
EXIT_IF(null_id != NULL_BUFFER_ID);
|
||||
@@ -467,6 +473,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 +665,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
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -326,7 +327,7 @@ void CommandScheduler::CheckActive() const {
|
||||
}
|
||||
|
||||
CommandBuffer& CommandScheduler::Current() {
|
||||
CheckActive();
|
||||
EXIT_IF(m_command.IsInvalid());
|
||||
return m_command;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -104,8 +104,14 @@ void CommandBuffer::BeginRendering(const RenderState& state) const {
|
||||
rendering.pDepthAttachment = depth_stencil.has_depth ? &depth : nullptr;
|
||||
rendering.pStencilAttachment = depth_stencil.has_stencil ? &stencil : nullptr;
|
||||
Handle().beginRendering(rendering);
|
||||
// The open pass holds the cleared contents, so a later clear begins a new pass.
|
||||
m_render_state = state;
|
||||
m_rendering = true;
|
||||
for (auto& color: m_render_state.color_attachments) {
|
||||
color.is_clear = false;
|
||||
}
|
||||
m_render_state.depth_stencil_attachment.depth_clear = false;
|
||||
m_render_state.depth_stencil_attachment.stencil_clear = false;
|
||||
m_rendering = true;
|
||||
}
|
||||
|
||||
void CommandBuffer::EndRendering() const {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,10 +373,9 @@ static void ClipPrint(const char* func, const HW::ClipControl& c) {
|
||||
static void ClipCheck(const HW::ClipControl& c) {
|
||||
// dx_linear_attr_clip_enable preserves linear (noperspective) attributes at clip-generated
|
||||
// vertices, which Vulkan provides as part of clipping and interpolation.
|
||||
EXIT_NOT_IMPLEMENTED(c.user_clip_planes != 0 || c.user_clip_plane_mode != 0 ||
|
||||
c.vertex_kill_any || c.user_clip_plane_negate_y ||
|
||||
c.user_clip_plane_cull_only || c.cull_on_clipping_error_disable ||
|
||||
c.force_viewport_index_from_vs_enable);
|
||||
EXIT_NOT_IMPLEMENTED(c.user_clip_plane_mode != 0 || c.vertex_kill_any ||
|
||||
c.user_clip_plane_negate_y || c.user_clip_plane_cull_only ||
|
||||
c.cull_on_clipping_error_disable || c.force_viewport_index_from_vs_enable);
|
||||
}
|
||||
|
||||
static void RcPrint(const char* func, const HW::RenderControl& c) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -143,7 +143,8 @@ NativeStorageBuffer(RenderContext& context, const PreparedBindings::BufferSource
|
||||
return result;
|
||||
}
|
||||
|
||||
bool IsSupportedDepthTextureEncoding(const ShaderTextureResource& descriptor, bool r128) {
|
||||
// Reserved and unsupported T# fields must be zero; perf_mod only scales sampler state.
|
||||
static bool IsCanonicalTextureEncoding(const ShaderTextureResource& descriptor, bool r128) {
|
||||
constexpr uint32_t field1_reserved_mask = 0x200fff00u;
|
||||
constexpr uint32_t field2_reserved_mask = 0xf0003000u;
|
||||
const uint32_t field3_expected = descriptor.DstSelXYZW() |
|
||||
@@ -154,17 +155,25 @@ bool IsSupportedDepthTextureEncoding(const ShaderTextureResource& descriptor, bo
|
||||
const uint32_t field4_expected = descriptor.Depth() | (descriptor.BaseArray5() << 16u);
|
||||
const uint32_t field5_expected = (static_cast<uint32_t>(descriptor.PerfMod5()) << 20u) |
|
||||
(static_cast<uint32_t>(descriptor.MaxMip()) << 4u);
|
||||
const bool common = (descriptor.fields[1] & field1_reserved_mask) == 0 &&
|
||||
(descriptor.fields[2] & field2_reserved_mask) == 0 &&
|
||||
descriptor.fields[3] == field3_expected;
|
||||
if (r128) {
|
||||
return common && descriptor.fields[4] == 0 && descriptor.fields[5] == 0 &&
|
||||
descriptor.fields[6] == 0 && descriptor.fields[7] == 0;
|
||||
if ((descriptor.fields[1] & field1_reserved_mask) != 0 ||
|
||||
(descriptor.fields[2] & field2_reserved_mask) != 0 || descriptor.fields[3] != field3_expected) {
|
||||
return false;
|
||||
}
|
||||
const bool full = common && descriptor.fields[4] == field4_expected &&
|
||||
descriptor.fields[5] == field5_expected;
|
||||
if (!full ||
|
||||
(descriptor.MsaaDepth() && !IsMultisampledTexture(descriptor.Type()))) {
|
||||
if (r128) {
|
||||
return descriptor.fields[4] == 0 && descriptor.fields[5] == 0 && descriptor.fields[6] == 0 &&
|
||||
descriptor.fields[7] == 0;
|
||||
}
|
||||
return descriptor.fields[4] == field4_expected && descriptor.fields[5] == field5_expected;
|
||||
}
|
||||
|
||||
bool IsSupportedDepthTextureEncoding(const ShaderTextureResource& descriptor, bool r128) {
|
||||
if (!IsCanonicalTextureEncoding(descriptor, r128)) {
|
||||
return false;
|
||||
}
|
||||
if (r128) {
|
||||
return true;
|
||||
}
|
||||
if (descriptor.MsaaDepth() && !IsMultisampledTexture(descriptor.Type())) {
|
||||
return false;
|
||||
}
|
||||
const auto metadata_control = descriptor.fields[6] & 0x00ffffffu;
|
||||
@@ -281,36 +290,12 @@ static bool IsSupportedStorageTextureDescriptor(const ShaderRecompiler::IR::Imag
|
||||
!descriptor.MsaaDepth();
|
||||
}
|
||||
|
||||
static bool IsSupportedStorageTextureEncoding(const ShaderRecompiler::IR::ImageResource& resource,
|
||||
const ShaderTextureResource& descriptor) {
|
||||
constexpr uint32_t field1_reserved_mask = 0x200fff00u;
|
||||
constexpr uint32_t field2_reserved_mask = 0xf0003000u;
|
||||
constexpr uint32_t field5_expected = 0x00700000u;
|
||||
constexpr uint32_t field5_max_mip_mask = 0x000000f0u;
|
||||
const uint32_t expected_field3 = descriptor.DstSelXYZW() |
|
||||
(static_cast<uint32_t>(descriptor.BaseLevel()) << 12u) |
|
||||
(static_cast<uint32_t>(descriptor.LastLevel()) << 16u) |
|
||||
(static_cast<uint32_t>(descriptor.TileMode()) << 20u) |
|
||||
(static_cast<uint32_t>(descriptor.Type()) << 28u);
|
||||
const uint32_t expected_field4 =
|
||||
descriptor.Depth() | (static_cast<uint32_t>(descriptor.BaseArray5()) << 16u);
|
||||
const bool common = (descriptor.fields[1] & field1_reserved_mask) == 0 &&
|
||||
(descriptor.fields[2] & field2_reserved_mask) == 0 &&
|
||||
descriptor.fields[3] == expected_field3;
|
||||
if (resource.r128) {
|
||||
return common && descriptor.fields[4] == 0 && descriptor.fields[5] == 0 &&
|
||||
descriptor.fields[6] == 0 && descriptor.fields[7] == 0;
|
||||
}
|
||||
return common && descriptor.fields[4] == expected_field4 &&
|
||||
(descriptor.fields[5] & ~field5_max_mip_mask) == field5_expected;
|
||||
}
|
||||
|
||||
void ValidateStorageTexture(const ShaderRecompiler::IR::ImageResource& resource,
|
||||
const ShaderTextureResource& descriptor, uint64_t size) {
|
||||
const auto format = descriptor.Format();
|
||||
const bool resource_ok = IsSupportedStorageImageResource(resource);
|
||||
const bool descriptor_ok = IsSupportedStorageTextureDescriptor(resource, descriptor);
|
||||
const bool encoding_ok = IsSupportedStorageTextureEncoding(resource, descriptor);
|
||||
const bool encoding_ok = IsCanonicalTextureEncoding(descriptor, resource.r128);
|
||||
const bool uint_resource = resource.numeric_class == Prospero::TextureNumericClass::Uint;
|
||||
const bool raw_sint_storage = uint_resource &&
|
||||
ShaderRecompiler::IR::RawSignedStorageFormat(resource, format) != Prospero::BufferFormat::kInvalid;
|
||||
@@ -740,6 +725,12 @@ void RenderExecutor::FindBuffers(std::span<PreparedBindings* const> stages) {
|
||||
const auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(snapshot.buffers[resource]);
|
||||
const auto address = descriptor.Base48();
|
||||
auto size = descriptor.GetSize();
|
||||
if (size != 0 && descriptor.Stride() == 0 && descriptor.OutOfBounds() == 2 &&
|
||||
!metadata.scalar) {
|
||||
// OOB_SELECT=2 only tests NumRecords != 0; a zero-stride fetch
|
||||
// still needs its full payload when NumRecords is smaller than it.
|
||||
size = std::max(size, uint64_t {metadata.max_byte_extent});
|
||||
}
|
||||
if (address == 0 || size == 0) {
|
||||
source = {};
|
||||
continue;
|
||||
@@ -757,14 +748,18 @@ void RenderExecutor::FindBuffers(std::span<PreparedBindings* const> stages) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const auto& graphics = m_context.GetGraphics();
|
||||
const auto limit = uint64_t {graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange} -
|
||||
(graphics.StorageMinAlignment() - 1);
|
||||
size = std::min({size, uint64_t {256} * 1024 * 1024, limit});
|
||||
}
|
||||
// PPSA15246
|
||||
const auto& graphics = m_context.GetGraphics();
|
||||
const auto limit =
|
||||
uint64_t {graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange} -
|
||||
(graphics.StorageMinAlignment() - 1);
|
||||
size = std::min({size, uint64_t {256} * 1024 * 1024, limit});
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
size = Libs::LibKernel::Memory::ClampRangeSize(address, size);
|
||||
source = {address, size, cache.FindBuffer(address, size)};
|
||||
}
|
||||
}
|
||||
@@ -946,8 +941,12 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer,
|
||||
for (const auto& resource: program.info.buffers) {
|
||||
if (!resource.written || resource.descriptor_index == UINT32_MAX) continue;
|
||||
const auto& written = writer->buffer_sources[resource.descriptor_index];
|
||||
if (written.size != 0 && ImageRangeOverlaps(address, size,
|
||||
written.address, written.size)) {
|
||||
// ADD_TID and swizzle move fixed-offset writes past write_extent.
|
||||
const bool linear = (resource.packed_stride & ((1u << 14u) | (1u << 20u))) == 0u;
|
||||
const auto written_size =
|
||||
linear ? std::min<uint64_t>(written.size, resource.write_extent) : written.size;
|
||||
if (written_size != 0 && ImageRangeOverlaps(address, size,
|
||||
written.address, written_size)) {
|
||||
EXIT("scalar resource reads overlap a shader buffer write\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -764,10 +780,12 @@ PipelineCache::GraphicsPrograms PipelineCache::GetGraphicsPrograms(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (context.GetClipControl().clip_disable) {
|
||||
auto& last_stage = vertex_info[tess_active ? 2u : 0u];
|
||||
const auto& clip_control = context.GetClipControl();
|
||||
if (clip_control.clip_disable) {
|
||||
const auto& viewport = context.GetScreenViewport().viewports[0];
|
||||
const auto& limits = m_graphics.GetPhysicalDeviceProperties().limits;
|
||||
auto& clip = vertex_info[tess_active ? 2u : 0u].clip_space;
|
||||
auto& clip = last_stage.clip_space;
|
||||
clip.scale[0] = viewport.xscale;
|
||||
clip.scale[1] = viewport.yscale;
|
||||
clip.offset[0] = viewport.xoffset;
|
||||
@@ -777,9 +795,16 @@ PipelineCache::GraphicsPrograms PipelineCache::GetGraphicsPrograms(
|
||||
clip.half_extent[1] =
|
||||
static_cast<float>(std::min(limits.maxViewportDimensions[1], 16384u)) * 0.5f;
|
||||
clip.enabled = true;
|
||||
} else if (clip_control.user_clip_planes != 0) {
|
||||
// CLIP_DISABLE also disables user planes. How they combine with shader clip and cull
|
||||
// distances is unverified; draw push constants only reach VS/mesh and PS, and the
|
||||
// generated RECTLIST stages do not forward clip distances.
|
||||
EXIT_NOT_IMPLEMENTED((last_stage.pa_cl_vs_out_cntl & 0xffffu) != 0 || tess_active ||
|
||||
Prospero::IsRectList(user_config.GetPrimType()));
|
||||
last_stage.user_clip_planes = clip_control.user_clip_planes;
|
||||
}
|
||||
uint32_t push_data_cursor = ShaderRecompiler::IR::PushData::GraphicsDrawDwords(
|
||||
mesh_active, object_primitive_id);
|
||||
mesh_active, object_primitive_id, last_stage.user_clip_planes);
|
||||
GraphicsPrograms result;
|
||||
if (pixel_active) {
|
||||
result.pixel = m_program_cache->Get(pixel_params, pixel_info, push_data_cursor);
|
||||
@@ -993,8 +1018,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 +1054,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);
|
||||
|
||||
@@ -92,7 +92,8 @@ bool HasShaderBufferWrites(const ShaderStageRuntime& runtime) {
|
||||
const auto& program = *runtime.program;
|
||||
const auto& resources = *runtime.resources;
|
||||
EXIT_IF(resources.buffers.size() != program.info.buffers.size());
|
||||
bool has_writes = false;
|
||||
// Address writes reach memory outside the bound buffer descriptors.
|
||||
bool has_writes = program.has_address_writes;
|
||||
for (uint32_t i = 0; i < program.info.buffers.size(); i++) {
|
||||
if (!program.info.buffers[i].written) {
|
||||
continue;
|
||||
|
||||
@@ -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 {};
|
||||
@@ -626,6 +636,9 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
EXIT_NOT_IMPLEMENTED(pipeline.pipeline_layout == nullptr);
|
||||
|
||||
vk::ComputePipelineCreateInfo info {};
|
||||
if (input_info.stage.program->info.uses_gds) {
|
||||
info.flags = vk::PipelineCreateFlagBits::eDispatchBase;
|
||||
}
|
||||
info.stage = comp_shader_stage_info;
|
||||
info.layout = pipeline.pipeline_layout;
|
||||
info.basePipelineIndex = -1;
|
||||
|
||||
@@ -201,11 +201,11 @@ private:
|
||||
void BindImage(ImageId id, bool storage);
|
||||
void BindRenderTarget(ImageId id);
|
||||
void ResetBindings();
|
||||
void UpdateComputeMetaClears(const ShaderComputeInputInfo& input,
|
||||
const CommandBuffer& buffer);
|
||||
[[nodiscard]] bool TryConsumeComputeImageClear(const ShaderComputeInputInfo& input,
|
||||
CommandBuffer& command, uint32_t group_x,
|
||||
uint32_t group_y, uint32_t group_z, uint32_t mode);
|
||||
[[nodiscard]] bool TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
|
||||
const CommandBuffer& buffer);
|
||||
[[nodiscard]] bool TryConsumeComputeFill(const ShaderComputeInputInfo& input,
|
||||
CommandBuffer& command, uint32_t group_x,
|
||||
uint32_t group_y, uint32_t group_z, uint32_t mode);
|
||||
|
||||
RenderContext& m_context;
|
||||
GraphicsBindings m_graphics_bindings;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "common/assert.h"
|
||||
#include "common/common.h"
|
||||
#include "common/debugCounters.h"
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/file.h"
|
||||
#include "common/logging/log.h"
|
||||
@@ -50,8 +51,8 @@ static bool FillSourcesDisjoint(std::span<const ShaderRecompiler::IR::Descriptor
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderExecutor::UpdateComputeMetaClears(const ShaderComputeInputInfo& input,
|
||||
const CommandBuffer& buffer) {
|
||||
bool RenderExecutor::TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
|
||||
const CommandBuffer& buffer) {
|
||||
const auto& program = *input.stage.program;
|
||||
const auto& resources = *input.stage.resources;
|
||||
if (resources.buffers.size() != program.info.buffers.size()) {
|
||||
@@ -61,9 +62,10 @@ void RenderExecutor::UpdateComputeMetaClears(const ShaderComputeInputInfo& input
|
||||
for (uint32_t i = 0; i < program.info.buffers.size(); i++) {
|
||||
const auto& resource = program.info.buffers[i];
|
||||
const auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(resources.buffers[i]);
|
||||
// Reads and transforms of metadata do not establish a clear state.
|
||||
// A metadata resource that is also read is not proven to be a full overwrite. Execute it
|
||||
// conservatively instead of replacing the dispatch with a coarse full-surface clear.
|
||||
if ((!resource.written || resource.read) && cache.IsMeta(descriptor.Base48())) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,10 +75,13 @@ void RenderExecutor::UpdateComputeMetaClears(const ShaderComputeInputInfo& input
|
||||
if (resource.written) {
|
||||
const auto descriptor =
|
||||
DecodeNativeDescriptor<ShaderBufferResource>(resources.buffers[i]);
|
||||
(void)cache.ClearMeta(descriptor.Base48());
|
||||
if (cache.ClearMeta(descriptor.Base48())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t group_x,
|
||||
@@ -127,9 +132,9 @@ bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t grou
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderExecutor::TryConsumeComputeImageClear(const ShaderComputeInputInfo& input,
|
||||
CommandBuffer& command, uint32_t group_x,
|
||||
uint32_t group_y, uint32_t group_z, uint32_t mode) {
|
||||
bool RenderExecutor::TryConsumeComputeFill(const ShaderComputeInputInfo& input,
|
||||
CommandBuffer& command, uint32_t group_x,
|
||||
uint32_t group_y, uint32_t group_z, uint32_t mode) {
|
||||
const auto& program = *input.stage.program;
|
||||
const auto& resources = *input.stage.resources;
|
||||
const auto& fill = resources.uniform_fill;
|
||||
@@ -185,14 +190,26 @@ bool RenderExecutor::TryConsumeComputeImageClear(const ShaderComputeInputInfo& i
|
||||
size)) {
|
||||
return false;
|
||||
}
|
||||
if (!cache.ClearImageFromBuffer(command, descriptor.Base48(), size, packed_clear)) {
|
||||
return false;
|
||||
const bool image_clear =
|
||||
cache.ClearImageFromBuffer(command, descriptor.Base48(), size, packed_clear);
|
||||
if (!image_clear) {
|
||||
// CPU fills invalidate overlapping cached images. Preserve the native dispatch for
|
||||
// partial or ambiguous image backings, including pooled metadata aliases.
|
||||
if ((descriptor.Base48() & 3u) != 0 ||
|
||||
!m_context.IsMapped(descriptor.Base48(), size) ||
|
||||
cache.IsRegionRegistered(descriptor.Base48(), size)) {
|
||||
return false;
|
||||
}
|
||||
// Metadata need not have an image backing yet. Keep proven fills in guest memory
|
||||
// when CPU-owned, so color-clear discovery can read them without a GPU download.
|
||||
m_context.GetBufferCache().FillBuffer(descriptor.Base48(), size, packed_clear, false);
|
||||
}
|
||||
static std::atomic<uint32_t> logged_clears {0};
|
||||
if (logged_clears.fetch_add(1, std::memory_order_relaxed) < 32) {
|
||||
LOGF("GraphicsRenderDispatchDirect: compute image clear shader=0x%016" PRIx64
|
||||
LOGF("GraphicsRenderDispatchDirect: compute %s shader=0x%016" PRIx64
|
||||
" addr=0x%016" PRIx64 " size=0x%016" PRIx64 " value=0x%08" PRIx32 "\n",
|
||||
input.stage.program->shader_hash, descriptor.Base48(), size, packed_clear);
|
||||
image_clear ? "image clear" : "buffer fill", input.stage.program->shader_hash,
|
||||
descriptor.Base48(), size, packed_clear);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -296,6 +313,7 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
}
|
||||
const auto compute_program =
|
||||
m_context.GetPipelineCache().GetComputeProgram(cs_regs, sh_regs, input_info);
|
||||
if (!compute_program) return;
|
||||
if (use_thread_dimensions) {
|
||||
input_info.dispatch_threads_num[0] = thread_group_x;
|
||||
input_info.dispatch_threads_num[1] = thread_group_y;
|
||||
@@ -304,14 +322,12 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
|
||||
const auto& program = *input_info.stage.program;
|
||||
const auto& resources = *input_info.stage.resources;
|
||||
if (resources.specialization_reads.empty()) {
|
||||
// Cached metadata addresses can be reused as ordinary buffers; preserve their guest writes.
|
||||
UpdateComputeMetaClears(input_info, buffer);
|
||||
if (TryConsumeComputeImageClear(input_info, buffer, thread_group_x, thread_group_y,
|
||||
thread_group_z, mode)) {
|
||||
ResetBindings();
|
||||
return;
|
||||
}
|
||||
if (resources.specialization_reads.empty() &&
|
||||
(TryConsumeComputeMetaClear(input_info, buffer) ||
|
||||
TryConsumeComputeFill(input_info, buffer, thread_group_x, thread_group_y,
|
||||
thread_group_z, mode))) {
|
||||
ResetBindings();
|
||||
return;
|
||||
}
|
||||
|
||||
if (use_thread_dimensions) {
|
||||
@@ -431,7 +447,28 @@ 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);
|
||||
constexpr uint32_t DISPATCH_INITIATOR_ORDERED_APPEND_ENABLE = 1u << 3u;
|
||||
const uint64_t group_threads = uint64_t {std::max(input_info.threads_num[0], 1u)} *
|
||||
std::max(input_info.threads_num[1], 1u) *
|
||||
std::max(input_info.threads_num[2], 1u);
|
||||
if ((mode & DISPATCH_INITIATOR_ORDERED_APPEND_ENABLE) != 0 && program.info.uses_gds &&
|
||||
group_threads <= input_info.wave_size && !input_info.lds_storage) {
|
||||
// A single guest wave per group can run in launch order without a device-wide
|
||||
// spinlock. Keep the guest workgroup IDs when splitting the dispatch.
|
||||
for (uint32_t z = 0; z < thread_group_z; ++z) {
|
||||
for (uint32_t y = 0; y < thread_group_y; ++y) {
|
||||
for (uint32_t x = 0; x < thread_group_x; ++x) {
|
||||
if ((x | y | z) != 0u) {
|
||||
ShaderAccessBarrier(vk_buffer, vk::PipelineStageFlagBits::eComputeShader);
|
||||
}
|
||||
vk_buffer.dispatchBase(x, y, z, 1, 1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
vk_buffer.dispatch(thread_group_x, thread_group_y, thread_group_z);
|
||||
}
|
||||
Common::DebugCounters::Add(Common::DebugCounters::Counter::Dispatches);
|
||||
|
||||
// The removed host fence also ordered read-only dispatches before later writers.
|
||||
ShaderAccessBarrier(vk_buffer, vk::PipelineStageFlagBits::eComputeShader);
|
||||
@@ -462,6 +499,7 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
input_info.indirect_args_address = args_addr;
|
||||
const auto compute_program = m_context.GetPipelineCache().GetComputeProgram(
|
||||
cs_regs, buffer.GetRegisters().GetShaderRegisters(), input_info);
|
||||
if (!compute_program) return;
|
||||
buffer.EndRendering();
|
||||
auto& pipeline = m_context.GetPipelineCache().GetComputePipeline(input_info, compute_program);
|
||||
auto& bindings = m_compute_bindings;
|
||||
@@ -501,6 +539,7 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
1, &barrier, 0, nullptr, 0, nullptr);
|
||||
vk_buffer.bindPipeline(vk::PipelineBindPoint::eCompute, pipeline.pipeline);
|
||||
vk_buffer.dispatchIndirect(args_buffer->Handle(), args_offset);
|
||||
Common::DebugCounters::Add(Common::DebugCounters::Counter::Dispatches);
|
||||
ShaderAccessBarrier(vk_buffer, vk::PipelineStageFlagBits::eComputeShader);
|
||||
ResetBindings();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/common.h"
|
||||
#include "common/debugCounters.h"
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/file.h"
|
||||
#include "common/logging/log.h"
|
||||
@@ -1192,13 +1193,14 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
|
||||
if (!mesh_active) {
|
||||
CommitIndexBuffer(vk_buffer, index_binding);
|
||||
}
|
||||
const auto draw_dwords = ShaderRecompiler::IR::PushData::GraphicsDrawDwords(
|
||||
mesh_active, state.ps_input_info.object_primitive_id);
|
||||
using ShaderRecompiler::IR::PushData;
|
||||
const auto draw_stages =
|
||||
vk::ShaderStageFlagBits::eFragment |
|
||||
(mesh_active ? vk::ShaderStageFlagBits::eMeshEXT : vk::ShaderStageFlagBits::eVertex);
|
||||
const auto draw_dwords =
|
||||
PushData::GraphicsDrawDwords(mesh_active, state.ps_input_info.object_primitive_id);
|
||||
if (draw_dwords != 0) {
|
||||
std::array<uint32_t, ShaderRecompiler::IR::PushData::MeshDrawDwordCount> draw_data {};
|
||||
const auto draw_stages = vk::ShaderStageFlagBits::eFragment |
|
||||
(mesh_active ? vk::ShaderStageFlagBits::eMeshEXT
|
||||
: vk::ShaderStageFlagBits::eVertex);
|
||||
std::array<uint32_t, PushData::MeshDrawDwordCount> draw_data {};
|
||||
if (mesh_active) {
|
||||
draw_data = {
|
||||
draw.index_count,
|
||||
@@ -1212,6 +1214,12 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
|
||||
vk_buffer.pushConstants(pipeline.pipeline_layout, draw_stages, 0,
|
||||
draw_dwords * sizeof(uint32_t), draw_data.data());
|
||||
}
|
||||
if (const auto planes = vertex_stages.back().user_clip_planes; planes != 0) {
|
||||
vk_buffer.pushConstants(pipeline.pipeline_layout, draw_stages,
|
||||
PushData::UserClipPlaneDword * sizeof(uint32_t),
|
||||
std::bit_width(planes) * 4u * sizeof(uint32_t),
|
||||
buffer.GetRegisters().GetUserClipPlanes().data());
|
||||
}
|
||||
|
||||
auto& dynamic_state = m_context.GetCommandScheduler().GetDynamicState();
|
||||
UpdateGraphicsDynamicState(buffer, dynamic_state, vertex_stages.back(), state.depth_info,
|
||||
@@ -1219,6 +1227,20 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
|
||||
dynamic_state.SetAttachmentFeedbackLoopAspects(feedback_aspects);
|
||||
dynamic_state.Commit(m_context.GetGraphics(), vk_buffer);
|
||||
|
||||
vk::PipelineStageFlags shader_write_stages = {};
|
||||
for (const auto& stage: vertex_stages) {
|
||||
if (HasShaderBufferWrites(stage.stage)) {
|
||||
shader_write_stages |= ShaderPipelineStages(NativeShaderStage(stage.logical_stage));
|
||||
}
|
||||
}
|
||||
if (state.ps_active && HasShaderBufferWrites(state.ps_input_info.stage)) {
|
||||
shader_write_stages |= vk::PipelineStageFlagBits::eFragmentShader;
|
||||
}
|
||||
if (shader_write_stages) {
|
||||
// As for dispatches, buffer writes wait for earlier accesses to the memory.
|
||||
m_context.GetCommandScheduler().EndRendering();
|
||||
ShaderWriteHazardBarrier(vk_buffer, shader_write_stages);
|
||||
}
|
||||
LogDrawPhase(draw.Name(), "BeginRendering");
|
||||
if (!draw.IsIndexed()) {
|
||||
SetDrawDebugPhase(buffer, submit_id, draw, 0x400u);
|
||||
@@ -1233,19 +1255,12 @@ 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);
|
||||
}
|
||||
vk::PipelineStageFlags shader_write_stages = {};
|
||||
for (const auto& stage: vertex_stages) {
|
||||
if (HasShaderBufferWrites(stage.stage)) {
|
||||
shader_write_stages |= ShaderPipelineStages(NativeShaderStage(stage.logical_stage));
|
||||
}
|
||||
}
|
||||
if (state.ps_active && HasShaderBufferWrites(state.ps_input_info.stage)) {
|
||||
shader_write_stages |= vk::PipelineStageFlagBits::eFragmentShader;
|
||||
}
|
||||
if (shader_write_stages) {
|
||||
m_context.GetCommandScheduler().EndRendering();
|
||||
ShaderWriteBarrier(vk_buffer, shader_write_stages);
|
||||
|
||||
@@ -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,762 @@
|
||||
#include "graphics/presentation/openxrHeadset.h"
|
||||
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/stringUtils.h"
|
||||
#include "common/threads.h"
|
||||
#include "graphics/host_gpu/graphicContext.h"
|
||||
#include "graphics/host_gpu/renderer/render.h"
|
||||
#include "graphics/host_gpu/renderer/renderContext.h"
|
||||
#include "graphics/presentation/vrInput.h"
|
||||
#include "libs/hmd2.h"
|
||||
|
||||
#define XR_USE_GRAPHICS_API_VULKAN
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fmt/format.h>
|
||||
#include <mutex>
|
||||
#include <openxr/openxr.h>
|
||||
#include <openxr/openxr_platform.h>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace Libs::Vr {
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace Libs::Graphics;
|
||||
|
||||
enum Action { TRIGGER, SQUEEZE, STICK, STICK_CLICK, PRIMARY, SECONDARY, MENU, GRIP, ACTION_COUNT };
|
||||
|
||||
// Touch controller bindings: the path under /user/hand/<side>/input/ per hand, or null.
|
||||
constexpr struct {
|
||||
const char* name;
|
||||
XrActionType type;
|
||||
const char* path[2];
|
||||
} ACTIONS[ACTION_COUNT] = {
|
||||
{"trigger", XR_ACTION_TYPE_FLOAT_INPUT, {"trigger/value", "trigger/value"}},
|
||||
{"squeeze", XR_ACTION_TYPE_FLOAT_INPUT, {"squeeze/value", "squeeze/value"}},
|
||||
{"thumbstick", XR_ACTION_TYPE_VECTOR2F_INPUT, {"thumbstick", "thumbstick"}},
|
||||
{"thumbstick_click", XR_ACTION_TYPE_BOOLEAN_INPUT, {"thumbstick/click", "thumbstick/click"}},
|
||||
{"primary", XR_ACTION_TYPE_BOOLEAN_INPUT, {"x/click", "a/click"}},
|
||||
{"secondary", XR_ACTION_TYPE_BOOLEAN_INPUT, {"y/click", "b/click"}},
|
||||
{"menu", XR_ACTION_TYPE_BOOLEAN_INPUT, {"menu/click", nullptr}},
|
||||
{"grip_pose", XR_ACTION_TYPE_POSE_INPUT, {"grip/pose", "grip/pose"}},
|
||||
};
|
||||
|
||||
constexpr const char* HANDS[2] = {"left", "right"};
|
||||
|
||||
// Eye images move from the render thread to the headset thread through these. One is being written,
|
||||
// one is the latest recorded frame and one may be held by the headset thread until the GPU is done.
|
||||
constexpr int STAGING_COUNT = 3;
|
||||
|
||||
struct Staging {
|
||||
VulkanImage image;
|
||||
uint64_t tick = 0;
|
||||
VrPose head; // The head pose the game last read, so the picture can be placed in the world.
|
||||
};
|
||||
|
||||
struct State {
|
||||
XrInstance instance = XR_NULL_HANDLE;
|
||||
XrSystemId system = XR_NULL_SYSTEM_ID;
|
||||
XrSession session = XR_NULL_HANDLE;
|
||||
XrSpace view_space = XR_NULL_HANDLE;
|
||||
XrSpace local_space = XR_NULL_HANDLE;
|
||||
XrSwapchain swapchain = XR_NULL_HANDLE; // Two array layers, one per eye.
|
||||
std::vector<XrSwapchainImageVulkanKHR> swapchain_images;
|
||||
vk::Extent2D eye {};
|
||||
std::vector<std::string> extensions[2]; // Vulkan instance and device extensions.
|
||||
PFN_xrGetVulkanGraphicsDeviceKHR get_graphics_device = nullptr;
|
||||
|
||||
GraphicContext* graphics = nullptr;
|
||||
std::thread thread;
|
||||
std::atomic_bool running {false};
|
||||
|
||||
// Headset thread only.
|
||||
XrSessionState session_state = XR_SESSION_STATE_UNKNOWN;
|
||||
bool session_running = false;
|
||||
uint64_t shown_tick = 0;
|
||||
VrPose shown_head;
|
||||
vk::CommandPool pool;
|
||||
vk::CommandBuffer command;
|
||||
vk::Fence fence;
|
||||
|
||||
std::mutex mutex;
|
||||
Staging staging[STAGING_COUNT];
|
||||
int latest = -1;
|
||||
int held = -1;
|
||||
MasterSemaphore* semaphore = nullptr;
|
||||
|
||||
XrActionSet action_set = XR_NULL_HANDLE;
|
||||
XrAction actions[ACTION_COUNT] {};
|
||||
XrPath hand_path[2] {};
|
||||
XrSpace grip_space[2] {};
|
||||
|
||||
std::mutex input_mutex;
|
||||
VrControllerInput input[2];
|
||||
VrPose head;
|
||||
VrPose last_head_read;
|
||||
};
|
||||
|
||||
State g;
|
||||
|
||||
bool Ok(XrResult result, const char* what) {
|
||||
if (XR_SUCCEEDED(result)) {
|
||||
return true;
|
||||
}
|
||||
char text[XR_MAX_RESULT_STRING_SIZE] = {};
|
||||
xrResultToString(g.instance, result, text);
|
||||
Log::WriteToConsoleAndLog(fmt::format("OpenXR: {} failed: {}\n", what, text));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Destroying the instance also destroys the session, spaces, swapchain and actions.
|
||||
void DestroyInstance(const char* reason) {
|
||||
if (reason != nullptr) {
|
||||
Log::WriteToConsoleAndLog(
|
||||
fmt::format("OpenXR: {}; showing the eye image in the window only\n", reason));
|
||||
}
|
||||
if (g.instance != XR_NULL_HANDLE) {
|
||||
xrDestroyInstance(g.instance);
|
||||
}
|
||||
g.instance = XR_NULL_HANDLE;
|
||||
g.session = XR_NULL_HANDLE;
|
||||
}
|
||||
|
||||
struct Access {
|
||||
vk::PipelineStageFlags2 stage;
|
||||
vk::AccessFlags2 access;
|
||||
};
|
||||
|
||||
constexpr Access NO_ACCESS {vk::PipelineStageFlagBits2::eNone, vk::AccessFlagBits2::eNone};
|
||||
constexpr Access TRANSFER {vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eNone};
|
||||
constexpr Access TRANSFER_READ {vk::PipelineStageFlagBits2::eTransfer,
|
||||
vk::AccessFlagBits2::eTransferRead};
|
||||
constexpr Access TRANSFER_WRITE {vk::PipelineStageFlagBits2::eTransfer,
|
||||
vk::AccessFlagBits2::eTransferWrite};
|
||||
constexpr Access COLOR_ATTACHMENT {vk::PipelineStageFlagBits2::eColorAttachmentOutput,
|
||||
vk::AccessFlagBits2::eColorAttachmentRead |
|
||||
vk::AccessFlagBits2::eColorAttachmentWrite};
|
||||
|
||||
void Barrier(vk::CommandBuffer command, vk::Image image, vk::ImageLayout from, vk::ImageLayout to,
|
||||
uint32_t layers, Access src, Access dst) {
|
||||
vk::ImageMemoryBarrier2 barrier {};
|
||||
barrier.srcStageMask = src.stage;
|
||||
barrier.srcAccessMask = src.access;
|
||||
barrier.dstStageMask = dst.stage;
|
||||
barrier.dstAccessMask = dst.access;
|
||||
barrier.oldLayout = from;
|
||||
barrier.newLayout = to;
|
||||
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
barrier.image = image;
|
||||
barrier.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, layers};
|
||||
vk::DependencyInfo dependency {};
|
||||
dependency.imageMemoryBarrierCount = 1;
|
||||
dependency.pImageMemoryBarriers = &barrier;
|
||||
command.pipelineBarrier2(dependency);
|
||||
}
|
||||
|
||||
XrQuaternionf Multiply(const XrQuaternionf& a, const XrQuaternionf& b) {
|
||||
return {a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
|
||||
a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,
|
||||
a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w,
|
||||
a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z};
|
||||
}
|
||||
|
||||
XrVector3f Rotate(const XrQuaternionf& q, const XrVector3f& v) {
|
||||
const XrVector3f t {2.0f * (q.y * v.z - q.z * v.y), 2.0f * (q.z * v.x - q.x * v.z),
|
||||
2.0f * (q.x * v.y - q.y * v.x)};
|
||||
return {v.x + q.w * t.x + (q.y * t.z - q.z * t.y), v.y + q.w * t.y + (q.z * t.x - q.x * t.z),
|
||||
v.z + q.w * t.z + (q.x * t.y - q.y * t.x)};
|
||||
}
|
||||
|
||||
VrPose Locate(XrSpace space, XrTime time) {
|
||||
VrPose pose;
|
||||
XrSpaceLocation location {XR_TYPE_SPACE_LOCATION};
|
||||
if (XR_SUCCEEDED(xrLocateSpace(space, g.local_space, time, &location)) &&
|
||||
(location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) != 0) {
|
||||
const auto& [q, p] = location.pose;
|
||||
pose = {true, {q.x, q.y, q.z, q.w}, {p.x, p.y, p.z}};
|
||||
}
|
||||
return pose;
|
||||
}
|
||||
|
||||
// The swapchain stores the eyes in a gamma-encoded format, so a raw copy of the display-encoded
|
||||
// game image comes out right. Staging uses the same layout with UNORM, which a blit writes from the
|
||||
// raw UNORM or float presentation frame without conversion.
|
||||
bool CreateSwapchain() {
|
||||
uint32_t count = 0;
|
||||
xrEnumerateSwapchainFormats(g.session, 0, &count, nullptr);
|
||||
std::vector<int64_t> formats(count);
|
||||
xrEnumerateSwapchainFormats(g.session, count, &count, formats.data());
|
||||
constexpr std::pair<vk::Format, vk::Format> candidates[] = {
|
||||
{vk::Format::eR8G8B8A8Srgb, vk::Format::eR8G8B8A8Unorm},
|
||||
{vk::Format::eB8G8R8A8Srgb, vk::Format::eB8G8R8A8Unorm},
|
||||
{vk::Format::eR8G8B8A8Unorm, vk::Format::eR8G8B8A8Unorm},
|
||||
{vk::Format::eB8G8R8A8Unorm, vk::Format::eB8G8R8A8Unorm},
|
||||
};
|
||||
const auto* format =
|
||||
std::find_if(std::begin(candidates), std::end(candidates), [&](const auto& c) {
|
||||
return std::find(formats.begin(), formats.end(), static_cast<int64_t>(c.first)) !=
|
||||
formats.end();
|
||||
});
|
||||
if (format == std::end(candidates)) {
|
||||
return false;
|
||||
}
|
||||
XrSwapchainCreateInfo info {XR_TYPE_SWAPCHAIN_CREATE_INFO};
|
||||
info.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT;
|
||||
info.format = static_cast<int64_t>(format->first);
|
||||
info.sampleCount = 1;
|
||||
info.width = g.eye.width;
|
||||
info.height = g.eye.height;
|
||||
info.faceCount = 1;
|
||||
info.arraySize = 2;
|
||||
info.mipCount = 1;
|
||||
if (!Ok(xrCreateSwapchain(g.session, &info, &g.swapchain), "xrCreateSwapchain")) {
|
||||
return false;
|
||||
}
|
||||
xrEnumerateSwapchainImages(g.swapchain, 0, &count, nullptr);
|
||||
g.swapchain_images.assign(count, {XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR});
|
||||
xrEnumerateSwapchainImages(
|
||||
g.swapchain, count, &count,
|
||||
reinterpret_cast<XrSwapchainImageBaseHeader*>(g.swapchain_images.data()));
|
||||
|
||||
vk::ImageCreateInfo staging {};
|
||||
staging.imageType = vk::ImageType::e2D;
|
||||
staging.format = format->second;
|
||||
staging.extent = vk::Extent3D {g.eye.width * 2, g.eye.height, 1};
|
||||
staging.mipLevels = 1;
|
||||
staging.arrayLayers = 1;
|
||||
staging.samples = vk::SampleCountFlagBits::e1;
|
||||
staging.tiling = vk::ImageTiling::eOptimal;
|
||||
staging.usage = vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst;
|
||||
for (auto& slot: g.staging) {
|
||||
if (!g.graphics->CreateImage(staging, slot.image)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Log::WriteToConsoleAndLog(fmt::format("OpenXR: eye swapchain {}x{} {}\n", g.eye.width,
|
||||
g.eye.height, vk::to_string(format->first)));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CreateInput() {
|
||||
XrActionSetCreateInfo set {XR_TYPE_ACTION_SET_CREATE_INFO};
|
||||
std::strcpy(set.actionSetName, "gameplay");
|
||||
std::strcpy(set.localizedActionSetName, "Gameplay");
|
||||
if (!Ok(xrCreateActionSet(g.instance, &set, &g.action_set), "xrCreateActionSet")) {
|
||||
return false;
|
||||
}
|
||||
for (int hand = 0; hand < 2; hand++) {
|
||||
xrStringToPath(g.instance, fmt::format("/user/hand/{}", HANDS[hand]).c_str(),
|
||||
&g.hand_path[hand]);
|
||||
}
|
||||
std::vector<XrActionSuggestedBinding> bindings;
|
||||
for (int i = 0; i < ACTION_COUNT; i++) {
|
||||
XrActionCreateInfo info {XR_TYPE_ACTION_CREATE_INFO};
|
||||
info.actionType = ACTIONS[i].type;
|
||||
info.countSubactionPaths = 2;
|
||||
info.subactionPaths = g.hand_path;
|
||||
std::strcpy(info.actionName, ACTIONS[i].name);
|
||||
std::strcpy(info.localizedActionName, ACTIONS[i].name);
|
||||
if (!Ok(xrCreateAction(g.action_set, &info, &g.actions[i]), "xrCreateAction")) {
|
||||
return false;
|
||||
}
|
||||
for (int hand = 0; hand < 2; hand++) {
|
||||
if (ACTIONS[i].path[hand] != nullptr) {
|
||||
XrPath path = XR_NULL_PATH;
|
||||
xrStringToPath(
|
||||
g.instance,
|
||||
fmt::format("/user/hand/{}/input/{}", HANDS[hand], ACTIONS[i].path[hand])
|
||||
.c_str(),
|
||||
&path);
|
||||
bindings.push_back({g.actions[i], path});
|
||||
}
|
||||
}
|
||||
}
|
||||
XrInteractionProfileSuggestedBinding suggested {XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING};
|
||||
xrStringToPath(g.instance, "/interaction_profiles/oculus/touch_controller",
|
||||
&suggested.interactionProfile);
|
||||
suggested.countSuggestedBindings = static_cast<uint32_t>(bindings.size());
|
||||
suggested.suggestedBindings = bindings.data();
|
||||
XrSessionActionSetsAttachInfo attach {XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO};
|
||||
attach.countActionSets = 1;
|
||||
attach.actionSets = &g.action_set;
|
||||
if (!Ok(xrSuggestInteractionProfileBindings(g.instance, &suggested), "suggest bindings") ||
|
||||
!Ok(xrAttachSessionActionSets(g.session, &attach), "attach action sets")) {
|
||||
return false;
|
||||
}
|
||||
for (int hand = 0; hand < 2; hand++) {
|
||||
XrActionSpaceCreateInfo space {XR_TYPE_ACTION_SPACE_CREATE_INFO};
|
||||
space.action = g.actions[GRIP];
|
||||
space.subactionPath = g.hand_path[hand];
|
||||
space.poseInActionSpace = {{0, 0, 0, 1}, {0, 0, 0}};
|
||||
if (!Ok(xrCreateActionSpace(g.session, &space, &g.grip_space[hand]), "grip space")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// The head is tracked while the frame loop runs; the controllers only while the session has focus.
|
||||
void PollInput(XrTime time) {
|
||||
const auto head = Locate(g.view_space, time);
|
||||
VrControllerInput input[2];
|
||||
XrActiveActionSet active {g.action_set, XR_NULL_PATH};
|
||||
XrActionsSyncInfo sync {XR_TYPE_ACTIONS_SYNC_INFO};
|
||||
sync.countActiveActionSets = 1;
|
||||
sync.activeActionSets = &active;
|
||||
if (g.action_set != XR_NULL_HANDLE && g.session_state == XR_SESSION_STATE_FOCUSED &&
|
||||
XR_SUCCEEDED(xrSyncActions(g.session, &sync))) {
|
||||
for (int hand = 0; hand < 2; hand++) {
|
||||
const auto get = [&](Action action, auto read, auto state) {
|
||||
XrActionStateGetInfo info {XR_TYPE_ACTION_STATE_GET_INFO};
|
||||
info.action = g.actions[action];
|
||||
info.subactionPath = g.hand_path[hand];
|
||||
read(g.session, &info, &state);
|
||||
return state;
|
||||
};
|
||||
const auto value = [&](Action action) {
|
||||
return get(action, xrGetActionStateFloat,
|
||||
XrActionStateFloat {XR_TYPE_ACTION_STATE_FLOAT})
|
||||
.currentState;
|
||||
};
|
||||
const auto flag = [&](Action action) {
|
||||
return get(action, xrGetActionStateBoolean,
|
||||
XrActionStateBoolean {XR_TYPE_ACTION_STATE_BOOLEAN})
|
||||
.currentState == XR_TRUE;
|
||||
};
|
||||
const auto stick = get(STICK, xrGetActionStateVector2f,
|
||||
XrActionStateVector2f {XR_TYPE_ACTION_STATE_VECTOR2F});
|
||||
auto& out = input[hand];
|
||||
out.trigger = value(TRIGGER);
|
||||
out.squeeze = value(SQUEEZE);
|
||||
out.stick_x = stick.currentState.x;
|
||||
out.stick_y = stick.currentState.y;
|
||||
out.stick_click = flag(STICK_CLICK);
|
||||
out.primary = flag(PRIMARY);
|
||||
out.secondary = flag(SECONDARY);
|
||||
out.menu = flag(MENU);
|
||||
out.pose = Locate(g.grip_space[hand], time);
|
||||
out.connected =
|
||||
get(GRIP, xrGetActionStatePose, XrActionStatePose {XR_TYPE_ACTION_STATE_POSE})
|
||||
.isActive == XR_TRUE;
|
||||
}
|
||||
}
|
||||
std::lock_guard lock(g.input_mutex);
|
||||
g.head = head;
|
||||
g.input[0] = input[0];
|
||||
g.input[1] = input[1];
|
||||
}
|
||||
|
||||
void HandleEvents() {
|
||||
for (XrEventDataBuffer event {XR_TYPE_EVENT_DATA_BUFFER};
|
||||
xrPollEvent(g.instance, &event) == XR_SUCCESS; event = {XR_TYPE_EVENT_DATA_BUFFER}) {
|
||||
if (event.type == XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING) {
|
||||
g.running = false;
|
||||
continue;
|
||||
}
|
||||
if (event.type != XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED) {
|
||||
continue;
|
||||
}
|
||||
g.session_state = reinterpret_cast<const XrEventDataSessionStateChanged&>(event).state;
|
||||
LOGF("OpenXR: session state %d\n", static_cast<int>(g.session_state));
|
||||
switch (g.session_state) {
|
||||
case XR_SESSION_STATE_READY: {
|
||||
XrSessionBeginInfo begin {XR_TYPE_SESSION_BEGIN_INFO};
|
||||
begin.primaryViewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
|
||||
g.session_running = Ok(xrBeginSession(g.session, &begin), "xrBeginSession");
|
||||
break;
|
||||
}
|
||||
case XR_SESSION_STATE_STOPPING: {
|
||||
xrEndSession(g.session);
|
||||
g.session_running = false;
|
||||
std::lock_guard lock(g.input_mutex);
|
||||
g.head = {};
|
||||
g.input[0] = {};
|
||||
g.input[1] = {};
|
||||
break;
|
||||
}
|
||||
case XR_SESSION_STATE_EXITING:
|
||||
case XR_SESSION_STATE_LOSS_PENDING: g.running = false; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool CopyToSwapchain(const VulkanImage& staging) {
|
||||
uint32_t image = 0;
|
||||
XrSwapchainImageAcquireInfo acquire {XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
|
||||
{
|
||||
Common::LockGuard queue_lock(g.graphics->queue_mutex);
|
||||
if (!Ok(xrAcquireSwapchainImage(g.swapchain, &acquire, &image), "acquire")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
XrSwapchainImageWaitInfo wait {XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};
|
||||
wait.timeout = XR_INFINITE_DURATION;
|
||||
bool copied = Ok(xrWaitSwapchainImage(g.swapchain, &wait), "wait swapchain image");
|
||||
if (copied) {
|
||||
const auto target = g.swapchain_images[image].image;
|
||||
(void)g.command.reset();
|
||||
vk::CommandBufferBeginInfo begin {};
|
||||
begin.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit;
|
||||
(void)g.command.begin(&begin);
|
||||
Barrier(g.command, target, vk::ImageLayout::eUndefined,
|
||||
vk::ImageLayout::eTransferDstOptimal, 2, NO_ACCESS, TRANSFER_WRITE);
|
||||
vk::ImageCopy copies[2] {};
|
||||
for (uint32_t eye = 0; eye < 2; eye++) {
|
||||
copies[eye].srcSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
|
||||
copies[eye].srcOffset = vk::Offset3D {static_cast<int32_t>(eye * g.eye.width), 0, 0};
|
||||
copies[eye].dstSubresource = {vk::ImageAspectFlagBits::eColor, 0, eye, 1};
|
||||
copies[eye].extent = vk::Extent3D {g.eye.width, g.eye.height, 1};
|
||||
}
|
||||
g.command.copyImage(staging.image, vk::ImageLayout::eTransferSrcOptimal, target,
|
||||
vk::ImageLayout::eTransferDstOptimal, 2, copies);
|
||||
Barrier(g.command, target, vk::ImageLayout::eTransferDstOptimal,
|
||||
vk::ImageLayout::eColorAttachmentOptimal, 2, TRANSFER_WRITE, COLOR_ATTACHMENT);
|
||||
(void)g.command.end();
|
||||
vk::SubmitInfo submit {};
|
||||
submit.commandBufferCount = 1;
|
||||
submit.pCommandBuffers = &g.command;
|
||||
(void)g.graphics->device.resetFences(1, &g.fence);
|
||||
vk::Result result {};
|
||||
{
|
||||
Common::LockGuard queue_lock(g.graphics->queue_mutex);
|
||||
result = g.graphics->queue.submit(1, &submit, g.fence);
|
||||
}
|
||||
copied = result == vk::Result::eSuccess &&
|
||||
g.graphics->device.waitForFences(1, &g.fence, VK_TRUE, UINT64_MAX) ==
|
||||
vk::Result::eSuccess;
|
||||
}
|
||||
XrSwapchainImageReleaseInfo release {XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
|
||||
Common::LockGuard queue_lock(g.graphics->queue_mutex);
|
||||
return Ok(xrReleaseSwapchainImage(g.swapchain, &release), "release") && copied;
|
||||
}
|
||||
|
||||
// Holds the newest recorded eye image until the GPU has finished it, without waiting, then copies
|
||||
// it into the swapchain. The swapchain keeps the last copy, so frames in between show it again.
|
||||
bool UpdateSwapchain() {
|
||||
int index = -1;
|
||||
MasterSemaphore* semaphore = nullptr;
|
||||
{
|
||||
std::lock_guard lock(g.mutex);
|
||||
if (g.held < 0 && g.latest >= 0 && g.staging[g.latest].tick != g.shown_tick) {
|
||||
g.held = g.latest;
|
||||
}
|
||||
index = g.held;
|
||||
semaphore = g.semaphore;
|
||||
}
|
||||
if (index >= 0) {
|
||||
const auto& slot = g.staging[index];
|
||||
semaphore->Refresh();
|
||||
if (semaphore->IsFree(slot.tick)) {
|
||||
if (CopyToSwapchain(slot.image)) {
|
||||
g.shown_tick = slot.tick;
|
||||
g.shown_head = slot.head;
|
||||
}
|
||||
std::lock_guard lock(g.mutex);
|
||||
g.held = -1;
|
||||
}
|
||||
}
|
||||
return g.shown_tick != 0;
|
||||
}
|
||||
|
||||
// The game drew the picture for the head pose it read, so it is placed in the world at that pose
|
||||
// and the runtime reprojects it as the head moves. Eye offsets come from the runtime.
|
||||
void PlaceViews(XrTime time, XrCompositionLayerProjection& layer,
|
||||
XrCompositionLayerProjectionView (&views)[2]) {
|
||||
XrViewLocateInfo locate {XR_TYPE_VIEW_LOCATE_INFO};
|
||||
locate.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
|
||||
locate.displayTime = time;
|
||||
locate.space = g.view_space;
|
||||
XrViewState state {XR_TYPE_VIEW_STATE};
|
||||
XrView located[2] {{XR_TYPE_VIEW}, {XR_TYPE_VIEW}};
|
||||
uint32_t count = 0;
|
||||
const bool valid =
|
||||
XR_SUCCEEDED(xrLocateViews(g.session, &locate, &state, 2, &count, located)) &&
|
||||
(state.viewStateFlags & XR_VIEW_STATE_POSITION_VALID_BIT) != 0;
|
||||
const auto& head = g.shown_head;
|
||||
const XrQuaternionf head_q {head.orientation[0], head.orientation[1], head.orientation[2],
|
||||
head.orientation[3]};
|
||||
const XrVector3f head_p {head.position[0], head.position[1], head.position[2]};
|
||||
// The outer side of each eye is its temporal side: left for the left eye.
|
||||
const float out = std::atan(Hmd2::FOV_TAN_OUT);
|
||||
const float in = std::atan(Hmd2::FOV_TAN_IN);
|
||||
const float top = std::atan(Hmd2::FOV_TAN_TOP);
|
||||
const float bottom = std::atan(Hmd2::FOV_TAN_BOTTOM);
|
||||
for (int eye = 0; eye < 2; eye++) {
|
||||
const float side = eye == 0 ? -Hmd2::EYE_OFFSET_M : Hmd2::EYE_OFFSET_M;
|
||||
auto& view = views[eye];
|
||||
view.pose = valid ? located[eye].pose : XrPosef {{0, 0, 0, 1}, {side, 0, 0}};
|
||||
if (head.valid) {
|
||||
const auto offset = Rotate(head_q, view.pose.position);
|
||||
view.pose.position = {head_p.x + offset.x, head_p.y + offset.y, head_p.z + offset.z};
|
||||
view.pose.orientation = Multiply(head_q, view.pose.orientation);
|
||||
}
|
||||
view.fov = {eye == 0 ? -out : -in, eye == 0 ? in : out, top, -bottom};
|
||||
view.subImage.swapchain = g.swapchain;
|
||||
view.subImage.imageRect = {
|
||||
{0, 0}, {static_cast<int32_t>(g.eye.width), static_cast<int32_t>(g.eye.height)}};
|
||||
view.subImage.imageArrayIndex = static_cast<uint32_t>(eye);
|
||||
}
|
||||
layer.space = head.valid ? g.local_space : g.view_space;
|
||||
layer.viewCount = 2;
|
||||
layer.views = views;
|
||||
}
|
||||
|
||||
void ThreadMain() {
|
||||
while (g.running) {
|
||||
HandleEvents();
|
||||
XrFrameWaitInfo wait {XR_TYPE_FRAME_WAIT_INFO};
|
||||
XrFrameState frame {XR_TYPE_FRAME_STATE};
|
||||
if (!g.session_running || !Ok(xrWaitFrame(g.session, &wait, &frame), "xrWaitFrame")) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
continue;
|
||||
}
|
||||
PollInput(frame.predictedDisplayTime);
|
||||
XrFrameBeginInfo begin {XR_TYPE_FRAME_BEGIN_INFO};
|
||||
{
|
||||
Common::LockGuard queue_lock(g.graphics->queue_mutex);
|
||||
if (!Ok(xrBeginFrame(g.session, &begin), "xrBeginFrame")) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
XrCompositionLayerProjection layer {XR_TYPE_COMPOSITION_LAYER_PROJECTION};
|
||||
XrCompositionLayerProjectionView views[2] {{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW},
|
||||
{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW}};
|
||||
const auto* layers = reinterpret_cast<const XrCompositionLayerBaseHeader*>(&layer);
|
||||
XrFrameEndInfo end {XR_TYPE_FRAME_END_INFO};
|
||||
end.displayTime = frame.predictedDisplayTime;
|
||||
end.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
|
||||
if (frame.shouldRender == XR_TRUE && UpdateSwapchain()) {
|
||||
PlaceViews(frame.predictedDisplayTime, layer, views);
|
||||
end.layerCount = 1;
|
||||
end.layers = &layers;
|
||||
}
|
||||
Common::LockGuard queue_lock(g.graphics->queue_mutex);
|
||||
(void)Ok(xrEndFrame(g.session, &end), "xrEndFrame");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void OpenXrInit() {
|
||||
if (!Config::VrEnabled()) {
|
||||
return;
|
||||
}
|
||||
if (const char* env = std::getenv("KYTY_OPENXR");
|
||||
env != nullptr && std::strcmp(env, "0") == 0) {
|
||||
return;
|
||||
}
|
||||
const char* extensions[] = {XR_KHR_VULKAN_ENABLE_EXTENSION_NAME};
|
||||
XrInstanceCreateInfo info {XR_TYPE_INSTANCE_CREATE_INFO};
|
||||
std::strcpy(info.applicationInfo.applicationName, "KytyPS5");
|
||||
std::strcpy(info.applicationInfo.engineName, "Kyty");
|
||||
info.applicationInfo.apiVersion = XR_API_VERSION_1_0;
|
||||
info.enabledExtensionCount = 1;
|
||||
info.enabledExtensionNames = extensions;
|
||||
if (XR_FAILED(xrCreateInstance(&info, &g.instance))) {
|
||||
DestroyInstance("no runtime available");
|
||||
return;
|
||||
}
|
||||
XrSystemGetInfo system {XR_TYPE_SYSTEM_GET_INFO};
|
||||
system.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;
|
||||
if (XR_FAILED(xrGetSystem(g.instance, &system, &g.system))) {
|
||||
DestroyInstance("no headset available");
|
||||
return;
|
||||
}
|
||||
PFN_xrGetVulkanGraphicsRequirementsKHR get_requirements = nullptr;
|
||||
PFN_xrGetVulkanInstanceExtensionsKHR get_extensions[2] {};
|
||||
xrGetInstanceProcAddr(g.instance, "xrGetVulkanGraphicsRequirementsKHR",
|
||||
reinterpret_cast<PFN_xrVoidFunction*>(&get_requirements));
|
||||
xrGetInstanceProcAddr(g.instance, "xrGetVulkanInstanceExtensionsKHR",
|
||||
reinterpret_cast<PFN_xrVoidFunction*>(&get_extensions[0]));
|
||||
// Both queries share one signature.
|
||||
xrGetInstanceProcAddr(g.instance, "xrGetVulkanDeviceExtensionsKHR",
|
||||
reinterpret_cast<PFN_xrVoidFunction*>(&get_extensions[1]));
|
||||
xrGetInstanceProcAddr(g.instance, "xrGetVulkanGraphicsDeviceKHR",
|
||||
reinterpret_cast<PFN_xrVoidFunction*>(&g.get_graphics_device));
|
||||
XrGraphicsRequirementsVulkanKHR requirements {XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN_KHR};
|
||||
if (get_requirements == nullptr || get_extensions[0] == nullptr ||
|
||||
get_extensions[1] == nullptr || g.get_graphics_device == nullptr ||
|
||||
XR_FAILED(get_requirements(g.instance, g.system, &requirements))) {
|
||||
DestroyInstance("the runtime does not support XR_KHR_vulkan_enable");
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < 2; i++) {
|
||||
uint32_t size = 0;
|
||||
get_extensions[i](g.instance, g.system, 0, &size, nullptr);
|
||||
std::string names(size, '\0');
|
||||
get_extensions[i](g.instance, g.system, size, &size, names.data());
|
||||
g.extensions[i] = Common::Split(names.c_str(), ' ');
|
||||
}
|
||||
XrViewConfigurationView views[2] {{XR_TYPE_VIEW_CONFIGURATION_VIEW},
|
||||
{XR_TYPE_VIEW_CONFIGURATION_VIEW}};
|
||||
uint32_t count = 0;
|
||||
xrEnumerateViewConfigurationViews(g.instance, g.system,
|
||||
XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, 2, &count, views);
|
||||
g.eye = {views[0].recommendedImageRectWidth, views[0].recommendedImageRectHeight};
|
||||
XrSystemProperties properties {XR_TYPE_SYSTEM_PROPERTIES};
|
||||
xrGetSystemProperties(g.instance, g.system, &properties);
|
||||
Log::WriteToConsoleAndLog(fmt::format("OpenXR: headset '{}' ready\n", properties.systemName));
|
||||
}
|
||||
|
||||
void OpenXrAppendExtensions(std::vector<const char*>& out,
|
||||
const std::vector<vk::ExtensionProperties>& available, bool device) {
|
||||
if (g.instance == XR_NULL_HANDLE) {
|
||||
return;
|
||||
}
|
||||
for (const auto& name: g.extensions[device ? 1 : 0]) {
|
||||
const auto same = [&](const char* other) { return name == other; };
|
||||
if (std::any_of(available.begin(), available.end(),
|
||||
[&](const auto& extension) { return same(extension.extensionName); }) &&
|
||||
std::none_of(out.begin(), out.end(), same)) {
|
||||
out.push_back(name.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool OpenXrPhysicalDevice(vk::Instance instance, vk::PhysicalDevice& out) {
|
||||
VkPhysicalDevice device = VK_NULL_HANDLE;
|
||||
if (g.instance == XR_NULL_HANDLE ||
|
||||
XR_FAILED(g.get_graphics_device(g.instance, g.system, static_cast<VkInstance>(instance),
|
||||
&device))) {
|
||||
return false;
|
||||
}
|
||||
out = vk::PhysicalDevice(device);
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenXrStartSession(GraphicContext& graphics) {
|
||||
if (g.instance == XR_NULL_HANDLE) {
|
||||
return;
|
||||
}
|
||||
// The runtime must be asked for its GPU before a session is created.
|
||||
vk::PhysicalDevice required;
|
||||
if (!OpenXrPhysicalDevice(graphics.instance, required) ||
|
||||
required != graphics.physical_device) {
|
||||
DestroyInstance("the headset runs on another GPU; select it with --gpu-index");
|
||||
return;
|
||||
}
|
||||
g.graphics = &graphics;
|
||||
XrGraphicsBindingVulkanKHR binding {XR_TYPE_GRAPHICS_BINDING_VULKAN_KHR};
|
||||
binding.instance = static_cast<VkInstance>(graphics.instance);
|
||||
binding.physicalDevice = static_cast<VkPhysicalDevice>(graphics.physical_device);
|
||||
binding.device = static_cast<VkDevice>(graphics.device);
|
||||
binding.queueFamilyIndex = graphics.queue_family;
|
||||
binding.queueIndex = 0;
|
||||
XrSessionCreateInfo info {XR_TYPE_SESSION_CREATE_INFO};
|
||||
info.next = &binding;
|
||||
info.systemId = g.system;
|
||||
XrReferenceSpaceCreateInfo space {XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
|
||||
space.poseInReferenceSpace = {{0, 0, 0, 1}, {0, 0, 0}};
|
||||
const auto reference = [&](XrReferenceSpaceType type, XrSpace& out) {
|
||||
space.referenceSpaceType = type;
|
||||
return Ok(xrCreateReferenceSpace(g.session, &space, &out), "xrCreateReferenceSpace");
|
||||
};
|
||||
if (!Ok(xrCreateSession(g.instance, &info, &g.session), "xrCreateSession") ||
|
||||
!reference(XR_REFERENCE_SPACE_TYPE_VIEW, g.view_space) ||
|
||||
!reference(XR_REFERENCE_SPACE_TYPE_LOCAL, g.local_space) || !CreateSwapchain()) {
|
||||
DestroyInstance("the headset session could not be created");
|
||||
return;
|
||||
}
|
||||
if (!CreateInput()) {
|
||||
g.action_set = XR_NULL_HANDLE;
|
||||
}
|
||||
vk::CommandPoolCreateInfo pool {};
|
||||
pool.queueFamilyIndex = graphics.queue_family;
|
||||
pool.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer;
|
||||
(void)graphics.device.createCommandPool(&pool, nullptr, &g.pool);
|
||||
vk::CommandBufferAllocateInfo allocate {};
|
||||
allocate.commandPool = g.pool;
|
||||
allocate.level = vk::CommandBufferLevel::ePrimary;
|
||||
allocate.commandBufferCount = 1;
|
||||
(void)graphics.device.allocateCommandBuffers(&allocate, &g.command);
|
||||
vk::FenceCreateInfo fence {};
|
||||
(void)graphics.device.createFence(&fence, nullptr, &g.fence);
|
||||
|
||||
g.running = true;
|
||||
g.thread = std::thread(ThreadMain);
|
||||
}
|
||||
|
||||
void OpenXrShutdown() {
|
||||
g.running = false;
|
||||
if (g.thread.joinable()) {
|
||||
g.thread.join();
|
||||
}
|
||||
DestroyInstance(nullptr);
|
||||
if (g.graphics == nullptr) {
|
||||
return;
|
||||
}
|
||||
{
|
||||
Common::LockGuard queue_lock(g.graphics->queue_mutex);
|
||||
(void)g.graphics->device.waitIdle();
|
||||
}
|
||||
for (auto& slot: g.staging) {
|
||||
if (slot.image.image != nullptr) {
|
||||
g.graphics->DeleteImage(slot.image);
|
||||
}
|
||||
}
|
||||
g.graphics->device.destroyFence(g.fence, nullptr);
|
||||
g.graphics->device.destroyCommandPool(g.pool, nullptr);
|
||||
g.graphics = nullptr;
|
||||
}
|
||||
|
||||
VrPose OpenXrGetHeadPose() {
|
||||
if (!g.running) {
|
||||
return {};
|
||||
}
|
||||
std::lock_guard lock(g.input_mutex);
|
||||
if (g.head.valid) {
|
||||
g.last_head_read = g.head;
|
||||
}
|
||||
return g.head;
|
||||
}
|
||||
|
||||
VrControllerInput OpenXrGetController(int hand) {
|
||||
if (!g.running) {
|
||||
return {};
|
||||
}
|
||||
std::lock_guard lock(g.input_mutex);
|
||||
return g.input[hand];
|
||||
}
|
||||
|
||||
void OpenXrQueueEyeSource(CommandBuffer& command, const VulkanImage& source) {
|
||||
if (!g.running) {
|
||||
return;
|
||||
}
|
||||
std::lock_guard lock(g.mutex);
|
||||
int index = 0;
|
||||
while (index == g.held || index == g.latest) {
|
||||
index++;
|
||||
}
|
||||
auto& slot = g.staging[index];
|
||||
const auto vk_command = command.Handle();
|
||||
Barrier(vk_command, slot.image.image, vk::ImageLayout::eUndefined,
|
||||
vk::ImageLayout::eTransferDstOptimal, 1, TRANSFER, TRANSFER_WRITE);
|
||||
vk::ImageBlit blit {};
|
||||
blit.srcSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
|
||||
blit.srcOffsets[1] = vk::Offset3D {static_cast<int32_t>(source.extent.width),
|
||||
static_cast<int32_t>(source.extent.height), 1};
|
||||
blit.dstSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
|
||||
blit.dstOffsets[1] = vk::Offset3D {static_cast<int32_t>(slot.image.extent.width),
|
||||
static_cast<int32_t>(slot.image.extent.height), 1};
|
||||
vk_command.blitImage(source.image, vk::ImageLayout::eTransferSrcOptimal, slot.image.image,
|
||||
vk::ImageLayout::eTransferDstOptimal, 1, &blit, vk::Filter::eLinear);
|
||||
Barrier(vk_command, slot.image.image, vk::ImageLayout::eTransferDstOptimal,
|
||||
vk::ImageLayout::eTransferSrcOptimal, 1, TRANSFER_WRITE, TRANSFER_READ);
|
||||
auto& scheduler = command.GetContext().GetCommandScheduler();
|
||||
slot.tick = scheduler.CurrentTick();
|
||||
g.semaphore = &scheduler.GetMasterSemaphore();
|
||||
g.latest = index;
|
||||
std::lock_guard input_lock(g.input_mutex);
|
||||
slot.head = g.last_head_read;
|
||||
}
|
||||
|
||||
} // namespace Libs::Vr
|
||||
@@ -0,0 +1,41 @@
|
||||
#ifndef EMULATOR_SRC_GRAPHICS_PRESENTATION_OPENXRHEADSET_H_
|
||||
#define EMULATOR_SRC_GRAPHICS_PRESENTATION_OPENXRHEADSET_H_
|
||||
|
||||
#include "graphics/host_gpu/vulkanCommon.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace Libs::Graphics {
|
||||
struct GraphicContext;
|
||||
struct VulkanImage;
|
||||
class CommandBuffer;
|
||||
} // namespace Libs::Graphics
|
||||
|
||||
// Output to a real headset through OpenXR. Everything here is inert unless the virtual headset is
|
||||
// enabled (--vr) and an OpenXR runtime with a headset is available; KYTY_OPENXR=0 turns it off.
|
||||
namespace Libs::Vr {
|
||||
|
||||
// Creates the OpenXR instance. Call before the Vulkan instance exists, because the runtime names
|
||||
// the Vulkan extensions and the GPU it needs.
|
||||
void OpenXrInit();
|
||||
|
||||
// Appends the Vulkan instance or device extensions the runtime needs, when available.
|
||||
void OpenXrAppendExtensions(std::vector<const char*>& out,
|
||||
const std::vector<vk::ExtensionProperties>& available, bool device);
|
||||
|
||||
// The physical device the runtime renders with. False when OpenXR is not in use.
|
||||
bool OpenXrPhysicalDevice(vk::Instance instance, vk::PhysicalDevice& out);
|
||||
|
||||
// Creates the session and starts the headset thread. Call once the device and queue exist.
|
||||
void OpenXrStartSession(Graphics::GraphicContext& graphics);
|
||||
|
||||
// Stops the headset thread and destroys the session. Call before the device is destroyed.
|
||||
void OpenXrShutdown();
|
||||
|
||||
// Render thread, headset flips only: scales the side-by-side eye image into a staging image for the
|
||||
// headset thread. The source is in transfer-source layout.
|
||||
void OpenXrQueueEyeSource(Graphics::CommandBuffer& command, const Graphics::VulkanImage& source);
|
||||
|
||||
} // namespace Libs::Vr
|
||||
|
||||
#endif // EMULATOR_SRC_GRAPHICS_PRESENTATION_OPENXRHEADSET_H_
|
||||
@@ -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_
|
||||
@@ -27,7 +27,7 @@ public:
|
||||
KYTY_CLASS_NO_COPY(Presenter);
|
||||
|
||||
[[nodiscard]] Frame& PrepareFrame(CommandBuffer& command, const ImageInfo& info,
|
||||
bool hdr = false);
|
||||
bool hdr = false, bool headset = false);
|
||||
[[nodiscard]] Frame& PrepareBlankFrame(uint32_t width, uint32_t height, bool opaque,
|
||||
CommandBuffer* producer = nullptr);
|
||||
[[nodiscard]] bool PresentLastFrame();
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#include "graphics/presentation/systemOverlay.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/file.h"
|
||||
@@ -11,10 +9,11 @@
|
||||
#include "gpu_blit_shaders/gpu_imgui_hdr_spv.h"
|
||||
#include "graphics/host_gpu/graphicContext.h"
|
||||
#include "graphics/presentation/ddsImage.h"
|
||||
#include "graphics/presentation/performanceOverlay.h"
|
||||
#include "graphics/presentation/window/hostInput.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "imgui_impl_vulkan.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "libs/controller.h"
|
||||
#include "libs/dialog.h"
|
||||
#include "libs/ime.h"
|
||||
@@ -22,6 +21,7 @@
|
||||
#include "loader/systemContent.h"
|
||||
#include "stb_image.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
@@ -712,7 +712,8 @@ SystemOverlayVisualState GetSystemOverlayVisualState() noexcept {
|
||||
return {core.active || dialog.active || system.active || trophy_active || splash ||
|
||||
(cursor.enabled && cursor.cursor_visible),
|
||||
core.revision + dialog.revision + system.revision + (trophy_active ? 1u : 0u) +
|
||||
(splash ? 2u : 0u) + static_cast<decltype(core.revision)>(cursor.revision)};
|
||||
(splash ? 2u : 0u) + static_cast<decltype(core.revision)>(cursor.revision) +
|
||||
PerformanceOverlayRevision()};
|
||||
}
|
||||
|
||||
bool ProcessSystemOverlayInput(const SDL_Event& event) {
|
||||
@@ -1662,8 +1663,7 @@ struct SystemOverlay::Impl {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void DrawTrophyNotification(vk::Extent2D frame_extent, Clock::time_point now) {
|
||||
void DrawTrophyNotification(vk::Extent2D frame_extent, Clock::time_point now, float min_top) {
|
||||
std::erase_if(retired_trophy_images, [](const auto& texture) {
|
||||
if (texture->Status != ImTextureStatus_Destroyed) {
|
||||
return false;
|
||||
@@ -1731,7 +1731,7 @@ struct SystemOverlay::Impl {
|
||||
const float alpha = std::min(fade_in, fade_out);
|
||||
const float slide = (1.0f - fade_in) * (width + 24.0f);
|
||||
const ImVec2 top_left {static_cast<float>(frame_extent.width) - 32.0f - width + slide,
|
||||
32.0f};
|
||||
std::max(32.0f, min_top + 12.0f)};
|
||||
const ImVec2 bottom_right {top_left.x + width, top_left.y + height};
|
||||
auto* draw = ImGui::GetForegroundDrawList();
|
||||
const auto fade = [alpha](int r, int g, int b, int a) {
|
||||
@@ -1799,7 +1799,7 @@ struct SystemOverlay::Impl {
|
||||
const auto now = Clock::now();
|
||||
if (!has_overlay && !g_trophy_notification_active.load(std::memory_order_acquire) &&
|
||||
!g_splash_active.load(std::memory_order_acquire) &&
|
||||
!GetHostInputTouchPadVisualState().enabled) {
|
||||
!GetHostInputTouchPadVisualState().enabled && !PerformanceOverlayEnabled()) {
|
||||
return false;
|
||||
}
|
||||
const auto prepared_session = snapshot.session;
|
||||
@@ -1830,6 +1830,7 @@ struct SystemOverlay::Impl {
|
||||
ImGui_ImplVulkan_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
DrawSplash(frame_extent);
|
||||
const float performance_bottom = DrawPerformanceOverlay(graphics, frame_extent);
|
||||
GetOverlaySnapshot(&snapshot);
|
||||
if (has_overlay && snapshot.session == prepared_session) {
|
||||
if (snapshot.session.kind == OverlayKind::Dialog) {
|
||||
@@ -1838,7 +1839,7 @@ struct SystemOverlay::Impl {
|
||||
DrawIme(snapshot.ime, frame_extent);
|
||||
}
|
||||
}
|
||||
DrawTrophyNotification(frame_extent, now);
|
||||
DrawTrophyNotification(frame_extent, now, performance_bottom);
|
||||
DrawTouchPadCursor(GetHostInputTouchPadVisualState());
|
||||
ImGui::Render();
|
||||
extent = frame_extent;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "common/abi.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/common.h"
|
||||
#include "common/debugCounters.h"
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/profiler.h"
|
||||
@@ -15,10 +16,12 @@
|
||||
#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"
|
||||
#include "libs/errno.h"
|
||||
#include "libs/hmd2.h"
|
||||
#include "libs/libs.h"
|
||||
#include "loader/systemContent.h"
|
||||
|
||||
@@ -49,6 +52,7 @@ constexpr int VIDEO_OUT_FALSE = 0;
|
||||
constexpr int VIDEO_OUT_BUS_TYPE_MAIN = 0;
|
||||
constexpr int VIDEO_OUT_BUS_TYPE_OVERLAY = 1;
|
||||
constexpr int VIDEO_OUT_BUS_TYPE_SUB = 2;
|
||||
constexpr int VIDEO_OUT_BUS_TYPE_HMD2_MAIN = 32;
|
||||
constexpr int VIDEO_OUT_FLIP_MODE_VSYNC = 1;
|
||||
constexpr int VIDEO_OUT_FLIP_MODE_VSYNC_MULTI = 4;
|
||||
constexpr int VIDEO_OUT_FLIP_MODE_SLAVE = 8;
|
||||
@@ -64,6 +68,7 @@ constexpr uint64_t VIDEO_OUT_REFRESH_RATE_59_94HZ = 3;
|
||||
constexpr uint64_t VIDEO_OUT_REFRESH_RATE_119_88HZ = 13;
|
||||
constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED = 0;
|
||||
constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED = 1;
|
||||
constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_HMD2 = 0x10;
|
||||
constexpr uint64_t VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY = 8;
|
||||
constexpr uint64_t VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_ALPHA_PREMULTIPLIED = 32;
|
||||
|
||||
@@ -250,8 +255,9 @@ private:
|
||||
|
||||
struct VideoOutDriver::Impl {
|
||||
public:
|
||||
static constexpr std::array VIDEO_OUT_BUSES {
|
||||
VIDEO_OUT_BUS_TYPE_MAIN, VIDEO_OUT_BUS_TYPE_OVERLAY, VIDEO_OUT_BUS_TYPE_SUB};
|
||||
static constexpr std::array VIDEO_OUT_BUSES {VIDEO_OUT_BUS_TYPE_MAIN,
|
||||
VIDEO_OUT_BUS_TYPE_OVERLAY, VIDEO_OUT_BUS_TYPE_SUB,
|
||||
VIDEO_OUT_BUS_TYPE_HMD2_MAIN};
|
||||
static constexpr int VIDEO_OUT_NUM_MAX = VIDEO_OUT_BUSES.size() + 1;
|
||||
|
||||
Impl(uint32_t width, uint32_t height, Graphics::Presenter& presenter)
|
||||
@@ -514,6 +520,10 @@ static bool IsValidFlipMode(int mode) {
|
||||
mode == VIDEO_OUT_FLIP_MODE_SLAVE || mode == VIDEO_OUT_FLIP_MODE_MASTER;
|
||||
}
|
||||
|
||||
// Set once the headset port shows eye images. The window then shows them instead of the main port,
|
||||
// so the picture is visible without a headset.
|
||||
static std::atomic<bool> g_headset_mirror {false};
|
||||
|
||||
static int ReserveFlipRequest(VideoOutDriver::Impl& driver, int handle, int index, int flip_mode,
|
||||
int64_t flip_arg, FlipRequestSource source, uint64_t& request_id) {
|
||||
auto* video_out = driver.Get(handle);
|
||||
@@ -532,8 +542,10 @@ static int ReserveFlipRequest(VideoOutDriver::Impl& driver, int handle, int inde
|
||||
(video_out->slave_count != 0) != (flip_mode == VIDEO_OUT_FLIP_MODE_MASTER)) {
|
||||
return VIDEO_OUT_ERROR_INVALID_FLIP_MODE;
|
||||
}
|
||||
// The headset port can flip before its reprojection buffers are registered; those show blank.
|
||||
if (video_out->closing ||
|
||||
(!IsSpecialBufferIndex(index) && !video_out->buffers[index].Occupied())) {
|
||||
(!IsSpecialBufferIndex(index) && !video_out->buffers[index].Occupied() &&
|
||||
video_out->bus != VIDEO_OUT_BUS_TYPE_HMD2_MAIN)) {
|
||||
return VIDEO_OUT_ERROR_INVALID_INDEX;
|
||||
}
|
||||
if (!driver.GetFlipQueue().Reserve(*video_out, index, flip_arg, source, request_id)) {
|
||||
@@ -738,6 +750,9 @@ bool VideoOutDriver::Impl::Close(int handle) {
|
||||
master->slave_count--;
|
||||
}
|
||||
m_flip_queue.Cancel(config);
|
||||
if (config.bus == VIDEO_OUT_BUS_TYPE_HMD2_MAIN) {
|
||||
g_headset_mirror = false;
|
||||
}
|
||||
m_presenter.ClearLayer(config.bus);
|
||||
DeleteVideoOutEvents(flip_events, VideoOutEventKind::Flip);
|
||||
DeleteVideoOutEvents(pre_vblank_events, VideoOutEventKind::PreVblankStart);
|
||||
@@ -909,6 +924,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 +956,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 +984,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;
|
||||
@@ -1059,16 +1079,19 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) {
|
||||
index = request->index;
|
||||
}
|
||||
|
||||
const bool special = IsSpecialBufferIndex(index);
|
||||
bool special = IsSpecialBufferIndex(index);
|
||||
Graphics::ImageInfo source_info;
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
bool current = false;
|
||||
bool premultiplied_alpha = false;
|
||||
bool hdr = false;
|
||||
bool headset = false;
|
||||
{
|
||||
Common::LockGuard lock(cfg->mutex);
|
||||
current = cfg->opened && !cfg->closing && cfg->generation == generation;
|
||||
headset = cfg->bus == VIDEO_OUT_BUS_TYPE_HMD2_MAIN;
|
||||
special = special || (headset && !cfg->buffers[index].Occupied());
|
||||
if (current && special) {
|
||||
width = cfg->width;
|
||||
height = cfg->height;
|
||||
@@ -1113,7 +1136,10 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) {
|
||||
frame = &m_presenter.PrepareBlankFrame(width, height, index == VIDEO_OUT_BUFFER_INDEX_BLACK,
|
||||
&buffer);
|
||||
} else {
|
||||
frame = &m_presenter.PrepareFrame(buffer, source_info, hdr);
|
||||
frame = &m_presenter.PrepareFrame(buffer, source_info, hdr, headset);
|
||||
if (headset) {
|
||||
g_headset_mirror = true;
|
||||
}
|
||||
}
|
||||
|
||||
Common::LockGuard lock(m_mutex);
|
||||
@@ -1232,6 +1258,8 @@ bool FlipQueue::Flip(uint32_t micros) {
|
||||
[](const auto& a, const auto& b) { return a.cfg->bus < b.cfg->bus; });
|
||||
bool due = true;
|
||||
std::array<Graphics::Presenter::Layer, VideoOutDriver::Impl::VIDEO_OUT_BUSES.size()> layers {};
|
||||
size_t layer_count = 0;
|
||||
Graphics::Presenter::Frame* hidden = nullptr; // The main port behind the headset image.
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
auto& r = requests[i];
|
||||
r.cfg->mutex.Lock();
|
||||
@@ -1239,10 +1267,25 @@ bool FlipQueue::Flip(uint32_t micros) {
|
||||
if (r.id == group) {
|
||||
due &= IsFlipDueLocked(*r.cfg, r.generation);
|
||||
}
|
||||
layers[i] = {r.frame, r.cfg->bus, r.premultiplied_alpha};
|
||||
if (r.cfg->bus == VIDEO_OUT_BUS_TYPE_MAIN &&
|
||||
g_headset_mirror.load(std::memory_order_relaxed)) {
|
||||
hidden = r.frame;
|
||||
} else {
|
||||
const bool headset = r.cfg->bus == VIDEO_OUT_BUS_TYPE_HMD2_MAIN;
|
||||
layers[layer_count++] = {r.frame, headset ? VIDEO_OUT_BUS_TYPE_MAIN : r.cfg->bus,
|
||||
r.premultiplied_alpha};
|
||||
}
|
||||
}
|
||||
if (due) {
|
||||
m_presenter.Present(std::span(layers.data(), count));
|
||||
if (hidden != nullptr) {
|
||||
m_presenter.Discard(*hidden);
|
||||
}
|
||||
if (layer_count != 0) {
|
||||
m_presenter.Present(std::span(layers.data(), layer_count));
|
||||
}
|
||||
if (requests[0].cfg->bus == 0) {
|
||||
Graphics::PerformanceOverlayRecordFlip(requests[0].cfg->width, requests[0].cfg->height);
|
||||
}
|
||||
}
|
||||
|
||||
m_mutex.Lock();
|
||||
@@ -1451,6 +1494,33 @@ KYTY_SYSV_ABI int VideoOutRegisterBuffers2(int handle, int set_index, int buffer
|
||||
if (option != nullptr) {
|
||||
return VIDEO_OUT_ERROR_INVALID_OPTION;
|
||||
}
|
||||
// The headset port takes reprojection buffers with a zeroed attribute. Each names its eye
|
||||
// texture, which is registered and flipped like a regular surface.
|
||||
const bool headset = ctx->bus == VIDEO_OUT_BUS_TYPE_HMD2_MAIN;
|
||||
if (headset != (category == VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_HMD2)) {
|
||||
return VIDEO_OUT_ERROR_INVALID_CATEGORY;
|
||||
}
|
||||
VideoOutBufferAttribute2 headset_attribute {};
|
||||
std::array<VideoOutBuffers, VIDEO_OUT_BUFFER_NUM_MAX> headset_buffers {};
|
||||
if (headset) {
|
||||
for (int i = 0; i < buffer_num; i++) {
|
||||
Hmd2::ReprojectionSurface surface {};
|
||||
if (buffers[i].metadata != nullptr ||
|
||||
!Hmd2::GetReprojectionSurface(buffers[i].data, surface) ||
|
||||
(i != 0 && (surface.width != headset_attribute.width ||
|
||||
surface.height != headset_attribute.height ||
|
||||
surface.pixel_format != headset_attribute.pixel_format))) {
|
||||
return VIDEO_OUT_ERROR_INVALID_VALUE;
|
||||
}
|
||||
headset_attribute.pixel_format = surface.pixel_format;
|
||||
headset_attribute.width = surface.width;
|
||||
headset_attribute.height = surface.height;
|
||||
headset_buffers[i].data = reinterpret_cast<const void*>(surface.address);
|
||||
}
|
||||
attribute = &headset_attribute;
|
||||
buffers = headset_buffers.data();
|
||||
category = VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED;
|
||||
}
|
||||
if (category != VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED &&
|
||||
category != VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED) {
|
||||
return VIDEO_OUT_ERROR_INVALID_CATEGORY;
|
||||
@@ -1861,7 +1931,8 @@ static int ValidateOutputConfig(int handle, uint64_t mode, const VideoOutOutputO
|
||||
}
|
||||
}
|
||||
|
||||
if (mode != VIDEO_OUT_OUTPUT_MODE_DEFAULT && mode != VIDEO_OUT_OUTPUT_MODE_119_88HZ) {
|
||||
if (mode != VIDEO_OUT_OUTPUT_MODE_DEFAULT && mode != VIDEO_OUT_OUTPUT_MODE_119_88HZ &&
|
||||
mode != Hmd2::OUTPUT_MODE_89_91HZ && mode != Hmd2::OUTPUT_MODE_119_88HZ) {
|
||||
return VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#ifndef EMULATOR_SRC_GRAPHICS_PRESENTATION_VRINPUT_H_
|
||||
#define EMULATOR_SRC_GRAPHICS_PRESENTATION_VRINPUT_H_
|
||||
|
||||
namespace Libs::Vr {
|
||||
|
||||
// A pose in the tracking space: metres, right-handed, +y up, -z forward (the OpenXR convention).
|
||||
struct VrPose {
|
||||
bool valid = false;
|
||||
float orientation[4] = {0.0f, 0.0f, 0.0f, 1.0f}; // x, y, z, w
|
||||
float position[3] = {0.0f, 0.0f, 0.0f};
|
||||
};
|
||||
|
||||
// One headset controller. Sticks are -1..1 with +y up, the trigger and squeeze 0..1.
|
||||
struct VrControllerInput {
|
||||
bool connected = false;
|
||||
VrPose pose;
|
||||
float trigger = 0.0f;
|
||||
float squeeze = 0.0f;
|
||||
float stick_x = 0.0f;
|
||||
float stick_y = 0.0f;
|
||||
bool primary = false; // X on the left controller, A on the right one.
|
||||
bool secondary = false; // Y on the left controller, B on the right one.
|
||||
bool stick_click = false;
|
||||
bool menu = false;
|
||||
};
|
||||
|
||||
// Latest head pose; not valid, at the origin, while no headset session tracks the head.
|
||||
[[nodiscard]] VrPose OpenXrGetHeadPose();
|
||||
|
||||
// Latest state of the left (0) or right (1) controller. While it is not connected the caller keeps
|
||||
// using the keyboard or gamepad.
|
||||
[[nodiscard]] VrControllerInput OpenXrGetController(int hand);
|
||||
|
||||
} // namespace Libs::Vr
|
||||
|
||||
#endif // EMULATOR_SRC_GRAPHICS_PRESENTATION_VRINPUT_H_
|
||||
@@ -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,8 @@
|
||||
#include "graphics/host_gpu/renderer/render.h"
|
||||
#include "graphics/host_gpu/renderer/renderContext.h"
|
||||
#include "graphics/host_gpu/vulkanCommon.h"
|
||||
#include "graphics/presentation/openxrHeadset.h"
|
||||
#include "graphics/presentation/performanceOverlay.h"
|
||||
#include "graphics/presentation/presenter.h"
|
||||
#include "graphics/presentation/systemOverlay.h"
|
||||
#include "graphics/presentation/videoOut.h"
|
||||
@@ -324,6 +327,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());
|
||||
}
|
||||
@@ -915,7 +919,8 @@ Presenter::Presenter(WindowContext& window): m_impl(std::make_unique<Impl>(windo
|
||||
|
||||
Presenter::~Presenter() = default;
|
||||
|
||||
Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo& info, bool hdr) {
|
||||
Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo& info, bool hdr,
|
||||
bool headset) {
|
||||
KYTY_PROFILER_FUNCTION();
|
||||
EXIT_IF(buffer.IsInvalid());
|
||||
auto frame_format = info.pixel_format;
|
||||
@@ -935,6 +940,12 @@ Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo
|
||||
frame->Configure(m_impl->window.graphic_ctx,
|
||||
{image.backing.extent.width, image.backing.extent.height}, frame_format);
|
||||
frame->CopyFrom(buffer, image);
|
||||
if (headset) {
|
||||
// The frame holds the raw display-encoded pixels the headset needs.
|
||||
frame->Transit(buffer.Handle(), vk::ImageLayout::eTransferSrcOptimal,
|
||||
vk::AccessFlagBits2::eTransferRead);
|
||||
Vr::OpenXrQueueEyeSource(buffer, frame->image);
|
||||
}
|
||||
return *frame;
|
||||
}
|
||||
|
||||
@@ -1039,8 +1050,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 +1071,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) {
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_vulkan.h>
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/common.h"
|
||||
#include "common/emulatorConfig.h"
|
||||
@@ -14,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/openxrHeadset.h"
|
||||
#include "graphics/presentation/presenter.h"
|
||||
#include "graphics/presentation/systemOverlay.h"
|
||||
#include "graphics/presentation/videoOut.h"
|
||||
@@ -23,6 +21,8 @@
|
||||
#include "libs/controller.h"
|
||||
#include "loader/systemContent.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_vulkan.h>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
@@ -170,6 +170,8 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
|
||||
LOGF("Vulkan GPU index %d is unavailable; selecting automatically\n",
|
||||
Config::GetGpuIndex());
|
||||
}
|
||||
} else if (vk::PhysicalDevice headset; Vr::OpenXrPhysicalDevice(instance, headset)) {
|
||||
devices = {headset}; // The headset runtime renders on this GPU.
|
||||
}
|
||||
|
||||
vk::PhysicalDevice best_device = nullptr;
|
||||
@@ -266,7 +268,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 +499,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 +625,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;
|
||||
@@ -859,8 +878,11 @@ void WindowContext::CreateVulkan() {
|
||||
}
|
||||
VULKAN_HPP_DEFAULT_DISPATCHER.init(get_instance_proc_addr);
|
||||
|
||||
Vr::OpenXrInit();
|
||||
|
||||
VulkanExtensions r;
|
||||
VulkanGetExtensions(r);
|
||||
Vr::OpenXrAppendExtensions(r.required_extensions, r.available_extensions, false);
|
||||
VulkanCheckInstanceVersion();
|
||||
|
||||
vk::ApplicationInfo app_info {};
|
||||
@@ -966,7 +988,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 +1045,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);
|
||||
}
|
||||
@@ -1034,6 +1056,7 @@ void WindowContext::CreateVulkan() {
|
||||
device_extensions.push_back(VK_EXT_ATTACHMENT_FEEDBACK_LOOP_LAYOUT_EXTENSION_NAME);
|
||||
device_extensions.push_back(VK_EXT_ATTACHMENT_FEEDBACK_LOOP_DYNAMIC_STATE_EXTENSION_NAME);
|
||||
}
|
||||
Vr::OpenXrAppendExtensions(device_extensions, available_extensions, true);
|
||||
}
|
||||
|
||||
graphic_ctx.device = VulkanCreateDevice(graphic_ctx, device_extensions);
|
||||
@@ -1051,6 +1074,7 @@ void WindowContext::CreateVulkan() {
|
||||
render_context = std::make_unique<RenderContext>(graphic_ctx);
|
||||
LibKernel::Memory::InstallGpuResources(render_context.get());
|
||||
presenter = std::make_unique<Presenter>(*this);
|
||||
Vr::OpenXrStartSession(graphic_ctx);
|
||||
}
|
||||
|
||||
void WindowContext::RefreshSurfaceCapabilities() {
|
||||
@@ -1073,6 +1097,7 @@ void WindowContext::RecreateSurface() {
|
||||
}
|
||||
|
||||
WindowContext::~WindowContext() {
|
||||
Vr::OpenXrShutdown();
|
||||
ShutdownSystemOverlayInput();
|
||||
presenter.reset();
|
||||
LibKernel::Memory::InstallGpuResources(nullptr);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -209,20 +209,20 @@ int BufferTableAttribFromOffset(uint32_t raw_offset, int dword) {
|
||||
}
|
||||
|
||||
Frontend::EmbeddedFetchPlan DetectEmbeddedVertexFetch(
|
||||
const Decoder::Program& decoded, const ShaderVertexInputInfo* input_info,
|
||||
const Decoder::Program& decoded, const ShaderVertexInputInfo& input_info,
|
||||
uint32_t user_data_base, uint32_t user_data_count, uint32_t wave_size) {
|
||||
const uint32_t vertex_index_reg = input_info->logical_stage == ShaderType::Local ? 2u : 5u;
|
||||
const uint32_t instance_index_reg = input_info->logical_stage == ShaderType::Local ? 5u : 8u;
|
||||
const uint32_t vertex_index_reg = input_info.logical_stage == ShaderType::Local ? 2u : 5u;
|
||||
const uint32_t instance_index_reg = input_info.logical_stage == ShaderType::Local ? 5u : 8u;
|
||||
Frontend::EmbeddedFetchPlan data;
|
||||
data.loads.reserve(input_info->resources_num);
|
||||
data.loads.reserve(input_info.resources_num);
|
||||
int32_t vertex_offset_candidate = -1;
|
||||
int32_t instance_offset_candidate = -1;
|
||||
bool vertex_offset_conflict = false;
|
||||
bool instance_offset_conflict = false;
|
||||
|
||||
const int shift_regs = 8;
|
||||
const int attrib_reg = input_info->fetch_attrib_reg + shift_regs;
|
||||
const int buffer_reg = input_info->fetch_buffer_reg + shift_regs;
|
||||
const int attrib_reg = input_info.fetch_attrib_reg + shift_regs;
|
||||
const int buffer_reg = input_info.fetch_buffer_reg + shift_regs;
|
||||
|
||||
std::array<EmbeddedFetchSgprInfo, 108> sgprs {};
|
||||
std::array<bool, 256> vgpr_is_index {};
|
||||
@@ -481,18 +481,13 @@ 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 +519,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 +556,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 +606,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 +614,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;
|
||||
@@ -763,9 +808,9 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
|
||||
const auto& cfg = *selected_cfg;
|
||||
Frontend::EmbeddedFetchPlan embedded_fetch;
|
||||
if ((options.stage == ShaderType::Vertex || options.stage == ShaderType::Local) &&
|
||||
options.input_info.vertex != nullptr && options.input_info.vertex->fetch_embedded) {
|
||||
options.input_info.vertex->fetch_embedded) {
|
||||
embedded_fetch = DetectEmbeddedVertexFetch(
|
||||
decoded, options.input_info.vertex, options.user_data_base,
|
||||
decoded, *options.input_info.vertex, options.user_data_base,
|
||||
static_cast<uint32_t>(options.user_data.size()), options.wave_size);
|
||||
if (!embedded_fetch.loads.empty()) {
|
||||
LOGF("%s embedded vertex fetch plan: detected=%" PRIu64 "\n", GetDumpLabel(options),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -21,15 +21,21 @@ struct TessellationAddress {
|
||||
Kind kind = Kind::Unknown;
|
||||
uint32_t coefficient = 0;
|
||||
uint32_t constant = 0;
|
||||
|
||||
bool operator==(const TessellationAddress&) const = default;
|
||||
};
|
||||
|
||||
uint32_t ReflectTessellationStride(const Decoder::Program& program, bool local,
|
||||
uint32_t control_points, uint32_t input_stride) {
|
||||
// Reflects the LS output stride, or the HS output stride and patch constant range.
|
||||
void ReflectTessellationLayout(const Decoder::Program& program, bool local,
|
||||
ShaderTessellationInputInfo& info) {
|
||||
using namespace Decoder;
|
||||
using Address = TessellationAddress;
|
||||
using Kind = Address::Kind;
|
||||
std::array<Address, IR::NumVectorRegs> registers {};
|
||||
registers[local ? 3u : 1u] =
|
||||
using Address = TessellationAddress;
|
||||
using Kind = Address::Kind;
|
||||
using Registers = std::array<Address, IR::NumVectorRegs>;
|
||||
|
||||
const auto control_points = local ? info.input_control_points : info.output_control_points;
|
||||
Registers entry {}, registers;
|
||||
entry[local ? 3u : 1u] =
|
||||
local ? Address {Kind::Affine, 1u, 0u} : Address {Kind::PackedControlPoint};
|
||||
const auto constant = [](uint32_t value) { return Address {Kind::Affine, 0u, value}; };
|
||||
const auto read = [&](const Operand& operand) -> Address {
|
||||
@@ -79,78 +85,122 @@ uint32_t ReflectTessellationStride(const Decoder::Program& program, bool local,
|
||||
? value
|
||||
: Address {};
|
||||
};
|
||||
uint32_t stride = 0;
|
||||
for (const auto& inst: program.instructions) {
|
||||
// Stage exits preserve the active path's definitions. An internal join
|
||||
// would require merging register definitions.
|
||||
EXIT_NOT_IMPLEMENTED(IsDirectBranch(inst.opcode) &&
|
||||
inst.branch_target != program.instructions.back().pc);
|
||||
const bool local_store =
|
||||
inst.opcode == Opcode::DS_WRITE_B32 || inst.opcode == Opcode::DS_WRITE2_B32;
|
||||
const bool buffer_store = inst.opcode == Opcode::BUFFER_STORE_DWORD ||
|
||||
inst.opcode == Opcode::BUFFER_STORE_DWORDX2 ||
|
||||
inst.opcode == Opcode::BUFFER_STORE_DWORDX3 ||
|
||||
inst.opcode == Opcode::BUFFER_STORE_DWORDX4;
|
||||
const bool control_store =
|
||||
!local && buffer_store && inst.src2.kind == OperandKind::Sgpr && inst.src2.reg == 2u;
|
||||
const bool control_read =
|
||||
!local && (inst.opcode == Opcode::DS_READ_B32 || inst.opcode == Opcode::DS_READ2_B32);
|
||||
if ((local && local_store) || control_store || control_read) {
|
||||
const auto address = read(inst.src0);
|
||||
if (address.kind != Kind::Affine) {
|
||||
EXIT("%s tessellation address is not affine at pc 0x%08x\n", local ? "LS" : "HS",
|
||||
inst.pc);
|
||||
}
|
||||
if (address.coefficient != 0u) {
|
||||
const auto expected = control_read ? input_stride : stride;
|
||||
EXIT_NOT_IMPLEMENTED((address.coefficient & 3u) != 0u ||
|
||||
(expected != 0u && expected != address.coefficient));
|
||||
if (!control_read) stride = address.coefficient;
|
||||
}
|
||||
// A branch joins its target with the fallthrough path. Definitions that differ between
|
||||
// the paths are unknown there. Loop headers repeat the walk until back edges are stable.
|
||||
std::unordered_map<uint32_t, Registers> joins, loops;
|
||||
const auto merge = [](Registers& into, const Registers& from) {
|
||||
bool changed = false;
|
||||
for (uint32_t index = 0; index < into.size(); index++) {
|
||||
if (into[index] == from[index]) continue;
|
||||
changed |= into[index] != Address {};
|
||||
into[index] = {};
|
||||
}
|
||||
// Store vdata is a source, despite occupying the decoder's dst field.
|
||||
if (local_store || buffer_store || inst.dst.kind != OperandKind::Vgpr) continue;
|
||||
const auto lhs = read(inst.src0);
|
||||
const auto rhs = read(inst.src1);
|
||||
const auto third = read(inst.src2);
|
||||
Address value;
|
||||
switch (inst.opcode) {
|
||||
case Opcode::V_BFE_U32:
|
||||
if (lhs.kind == Kind::PackedControlPoint && rhs.kind == Kind::Affine &&
|
||||
rhs.coefficient == 0u && third.kind == Kind::Affine &&
|
||||
third.coefficient == 0u) {
|
||||
if (rhs.constant == 8u && third.constant == 5u) value = {Kind::Affine, 1u, 0u};
|
||||
if (rhs.constant == 0u && third.constant == 8u) value = constant(0u);
|
||||
return changed;
|
||||
};
|
||||
|
||||
uint32_t stride = 0, patch_begin = UINT32_MAX, patch_end = 0;
|
||||
for (bool repeat = true; repeat;) {
|
||||
repeat = false;
|
||||
registers = entry;
|
||||
for (const auto& inst: program.instructions) {
|
||||
if (const auto join = joins.find(inst.pc); join != joins.end()) {
|
||||
merge(registers, join->second);
|
||||
joins.erase(join);
|
||||
}
|
||||
if (const auto loop = loops.find(inst.pc); loop != loops.end())
|
||||
merge(registers, loop->second);
|
||||
if (IsDirectBranch(inst.opcode)) {
|
||||
const bool back = inst.branch_target <= inst.pc;
|
||||
const auto [join, inserted] =
|
||||
(back ? loops : joins).try_emplace(inst.branch_target, registers);
|
||||
const bool changed = !inserted && merge(join->second, registers);
|
||||
repeat |= back && (inserted || changed);
|
||||
}
|
||||
const bool local_store = IsDsWriteOpcode(inst.opcode);
|
||||
const bool buffer_store = inst.opcode == Opcode::BUFFER_STORE_DWORD ||
|
||||
inst.opcode == Opcode::BUFFER_STORE_DWORDX2 ||
|
||||
inst.opcode == Opcode::BUFFER_STORE_DWORDX3 ||
|
||||
inst.opcode == Opcode::BUFFER_STORE_DWORDX4;
|
||||
const bool control_store = !local && buffer_store &&
|
||||
inst.src2.kind == OperandKind::Sgpr && inst.src2.reg == 2u;
|
||||
if ((local && local_store) || control_store) {
|
||||
const auto address = read(inst.src0);
|
||||
if (address.kind != Kind::Affine) {
|
||||
EXIT("%s tessellation address is not affine at pc 0x%08x\n",
|
||||
local ? "LS" : "HS", inst.pc);
|
||||
}
|
||||
break;
|
||||
case Opcode::V_MUL_U32_U24: value = multiply(low24(lhs), low24(rhs)); break;
|
||||
case Opcode::V_MAD_U32_U24: value = add(multiply(low24(lhs), low24(rhs)), third); break;
|
||||
case Opcode::V_LSHL_ADD_U32:
|
||||
if (rhs.kind == Kind::Affine && rhs.coefficient == 0u && rhs.constant < 32u)
|
||||
value = add(multiply(lhs, constant(1u << rhs.constant)), third);
|
||||
break;
|
||||
case Opcode::V_LSHLREV_B32:
|
||||
if (lhs.kind == Kind::Affine && lhs.coefficient == 0u && lhs.constant < 32u)
|
||||
value = multiply(rhs, constant(1u << lhs.constant));
|
||||
break;
|
||||
case Opcode::V_SUB_NC_U32:
|
||||
if (lhs.kind == Kind::Affine && rhs.kind == Kind::Affine &&
|
||||
lhs.coefficient >= rhs.coefficient && lhs.constant >= rhs.constant)
|
||||
value = affine(lhs.coefficient - rhs.coefficient, lhs.constant - rhs.constant);
|
||||
break;
|
||||
default: break;
|
||||
if (address.coefficient != 0u) {
|
||||
EXIT_NOT_IMPLEMENTED((address.coefficient & 3u) != 0u ||
|
||||
(stride != 0u && stride != address.coefficient));
|
||||
stride = address.coefficient;
|
||||
} else if (control_store) {
|
||||
// Control-point-independent HS outputs are patch constants.
|
||||
const auto begin = address.constant + inst.offset;
|
||||
patch_begin = std::min(patch_begin, begin);
|
||||
patch_end = std::max(patch_end, begin + 4u * inst.data_dwords);
|
||||
}
|
||||
}
|
||||
// Store vdata is a source, despite occupying the decoder's dst field.
|
||||
if (local_store || buffer_store || inst.dst.kind != OperandKind::Vgpr) continue;
|
||||
const auto lhs = read(inst.src0);
|
||||
const auto rhs = read(inst.src1);
|
||||
const auto third = read(inst.src2);
|
||||
Address value;
|
||||
switch (inst.opcode) {
|
||||
case Opcode::V_BFE_U32:
|
||||
if (lhs.kind == Kind::PackedControlPoint && rhs.kind == Kind::Affine &&
|
||||
rhs.coefficient == 0u && third.kind == Kind::Affine &&
|
||||
third.coefficient == 0u) {
|
||||
if (rhs.constant == 8u && third.constant == 5u)
|
||||
value = {Kind::Affine, 1u, 0u};
|
||||
if (rhs.constant == 0u && third.constant == 8u) value = constant(0u);
|
||||
}
|
||||
break;
|
||||
case Opcode::V_AND_B32:
|
||||
if ((lhs.kind == Kind::PackedControlPoint && rhs == constant(0xffu)) ||
|
||||
(rhs.kind == Kind::PackedControlPoint && lhs == constant(0xffu)))
|
||||
value = constant(0u);
|
||||
break;
|
||||
case Opcode::V_ADD_NC_U32: value = add(lhs, rhs); break;
|
||||
case Opcode::V_MUL_LO_U32: value = multiply(lhs, rhs); break;
|
||||
case Opcode::V_MUL_U32_U24: value = multiply(low24(lhs), low24(rhs)); break;
|
||||
case Opcode::V_MAD_U32_U24:
|
||||
value = add(multiply(low24(lhs), low24(rhs)), third);
|
||||
break;
|
||||
case Opcode::V_LSHL_ADD_U32:
|
||||
if (rhs.kind == Kind::Affine && rhs.coefficient == 0u && rhs.constant < 32u)
|
||||
value = add(multiply(lhs, constant(1u << rhs.constant)), third);
|
||||
break;
|
||||
case Opcode::V_LSHLREV_B32:
|
||||
if (lhs.kind == Kind::Affine && lhs.coefficient == 0u && lhs.constant < 32u)
|
||||
value = multiply(rhs, constant(1u << lhs.constant));
|
||||
break;
|
||||
case Opcode::V_SUB_NC_U32:
|
||||
if (lhs.kind == Kind::Affine && rhs.kind == Kind::Affine &&
|
||||
lhs.coefficient >= rhs.coefficient && lhs.constant >= rhs.constant)
|
||||
value =
|
||||
affine(lhs.coefficient - rhs.coefficient, lhs.constant - rhs.constant);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
if (inst.dst.sdwa_sel != 6u || inst.dst.op_sel || inst.dst.omod || inst.dst.clamp ||
|
||||
inst.dst.dpp)
|
||||
value = {};
|
||||
for (uint32_t index = 0;
|
||||
index < std::max(inst.data_dwords, 1u) && inst.dst.reg + index < registers.size();
|
||||
index++) {
|
||||
registers[inst.dst.reg + index] = {};
|
||||
}
|
||||
registers.at(inst.dst.reg) = value;
|
||||
}
|
||||
if (inst.dst.sdwa_sel != 6u || inst.dst.op_sel || inst.dst.omod || inst.dst.clamp ||
|
||||
inst.dst.dpp)
|
||||
value = {};
|
||||
for (uint32_t index = 0;
|
||||
index < std::max(inst.data_dwords, 1u) && inst.dst.reg + index < registers.size(); index++) {
|
||||
registers[inst.dst.reg + index] = {};
|
||||
}
|
||||
registers.at(inst.dst.reg) = value;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(stride == 0u);
|
||||
return stride;
|
||||
EXIT_NOT_IMPLEMENTED(!joins.empty() || stride == 0u);
|
||||
(local ? info.ls_stride : info.hs_stride) = stride;
|
||||
if (patch_end != 0u) {
|
||||
EXIT_NOT_IMPLEMENTED(patch_begin < stride * control_points);
|
||||
info.patch_base = patch_begin;
|
||||
info.patch_size = patch_end - patch_begin;
|
||||
}
|
||||
}
|
||||
|
||||
const IR::Inst* TessellationBufferBase(const IR::Inst& inst) {
|
||||
@@ -161,6 +211,15 @@ const IR::Inst* TessellationBufferBase(const IR::Inst& inst) {
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
// Whether an access predicate, possibly a conjunction of EXEC masks, implies a condition.
|
||||
bool Implies(IR::Value predicate, IR::Value condition) {
|
||||
predicate = predicate.Resolve();
|
||||
if (predicate == condition.Resolve()) return true;
|
||||
const auto* inst = predicate.TryInstruction();
|
||||
return inst != nullptr && inst->GetOpcode() == IR::ValueOpcode::LogicalAnd &&
|
||||
(Implies(inst->Arg(0), condition) || Implies(inst->Arg(1), condition));
|
||||
}
|
||||
|
||||
IR::Value ActiveAddress(IR::Block& block, IR::Block::iterator before, IR::Value value,
|
||||
IR::Value predicate, std::unordered_map<IR::Inst*, IR::Value>& resolved) {
|
||||
using namespace IR;
|
||||
@@ -169,18 +228,28 @@ IR::Value ActiveAddress(IR::Block& block, IR::Block::iterator before, IR::Value
|
||||
if (source == nullptr) return value;
|
||||
if (const auto found = resolved.find(source); found != resolved.end()) return found->second;
|
||||
if (source->GetOpcode() == ValueOpcode::SelectU32) {
|
||||
return source->Arg(0).Resolve() == predicate.Resolve()
|
||||
return Implies(predicate, source->Arg(0))
|
||||
? ActiveAddress(block, before, source->Arg(1), predicate, resolved)
|
||||
: value;
|
||||
}
|
||||
if (source->GetOpcode() != ValueOpcode::IAdd32 && source->GetOpcode() != ValueOpcode::ISub32)
|
||||
// Pure address arithmetic that stays behind EXEC selects; mul24 lowers to IMul32 of BFEs.
|
||||
const auto opcode = source->GetOpcode();
|
||||
if (opcode != ValueOpcode::IAdd32 && opcode != ValueOpcode::ISub32 &&
|
||||
opcode != ValueOpcode::IMul32 && opcode != ValueOpcode::BitFieldUExtract)
|
||||
return value;
|
||||
const auto lhs = ActiveAddress(block, before, source->Arg(0), predicate, resolved);
|
||||
const auto rhs = ActiveAddress(block, before, source->Arg(1), predicate, resolved);
|
||||
if (lhs != source->Arg(0).Resolve() || rhs != source->Arg(1).Resolve()) {
|
||||
auto copy = block.PrependNewInst(before, source->GetOpcode(), {lhs, rhs},
|
||||
source->Flags<uint64_t>());
|
||||
value = Value(&*copy);
|
||||
std::array<Value, 3> args;
|
||||
bool changed = false;
|
||||
for (size_t index = 0; index < source->NumArgs(); index++) {
|
||||
args[index] = ActiveAddress(block, before, source->Arg(index), predicate, resolved);
|
||||
changed |= args[index] != source->Arg(index).Resolve();
|
||||
}
|
||||
if (changed) {
|
||||
const auto flags = source->Flags<uint64_t>();
|
||||
const auto copy =
|
||||
source->NumArgs() == 2u
|
||||
? block.PrependNewInst(before, opcode, {args[0], args[1]}, flags)
|
||||
: block.PrependNewInst(before, opcode, {args[0], args[1], args[2]}, flags);
|
||||
value = Value(&*copy);
|
||||
}
|
||||
resolved.emplace(source, value);
|
||||
return value;
|
||||
@@ -193,11 +262,12 @@ void AnalyzeTessellationPrograms(std::span<const uint32_t> local, std::span<cons
|
||||
const auto local_program = Decoder::DecodeFrontProgram(local);
|
||||
Decoder::Program control_program;
|
||||
Decoder::DecodeProgram(control, control_program);
|
||||
info.ls_stride = ReflectTessellationStride(local_program, true, info.input_control_points, 0u);
|
||||
info.hs_stride = ReflectTessellationStride(control_program, false, info.output_control_points,
|
||||
info.ls_stride);
|
||||
LOGF("Tessellation interface: input_cp=%u output_cp=%u ls_stride=%u hs_stride=%u\n",
|
||||
info.input_control_points, info.output_control_points, info.ls_stride, info.hs_stride);
|
||||
ReflectTessellationLayout(local_program, true, info);
|
||||
ReflectTessellationLayout(control_program, false, info);
|
||||
LOGF("Tessellation interface: input_cp=%u output_cp=%u ls_stride=%u hs_stride=%u "
|
||||
"patch_base=0x%x patch_size=%u\n",
|
||||
info.input_control_points, info.output_control_points, info.ls_stride, info.hs_stride,
|
||||
info.patch_base, info.patch_size);
|
||||
}
|
||||
|
||||
void LowerTessellationMemory(IR::Program& program, const CompileOptions& options) {
|
||||
@@ -218,7 +288,7 @@ void LowerTessellationMemory(IR::Program& program, const CompileOptions& options
|
||||
}
|
||||
}
|
||||
ConstantPropagationPass(program.blocks, program.wave_size);
|
||||
uint32_t reads = 0, writes = 0, factors = 0;
|
||||
uint32_t reads = 0, writes = 0, factors = 0, patches = 0;
|
||||
for (auto* block: program.blocks) {
|
||||
for (auto it = block->begin(); it != block->end(); ++it) {
|
||||
auto& inst = *it;
|
||||
@@ -261,15 +331,22 @@ void LowerTessellationMemory(IR::Program& program, const CompileOptions& options
|
||||
kind = TessellationAttribute::Factor;
|
||||
factors += components;
|
||||
} else {
|
||||
kind = write ? TessellationAttribute::ControlOutput
|
||||
: TessellationAttribute::EvaluationInput;
|
||||
EXIT_NOT_IMPLEMENTED(write
|
||||
? options.stage != ShaderType::TessellationControl
|
||||
: options.stage != ShaderType::TessellationEvaluation);
|
||||
// After its barrier, the HS may read back outputs of other control points.
|
||||
const bool output = options.stage == ShaderType::TessellationControl;
|
||||
EXIT_NOT_IMPLEMENTED(
|
||||
!output && (write || options.stage != ShaderType::TessellationEvaluation));
|
||||
kind = output ? TessellationAttribute::ControlOutput
|
||||
: TessellationAttribute::EvaluationInput;
|
||||
if (address.IsImmediate() &&
|
||||
address.U32() >= tess.hs_stride * tess.output_control_points) {
|
||||
EXIT_NOT_IMPLEMENTED(!write);
|
||||
kind = TessellationAttribute::PatchOutput;
|
||||
const auto begin = address.U32() + memory.offset;
|
||||
EXIT_NOT_IMPLEMENTED(begin < tess.patch_base ||
|
||||
begin + 4u * components >
|
||||
tess.patch_base + tess.patch_size);
|
||||
kind = output ? TessellationAttribute::PatchOutput
|
||||
: TessellationAttribute::PatchInput;
|
||||
// HS read-backs must not hide misplaced patch stores from the check below.
|
||||
if (write || !output) patches += components;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -320,11 +397,14 @@ void LowerTessellationMemory(IR::Program& program, const CompileOptions& options
|
||||
ConstantPropagationPass(program.blocks, program.wave_size);
|
||||
RemoveIdentities(program);
|
||||
EliminateDeadCode(program.blocks);
|
||||
LOGF("%s tessellation lowering: reads=%u writes=%u factors=%u\n",
|
||||
// Patch constant stores whose EXEC selects were not stripped would land in control points.
|
||||
EXIT_NOT_IMPLEMENTED(options.stage == ShaderType::TessellationControl &&
|
||||
tess.patch_size != 0u && patches == 0u);
|
||||
LOGF("%s tessellation lowering: reads=%u writes=%u factors=%u patches=%u\n",
|
||||
options.stage == ShaderType::Local ? "LS"
|
||||
: options.stage == ShaderType::TessellationControl ? "HS"
|
||||
: "TES",
|
||||
reads, writes, factors);
|
||||
reads, writes, factors, patches);
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler
|
||||
|
||||
@@ -233,8 +233,8 @@ std::vector<uint32_t> EmitProgram(IR::Program& program, ShaderStageInputInfo inp
|
||||
? 2u
|
||||
: 1u;
|
||||
DefineModule(state);
|
||||
ValidateNativeProgram(program, program.stage == ShaderType::Compute &&
|
||||
input_info.compute != nullptr && input_info.compute->lds_storage);
|
||||
ValidateNativeProgram(program,
|
||||
program.stage == ShaderType::Compute && input_info.compute->lds_storage);
|
||||
EmitProgram(state);
|
||||
state.builder.AddEntryPoint(ExecutionModelForStage(state.program.stage), state.main_func,
|
||||
"main", state.interface_variables);
|
||||
|
||||
@@ -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();
|
||||
@@ -77,7 +115,7 @@ uint32_t EmitF32ToU32(EmitterState& state, uint32_t src, bool signed_value) {
|
||||
} else {
|
||||
state.builder.AddFunction(spv::OpConvertFToU, TypeU32(state), converted_raw, trunc);
|
||||
}
|
||||
const auto nan = EmitClassifyF32(state, src).nan;
|
||||
const auto nan = Unary(state, spv::OpIsNan, TypeBool(state), src);
|
||||
if (signed_value) {
|
||||
const auto below = Binary(state, spv::OpFOrdLessThanEqual, TypeBool(state), src,
|
||||
ConstantF32(state, 0xcf000000u));
|
||||
@@ -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,31 +235,16 @@ 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) {
|
||||
// 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);
|
||||
// The clamp modifier runs in DX10 clamp mode: NaN becomes 0, as NClamp's NMax step does.
|
||||
return EmitExt(state, TypeF32(state), GLSLstd450NClamp,
|
||||
{arg0, ConstantF32(state, 0), ConstantF32(state, 0x3f800000u)});
|
||||
}
|
||||
|
||||
uint32_t EmitSMulHi(EmitterState& state, uint32_t arg0, uint32_t arg1) {
|
||||
|
||||
@@ -17,16 +17,9 @@ uint32_t EmitCompareU32Constant(EmitterState& state, spv::Op opcode, uint32_t va
|
||||
}
|
||||
|
||||
F32Class EmitClassifyF32Bits(EmitterState& state, uint32_t bits) {
|
||||
F32Class cls;
|
||||
cls.bits = bits;
|
||||
const auto abs_bits = EmitAndConstant(state, cls.bits, 0x7fffffffu);
|
||||
cls.nan = EmitCompareU32Constant(state, spv::OpUGreaterThan, abs_bits, 0x7f800000u);
|
||||
cls.zero = EmitCompareU32Constant(state, spv::OpIEqual, abs_bits, 0);
|
||||
return cls;
|
||||
}
|
||||
|
||||
F32Class EmitClassifyF32(EmitterState& state, uint32_t value) {
|
||||
return EmitClassifyF32Bits(state, EmitBitcastF32ToU32(state, value));
|
||||
const auto abs_bits = EmitAndConstant(state, bits, 0x7fffffffu);
|
||||
return {EmitCompareU32Constant(state, spv::OpUGreaterThan, abs_bits, 0x7f800000u),
|
||||
EmitCompareU32Constant(state, spv::OpIEqual, abs_bits, 0)};
|
||||
}
|
||||
|
||||
uint32_t EmitClassMaskBitMatch(EmitterState& state, uint32_t mask, uint32_t bit,
|
||||
@@ -100,25 +93,54 @@ uint32_t EmitClassMaskF16(EmitterState& state, uint32_t bits, uint32_t mask) {
|
||||
return EmitClassMaskBits(state, bits, mask, true);
|
||||
}
|
||||
|
||||
static uint32_t EmitMinMaxF32(EmitterState& state, uint32_t lhs, uint32_t rhs, uint32_t lhs_bits,
|
||||
uint32_t rhs_bits, bool max_value) {
|
||||
// RDNA2 V_MIN/V_MAX_F32 return the other source for a NaN source (S1 when both are NaN)
|
||||
// and order -0 below +0. An unordered compare keeps S0 for a NaN S1 and, unlike NMin/NMax,
|
||||
// always returns the exact bits of one source.
|
||||
const auto either = EmitOrU32(state, lhs_bits, rhs_bits);
|
||||
const auto both_zero = EmitCompareU32Constant(state, spv::OpIEqual,
|
||||
EmitAndConstant(state, either, 0x7fffffffu), 0);
|
||||
const auto zero =
|
||||
EmitBitcastU32ToF32(state, max_value ? EmitAndU32(state, lhs_bits, rhs_bits) : either);
|
||||
const auto keep_lhs =
|
||||
Binary(state, max_value ? spv::OpFUnordGreaterThanEqual : spv::OpFUnordLessThan,
|
||||
TypeBool(state), lhs, rhs);
|
||||
const auto ordered = Select(state, TypeF32(state), keep_lhs, lhs, rhs);
|
||||
const auto numeric = Select(state, TypeF32(state), both_zero, zero, ordered);
|
||||
return Select(state, TypeF32(state), Unary(state, spv::OpIsNan, TypeBool(state), lhs), rhs,
|
||||
numeric);
|
||||
}
|
||||
|
||||
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, EmitBitcastF32ToU32(state, lhs),
|
||||
EmitBitcastF32ToU32(state, rhs), max_value);
|
||||
}
|
||||
|
||||
const auto numeric_cond = state.builder.AllocateId();
|
||||
state.builder.AddFunction(max_value ? spv::OpFOrdGreaterThanEqual : spv::OpFOrdLessThan,
|
||||
TypeBool(state), numeric_cond, lhs, rhs);
|
||||
const auto ordered_bits =
|
||||
EmitSelectValueU32(state, numeric_cond, lhs_class.bits, rhs_class.bits);
|
||||
|
||||
const auto both_zero = EmitLogicalAndBool(state, lhs_class.zero, rhs_class.zero);
|
||||
const auto zero_bits = max_value ? EmitAndU32(state, lhs_class.bits, rhs_class.bits)
|
||||
: EmitOrU32(state, lhs_class.bits, rhs_class.bits);
|
||||
const auto numeric_bits = EmitSelectValueU32(state, both_zero, zero_bits, ordered_bits);
|
||||
|
||||
const auto rhs_nan_bits =
|
||||
EmitSelectValueU32(state, rhs_class.nan, lhs_class.bits, numeric_bits);
|
||||
const auto result_bits = EmitSelectValueU32(state, lhs_class.nan, rhs_class.bits, rhs_nan_bits);
|
||||
return EmitBitcastU32ToF32(state, result_bits);
|
||||
uint32_t EmitFPMedTri32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
|
||||
const auto a_bits = EmitBitcastF32ToU32(state, a);
|
||||
const auto b_bits = EmitBitcastF32ToU32(state, b);
|
||||
const auto c_bits = EmitBitcastF32ToU32(state, c);
|
||||
const auto min_ab = EmitMinMaxF32(state, a, b, a_bits, b_bits, false);
|
||||
const auto min3 =
|
||||
EmitMinMaxF32(state, min_ab, c, EmitBitcastF32ToU32(state, min_ab), c_bits, false);
|
||||
const auto max_ab = EmitMinMaxF32(state, a, b, a_bits, b_bits, true);
|
||||
const auto max3 =
|
||||
EmitMinMaxF32(state, max_ab, c, EmitBitcastF32ToU32(state, max_ab), c_bits, true);
|
||||
// RDNA2 compares numerically against MAX3, including ties between signed zeros, then takes
|
||||
// V_MAX_F32 of the other two sources in their original order.
|
||||
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 x = Select(state, TypeF32(state), a_is_max, b, a);
|
||||
const auto y =
|
||||
Select(state, TypeF32(state), EmitLogicalOrBool(state, a_is_max, b_is_max), c, b);
|
||||
const auto median = EmitMinMaxF32Value(state, x, y, true);
|
||||
const auto is_nan = [&](uint32_t value) {
|
||||
return Unary(state, spv::OpIsNan, TypeBool(state), value);
|
||||
};
|
||||
const auto any_nan =
|
||||
EmitLogicalOrBool(state, EmitLogicalOrBool(state, is_nan(a), is_nan(b)), is_nan(c));
|
||||
return Select(state, TypeF32(state), any_nan, min3, median);
|
||||
}
|
||||
|
||||
uint32_t EmitFlushF32DenormToSignedZero(EmitterState& state, uint32_t value) {
|
||||
|
||||
@@ -53,8 +53,7 @@ uint32_t EmitBuiltinU32(EmitterState& state, IR::StageInputKind kind, uint32_t c
|
||||
const auto bits = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpLoad, TypeBool(state), value, variable);
|
||||
// SPI_BARYC_CNTL.FRONT_FACE_ALL_BITS selects uint masks or float +/-1.
|
||||
const bool all_bits =
|
||||
state.input_info.pixel != nullptr && state.input_info.pixel->ps_front_face_all_bits;
|
||||
const bool all_bits = state.input_info.pixel->ps_front_face_all_bits;
|
||||
const auto front = ConstantU32(state, all_bits ? 0xffffffffu : 0x3f800000u);
|
||||
const auto back = ConstantU32(state, all_bits ? 0u : 0xbf800000u);
|
||||
state.builder.AddFunction(spv::OpSelect, TypeU32(state), bits, value, front, back);
|
||||
@@ -350,6 +349,38 @@ uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo
|
||||
return vector;
|
||||
}
|
||||
|
||||
uint32_t DistancePointer(EmitterState& state, IR::StageOutputKind kind, uint32_t index) {
|
||||
if (state.program.stage == ShaderType::Mesh) {
|
||||
return MeshOutputPointer(state, kind, index);
|
||||
}
|
||||
const auto variable = kind == IR::StageOutputKind::ClipDistance ? state.clip_distance_variable
|
||||
: state.cull_distance_variable;
|
||||
const auto pointer = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpAccessChain,
|
||||
TypePointer(state, spv::StorageClassOutput, TypeF32(state)), pointer,
|
||||
variable, ConstantU32(state, index));
|
||||
return pointer;
|
||||
}
|
||||
|
||||
// The clipper keeps the half space where dot(plane, position) >= 0 for each UCP_ENA plane.
|
||||
void EmitUserClipDistances(EmitterState& state, uint32_t position) {
|
||||
uint32_t index = 0;
|
||||
for (auto mask = state.user_clip_planes; mask != 0; mask &= mask - 1u) {
|
||||
const auto dword = IR::PushData::UserClipPlaneDword + 4u * std::countr_zero(mask);
|
||||
uint32_t components[4] {};
|
||||
for (uint32_t i = 0; i < 4; i++) {
|
||||
components[i] = Unary(state, spv::OpBitcast, TypeF32(state),
|
||||
EmitPushDataLoad(state, ConstantU32(state, dword + i)));
|
||||
}
|
||||
const auto plane = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 4), plane,
|
||||
components[0], components[1], components[2], components[3]);
|
||||
state.builder.AddFunction(
|
||||
spv::OpStore, DistancePointer(state, IR::StageOutputKind::ClipDistance, index++),
|
||||
Binary(state, spv::OpDot, TypeF32(state), plane, position));
|
||||
}
|
||||
}
|
||||
|
||||
void EmitAuxPositionExport(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo& exp) {
|
||||
auto& state = ctx.state;
|
||||
for (uint32_t component = 0; component < 4; component++) {
|
||||
@@ -390,23 +421,13 @@ void EmitAuxPositionExport(ValueEmitContext& ctx, uint32_t data, const IR::Expor
|
||||
state.builder.AddFunction(spv::OpStore, state.point_size_variable, f32);
|
||||
continue;
|
||||
}
|
||||
auto StoreDistance = [&](IR::StageOutputKind kind, uint32_t variable, uint32_t index) {
|
||||
if (index == UINT32_MAX) {
|
||||
return;
|
||||
auto StoreDistance = [&](IR::StageOutputKind kind, uint32_t index) {
|
||||
if (index != UINT32_MAX) {
|
||||
state.builder.AddFunction(spv::OpStore, DistancePointer(state, kind, index), f32);
|
||||
}
|
||||
uint32_t pointer;
|
||||
if (state.program.stage == ShaderType::Mesh) {
|
||||
pointer = MeshOutputPointer(state, kind, index);
|
||||
} else {
|
||||
pointer = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpAccessChain,
|
||||
TypePointer(state, spv::StorageClassOutput, TypeF32(state)),
|
||||
pointer, variable, ConstantU32(state, index));
|
||||
}
|
||||
state.builder.AddFunction(spv::OpStore, pointer, f32);
|
||||
};
|
||||
StoreDistance(IR::StageOutputKind::ClipDistance, state.clip_distance_variable, output.clip_distance);
|
||||
StoreDistance(IR::StageOutputKind::CullDistance, state.cull_distance_variable, output.cull_distance);
|
||||
StoreDistance(IR::StageOutputKind::ClipDistance, output.clip_distance);
|
||||
StoreDistance(IR::StageOutputKind::CullDistance, output.cull_distance);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,6 +618,9 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
state.input_info.vertex->clip_space.enabled) {
|
||||
value = ConvertPositionToClipSpace(state, value);
|
||||
}
|
||||
if (exp.kind == IR::ExportTargetKind::Position && state.user_clip_planes != 0) {
|
||||
EmitUserClipDistances(state, value);
|
||||
}
|
||||
if (state.program.stage == ShaderType::Mesh) {
|
||||
const auto kind = exp.kind == IR::ExportTargetKind::Position
|
||||
? IR::StageOutputKind::Position
|
||||
@@ -609,7 +633,6 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
const auto equal = state.builder.AllocateId();
|
||||
const auto invalid = state.builder.AllocateId();
|
||||
const auto distance = state.builder.AllocateId();
|
||||
const auto distance_pointer = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpFOrdEqual, TypeBoolVector(state, 4), equal,
|
||||
value, zero);
|
||||
state.builder.AddFunction(spv::OpAll, TypeBool(state), invalid, equal);
|
||||
@@ -618,11 +641,10 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
state.builder.AddFunction(spv::OpSelect, TypeF32(state), distance, invalid,
|
||||
ConstantF32Value(state, -1.0f),
|
||||
ConstantF32Value(state, 0.0f));
|
||||
state.builder.AddFunction(
|
||||
spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeF32(state)),
|
||||
distance_pointer, state.clip_distance_variable,
|
||||
ConstantU32(state, state.invalid_position_clip_distance));
|
||||
state.builder.AddFunction(spv::OpStore, distance_pointer, distance);
|
||||
state.builder.AddFunction(spv::OpStore,
|
||||
DistancePointer(state, IR::StageOutputKind::ClipDistance,
|
||||
state.invalid_position_clip_distance),
|
||||
distance);
|
||||
static std::atomic_bool logged = false;
|
||||
if (!logged.exchange(true, std::memory_order_relaxed)) {
|
||||
Log::WriteToConsoleAndLog(
|
||||
@@ -655,6 +677,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);
|
||||
|
||||
@@ -80,7 +80,7 @@ struct EmitterState {
|
||||
lds_storage_class = spv::StorageClassWorkgroup;
|
||||
}
|
||||
if (program.info.uses_lds && program.stage == ShaderType::Compute &&
|
||||
input_info.compute != nullptr && input_info.compute->lds_storage) {
|
||||
input_info.compute->lds_storage) {
|
||||
lds_storage_class = spv::StorageClassStorageBuffer;
|
||||
}
|
||||
}
|
||||
@@ -104,9 +104,8 @@ struct EmitterState {
|
||||
std::array<uint32_t, 3> u32_vector_types {};
|
||||
std::array<uint32_t, 3> i32_vector_types {};
|
||||
std::array<uint32_t, 3> f32_vector_types {};
|
||||
std::array<uint32_t, 6> tess_variables {};
|
||||
std::array<uint32_t, IR::NumTessellationAttributes> tess_variables {};
|
||||
uint32_t tess_inner_variable = 0;
|
||||
uint32_t tess_patch_base = 0;
|
||||
|
||||
uint32_t lane_count = 1;
|
||||
uint32_t lane_half = 0;
|
||||
@@ -148,6 +147,7 @@ struct EmitterState {
|
||||
uint32_t point_size_variable = 0;
|
||||
uint32_t clip_distance_variable = 0;
|
||||
uint32_t invalid_position_clip_distance = UINT32_MAX;
|
||||
uint32_t user_clip_planes = 0;
|
||||
uint32_t cull_distance_variable = 0;
|
||||
uint32_t layer_variable = 0;
|
||||
uint32_t viewport_index_variable = 0;
|
||||
@@ -285,7 +285,6 @@ struct ImageSampleLayout {
|
||||
};
|
||||
|
||||
struct F32Class {
|
||||
uint32_t bits = 0;
|
||||
uint32_t nan = 0;
|
||||
uint32_t zero = 0;
|
||||
};
|
||||
@@ -510,8 +509,6 @@ inline constexpr auto EmitLogicalNotBool = EmitNative<spv::OpLogicalNot, IR::Typ
|
||||
|
||||
F32Class EmitClassifyF32Bits(EmitterState& state, uint32_t bits);
|
||||
|
||||
F32Class EmitClassifyF32(EmitterState& state, uint32_t value);
|
||||
|
||||
uint32_t EmitClassMaskBitMatch(EmitterState& state, uint32_t mask, uint32_t bit,
|
||||
uint32_t class_match);
|
||||
|
||||
|
||||
@@ -157,9 +157,25 @@ void EmitMemoryOffsets(EmitterState& state) {
|
||||
}
|
||||
}
|
||||
|
||||
// Stages without a workgroup emulate LDS with one private array per invocation. A GPU keeps
|
||||
// hundreds of thousands of invocations in flight, so this size multiplies into driver local memory:
|
||||
// a fixed 8192 dwords (32 KB) asked for gigabytes and killed the device (a write fault in the
|
||||
// first heavy pixel shader that touched LDS). A pixel shader can only address the LDS that
|
||||
// EXTRA_LDS_SIZE reserves, a vertex shader the LDS_SIZE of its GS stage; hardware reads zero and
|
||||
// drops writes beyond it, as the bounds checks do.
|
||||
constexpr uint32_t FUNCTION_LDS_DWORDS = 1024u;
|
||||
|
||||
uint32_t LdsDwordCount(const EmitterState& state) {
|
||||
const auto* workgroup = ShaderWorkgroupInput(state.program.stage, state.input_info);
|
||||
return workgroup != nullptr ? workgroup->lds_size_dwords : 8192u;
|
||||
if (const auto* workgroup = ShaderWorkgroupInput(state.program.stage, state.input_info)) {
|
||||
return workgroup->lds_size_dwords;
|
||||
}
|
||||
if (state.program.stage == ShaderType::Pixel) {
|
||||
return std::max(state.input_info.pixel->lds_size_dwords, 1u);
|
||||
}
|
||||
if (state.program.stage == ShaderType::Vertex) {
|
||||
return std::max(state.input_info.vertex->lds_size_dwords, 1u);
|
||||
}
|
||||
return FUNCTION_LDS_DWORDS;
|
||||
}
|
||||
|
||||
void EnsureLdsStorage(EmitterState& state) {
|
||||
|
||||
@@ -244,7 +244,7 @@ void DefineDescriptors(EmitterState& state) {
|
||||
counts[static_cast<size_t>(Kind::ShaderData)] =
|
||||
bindings.ShaderDataDwords() != 0 && !bindings.UsesPushData();
|
||||
if (state.program.bindings.UsesPushData() || state.program.stage == ShaderType::Mesh ||
|
||||
state.object_primitive_id_variable != 0) {
|
||||
state.object_primitive_id_variable != 0 || state.user_clip_planes != 0) {
|
||||
const auto type = PushConstantBlockType(state);
|
||||
state.push_constant_variable = state.builder.DefineGlobalVariable(
|
||||
TypePointer(state, spv::StorageClassPushConstant, type), spv::StorageClassPushConstant);
|
||||
@@ -600,14 +600,24 @@ void DefineOutputs(EmitterState& state) {
|
||||
cull_distance_count = std::max(cull_distance_count, output.index + 1);
|
||||
}
|
||||
}
|
||||
const bool position = std::ranges::any_of(state.outputs, [](const OutputBinding& output) {
|
||||
return output.kind == IR::StageOutputKind::Position;
|
||||
});
|
||||
if (position && state.input_info.vertex->user_clip_planes != 0) {
|
||||
// PipelineCache rejects UCP_ENA with shader clip or cull distances.
|
||||
EXIT_IF(clip_distance_count + cull_distance_count != 0);
|
||||
state.user_clip_planes = state.input_info.vertex->user_clip_planes;
|
||||
for (auto mask = state.user_clip_planes; mask != 0; mask &= mask - 1u) {
|
||||
state.outputs.push_back(
|
||||
{{IR::StageOutputKind::ClipDistance, clip_distance_count++, 0, "gl_ClipDistance"}});
|
||||
}
|
||||
}
|
||||
if (state.program.stage == ShaderType::Mesh) {
|
||||
DefineMeshOutputs(state, clip_distance_count, cull_distance_count);
|
||||
return;
|
||||
}
|
||||
if (state.program.stage == ShaderType::Vertex && clip_distance_count + cull_distance_count < 8u &&
|
||||
std::ranges::any_of(state.outputs, [](const OutputBinding& output) {
|
||||
return output.kind == IR::StageOutputKind::Position;
|
||||
})) {
|
||||
if (state.program.stage == ShaderType::Vertex &&
|
||||
clip_distance_count + cull_distance_count < 8u && position) {
|
||||
// Reserve one plane for the enabled PA_CL_CLIP_CNTL clipping-error cull.
|
||||
state.invalid_position_clip_distance = clip_distance_count++;
|
||||
state.outputs.push_back({{IR::StageOutputKind::ClipDistance,
|
||||
|
||||
@@ -233,14 +233,17 @@ void Invoke(Return (*emit)(Context&, Args...), ValueEmitContext& ctx, const IR::
|
||||
}
|
||||
|
||||
void EmitDirectInstruction(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
if (ctx.half != 0 && (inst.GetOpcode() == IR::ValueOpcode::Ballot ||
|
||||
inst.GetOpcode() == IR::ValueOpcode::ReadFirstLane ||
|
||||
inst.GetOpcode() == IR::ValueOpcode::SubgroupUMinU32)) {
|
||||
// These operations combine both emulated halves into one whole-wave result.
|
||||
using Op = IR::ValueOpcode;
|
||||
const auto op = inst.GetOpcode();
|
||||
if (ctx.half != 0 &&
|
||||
(op == Op::Ballot || op == Op::ReadFirstLane || op == Op::SubgroupUMinU32 ||
|
||||
op == Op::ReadConstBuffer ||
|
||||
(op == Op::LoadAddressU32 && ctx.Memory(inst).kind == IR::ResourceKind::ScalarAddress))) {
|
||||
// Whole-wave results and SMEM loads (SGPR operands, no EXEC) are identical in both halves.
|
||||
ctx.Define(inst, ctx.other_half->Result(inst));
|
||||
return;
|
||||
}
|
||||
switch (inst.GetOpcode()) {
|
||||
switch (op) {
|
||||
#define VALUE_OPCODE(name, ...) \
|
||||
case IR::ValueOpcode::name: return Invoke(Emit##name, ctx, inst);
|
||||
#include "graphics/shader/recompiler/ir/opcodes/ValueOpcodes.inc"
|
||||
@@ -307,6 +310,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);
|
||||
}
|
||||
@@ -622,6 +626,12 @@ void EmitProgram(EmitterState& state) {
|
||||
}
|
||||
if (state.program.dispatcher_fallback) {
|
||||
auto& dispatch = dispatcher.emplace();
|
||||
// Allocate both halves in program order so the SPIR-V is identical across runs.
|
||||
const auto add_spill = [&](const IR::Inst* inst) {
|
||||
for (uint32_t half = 0; half < state.lane_count; half++) {
|
||||
dispatch.spills[half].emplace(inst, state.builder.AllocateId());
|
||||
}
|
||||
};
|
||||
for (const auto* block: program.blocks) {
|
||||
for (const auto& inst: *block) {
|
||||
if (inst.GetOpcode() != IR::ValueOpcode::Phi) {
|
||||
@@ -631,7 +641,7 @@ void EmitProgram(EmitterState& state) {
|
||||
ctx.Fail(inst, "cannot be stored by the dispatcher");
|
||||
break;
|
||||
}
|
||||
dispatch.spills[0].emplace(&inst, state.builder.AllocateId());
|
||||
add_spill(&inst);
|
||||
}
|
||||
}
|
||||
const auto mark_cross_block = [&](IR::Value value, const IR::Block* consumer) {
|
||||
@@ -646,7 +656,7 @@ void EmitProgram(EmitterState& state) {
|
||||
return;
|
||||
}
|
||||
if (!dispatch.spills[0].contains(definition)) {
|
||||
dispatch.spills[0].emplace(definition, state.builder.AllocateId());
|
||||
add_spill(definition);
|
||||
}
|
||||
};
|
||||
for (const auto* block: program.blocks) {
|
||||
@@ -668,12 +678,7 @@ void EmitProgram(EmitterState& state) {
|
||||
dispatch.continue_label = state.builder.AllocateId();
|
||||
dispatch.merge_label = state.builder.AllocateId();
|
||||
ctx.dispatcher_spills = &dispatch.spills[0];
|
||||
if (state.lane_count == 2) {
|
||||
for (const auto& [inst, id]: dispatch.spills[0]) {
|
||||
dispatch.spills[1].emplace(inst, state.builder.AllocateId());
|
||||
}
|
||||
high.dispatcher_spills = &dispatch.spills[1];
|
||||
}
|
||||
high.dispatcher_spills = &dispatch.spills[1];
|
||||
}
|
||||
DefineGetBdaPointer(state);
|
||||
DefineIndirectBufferLoads(ctx);
|
||||
|
||||
@@ -5,6 +5,17 @@
|
||||
namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
|
||||
namespace {
|
||||
|
||||
bool IsTessellationInput(IR::TessellationAttribute kind) {
|
||||
using Attribute = IR::TessellationAttribute;
|
||||
return kind == Attribute::ControlInput || kind == Attribute::EvaluationInput ||
|
||||
kind == Attribute::PatchInput;
|
||||
}
|
||||
|
||||
bool IsTessellationPatch(IR::TessellationAttribute kind) {
|
||||
using Attribute = IR::TessellationAttribute;
|
||||
return kind == Attribute::PatchOutput || kind == Attribute::PatchInput;
|
||||
}
|
||||
|
||||
uint32_t TessellationPointer(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
auto& state = ctx.state;
|
||||
using Attribute = IR::TessellationAttribute;
|
||||
@@ -12,8 +23,9 @@ uint32_t TessellationPointer(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
const auto& tess = state.input_info.vertex->tess;
|
||||
const auto variable = state.tess_variables.at(static_cast<uint32_t>(kind));
|
||||
EXIT_IF(variable == 0);
|
||||
const auto input = kind == Attribute::ControlInput || kind == Attribute::EvaluationInput;
|
||||
const auto storage = input ? spv::StorageClassInput : spv::StorageClassOutput;
|
||||
const bool patch = IsTessellationPatch(kind);
|
||||
const auto storage =
|
||||
IsTessellationInput(kind) ? spv::StorageClassInput : spv::StorageClassOutput;
|
||||
const auto pointer = state.builder.AllocateId();
|
||||
if (kind == Attribute::Factor) {
|
||||
EXIT_NOT_IMPLEMENTED(!inst.Arg(1).IsImmediate());
|
||||
@@ -26,15 +38,13 @@ uint32_t TessellationPointer(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
return pointer;
|
||||
}
|
||||
auto address = ctx.Arg(inst, 1);
|
||||
if (kind == Attribute::PatchOutput) {
|
||||
address =
|
||||
EmitBinaryU32(state, spv::OpISub, address, ConstantU32(state, state.tess_patch_base));
|
||||
if (patch) {
|
||||
address = EmitBinaryU32(state, spv::OpISub, address, ConstantU32(state, tess.patch_base));
|
||||
}
|
||||
const bool local = kind == Attribute::LocalOutput || kind == Attribute::ControlInput;
|
||||
const auto stride = local ? tess.ls_stride : tess.hs_stride;
|
||||
const auto offset = kind == Attribute::PatchOutput ? address
|
||||
: EmitBinaryU32(state, spv::OpUMod, address,
|
||||
ConstantU32(state, stride));
|
||||
const auto offset =
|
||||
patch ? address : EmitBinaryU32(state, spv::OpUMod, address, ConstantU32(state, stride));
|
||||
const auto attribute =
|
||||
EmitBinaryU32(state, spv::OpShiftRightLogical, offset, ConstantU32(state, 4));
|
||||
const auto component =
|
||||
@@ -42,12 +52,14 @@ uint32_t TessellationPointer(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
EmitBinaryU32(state, spv::OpShiftRightLogical, offset, ConstantU32(state, 2)),
|
||||
ConstantU32(state, 3));
|
||||
const auto type = TypePointer(state, storage, TypeU32(state));
|
||||
if (kind == Attribute::LocalOutput || kind == Attribute::PatchOutput) {
|
||||
if (kind == Attribute::LocalOutput || patch) {
|
||||
state.builder.AddFunction(spv::OpAccessChain, type, pointer, variable, attribute,
|
||||
component);
|
||||
} else {
|
||||
// An HS writes only its own control point but may read any other.
|
||||
const auto vertex =
|
||||
kind == Attribute::ControlOutput
|
||||
kind == Attribute::ControlOutput &&
|
||||
inst.GetOpcode() == IR::ValueOpcode::SetTessellationAttribute
|
||||
? EmitLaneId(state)
|
||||
: EmitBinaryU32(state, spv::OpUDiv, address, ConstantU32(state, stride));
|
||||
state.builder.AddFunction(spv::OpAccessChain, type, pointer, variable, vertex, attribute,
|
||||
@@ -60,20 +72,12 @@ uint32_t TessellationPointer(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
|
||||
void DefineTessellationInterfaces(EmitterState& state) {
|
||||
using Attribute = IR::TessellationAttribute;
|
||||
std::array<bool, 6> used {};
|
||||
uint32_t patch_begin = UINT32_MAX, patch_end = 0;
|
||||
std::array<bool, IR::NumTessellationAttributes> used {};
|
||||
for (const auto* block: state.program.blocks) {
|
||||
for (const auto& inst: *block) {
|
||||
if (inst.GetOpcode() != IR::ValueOpcode::GetTessellationAttribute &&
|
||||
inst.GetOpcode() != IR::ValueOpcode::SetTessellationAttribute)
|
||||
continue;
|
||||
const auto kind = inst.Arg(0).U32();
|
||||
used.at(kind) = true;
|
||||
if (kind == static_cast<uint32_t>(Attribute::PatchOutput)) {
|
||||
EXIT_NOT_IMPLEMENTED(!inst.Arg(1).IsImmediate());
|
||||
patch_begin = std::min(patch_begin, inst.Arg(1).U32());
|
||||
patch_end = std::max(patch_end, inst.Arg(1).U32() + 4u);
|
||||
}
|
||||
if (inst.GetOpcode() == IR::ValueOpcode::GetTessellationAttribute ||
|
||||
inst.GetOpcode() == IR::ValueOpcode::SetTessellationAttribute)
|
||||
used.at(inst.Arg(0).U32()) = true;
|
||||
}
|
||||
}
|
||||
if (std::ranges::none_of(used, [](bool value) { return value; })) return;
|
||||
@@ -83,15 +87,16 @@ void DefineTessellationInterfaces(EmitterState& state) {
|
||||
};
|
||||
for (uint32_t index = 0; index < used.size(); index++) {
|
||||
if (!used[index]) continue;
|
||||
const auto kind = static_cast<Attribute>(index);
|
||||
const bool input = kind == Attribute::ControlInput || kind == Attribute::EvaluationInput;
|
||||
const auto storage = input ? spv::StorageClassInput : spv::StorageClassOutput;
|
||||
uint32_t type;
|
||||
const auto kind = static_cast<Attribute>(index);
|
||||
const bool patch = IsTessellationPatch(kind);
|
||||
const auto storage =
|
||||
IsTessellationInput(kind) ? spv::StorageClassInput : spv::StorageClassOutput;
|
||||
uint32_t type;
|
||||
if (kind == Attribute::Factor) {
|
||||
type = array(TypeF32(state), 4u);
|
||||
} else if (kind == Attribute::PatchOutput) {
|
||||
state.tess_patch_base = patch_begin;
|
||||
type = array(TypeU32Vector(state, 4), (patch_end - patch_begin + 15u) / 16u);
|
||||
} else if (patch) {
|
||||
// HS and TES share the reflected patch layout so their interfaces match.
|
||||
type = array(TypeU32Vector(state, 4), (tess.patch_size + 15u) / 16u);
|
||||
} else {
|
||||
const bool local = kind == Attribute::LocalOutput || kind == Attribute::ControlInput;
|
||||
const auto stride = local ? tess.ls_stride : tess.hs_stride;
|
||||
@@ -112,11 +117,10 @@ void DefineTessellationInterfaces(EmitterState& state) {
|
||||
state.builder.AddAnnotation(spv::OpDecorate, state.tess_inner_variable,
|
||||
spv::DecorationPatch);
|
||||
} else {
|
||||
state.builder.AddAnnotation(
|
||||
spv::OpDecorate, variable, spv::DecorationLocation,
|
||||
kind == Attribute::PatchOutput ? (tess.hs_stride + 15u) / 16u : 0u);
|
||||
state.builder.AddAnnotation(spv::OpDecorate, variable, spv::DecorationLocation,
|
||||
patch ? (tess.hs_stride + 15u) / 16u : 0u);
|
||||
}
|
||||
if (kind == Attribute::Factor || kind == Attribute::PatchOutput) {
|
||||
if (kind == Attribute::Factor || patch) {
|
||||
state.builder.AddAnnotation(spv::OpDecorate, variable, spv::DecorationPatch);
|
||||
}
|
||||
}
|
||||
@@ -124,7 +128,9 @@ void DefineTessellationInterfaces(EmitterState& state) {
|
||||
|
||||
void DefineTessellationExecutionModes(EmitterState& state) {
|
||||
const auto& tess = state.input_info.vertex->tess;
|
||||
EXIT_NOT_IMPLEMENTED(tess.domain != 1u || tess.partitioning != 2u ||
|
||||
// CxVgtTfParam: triangles, integer or fractional-odd partitioning, clockwise output.
|
||||
EXIT_NOT_IMPLEMENTED(tess.domain != 1u ||
|
||||
(tess.partitioning != 0u && tess.partitioning != 2u) ||
|
||||
tess.output_topology != 2u);
|
||||
state.builder.RequireCapability(spv::CapabilityTessellation);
|
||||
if (state.program.stage == ShaderType::TessellationControl) {
|
||||
@@ -132,7 +138,9 @@ void DefineTessellationExecutionModes(EmitterState& state) {
|
||||
tess.output_control_points);
|
||||
} else {
|
||||
state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeTriangles);
|
||||
state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeSpacingFractionalOdd);
|
||||
state.builder.AddExecutionMode(
|
||||
state.main_func, tess.partitioning == 0u ? spv::ExecutionModeSpacingEqual
|
||||
: spv::ExecutionModeSpacingFractionalOdd);
|
||||
state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeVertexOrderCw);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <deque>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <span>
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::CFG {
|
||||
@@ -468,8 +470,10 @@ bool ResolveSetpcDwordJumpTable(const Decoder::Program& program, uint32_t setpc_
|
||||
if (!ResolveJumpTableEntryCount(program, load_index, load.src1, 2u, &entry_count)) {
|
||||
return false;
|
||||
}
|
||||
// Code-table loads are not translated, so a selector that is also the load destination
|
||||
// keeps its scaled index at the branch.
|
||||
uint32_t selector_code = UINT32_MAX;
|
||||
if (!ScalarOperandCode(load.src1, selector_code) || selector_code == offset_code ||
|
||||
if (!ScalarOperandCode(load.src1, selector_code) ||
|
||||
ScalarCodeWrittenInRange(program, load_index + 1u, setpc_index, selector_code)) {
|
||||
return false;
|
||||
}
|
||||
@@ -1650,6 +1654,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 +1711,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);
|
||||
|
||||
@@ -195,6 +195,9 @@ constexpr MimgGatherInfo MIMG_GATHER_OPCODE_LIST[] = {
|
||||
{0x5fu, Opcode::IMAGE_GATHER4_C_LZ_O,
|
||||
ImageSampleFlagCompare | ImageSampleFlagLevelZero | ImageSampleFlagOffset},
|
||||
{0x61u, Opcode::IMAGE_GATHER4H, ImageSampleFlagGatherHorizontal},
|
||||
{0xc8u, Opcode::IMAGE_GATHER4_C, ImageSampleFlagCompare | ImageSampleFlagAdjust},
|
||||
{0xd8u, Opcode::IMAGE_GATHER4_C_O,
|
||||
ImageSampleFlagCompare | ImageSampleFlagOffset | ImageSampleFlagAdjust},
|
||||
};
|
||||
|
||||
constexpr MimgAtomicInfo MIMG_ATOMIC_OPCODE_LIST[] = {
|
||||
|
||||
@@ -70,6 +70,8 @@ constexpr MemoryOpcodeInfo MUBUF_OPCODE_LIST[] = {
|
||||
{0x52u, Opcode::BUFFER_ATOMIC_ADD_X2, 2, 32},
|
||||
{0x59u, Opcode::BUFFER_ATOMIC_AND_X2, 2, 32},
|
||||
{0x5au, Opcode::BUFFER_ATOMIC_OR_X2, 2, 32},
|
||||
{0x71u, Opcode::BUFFER_GL0_INV, 0, 0},
|
||||
{0x72u, Opcode::BUFFER_GL1_INV, 0, 0},
|
||||
{0x80u, Opcode::BUFFER_LOAD_FORMAT_D16_X, 1, 16, false, false, true},
|
||||
{0x83u, Opcode::BUFFER_LOAD_FORMAT_D16_XYZW, 2, 16, false, false, true},
|
||||
{0x84u, Opcode::BUFFER_STORE_FORMAT_D16_X, 1, 16, false, false, true},
|
||||
@@ -164,6 +166,8 @@ void ApplyMemoryInfo(Instruction& inst, const MemoryOpcodeInfo* info) {
|
||||
inst.formatted = info->formatted;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsDsWriteOpcode(Opcode opcode) {
|
||||
switch (opcode) {
|
||||
case Opcode::DS_WRITE_B8:
|
||||
@@ -182,6 +186,8 @@ bool IsDsWriteOpcode(Opcode opcode) {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsDsAtomicOpcode(Opcode opcode) {
|
||||
switch (opcode) {
|
||||
case Opcode::DS_ADD_U32:
|
||||
@@ -312,6 +318,9 @@ void DecodeMubuf(uint32_t pc, std::span<const uint32_t> code, uint32_t word_inde
|
||||
if (inst.opcode == Opcode::UNSUPPORTED) {
|
||||
SetUnsupported(inst, Family::MUBUF, opcode, "MUBUF opcode is not implemented");
|
||||
}
|
||||
if (inst.opcode == Opcode::BUFFER_GL0_INV || inst.opcode == Opcode::BUFFER_GL1_INV) {
|
||||
return;
|
||||
}
|
||||
|
||||
DecodeVectorGpr(vdata, inst.dst);
|
||||
if (inst.opcode == Opcode::BUFFER_LOAD_FORMAT_D16_XYZW ||
|
||||
|
||||
@@ -164,9 +164,23 @@ void DecodeSop1(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
switch (inst.opcode) {
|
||||
case Opcode::S_SWAPPC_B64:
|
||||
inst.branch_target = UINT32_MAX;
|
||||
inst.data_dwords = 2;
|
||||
[[fallthrough]];
|
||||
case Opcode::S_MOV_B64:
|
||||
case Opcode::S_CMOV_B64:
|
||||
case Opcode::S_NOT_B64:
|
||||
case Opcode::S_WQM_B64:
|
||||
case Opcode::S_BREV_B64:
|
||||
case Opcode::S_BITSET0_B64:
|
||||
case Opcode::S_BITSET1_B64:
|
||||
case Opcode::S_AND_SAVEEXEC_B64:
|
||||
case Opcode::S_ORN2_SAVEEXEC_B64:
|
||||
case Opcode::S_QUADMASK_B64:
|
||||
case Opcode::S_ANDN1_SAVEEXEC_B64:
|
||||
case Opcode::S_BITREPLICATE_B64_B32:
|
||||
inst.data_dwords = 2u;
|
||||
break;
|
||||
case Opcode::S_GETPC_B64:
|
||||
inst.data_dwords = 2u;
|
||||
inst.src_count = 0;
|
||||
DecodeScalarDestination(sdst, pc, inst.dst);
|
||||
return;
|
||||
@@ -206,6 +220,8 @@ void DecodeSop2(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
|
||||
DecodeScalarDestination(sdst, pc, inst.dst);
|
||||
DecodeBinarySources(pc, code, word_index, inst, ssrc0, ssrc1);
|
||||
if (opcode == 0x0bu || (opcode >= 0x0fu && opcode <= 0x25u && (opcode & 1u)) || opcode == 0x29u)
|
||||
inst.data_dwords = 2u;
|
||||
}
|
||||
|
||||
void DecodeSopk(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
|
||||
|
||||
@@ -566,6 +566,9 @@ std::string InstructionToString(const Instruction& inst) {
|
||||
OperandToString(inst.src0).c_str()));
|
||||
case Opcode::S_BARRIER:
|
||||
return WithUnsupportedReason(inst, fmt::format("0x{:08x}: s_barrier", inst.pc));
|
||||
case Opcode::BUFFER_GL0_INV:
|
||||
case Opcode::BUFFER_GL1_INV:
|
||||
return fmt::format("0x{:08x}: {}", inst.pc, magic_enum::enum_name(inst.opcode));
|
||||
case Opcode::V_NOP:
|
||||
return WithUnsupportedReason(inst, fmt::format("0x{:08x}: v_nop", inst.pc));
|
||||
case Opcode::S_ENDPGM:
|
||||
|
||||
@@ -599,6 +599,8 @@ enum class Opcode {
|
||||
BUFFER_ATOMIC_XOR,
|
||||
BUFFER_ATOMIC_FMIN,
|
||||
BUFFER_ATOMIC_FMAX,
|
||||
BUFFER_GL0_INV,
|
||||
BUFFER_GL1_INV,
|
||||
FLAT_LOAD_UBYTE,
|
||||
FLAT_LOAD_SBYTE,
|
||||
FLAT_LOAD_USHORT,
|
||||
@@ -880,6 +882,7 @@ Program DecodeFrontProgram(std::span<const uint32_t> front);
|
||||
void DecodeProgram(std::span<const uint32_t> code, Program& program);
|
||||
bool IsConditionalBranch(Opcode opcode);
|
||||
bool IsDirectBranch(Opcode opcode);
|
||||
bool IsDsWriteOpcode(Opcode opcode);
|
||||
|
||||
void DecodeScalarSource(uint32_t code, uint32_t pc, Operand& operand);
|
||||
void DecodeScalarDestination(uint32_t code, uint32_t pc, Operand& operand);
|
||||
|
||||
@@ -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()},
|
||||
|
||||
@@ -987,30 +987,6 @@ void ValidateTranslateOptions(const TranslateOptions& options) {
|
||||
options.stage != ShaderType::Local) {
|
||||
EXIT("embedded fetch requires a vertex or local shader");
|
||||
}
|
||||
switch (options.stage) {
|
||||
case ShaderType::Vertex:
|
||||
case ShaderType::Local:
|
||||
case ShaderType::TessellationControl:
|
||||
case ShaderType::TessellationEvaluation:
|
||||
case ShaderType::Mesh:
|
||||
if (options.input_info.vertex == nullptr) {
|
||||
EXIT("vertex shader translation has no vertex input metadata");
|
||||
}
|
||||
return;
|
||||
case ShaderType::Pixel:
|
||||
if (options.input_info.pixel == nullptr) {
|
||||
EXIT("pixel shader translation has no pixel input metadata");
|
||||
}
|
||||
return;
|
||||
case ShaderType::Compute:
|
||||
if (options.input_info.compute == nullptr) {
|
||||
EXIT("compute shader translation has no compute input metadata");
|
||||
}
|
||||
return;
|
||||
default:
|
||||
EXIT("shader translation has unsupported stage %u",
|
||||
static_cast<uint32_t>(options.stage));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1044,7 +1020,9 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
|
||||
case ShaderType::Compute:
|
||||
result.scratch_dwords = options.input_info.compute->scratch_size_dwords;
|
||||
break;
|
||||
default: break; // ValidateTranslateOptions rejects unsupported stages.
|
||||
default:
|
||||
EXIT("shader translation has unsupported stage %u",
|
||||
static_cast<uint32_t>(options.stage));
|
||||
}
|
||||
result.dispatcher_fallback = cfg.irreducible || cfg.unsupported;
|
||||
result.cfg_failure_kind = cfg.failure_kind;
|
||||
|
||||
@@ -125,6 +125,7 @@ struct BufferResource {
|
||||
uint32_t descriptor_index = UINT32_MAX;
|
||||
uint32_t first_use_pc = 0;
|
||||
uint32_t max_byte_extent = 0;
|
||||
uint32_t write_extent = 0; // UINT32_MAX once a write is runtime-addressed or formatted
|
||||
uint32_t packed_stride = 0;
|
||||
Prospero::BufferFormat descriptor_format = Prospero::BufferFormat::kInvalid;
|
||||
uint32_t descriptor_swizzle = DstSel(4, 5, 6, 7);
|
||||
@@ -220,8 +221,11 @@ enum class TessellationAttribute {
|
||||
ControlOutput,
|
||||
EvaluationInput,
|
||||
PatchOutput,
|
||||
PatchInput,
|
||||
Factor
|
||||
};
|
||||
constexpr uint32_t NumTessellationAttributes =
|
||||
static_cast<uint32_t>(TessellationAttribute::Factor) + 1u;
|
||||
|
||||
enum class StageInputKind {
|
||||
VertexIndex,
|
||||
@@ -356,7 +360,14 @@ struct PushData {
|
||||
static constexpr uint32_t NoStart = UINT32_MAX;
|
||||
std::array<uint32_t, DwordCount> dwords {};
|
||||
|
||||
[[nodiscard]] static constexpr uint32_t GraphicsDrawDwords(bool mesh, bool object_id) {
|
||||
// User clip planes 0..N follow the mesh draw parameters or object ID, four dwords each.
|
||||
static constexpr uint32_t UserClipPlaneDword = MeshDrawDwordCount;
|
||||
|
||||
[[nodiscard]] static constexpr uint32_t GraphicsDrawDwords(bool mesh, bool object_id,
|
||||
uint32_t user_clip_planes = 0) {
|
||||
if (user_clip_planes != 0) {
|
||||
return UserClipPlaneDword + 4u * std::bit_width(user_clip_planes);
|
||||
}
|
||||
return mesh ? MeshDrawDwordCount : static_cast<uint32_t>(object_id);
|
||||
}
|
||||
|
||||
@@ -495,7 +506,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;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user