mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-23 04:16:57 +02:00
Compare commits
138 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8502d8a77d | |||
| cb8042c5aa | |||
| b46eeb7866 | |||
| 05694abad5 | |||
| 919fac13d0 | |||
| a52d1ed4e2 | |||
| 2b37dee2b7 | |||
| a3aece2762 | |||
| 64186404a3 | |||
| 11465a5e3c | |||
| 09a9654458 | |||
| 6123419437 | |||
| 8177969f33 | |||
| 6ee2ef8715 | |||
| 9e4234cdbe | |||
| ce54aa8d14 | |||
| 2d00e13bf3 | |||
| 3bb09ed2a4 | |||
| 1e4ac1371d | |||
| 9c39062d3e | |||
| 93071ebdf8 | |||
| 3a842e1a6b | |||
| da19501889 | |||
| 0f7d112545 | |||
| ae47ac26e7 | |||
| 330d373dc6 | |||
| 7c40dbc36c | |||
| 0cdc5c1224 | |||
| fa32940d14 | |||
| 2862ad5b25 | |||
| aacf15fd0b | |||
| 10a852c2ac | |||
| 4a1440197f | |||
| 6e5167598e | |||
| c571d8ed5a | |||
| 286481889f | |||
| c07ad621f2 | |||
| 80932f8da1 | |||
| f0e55803ba | |||
| ca6a7b88ce | |||
| 13b291f998 | |||
| 2944d126b3 | |||
| 7b7294aa03 | |||
| 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 | |||
| c2173801d4 | |||
| 56d03f16e9 | |||
| 3ecc6b7d1b | |||
| 5bf37f52ad | |||
| 57791645e4 | |||
| d1fe1324c8 | |||
| b485dd24be | |||
| ae25558321 | |||
| ec08ea197b | |||
| f6f1ac8b40 | |||
| b82512be06 | |||
| ba8b0c85b8 | |||
| b6bd9ecae4 | |||
| b95ea4bb5f | |||
| e93b4b0378 | |||
| 0fd41ae206 | |||
| 9202bddad2 | |||
| 32587337b5 | |||
| 02ead0e2e1 | |||
| 4f1f3b1bb3 | |||
| 42bb0da520 | |||
| 5dec523143 | |||
| d01633e18e | |||
| e7ac242039 | |||
| d7974dc763 | |||
| b899717900 | |||
| d58aefde49 |
@@ -6,3 +6,4 @@ _Build/vscode-clang/
|
||||
_Build/
|
||||
cmake-build-debug/
|
||||
_PipelineCache/
|
||||
CMakeUserPresets.json
|
||||
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)
|
||||
|
||||
+6
-2
@@ -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)
|
||||
@@ -350,7 +350,7 @@ set(inc_headers
|
||||
${KYTY_THIRD_PARTY_DIR}/SPIRV-Tools/include
|
||||
${KYTY_THIRD_PARTY_DIR}/SPIRV-Headers/include
|
||||
${KYTY_THIRD_PARTY_DIR}/fmt/include
|
||||
${KYTY_THIRD_PARTY_DIR}/xxhash
|
||||
${KYTY_THIRD_PARTY_DIR}/xxHash
|
||||
${KYTY_THIRD_PARTY_DIR}/cpuinfo/include
|
||||
${KYTY_THIRD_PARTY_DIR}/renderdoc
|
||||
${KYTY_THIRD_PARTY_DIR}/stb
|
||||
@@ -655,6 +655,10 @@ 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_float_min_max
|
||||
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --float-min-max-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": "訪客"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs>
|
||||
<linearGradient id="body" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#e3e9f2"/></linearGradient>
|
||||
</defs>
|
||||
<g transform="translate(256 268) scale(1.16) translate(-256 -268)" stroke-linejoin="round" stroke-linecap="round">
|
||||
<path d="M100 196 L120 104 Q124 92 134 99 L214 160 Z" fill="url(#body)"/><path d="M412 196 L392 104 Q388 92 378 99 L298 160 Z" fill="url(#body)"/>
|
||||
<path d="M128 164 L137 128 L174 156 Z" fill="#0b0d12"/><path d="M384 164 L375 128 L338 156 Z" fill="#0b0d12"/>
|
||||
<g fill="none" stroke="#0b0d12" stroke-width="16"><path d="M100 196 L120 104 Q124 92 134 99 L214 160 Z"/><path d="M412 196 L392 104 Q388 92 378 99 L298 160 Z"/></g>
|
||||
<path d="M256 154 C330 154 380 160 406 178 C432 196 447 244 459 302 C471 358 466 398 430 408 C400 416 380 398 360 368 L326 318 C316 304 306 300 290 300 L222 300 C206 300 196 304 186 318 L152 368 C132 398 112 416 82 408 C46 398 41 358 53 302 C65 244 80 196 106 178 C132 160 182 154 256 154 Z" fill="url(#body)"/>
|
||||
<path d="M256 154 C330 154 380 160 406 178 C432 196 447 244 459 302 C471 358 466 398 430 408 C400 416 380 398 360 368 L326 318 C316 304 306 300 290 300 L222 300 C206 300 196 304 186 318 L152 368 C132 398 112 416 82 408 C46 398 41 358 53 302 C65 244 80 196 106 178 C132 160 182 154 256 154 Z" fill="none" stroke="#0b0d12" stroke-width="16"/>
|
||||
<g fill="none" stroke="#0b0d12" stroke-width="9"><path d="M82 260 C100 274 118 288 128 304 C134 316 121 350 108 388"/><path d="M430 260 C412 274 394 288 384 304 C378 316 391 350 404 388"/></g>
|
||||
<path d="M64 322 L82 340 M62 340 L76 354 M448 322 L430 340 M450 340 L436 354" fill="none" stroke="#0b0d12" stroke-width="7"/>
|
||||
<path d="M168 230 Q194 202 220 230" fill="none" stroke="#0b0d12" stroke-width="15"/>
|
||||
<circle cx="322" cy="220" r="22" fill="#0b0d12"/>
|
||||
<path d="M238 252 L274 252 L256 276 Z" fill="#0b0d12" stroke="#0b0d12" stroke-width="8"/>
|
||||
</g></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -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})
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
@@ -45,6 +46,12 @@ private:
|
||||
// Accepts either a host archive filename or a virtual path inside the archive.
|
||||
[[nodiscard]] std::shared_ptr<ArchiveReader> OpenArchive(const std::filesystem::path& path);
|
||||
|
||||
// Packs a host directory into a new ZArchive outside it. progress gets 0 < done <= total and
|
||||
// returns false to cancel. A failed or cancelled pack removes the partial archive.
|
||||
[[nodiscard]] bool PackArchive(const std::filesystem::path& directory,
|
||||
const std::filesystem::path& archive,
|
||||
const std::function<bool(uint64_t done, uint64_t total)>& progress);
|
||||
|
||||
} // namespace Common
|
||||
|
||||
#endif /* KYTY_COMMON_ARCHIVE_H_ */
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// Kept out of archive.cpp so the statically linked emulator does not pull in the writer and the
|
||||
// zstd compressor.
|
||||
#include "common/archive.h"
|
||||
|
||||
#include "common/stringUtils.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <zarchive/zarchivewriter.h>
|
||||
|
||||
namespace Common {
|
||||
|
||||
bool PackArchive(const std::filesystem::path& directory, const std::filesystem::path& archive,
|
||||
const std::function<bool(uint64_t done, uint64_t total)>& progress) {
|
||||
namespace fs = std::filesystem;
|
||||
struct Output {
|
||||
File file;
|
||||
bool ok = true;
|
||||
} output;
|
||||
// Create the archive before walking, so a re-run started while a cancelled pack is still
|
||||
// walking sees it exists instead of losing its archive to that pack's cleanup.
|
||||
if (!output.file.Create(archive)) {
|
||||
return false;
|
||||
}
|
||||
struct Entry {
|
||||
fs::path path;
|
||||
uint64_t size;
|
||||
bool is_file;
|
||||
};
|
||||
std::vector<Entry> entries;
|
||||
uint64_t total = 0;
|
||||
std::error_code error;
|
||||
// Follow directory links, as the emulator does when it runs the folder.
|
||||
fs::recursive_directory_iterator it(directory, fs::directory_options::follow_directory_symlink,
|
||||
error);
|
||||
for (; !error && it != fs::end(it); it.increment(error)) {
|
||||
const bool is_file = !it->is_directory(error);
|
||||
const auto size = is_file && !error ? it->file_size(error) : 0;
|
||||
if (error) {
|
||||
break;
|
||||
}
|
||||
entries.push_back({it->path(), size, is_file});
|
||||
total += size;
|
||||
}
|
||||
output.ok = !error;
|
||||
ZArchiveWriter writer(
|
||||
[](int32_t, void*) {},
|
||||
[](const void* data, size_t size, void* opaque) {
|
||||
auto& out = *static_cast<Output*>(opaque);
|
||||
uint32_t written = 0;
|
||||
out.file.Write(data, static_cast<uint32_t>(size), &written);
|
||||
out.ok = out.ok && written == size;
|
||||
},
|
||||
&output);
|
||||
std::vector<uint8_t> buffer(1u << 20u);
|
||||
uint64_t done = 0;
|
||||
for (size_t index = 0; output.ok && index < entries.size(); index++) {
|
||||
const auto& entry = entries[index];
|
||||
const auto name = PathToGenericString(entry.path.lexically_relative(directory));
|
||||
File input;
|
||||
output.ok = entry.is_file ? writer.StartNewFile(name.c_str()) &&
|
||||
input.Open(entry.path, File::Mode::Read)
|
||||
: writer.MakeDir(name.c_str());
|
||||
for (uint64_t left = entry.size; output.ok && left != 0;) {
|
||||
uint32_t read = 0;
|
||||
input.Read(buffer.data(), static_cast<uint32_t>(std::min<uint64_t>(left, buffer.size())),
|
||||
&read);
|
||||
writer.AppendData(buffer.data(), read);
|
||||
done += read;
|
||||
left -= read;
|
||||
output.ok = output.ok && read != 0 && progress(done, total);
|
||||
}
|
||||
}
|
||||
if (output.ok) {
|
||||
writer.Finalize();
|
||||
}
|
||||
output.ok = output.ok && output.file.Flush();
|
||||
output.file.Close();
|
||||
if (!output.ok) {
|
||||
File::DeleteFile(archive);
|
||||
}
|
||||
return output.ok;
|
||||
}
|
||||
|
||||
} // namespace Common
|
||||
@@ -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;
|
||||
}
|
||||
@@ -160,6 +185,10 @@ bool ReadbackLinearImagesEnabled() {
|
||||
return g_config->readback_linear_images;
|
||||
}
|
||||
|
||||
bool ReadbackTiledImagesEnabled() {
|
||||
return g_config->readback_tiled_images;
|
||||
}
|
||||
|
||||
bool SyncRawImageBuffersEnabled() {
|
||||
return g_config->sync_raw_image_buffers;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -76,10 +85,12 @@ struct ConfigOptions {
|
||||
bool gpu_assisted_validation_enabled = false;
|
||||
bool renderdoc_enabled = false;
|
||||
bool readback_linear_images = false;
|
||||
bool readback_tiled_images = false;
|
||||
bool sync_raw_image_buffers = false;
|
||||
bool tessellation_enabled = false;
|
||||
bool trophy_enabled = true;
|
||||
uint32_t trophy_duration_seconds = 6;
|
||||
bool perf_overlay_enabled = false;
|
||||
bool playgo_hack_enabled = false;
|
||||
bool skip_notice_screen = false;
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
@@ -100,6 +111,13 @@ const std::string& GetAudioInputDevice();
|
||||
const std::optional<ControllerColor>& GetControllerColor();
|
||||
uint32_t GetControllerSpeakerVolume();
|
||||
uint32_t GetControllerVibrationIntensity();
|
||||
uint32_t GetKeyboardPlayer();
|
||||
// Player 0 is the configured user; later players derive their IDs from it.
|
||||
int32_t GetPlayerUserId(int player);
|
||||
// Returns -1 when the user ID does not belong to any player.
|
||||
int GetPlayerFromUserId(int32_t user_id);
|
||||
// Players after the first are guests: they have no profile, trophies or NP account.
|
||||
bool IsGuestUserId(int32_t user_id);
|
||||
PresentMode GetPresentMode();
|
||||
int32_t GetGpuIndex();
|
||||
bool FullscreenEnabled();
|
||||
@@ -133,6 +151,7 @@ bool GpuAssistedValidationEnabled();
|
||||
|
||||
bool RenderDocEnabled();
|
||||
bool ReadbackLinearImagesEnabled();
|
||||
bool ReadbackTiledImagesEnabled();
|
||||
bool SyncRawImageBuffersEnabled();
|
||||
bool TessellationEnabled();
|
||||
bool TrophyEnabled();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -73,7 +73,6 @@ constexpr auto MakeContextDispatchTable() {
|
||||
g_hw_ctx_func[Pm4::PA_SC_BINNER_CNTL_0] = HwCtxSetPaScExtendedControl;
|
||||
g_hw_ctx_func[Pm4::PA_SC_BINNER_CNTL_1] = HwCtxSetPaScExtendedControl;
|
||||
g_hw_ctx_func[Pm4::PA_SC_CONSERVATIVE_RASTERIZATION_CNTL] = HwCtxSetPaScExtendedControl;
|
||||
g_hw_ctx_func[Pm4::DB_ALPHA_TO_MASK] = HwCtxSetAlphaToMask;
|
||||
g_hw_ctx_func[Pm4::VGT_DRAW_PAYLOAD_CNTL] = HwCtxSetDrawPayloadControl;
|
||||
g_hw_ctx_func[Pm4::VGT_PRIMITIVEID_RESET] = HwCtxSetPrimitiveIdReset;
|
||||
g_hw_ctx_func[Pm4::PA_CL_OBJPRIM_ID_CNTL] = HwCtxSetObjprimIdControl;
|
||||
|
||||
@@ -79,7 +79,6 @@ uint32_t HwCtxSetAaMask(CommandProcessor&, uint32_t, uint32_t, const uint32_t*,
|
||||
uint32_t HwCtxSetVtxControl(CommandProcessor&, uint32_t, uint32_t, const uint32_t*, uint32_t);
|
||||
uint32_t HwCtxSetPaScExtendedControl(CommandProcessor&, uint32_t, uint32_t, const uint32_t*,
|
||||
uint32_t);
|
||||
uint32_t HwCtxSetAlphaToMask(CommandProcessor&, uint32_t, uint32_t, const uint32_t*, uint32_t);
|
||||
uint32_t HwCtxSetDrawPayloadControl(CommandProcessor&, uint32_t, uint32_t, const uint32_t*,
|
||||
uint32_t);
|
||||
uint32_t HwCtxSetPrimitiveIdReset(CommandProcessor&, uint32_t, uint32_t, const uint32_t*, uint32_t);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -289,12 +283,6 @@ KYTY_HW_CTX_PARSER(HwCtxSetCentroidPriority) {
|
||||
static void HwCtxIgnoreAaMaskRegister([[maybe_unused]] uint32_t cmd_offset,
|
||||
[[maybe_unused]] uint32_t value) {}
|
||||
|
||||
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) {}
|
||||
@@ -439,16 +427,6 @@ KYTY_HW_CTX_PARSER(HwCtxSetPointState) {
|
||||
return num_values;
|
||||
}
|
||||
|
||||
KYTY_HW_CTX_PARSER(HwCtxSetAlphaToMask) {
|
||||
auto num_values = KYTY_PM4_LEN(cmd_id) - 2u;
|
||||
|
||||
for (uint32_t i = 0; i < num_values; i++) {
|
||||
HwCtxIgnoreAlphaToMaskRegister(buffer[i]);
|
||||
}
|
||||
|
||||
return num_values;
|
||||
}
|
||||
|
||||
KYTY_HW_CTX_PARSER(HwCtxSetDrawPayloadControl) {
|
||||
cp.GetCtx().SetDrawPayloadControl(buffer[0]);
|
||||
return KYTY_PM4_LEN(cmd_id) - 2u;
|
||||
@@ -2263,30 +2241,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 +2861,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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3491,7 +3453,8 @@ void GraphicsInitJmpTablesCxIndirect() {
|
||||
g_hw_ctx_indirect_func[Pm4::PA_SC_CONSERVATIVE_RASTERIZATION_CNTL] =
|
||||
[](KYTY_HW_CTX_INDIRECT_ARGS) { HwCtxIgnorePaScExtendedControl(cmd_offset, value); };
|
||||
g_hw_ctx_indirect_func[Pm4::DB_ALPHA_TO_MASK] = [](KYTY_HW_CTX_INDIRECT_ARGS) {
|
||||
HwCtxIgnoreAlphaToMaskRegister(value);
|
||||
cp.GetCtx().SetAlphaToMaskEnable(
|
||||
KYTY_PM4_GET(value, DB_ALPHA_TO_MASK, ALPHA_TO_MASK_ENABLE) != 0);
|
||||
};
|
||||
g_hw_ctx_indirect_func[Pm4::CB_COLOR_CONTROL] = [](KYTY_HW_CTX_INDIRECT_ARGS) {
|
||||
cp.GetCtx().SetColorControl(DecodeColorControl(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 {
|
||||
@@ -574,6 +576,12 @@ struct ShaderRegisters {
|
||||
uint32_t m_paScShaderControl = 0;
|
||||
|
||||
DepthShaderControl db_shader_control;
|
||||
bool alpha_to_mask_enable = false;
|
||||
|
||||
// DB_SHADER_CONTROL.ALPHA_TO_MASK_DISABLE overrides DB_ALPHA_TO_MASK per shader.
|
||||
[[nodiscard]] bool AlphaToMask() const {
|
||||
return alpha_to_mask_enable && !db_shader_control.alpha_to_mask_disable;
|
||||
}
|
||||
|
||||
[[nodiscard]] uint32_t GetExportCount() const {
|
||||
return 1u + ((m_spiVsOutConfig >> 1u) & 0x1Fu);
|
||||
@@ -824,6 +832,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 {
|
||||
@@ -908,6 +921,7 @@ public:
|
||||
void SetDepthShaderControl(const DepthShaderControl& value) {
|
||||
m_sh_regs.db_shader_control = value;
|
||||
}
|
||||
void SetAlphaToMaskEnable(bool value) { m_sh_regs.alpha_to_mask_enable = value; }
|
||||
|
||||
void SetScShaderControl(uint32_t value) { m_sh_regs.m_paScShaderControl = value; }
|
||||
|
||||
@@ -924,6 +938,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;
|
||||
|
||||
|
||||
@@ -549,6 +549,8 @@ constexpr uint32_t VGT_SHADER_STAGES_EN = 0x2D5;
|
||||
constexpr uint32_t VGT_LS_HS_CONFIG = 0x2D6;
|
||||
constexpr uint32_t VGT_TF_PARAM = 0x2DB;
|
||||
constexpr uint32_t DB_ALPHA_TO_MASK = 0x2DC;
|
||||
constexpr uint32_t DB_ALPHA_TO_MASK_ALPHA_TO_MASK_ENABLE_SHIFT = 0;
|
||||
constexpr uint32_t DB_ALPHA_TO_MASK_ALPHA_TO_MASK_ENABLE_MASK = 0x1;
|
||||
constexpr uint32_t PA_SU_POLY_OFFSET_DB_FMT_CNTL = 0x2DE;
|
||||
constexpr uint32_t PA_SU_POLY_OFFSET_DB_FMT_CNTL_NEG_NUM_DB_BITS_SHIFT = 0;
|
||||
constexpr uint32_t PA_SU_POLY_OFFSET_DB_FMT_CNTL_NEG_NUM_DB_BITS_MASK = 0xFF;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -66,6 +66,18 @@ RegionManager* MemoryTracker::GetOrCreateRegion(uint64_t index) {
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void MemoryTracker::QueueBdaSync(uint64_t vaddr, uint64_t size) {
|
||||
const auto begin = Common::AlignDown(vaddr, TRACKER_PAGE_SIZE);
|
||||
const auto end = Common::AlignUp(vaddr + size, TRACKER_PAGE_SIZE);
|
||||
std::lock_guard lock(m_bda_sync_mutex);
|
||||
m_bda_sync.Add(begin, end - begin);
|
||||
}
|
||||
|
||||
RangeSet MemoryTracker::TakeBdaSync() {
|
||||
std::lock_guard lock(m_bda_sync_mutex);
|
||||
return std::exchange(m_bda_sync, RangeSet {});
|
||||
}
|
||||
|
||||
bool MemoryTracker::IsRegionCpuModified(uint64_t vaddr, uint64_t size) {
|
||||
CheckNotInUploadCallback();
|
||||
return Iterate<true>(vaddr, size, [](RegionManager* manager, uint64_t offset, uint64_t bytes) {
|
||||
@@ -88,6 +100,7 @@ void MemoryTracker::MarkRegionAsCpuModified(uint64_t vaddr, uint64_t size) {
|
||||
std::scoped_lock lock(manager->lock);
|
||||
manager->ChangeState<DirtySource::Cpu, true>(manager->GetCpuAddr() + offset, bytes);
|
||||
});
|
||||
QueueBdaSync(vaddr, size);
|
||||
}
|
||||
|
||||
void MemoryTracker::MarkRegionAsGpuModified(uint64_t vaddr, uint64_t size) {
|
||||
@@ -128,6 +141,8 @@ void MemoryTracker::UntrackMemory(uint64_t vaddr, uint64_t size) {
|
||||
Iterate<false>(vaddr, size, [](RegionManager* manager, uint64_t offset, uint64_t bytes) {
|
||||
manager->ChangeState<DirtySource::Cpu, true>(manager->GetCpuAddr() + offset, bytes);
|
||||
});
|
||||
locks.clear();
|
||||
QueueBdaSync(vaddr, size);
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics
|
||||
|
||||
@@ -29,6 +29,10 @@ public:
|
||||
void MarkRegionAsGpuModified(uint64_t vaddr, uint64_t size);
|
||||
void UnmarkRegionAsGpuModified(uint64_t vaddr, uint64_t size);
|
||||
void UntrackMemory(uint64_t vaddr, uint64_t size);
|
||||
// Queue after changing ownership; a concurrent drain must not consume an event
|
||||
// before the dirty pages it describes become visible.
|
||||
void QueueBdaSync(uint64_t vaddr, uint64_t size);
|
||||
[[nodiscard]] RangeSet TakeBdaSync();
|
||||
// Removes protection from a range and flushes GPU-owned data when required.
|
||||
template <typename Flush>
|
||||
void InvalidateRegion(uint64_t vaddr, uint64_t size, Flush&& on_flush) noexcept {
|
||||
@@ -51,6 +55,7 @@ public:
|
||||
on_flush();
|
||||
}
|
||||
});
|
||||
QueueBdaSync(vaddr, size);
|
||||
}
|
||||
#if KYTY_BUILD == KYTY_BUILD_DEBUG
|
||||
void ValidateGpuDirtyPages(const RangeSet& dirty, uint64_t vaddr, uint64_t size,
|
||||
@@ -149,6 +154,8 @@ private:
|
||||
std::unique_ptr<std::atomic<RegionManager*>[]> m_regions;
|
||||
std::vector<std::unique_ptr<RegionManager>> m_region_storage;
|
||||
std::mutex m_region_mutex;
|
||||
std::mutex m_bda_sync_mutex;
|
||||
RangeSet m_bda_sync;
|
||||
PageManager& m_page_manager;
|
||||
};
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -126,6 +127,7 @@ void BufferCache::ChangeRegister(BufferId id) {
|
||||
}
|
||||
WriteDataBuffer(m_bda_pagetable_buffer, table_offset,
|
||||
addresses.data(), addresses.size() * sizeof(vk::DeviceAddress));
|
||||
m_memory_tracker.QueueBdaSync(buffer.CpuAddress(), buffer.Size());
|
||||
} else {
|
||||
const auto found = m_buffers.find(buffer.CpuAddress());
|
||||
EXIT_IF(found == m_buffers.end() || found->second != id);
|
||||
@@ -176,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;
|
||||
@@ -253,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);
|
||||
@@ -466,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) {
|
||||
@@ -657,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;
|
||||
});
|
||||
@@ -685,6 +694,16 @@ void BufferCache::ProcessFaultBuffer() {
|
||||
m_fault_manager.ProcessFaultBuffer();
|
||||
}
|
||||
|
||||
void BufferCache::SynchronizeBda(const RangeSet& mapped) {
|
||||
// Take before walking: writes queued during synchronization remain pending.
|
||||
const auto pending = m_memory_tracker.TakeBdaSync();
|
||||
pending.ForEach([&](uint64_t begin, uint64_t end) {
|
||||
mapped.ForEachInRange(begin, end - begin, [this](uint64_t start, uint64_t finish) {
|
||||
SynchronizeBuffersInRange(start, finish - start);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void BufferCache::SynchronizeBuffersInRange(uint64_t vaddr, uint64_t size) {
|
||||
const auto end = vaddr + size;
|
||||
auto it = m_buffers.upper_bound(vaddr);
|
||||
|
||||
+5
-1
@@ -80,7 +80,10 @@ public:
|
||||
[[nodiscard]] bool IsRegionCpuModified(uint64_t vaddr, uint64_t size);
|
||||
[[nodiscard]] bool IsRegionGpuModified(uint64_t vaddr, uint64_t size);
|
||||
void ProcessFaultBuffer();
|
||||
void SynchronizeBuffersInRange(uint64_t vaddr, uint64_t size);
|
||||
void SynchronizeBda(const RangeSet& mapped);
|
||||
void QueueBdaSync(uint64_t vaddr, uint64_t size) {
|
||||
m_memory_tracker.QueueBdaSync(vaddr, size);
|
||||
}
|
||||
void RunGarbageCollector();
|
||||
|
||||
private:
|
||||
@@ -114,6 +117,7 @@ private:
|
||||
void DeleteBuffer(BufferId id);
|
||||
[[nodiscard]] bool SynchronizeBuffer(Buffer& buffer, uint64_t vaddr, uint64_t size,
|
||||
bool is_written, bool is_texel_buffer);
|
||||
void SynchronizeBuffersInRange(uint64_t vaddr, uint64_t size);
|
||||
[[nodiscard]] vk::Buffer UploadCopies(Buffer& buffer, std::span<vk::BufferCopy> copies,
|
||||
uint64_t total_size);
|
||||
[[nodiscard]] bool SynchronizeBufferFromImage(Buffer& buffer, uint64_t vaddr, uint64_t size);
|
||||
|
||||
+1
-3
@@ -67,11 +67,9 @@ Buffer::Buffer(GraphicContext& graphics, CommandScheduler& scheduler, MemoryUsag
|
||||
buffer_info.usage = flags;
|
||||
|
||||
const bool with_bda = bool(flags & vk::BufferUsageFlagBits::eShaderDeviceAddress);
|
||||
const VmaAllocationCreateFlags bda_flag =
|
||||
with_bda ? VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT : 0;
|
||||
VmaAllocationCreateInfo allocation_info {};
|
||||
allocation_info.flags =
|
||||
VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT | bda_flag | AllocationFlags(usage);
|
||||
VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT | AllocationFlags(usage);
|
||||
allocation_info.usage = AllocationUsage(usage);
|
||||
allocation_info.preferredFlags = usage == MemoryUsage::DeviceLocal
|
||||
? VkMemoryPropertyFlags {}
|
||||
|
||||
+23
-17
@@ -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"
|
||||
@@ -33,6 +34,8 @@ namespace Libs::Graphics {
|
||||
namespace {
|
||||
|
||||
constexpr uint64_t NumFramesBeforeRemoval = 32;
|
||||
// Largest GPU-written tiled image (in texels) that is written back to guest memory on submit.
|
||||
constexpr uint64_t SmallTiledReadbackTexels = 64 * 64;
|
||||
|
||||
[[nodiscard]] bool DecodeColorClear(const TextureCache::ImageDesc& desc, uint8_t code,
|
||||
vk::ClearColorValue& clear) {
|
||||
@@ -121,7 +124,8 @@ TextureCache::TextureCache(GraphicContext& graphics, CommandScheduler& scheduler
|
||||
m_blit_helper(graphics, scheduler),
|
||||
m_tiler(graphics, scheduler, buffer_cache.GetUtilityBuffer(MemoryUsage::Stream)),
|
||||
m_buffer_cache(buffer_cache),
|
||||
m_readback_linear_images(Config::ReadbackLinearImagesEnabled()) {
|
||||
m_readback_linear_images(Config::ReadbackLinearImagesEnabled()),
|
||||
m_readback_tiled_images(Config::ReadbackTiledImagesEnabled()) {
|
||||
if (m_graphics.CanReportMemoryUsage()) {
|
||||
constexpr int64_t GiB = 1024ll * 1024 * 1024;
|
||||
const auto budget =
|
||||
@@ -262,14 +266,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()) {
|
||||
@@ -406,7 +404,14 @@ void TextureCache::UntrackImageTail(ImageId id) {
|
||||
}
|
||||
|
||||
void TextureCache::TrackImageDownload(ImageId id, Image& image) {
|
||||
if (m_readback_linear_images && !image.info.IsTiled()) {
|
||||
// Small tiled targets are written back too: games read GPU results such as reductions and
|
||||
// simulation state from them on the CPU (PPSA04263 keeps the origin of its ocean wave grid
|
||||
// in a 1x1 R32G32 target, and its water physics read it to sample the wave heights).
|
||||
const auto& info = image.info;
|
||||
const auto texels = static_cast<uint64_t>(info.extent.width) * info.extent.height *
|
||||
info.extent.depth * info.resources.layers;
|
||||
if (info.IsTiled() ? m_readback_tiled_images && texels <= SmallTiledReadbackTexels
|
||||
: m_readback_linear_images) {
|
||||
if (!image.IsGpuModified()) {
|
||||
EXIT("TextureCache: cannot enroll a non-GPU-owned image for download\n");
|
||||
}
|
||||
@@ -954,6 +959,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 +1005,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 +1046,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 +1424,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 +1778,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 +1948,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;
|
||||
@@ -1960,7 +1966,7 @@ void TextureCache::RunGarbageCollector() {
|
||||
}
|
||||
|
||||
void TextureCache::ProcessDownloadImages() {
|
||||
if (!m_readback_linear_images) {
|
||||
if (!m_readback_linear_images && !m_readback_tiled_images) {
|
||||
return;
|
||||
}
|
||||
std::scoped_lock lock {m_lock};
|
||||
|
||||
+1
-3
@@ -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;
|
||||
};
|
||||
@@ -183,6 +180,7 @@ private:
|
||||
uint64_t m_gc_tick = 0;
|
||||
mutable uint32_t m_image_query_epoch = 0;
|
||||
bool m_readback_linear_images = false;
|
||||
bool m_readback_tiled_images = false;
|
||||
|
||||
friend struct TextureCacheTestAccess;
|
||||
friend class BufferCache;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -311,7 +311,6 @@ void RenderExecutor::ResolveRenderDepthTarget(CommandBuffer& buffer, RenderDepth
|
||||
}
|
||||
r.desc = MakeDepthTargetDesc(buffer, z);
|
||||
r.depth_clear_enable = rc.depth_clear_enable;
|
||||
r.depth_load_clear_enable = r.depth_clear_enable;
|
||||
r.depth_clear_value = hw.GetDepthClearValue();
|
||||
r.depth_test_enable = dc.z_enable;
|
||||
r.depth_write_enable = r.depth_test_enable && dc.z_write_enable &&
|
||||
@@ -421,9 +420,9 @@ vk::ImageAspectFlags RenderDepthInfo::AttachmentWriteAspects() const {
|
||||
}
|
||||
|
||||
const auto available = ImageViewOps::DepthAspectMask(format);
|
||||
vk::ImageAspectFlags writes {};
|
||||
vk::ImageAspectFlags writes = load_clear_aspects & available;
|
||||
if ((available & vk::ImageAspectFlagBits::eDepth) &&
|
||||
(depth_load_clear_enable || depth_write_enable)) {
|
||||
depth_write_enable) {
|
||||
writes |= vk::ImageAspectFlagBits::eDepth;
|
||||
}
|
||||
if (!(available & vk::ImageAspectFlagBits::eStencil)) {
|
||||
@@ -461,8 +460,7 @@ vk::ImageAspectFlags RenderDepthInfo::AttachmentWriteAspects() const {
|
||||
(can_pass && depth_pass && state.passOp != vk::StencilOp::eKeep) ||
|
||||
(can_pass && depth_fail && state.depthFailOp != vk::StencilOp::eKeep);
|
||||
};
|
||||
if (stencil_clear_enable ||
|
||||
(stencil_test_enable && (face_writes(stencil_front) || face_writes(stencil_back)))) {
|
||||
if (stencil_test_enable && (face_writes(stencil_front) || face_writes(stencil_back))) {
|
||||
writes |= vk::ImageAspectFlagBits::eStencil;
|
||||
}
|
||||
return writes;
|
||||
|
||||
@@ -22,7 +22,7 @@ struct RenderDepthInfo {
|
||||
// Discovery keeps guest image information but can remap the view into a larger cache image.
|
||||
TextureCache::ImageDesc desc;
|
||||
bool depth_clear_enable = false;
|
||||
bool depth_load_clear_enable = false;
|
||||
vk::ImageAspectFlags load_clear_aspects;
|
||||
float depth_clear_value = 0.0f;
|
||||
bool depth_test_enable = false;
|
||||
// Effective draw writes; discovery applies test, target-write and clear controls.
|
||||
|
||||
@@ -30,16 +30,18 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] vk::ImageCreateFlags ImageCreateFlags(const GraphicContext& graphics,
|
||||
const ImageInfo& info) {
|
||||
[[nodiscard]] vk::ImageCreateFlags
|
||||
ImageCreateFlags(const GraphicContext& graphics, const ImageInfo& info, vk::ImageUsageFlags usage) {
|
||||
vk::ImageCreateFlags flags {};
|
||||
if (DepthAspectTransferFormat(info.pixel_format) == vk::Format::eUndefined) {
|
||||
flags |= vk::ImageCreateFlagBits::eMutableFormat;
|
||||
flags |= vk::ImageCreateFlagBits::eExtendedUsage;
|
||||
if (info.IsBlock() && graphics.supports_block_texel_view) {
|
||||
flags |= vk::ImageCreateFlagBits::eBlockTexelViewCompatible;
|
||||
}
|
||||
}
|
||||
if (usage & vk::ImageUsageFlagBits::eStorage) {
|
||||
flags |= vk::ImageCreateFlagBits::eExtendedUsage;
|
||||
}
|
||||
if (info.IsVolume()) {
|
||||
flags |= vk::ImageCreateFlagBits::e2DArrayCompatible;
|
||||
}
|
||||
@@ -59,7 +61,7 @@ namespace {
|
||||
const auto storage = usage | vk::ImageUsageFlagBits::eStorage;
|
||||
if (graphics.GetImageFormatProperties(info.pixel_format, HostImageType(info.type),
|
||||
vk::ImageTiling::eOptimal, storage,
|
||||
ImageCreateFlags(graphics, info),
|
||||
ImageCreateFlags(graphics, info, storage),
|
||||
nullptr) == vk::Result::eSuccess) {
|
||||
usage = storage;
|
||||
} else {
|
||||
@@ -353,8 +355,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 {};
|
||||
@@ -698,7 +704,8 @@ Image::Image(GraphicContext& graphics, CommandScheduler& scheduler, const ImageI
|
||||
}
|
||||
|
||||
vk::ImageCreateInfo create {};
|
||||
create.flags = ImageCreateFlags(graphics, info);
|
||||
create.usage = ImageUsageFlags(graphics, info);
|
||||
create.flags = ImageCreateFlags(graphics, info, create.usage);
|
||||
create.imageType = HostImageType(info.type);
|
||||
create.extent = info.extent;
|
||||
create.mipLevels = info.resources.levels;
|
||||
@@ -706,7 +713,6 @@ Image::Image(GraphicContext& graphics, CommandScheduler& scheduler, const ImageI
|
||||
create.format = info.pixel_format;
|
||||
create.tiling = vk::ImageTiling::eOptimal;
|
||||
create.initialLayout = vk::ImageLayout::eUndefined;
|
||||
create.usage = ImageUsageFlags(graphics, info);
|
||||
create.samples = vulkan_sample_count(info.samples);
|
||||
|
||||
vk::ImageFormatProperties properties {};
|
||||
|
||||
@@ -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"
|
||||
@@ -18,13 +19,13 @@
|
||||
#include "graphics/shader/recompiler/ShaderRecompiler.h"
|
||||
#include "graphics/shader/shaderCompiler.h"
|
||||
#include "kernel/memory.h"
|
||||
#include "kytyGitVersion.h"
|
||||
#include "loader/systemContent.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fmt/format.h>
|
||||
@@ -81,7 +82,8 @@ std::string DriverCacheSignature(const vk::PhysicalDeviceProperties& properties)
|
||||
uuid[i * 2] = hex[properties.pipelineCacheUUID[i] >> 4u];
|
||||
uuid[i * 2 + 1] = hex[properties.pipelineCacheUUID[i] & 0xfu];
|
||||
}
|
||||
return fmt::format("KytyPC1:{}:{:08x}:{:08x}:{:08x}:{}\n", KYTY_GIT_REVISION,
|
||||
// Driver cache entries already distinguish shader code and pipeline state across builds.
|
||||
return fmt::format("KytyPC2:{:08x}:{:08x}:{:08x}:{}\n",
|
||||
properties.vendorID, properties.deviceID, properties.driverVersion, uuid);
|
||||
}
|
||||
|
||||
@@ -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,10 +302,13 @@ 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,
|
||||
options.maintenance8_enabled)) {
|
||||
options.profile->maintenance8)) {
|
||||
DumpShaderSpirv(stage_name, options.shader_hash, result.spirv);
|
||||
EXIT("%s failed hash=0x%016" PRIx64 ": SPIR-V validation failed\n", options.dump_label,
|
||||
options.shader_hash);
|
||||
@@ -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);
|
||||
@@ -391,7 +398,7 @@ struct PipelineCache::ProgramCache {
|
||||
}
|
||||
ShaderRecompiler::CompileOptions options;
|
||||
options.stage = stage;
|
||||
options.maintenance8_enabled = graphics.maintenance8_enabled;
|
||||
options.profile = &profile;
|
||||
options.shader_hash = params.hash;
|
||||
options.user_data = user_data;
|
||||
options.back_code = params.back_code;
|
||||
@@ -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(
|
||||
@@ -465,7 +480,13 @@ struct PipelineCache::ProgramCache {
|
||||
return permutation.handle;
|
||||
}
|
||||
|
||||
explicit ProgramCache(GraphicContext& graphics): graphics(graphics) {
|
||||
explicit ProgramCache(GraphicContext& graphics)
|
||||
: graphics(graphics),
|
||||
// AMD GPUs (vendor 0x1002) run NMin/NMax as v_min/v_max, which give the guest's NaN and
|
||||
// signed-zero results; GLSL.std.450 alone does not order -0 below +0.
|
||||
profile {.maintenance8 = graphics.maintenance8_enabled,
|
||||
.native_float_min_max =
|
||||
graphics.physical_device_properties.vendorID == 0x1002u} {
|
||||
lookup_key.static_state.reserve(MaxStaticKeyWords);
|
||||
}
|
||||
~ProgramCache() {
|
||||
@@ -480,6 +501,7 @@ struct PipelineCache::ProgramCache {
|
||||
std::unordered_map<ProgramKey, SourceEntry, ProgramKeyHash> programs;
|
||||
ProgramKey lookup_key;
|
||||
GraphicContext& graphics;
|
||||
const ShaderRecompiler::Spirv::Profile profile;
|
||||
uint64_t next_shader_id = 0;
|
||||
};
|
||||
|
||||
@@ -516,17 +538,6 @@ void PipelineCache::InitializeDriverCache() {
|
||||
PipelineCacheLog("Vulkan pipeline cache: disabled (non-Release build)");
|
||||
return;
|
||||
}
|
||||
const std::string_view git_hash = KYTY_GIT_HASH;
|
||||
const std::string_view git_revision = KYTY_GIT_REVISION;
|
||||
if (git_hash == "unknown" || git_revision == "unknown") {
|
||||
PipelineCacheLog("Vulkan pipeline cache: disabled (unknown git revision)");
|
||||
return;
|
||||
}
|
||||
if (git_hash.ends_with("-dirty")) {
|
||||
PipelineCacheLog("Vulkan pipeline cache: disabled (dirty build)");
|
||||
return;
|
||||
}
|
||||
|
||||
m_driver_cache_path = std::filesystem::path("_PipelineCache") / (title_id + ".bin");
|
||||
const auto path = Common::PathToString(m_driver_cache_path);
|
||||
const bool cache_exists = Common::File::IsFileExisting(m_driver_cache_path);
|
||||
@@ -559,7 +570,7 @@ void PipelineCache::InitializeDriverCache() {
|
||||
XXH3_64bits(initial_data.data(), initial_data.size()) != payload_hash) {
|
||||
initial_data.clear();
|
||||
PipelineCacheLog(
|
||||
"Vulkan pipeline cache: invalidating {} (driver, emulator, or data mismatch)",
|
||||
"Vulkan pipeline cache: invalidating {} (driver or data mismatch)",
|
||||
path);
|
||||
}
|
||||
} else {
|
||||
@@ -586,8 +597,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");
|
||||
}
|
||||
@@ -627,8 +638,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);
|
||||
@@ -660,7 +671,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;
|
||||
}
|
||||
@@ -775,10 +787,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;
|
||||
@@ -788,9 +802,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);
|
||||
@@ -957,6 +978,7 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(
|
||||
if (static_params.sample_shading_enable && !m_graphics.sample_rate_shading_enabled) {
|
||||
EXIT("Pipeline: sample-rate shading is required but unsupported by the host\n");
|
||||
}
|
||||
static_params.alpha_to_coverage_enable = ps_active && ctx.GetShaderRegisters().AlphaToMask();
|
||||
const bool rect_list = Prospero::IsRectList(command.GetUserConfig().GetPrimType());
|
||||
static_params.cull_back = !rect_list && mc.cull_back;
|
||||
static_params.cull_front = !rect_list && mc.cull_front;
|
||||
@@ -1004,8 +1026,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);
|
||||
@@ -1035,7 +1062,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);
|
||||
|
||||
@@ -39,6 +39,7 @@ struct PipelineStaticParameters {
|
||||
bool primitive_restart_enable = false;
|
||||
uint32_t samples = 1;
|
||||
bool sample_shading_enable = false;
|
||||
bool alpha_to_coverage_enable = false;
|
||||
uint32_t color_mask[RENDER_COLOR_ATTACHMENTS_MAX] = {};
|
||||
bool cull_front = false;
|
||||
bool cull_back = false;
|
||||
@@ -62,7 +63,7 @@ struct PipelineStaticParameters {
|
||||
static_assert(std::is_trivially_copyable_v<PipelineStaticParameters>);
|
||||
static_assert(std::is_standard_layout_v<PipelineStaticParameters>);
|
||||
static_assert(alignof(PipelineStaticParameters) == 1);
|
||||
static_assert(sizeof(PipelineStaticParameters) == 116);
|
||||
static_assert(sizeof(PipelineStaticParameters) == 117);
|
||||
|
||||
struct PipelineRenderingState {
|
||||
std::array<vk::Format, RENDER_COLOR_ATTACHMENTS_MAX> color_formats {};
|
||||
|
||||
@@ -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 {};
|
||||
@@ -412,6 +422,8 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
multisampling.sampleShadingEnable = static_params.sample_shading_enable ? VK_TRUE : VK_FALSE;
|
||||
multisampling.rasterizationSamples = vulkan_sample_count(static_params.samples);
|
||||
multisampling.minSampleShading = 1.0f;
|
||||
multisampling.alphaToCoverageEnable =
|
||||
static_params.alpha_to_coverage_enable ? VK_TRUE : VK_FALSE;
|
||||
|
||||
vk::PipelineColorBlendAttachmentState color_blend_attachment[RENDER_COLOR_ATTACHMENTS_MAX] = {};
|
||||
for (uint32_t i = 0; i < rendering.color_count; i++) {
|
||||
@@ -626,6 +638,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,
|
||||
@@ -94,9 +99,10 @@ bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t grou
|
||||
constexpr std::array formats {
|
||||
Prospero::BufferFormat::k32UInt, Prospero::BufferFormat::k32_32UInt,
|
||||
Prospero::BufferFormat::k32_32_32UInt, Prospero::BufferFormat::k32_32_32_32UInt};
|
||||
// Offset-or-swizzle bounds do not describe the element range proved by the shader.
|
||||
if (descriptor.Stride() != element_size || descriptor.Format() != formats[fill.words - 1] ||
|
||||
descriptor.SwizzleEnabled() || descriptor.IndexStride() != 0 || descriptor.AddTid() ||
|
||||
descriptor.Base48() == 0) {
|
||||
descriptor.OutOfBounds() == 3 || descriptor.Base48() == 0) {
|
||||
return false;
|
||||
}
|
||||
if (input.threads_num[0] == 0 || input.threads_num[0] != fill.group_stride[0] ||
|
||||
@@ -108,7 +114,11 @@ bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t grou
|
||||
? group_x
|
||||
: static_cast<uint64_t>(group_x) * input.threads_num[0];
|
||||
const auto size = descriptor.GetSize();
|
||||
if (invocations != descriptor.NumRecords() || size == 0 || size > UINT32_MAX ||
|
||||
const auto elements = descriptor.NumRecords();
|
||||
const bool covered = fill.bounded_elements != 0
|
||||
? fill.bounded_elements == elements && invocations >= elements
|
||||
: invocations == elements;
|
||||
if (!covered || invocations > uint64_t {UINT32_MAX} + 1 || size == 0 || size > UINT32_MAX ||
|
||||
(input.dispatch_thread_dimensions &&
|
||||
(group_x % input.threads_num[0] != 0 || input.dispatch_threads_num[0] != group_x ||
|
||||
input.dispatch_threads_num[1] != 1 || input.dispatch_threads_num[2] != 1))) {
|
||||
@@ -122,9 +132,9 @@ bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t grou
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderExecutor::TryConsumeComputeImageClear(const ShaderComputeInputInfo& input,
|
||||
CommandBuffer& command, uint32_t group_x,
|
||||
uint32_t group_y, uint32_t group_z, uint32_t mode) {
|
||||
bool RenderExecutor::TryConsumeComputeFill(const ShaderComputeInputInfo& input,
|
||||
CommandBuffer& command, uint32_t group_x,
|
||||
uint32_t group_y, uint32_t group_z, uint32_t mode) {
|
||||
const auto& program = *input.stage.program;
|
||||
const auto& resources = *input.stage.resources;
|
||||
const auto& fill = resources.uniform_fill;
|
||||
@@ -180,14 +190,26 @@ bool RenderExecutor::TryConsumeComputeImageClear(const ShaderComputeInputInfo& i
|
||||
size)) {
|
||||
return false;
|
||||
}
|
||||
if (!cache.ClearImageFromBuffer(command, descriptor.Base48(), size, packed_clear)) {
|
||||
return false;
|
||||
const bool image_clear =
|
||||
cache.ClearImageFromBuffer(command, descriptor.Base48(), size, packed_clear);
|
||||
if (!image_clear) {
|
||||
// CPU fills invalidate overlapping cached images. Preserve the native dispatch for
|
||||
// partial or ambiguous image backings, including pooled metadata aliases.
|
||||
if ((descriptor.Base48() & 3u) != 0 ||
|
||||
!m_context.IsMapped(descriptor.Base48(), size) ||
|
||||
cache.IsRegionRegistered(descriptor.Base48(), size)) {
|
||||
return false;
|
||||
}
|
||||
// Metadata need not have an image backing yet. Keep proven fills in guest memory
|
||||
// when CPU-owned, so color-clear discovery can read them without a GPU download.
|
||||
m_context.GetBufferCache().FillBuffer(descriptor.Base48(), size, packed_clear, false);
|
||||
}
|
||||
static std::atomic<uint32_t> logged_clears {0};
|
||||
if (logged_clears.fetch_add(1, std::memory_order_relaxed) < 32) {
|
||||
LOGF("GraphicsRenderDispatchDirect: compute image clear shader=0x%016" PRIx64
|
||||
LOGF("GraphicsRenderDispatchDirect: compute %s shader=0x%016" PRIx64
|
||||
" addr=0x%016" PRIx64 " size=0x%016" PRIx64 " value=0x%08" PRIx32 "\n",
|
||||
input.stage.program->shader_hash, descriptor.Base48(), size, packed_clear);
|
||||
image_clear ? "image clear" : "buffer fill", input.stage.program->shader_hash,
|
||||
descriptor.Base48(), size, packed_clear);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -252,10 +274,6 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
return;
|
||||
}
|
||||
|
||||
if (sh_ctx.GetCs().cs_regs.data_addr == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr uint32_t DISPATCH_INITIATOR_USE_THREAD_DIMENSIONS = 1u << 5u;
|
||||
constexpr uint32_t DISPATCH_INITIATOR_BASE_BITS = 0x41u;
|
||||
constexpr uint32_t DISPATCH_INITIATOR_MODIFIER_BITS = 0xa038u;
|
||||
@@ -295,6 +313,7 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
}
|
||||
const auto compute_program =
|
||||
m_context.GetPipelineCache().GetComputeProgram(cs_regs, sh_regs, input_info);
|
||||
if (!compute_program) return;
|
||||
if (use_thread_dimensions) {
|
||||
input_info.dispatch_threads_num[0] = thread_group_x;
|
||||
input_info.dispatch_threads_num[1] = thread_group_y;
|
||||
@@ -303,14 +322,12 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
|
||||
const auto& program = *input_info.stage.program;
|
||||
const auto& resources = *input_info.stage.resources;
|
||||
if (resources.specialization_reads.empty()) {
|
||||
// Cached metadata addresses can be reused as ordinary buffers; preserve their guest writes.
|
||||
UpdateComputeMetaClears(input_info, buffer);
|
||||
if (TryConsumeComputeImageClear(input_info, buffer, thread_group_x, thread_group_y,
|
||||
thread_group_z, mode)) {
|
||||
ResetBindings();
|
||||
return;
|
||||
}
|
||||
if (resources.specialization_reads.empty() &&
|
||||
(TryConsumeComputeMetaClear(input_info, buffer) ||
|
||||
TryConsumeComputeFill(input_info, buffer, thread_group_x, thread_group_y,
|
||||
thread_group_z, mode))) {
|
||||
ResetBindings();
|
||||
return;
|
||||
}
|
||||
|
||||
if (use_thread_dimensions) {
|
||||
@@ -430,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);
|
||||
@@ -461,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;
|
||||
@@ -500,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();
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ bool RenderContext::IsMapped(uint64_t vaddr, uint64_t size) const noexcept {
|
||||
void RenderContext::MapMemory(uint64_t vaddr, uint64_t size) {
|
||||
std::lock_guard lock(m_mapped_ranges_mutex);
|
||||
m_mapped_ranges.Add(vaddr, size);
|
||||
m_buffer_cache.QueueBdaSync(vaddr, size);
|
||||
}
|
||||
|
||||
void RenderContext::UnmapMemory(uint64_t vaddr, uint64_t size) {
|
||||
@@ -129,9 +130,7 @@ void RenderContext::PrepareBda() {
|
||||
m_bda_logged = true;
|
||||
}
|
||||
std::shared_lock lock(m_mapped_ranges_mutex);
|
||||
m_mapped_ranges.ForEach([this](uint64_t start, uint64_t end) {
|
||||
m_buffer_cache.SynchronizeBuffersInRange(start, end - start);
|
||||
});
|
||||
m_buffer_cache.SynchronizeBda(m_mapped_ranges);
|
||||
m_fault_process_pending = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -435,7 +436,7 @@ static bool DrawHasValidVertexShader(const HW::Shader& sh_ctx) {
|
||||
static bool PixelShaderHasDepthOrCoverageSideEffects(const HW::ShaderRegisters& sh_regs) {
|
||||
const auto& db = sh_regs.db_shader_control;
|
||||
return db.shader_kill_enable || db.shader_z_export_enable || db.shader_mask_export_enable ||
|
||||
db.shader_dual_export_enable || db.shader_execute_on_noop;
|
||||
db.shader_dual_export_enable || db.shader_execute_on_noop || sh_regs.AlphaToMask();
|
||||
}
|
||||
|
||||
struct DrawRenderState {
|
||||
@@ -507,14 +508,15 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
|
||||
}
|
||||
const auto image_view = cache.FindDepthTarget(depth.image_id, depth.desc);
|
||||
const auto& metadata = depth.desc.info.metadata;
|
||||
if (metadata.kind == ImageMetadataKind::Htile && depth.depth_clear_enable &&
|
||||
!cache.ClearMeta(metadata.range.address)) {
|
||||
EXIT("failed to acquire HTile metadata for a depth clear\n");
|
||||
}
|
||||
const bool meta_clear =
|
||||
metadata.kind == ImageMetadataKind::Htile &&
|
||||
cache.IsMetaCleared(metadata.range.address, depth.desc.view_info.base_layer);
|
||||
depth.depth_load_clear_enable = depth.depth_clear_enable || meta_clear;
|
||||
depth.load_clear_aspects = {};
|
||||
if (depth.depth_clear_enable || meta_clear)
|
||||
depth.load_clear_aspects |= vk::ImageAspectFlagBits::eDepth;
|
||||
// Apply pending clears to compressed stencil as well as depth.
|
||||
if (depth.stencil_clear_enable || (meta_clear && metadata.stencil_compressed))
|
||||
depth.load_clear_aspects |= vk::ImageAspectFlagBits::eStencil;
|
||||
if (meta_clear &&
|
||||
!cache.TouchMeta(metadata.range.address, depth.desc.view_info.base_layer, false)) {
|
||||
EXIT("failed to consume HTile clear state\n");
|
||||
@@ -536,7 +538,10 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
|
||||
}
|
||||
}
|
||||
if (feedback_aspects && !m_context.GetGraphics().attachment_feedback_loop_enabled) {
|
||||
EXIT("depth attachment feedback loop is not supported by the host\n");
|
||||
static std::atomic_bool logged = false;
|
||||
if (!logged.exchange(true, std::memory_order_relaxed)) {
|
||||
std::printf("depth attachment feedback loop is not supported by the host\n");
|
||||
}
|
||||
}
|
||||
auto layout = depth_attachment_layout(depth);
|
||||
if (sampled_aspects & ~DepthReadableAspects(layout)) {
|
||||
@@ -564,9 +569,11 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
|
||||
attachment.clear_value[0] = std::bit_cast<uint32_t>(depth.depth_clear_value);
|
||||
attachment.clear_value[1] = depth.stencil_clear_value;
|
||||
attachment.has_depth = static_cast<bool>(aspects & vk::ImageAspectFlagBits::eDepth);
|
||||
attachment.depth_clear = depth.depth_load_clear_enable;
|
||||
attachment.depth_clear =
|
||||
static_cast<bool>(depth.load_clear_aspects & vk::ImageAspectFlagBits::eDepth);
|
||||
attachment.has_stencil = static_cast<bool>(aspects & vk::ImageAspectFlagBits::eStencil);
|
||||
attachment.stencil_clear = depth.stencil_clear_enable;
|
||||
attachment.stencil_clear =
|
||||
static_cast<bool>(depth.load_clear_aspects & vk::ImageAspectFlagBits::eStencil);
|
||||
}
|
||||
if (color_count == 0 && !depth.image_id) {
|
||||
const auto& limits = buffer.GetGraphics().GetPhysicalDeviceProperties().limits;
|
||||
@@ -928,6 +935,29 @@ bool RenderExecutor::PrepareDrawRenderState(CommandBuffer& buffer, const DrawCal
|
||||
LogDrawPhase(draw.Name(), "ResolveRenderDepthTarget");
|
||||
}
|
||||
ResolveRenderDepthTarget(buffer, state.depth_info);
|
||||
// TODO(PPSA09243): Vulkan passes take one sample count. Where PS5 attachments mix counts,
|
||||
// keep those matching the raster count instead of resolving multisampled shadows.
|
||||
const auto colors = std::span {state.color_info, state.color_count};
|
||||
const auto pass_samples = state.depth_info.image_id ? state.depth_info.desc.info.samples
|
||||
: state.color_info[0].desc.info.samples;
|
||||
if (std::ranges::any_of(colors, [&](const RenderColorInfo& color) {
|
||||
return color.desc.info.samples != pass_samples;
|
||||
})) {
|
||||
const auto raster =
|
||||
render_sample_count(buffer.GetRegisters().GetAaConfig().msaa_num_samples);
|
||||
const auto dropped = std::ranges::remove_if(colors, [&](const RenderColorInfo& color) {
|
||||
return color.desc.info.samples != raster;
|
||||
});
|
||||
state.color_count -= static_cast<uint32_t>(dropped.size());
|
||||
if (state.depth_info.image_id && state.depth_info.desc.info.samples != raster) {
|
||||
state.depth_info = {};
|
||||
}
|
||||
static std::atomic_bool logged = false;
|
||||
if (!logged.exchange(true, std::memory_order_relaxed)) {
|
||||
Log::WriteToConsoleAndLog(
|
||||
"Warning: dropping attachments whose sample count differs from the raster.\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (state.color_count == 0 && !state.depth_info.image_id && !state.ps_active) {
|
||||
LogFramebufferSkip(draw.Name(), state.color_info[0], state.depth_info, buffer,
|
||||
@@ -1057,7 +1087,7 @@ bool RenderExecutor::CopyDepthToColor(CommandBuffer& buffer, const DrawCallInfo&
|
||||
state.ps_input_info.ps_sample_mask_export_enable);
|
||||
vk::ImageAspectFlags feedback;
|
||||
const auto rendering = AcquireRenderTargets(buffer, &color, 1, depth, feedback);
|
||||
if (depth.depth_load_clear_enable) {
|
||||
if (depth.load_clear_aspects) {
|
||||
m_context.GetCommandScheduler().BeginRendering(rendering);
|
||||
}
|
||||
auto& cache = m_context.GetTextureCache();
|
||||
@@ -1186,13 +1216,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,
|
||||
@@ -1206,6 +1237,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,
|
||||
@@ -1213,6 +1250,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);
|
||||
@@ -1227,19 +1278,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();
|
||||
|
||||
@@ -162,7 +162,8 @@ void RenderDocOnGuestFlip(RenderContext& renderer) {
|
||||
return;
|
||||
}
|
||||
if (state == RenderDocState::Capturing) {
|
||||
LOGF("RenderDoc: captured guest flip %u/2\n", ++g_captured_flips);
|
||||
++g_captured_flips;
|
||||
LOGF("RenderDoc: captured guest flip %u/2\n", g_captured_flips);
|
||||
if (g_captured_flips < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -945,7 +956,6 @@ Presenter::Frame& Presenter::PrepareBlankFrame(uint32_t width, uint32_t height,
|
||||
auto* frame = m_impl->frames.Acquire({width, height}, format);
|
||||
frame->hdr = false;
|
||||
frame->blank = true;
|
||||
Common::LockGuard render_lock(m_impl->renderer.GetMutex());
|
||||
frame->Configure(m_impl->window.graphic_ctx, {width, height}, format);
|
||||
vk::ClearColorValue clear {};
|
||||
clear.float32[3] = opaque ? 1.0f : 0.0f;
|
||||
@@ -953,6 +963,7 @@ Presenter::Frame& Presenter::PrepareBlankFrame(uint32_t width, uint32_t height,
|
||||
EXIT_IF(producer->IsInvalid());
|
||||
frame->Clear(*producer, clear);
|
||||
} else {
|
||||
Common::LockGuard lock(m_impl->present_mutex);
|
||||
auto& command = m_impl->present_scheduler.BeginCommand();
|
||||
frame->Clear(command, clear);
|
||||
frame->present_tick = m_impl->present_scheduler.Submit();
|
||||
@@ -1036,18 +1047,17 @@ void Presenter::Impl::Present() {
|
||||
RecoverSwapchain(status);
|
||||
continue;
|
||||
}
|
||||
{
|
||||
Common::LockGuard render_lock(renderer.GetMutex());
|
||||
auto& command = present_scheduler.BeginCommand();
|
||||
const bool draw_system_overlay =
|
||||
overlay_visual.active && swapchain.PrepareSystemOverlay();
|
||||
swapchain.RecordPresentCommands(command, layers[0].frame, layers[1],
|
||||
draw_system_overlay);
|
||||
const auto tick = swapchain.Submit(present_scheduler);
|
||||
for (const auto& layer: layers) {
|
||||
if (layer.frame != nullptr) {
|
||||
layer.frame->present_tick = tick;
|
||||
}
|
||||
// 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 || PerformanceOverlayEnabled()) &&
|
||||
swapchain.PrepareSystemOverlay();
|
||||
swapchain.RecordPresentCommands(command, layers[0].frame, layers[1],
|
||||
draw_system_overlay);
|
||||
const auto tick = swapchain.Submit(present_scheduler);
|
||||
for (const auto& layer: layers) {
|
||||
if (layer.frame != nullptr) {
|
||||
layer.frame->present_tick = tick;
|
||||
}
|
||||
}
|
||||
status = swapchain.Present();
|
||||
@@ -1061,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),
|
||||
@@ -885,8 +930,8 @@ CompileResult CompileProgram(TranslateResult translated, const CompileOptions& o
|
||||
|
||||
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " SPIR-V EmitProgram\n",
|
||||
GetDumpLabel(options), StageName(ir.stage), ir.shader_hash);
|
||||
auto spirv = Spirv::EmitProgram(ir, options.input_info, push_data_start_dword,
|
||||
options.maintenance8_enabled, options.emit_debug_info);
|
||||
auto spirv = Spirv::EmitProgram(ir, options.input_info, push_data_start_dword, *options.profile,
|
||||
options.emit_debug_info);
|
||||
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " SPIR-V EmitProgram words=%" PRIu64
|
||||
" elapsed_ms=%" PRIu64 "\n",
|
||||
GetDumpLabel(options), StageName(ir.stage), ir.shader_hash,
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
|
||||
#include "common/common.h"
|
||||
#include "common/stringUtils.h"
|
||||
#include "graphics/shader/recompiler/backend/spirv/SpirvEmitter.h"
|
||||
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
|
||||
#include "graphics/shader/recompiler/frontend/decode/ShaderDecoder.h"
|
||||
#include "graphics/shader/shader.h"
|
||||
|
||||
#include <array>
|
||||
#include <bitset>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
@@ -22,7 +24,7 @@ struct CompileOptions {
|
||||
bool dump_ir = true;
|
||||
bool early_dump = false;
|
||||
bool emit_debug_info = false;
|
||||
bool maintenance8_enabled = false;
|
||||
const Spirv::Profile* profile = &Spirv::DefaultProfile;
|
||||
const char* dump_label = nullptr;
|
||||
std::span<const uint32_t> user_data;
|
||||
std::span<const uint32_t> back_code;
|
||||
@@ -47,16 +49,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
|
||||
|
||||
@@ -204,7 +204,7 @@ void ValidateNativeProgram(const IR::Program& program, bool lds_storage) {
|
||||
} // namespace
|
||||
|
||||
std::vector<uint32_t> EmitProgram(IR::Program& program, ShaderStageInputInfo input_info,
|
||||
uint32_t push_data_start_dword, bool maintenance8_enabled,
|
||||
uint32_t push_data_start_dword, const Profile& profile,
|
||||
bool emit_debug_info) {
|
||||
using namespace Emitter;
|
||||
|
||||
@@ -220,7 +220,7 @@ std::vector<uint32_t> EmitProgram(IR::Program& program, ShaderStageInputInfo inp
|
||||
}
|
||||
IR::ValidateProgram(program, true);
|
||||
program.bindings = {.push_data_start_dword = push_data_start_dword};
|
||||
EmitterState state(program, input_info, maintenance8_enabled);
|
||||
EmitterState state(program, input_info, profile);
|
||||
if (emit_debug_info) {
|
||||
char name[64];
|
||||
std::snprintf(name, sizeof(name), "rdna2-%016" PRIx64 "-block-byte-pc-plus-one",
|
||||
@@ -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);
|
||||
|
||||
@@ -9,11 +9,19 @@
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::Spirv {
|
||||
|
||||
// Host device capabilities that change the emitted SPIR-V. Built once per device.
|
||||
struct Profile {
|
||||
bool maintenance8 = false; // Dynamic texel offsets on image samples.
|
||||
bool native_float_min_max = false; // NMin/NMax give the guest's NaN and signed-zero results.
|
||||
};
|
||||
|
||||
inline constexpr Profile DefaultProfile {};
|
||||
|
||||
// Resets and writes backend IDs and descriptor indices; do not emit the same program concurrently.
|
||||
std::vector<uint32_t> EmitProgram(IR::Program& program, ShaderStageInputInfo input_info,
|
||||
uint32_t push_data_start_dword = 0,
|
||||
bool maintenance8_enabled = false,
|
||||
bool emit_debug_info = false);
|
||||
uint32_t push_data_start_dword = 0,
|
||||
const Profile& profile = DefaultProfile,
|
||||
bool emit_debug_info = false);
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::Spirv
|
||||
|
||||
|
||||
@@ -5,18 +5,6 @@
|
||||
namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
|
||||
namespace {
|
||||
|
||||
struct Pair {
|
||||
uint32_t low = 0;
|
||||
uint32_t high = 0;
|
||||
};
|
||||
|
||||
Pair ExtractPair(EmitterState& state, uint32_t value) {
|
||||
Pair result {state.builder.AllocateId(), state.builder.AllocateId()};
|
||||
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), result.low, value, 0);
|
||||
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), result.high, value, 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t EmitMinMaxF64(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value) {
|
||||
const auto bits_type = TypeU32Vector(state, 2);
|
||||
const auto lhs_bits = Unary(state, spv::OpBitcast, bits_type, lhs);
|
||||
@@ -32,11 +20,10 @@ uint32_t EmitMinMaxF64(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max
|
||||
result = Select(state, TypeF64(state), equal,
|
||||
Unary(state, spv::OpBitcast, TypeF64(state), equal_bits), result);
|
||||
|
||||
// Non-IEEE mode selects the other operand for NaN, including rhs when both are NaN.
|
||||
result = Select(state, TypeF64(state), Unary(state, spv::OpIsNan, TypeBool(state), rhs),
|
||||
lhs, result);
|
||||
return Select(state, TypeF64(state), Unary(state, spv::OpIsNan, TypeBool(state), lhs),
|
||||
rhs, result);
|
||||
// Non-IEEE mode selects the other operand for NaN, and lhs when both are NaN. The ordered
|
||||
// compares already pick rhs for a NaN lhs.
|
||||
return Select(state, TypeF64(state), Unary(state, spv::OpIsNan, TypeBool(state), rhs),
|
||||
lhs, result);
|
||||
}
|
||||
|
||||
uint32_t EmitMulHigh(EmitterState& state, uint32_t lhs, uint32_t rhs, bool signed_value) {
|
||||
@@ -67,36 +54,62 @@ 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();
|
||||
if (signed_value) {
|
||||
const auto converted_i = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpConvertFToS, TypeI32(state), converted_i, trunc);
|
||||
state.builder.AddFunction(spv::OpBitcast, TypeU32(state), converted_raw, converted_i);
|
||||
} else {
|
||||
state.builder.AddFunction(spv::OpConvertFToU, TypeU32(state), converted_raw, trunc);
|
||||
}
|
||||
const auto nan = EmitClassifyF32(state, src).nan;
|
||||
if (signed_value) {
|
||||
const auto below = Binary(state, spv::OpFOrdLessThanEqual, TypeBool(state), src,
|
||||
ConstantF32(state, 0xcf000000u));
|
||||
const auto above = Binary(state, spv::OpFOrdGreaterThanEqual, TypeBool(state), src,
|
||||
ConstantF32(state, 0x4f000000u));
|
||||
const auto high =
|
||||
Select(state, TypeU32(state), above, ConstantU32(state, 0x7fffffffu), converted_raw);
|
||||
const auto low =
|
||||
Select(state, TypeU32(state), below, ConstantU32(state, 0x80000000u), high);
|
||||
return Select(state, TypeU32(state), nan, ConstantU32(state, 0), low);
|
||||
}
|
||||
const auto below =
|
||||
Binary(state, spv::OpFOrdLessThanEqual, TypeBool(state), src, ConstantF32(state, 0));
|
||||
const auto above = Binary(state, spv::OpFOrdGreaterThanEqual, TypeBool(state), src,
|
||||
ConstantF32(state, 0x4f800000u));
|
||||
const auto zero = Binary(state, spv::OpLogicalOr, TypeBool(state), nan, below);
|
||||
// Clamping first keeps the truncating conversion defined; NaN and values past the largest
|
||||
// float below the integer limit are selected afterwards.
|
||||
const auto clamped =
|
||||
EmitExt(state, TypeF32(state), GLSLstd450NClamp,
|
||||
{src, ConstantF32(state, signed_value ? 0xcf000000u : 0u),
|
||||
ConstantF32(state, signed_value ? 0x4effffffu : 0x4f7fffffu)});
|
||||
const auto converted = signed_value
|
||||
? Unary(state, spv::OpBitcast, TypeU32(state),
|
||||
Unary(state, spv::OpConvertFToS, TypeI32(state), clamped))
|
||||
: Unary(state, spv::OpConvertFToU, TypeU32(state), clamped);
|
||||
const auto above = Binary(state, spv::OpFOrdGreaterThanEqual, TypeBool(state), src,
|
||||
ConstantF32(state, signed_value ? 0x4f000000u : 0x4f800000u));
|
||||
const auto high =
|
||||
Select(state, TypeU32(state), above, ConstantU32(state, 0xffffffffu), converted_raw);
|
||||
return Select(state, TypeU32(state), zero, ConstantU32(state, 0), high);
|
||||
Select(state, TypeU32(state), above,
|
||||
ConstantU32(state, signed_value ? 0x7fffffffu : 0xffffffffu), converted);
|
||||
return Select(state, TypeU32(state), Unary(state, spv::OpIsNan, TypeBool(state), src),
|
||||
ConstantU32(state, 0), high);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -122,31 +135,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);
|
||||
const auto pair =
|
||||
ExtractPair(state, EmitExt(state, TypeU32Vector(state, 2), GLSLstd450FindUMsb, {unpacked}));
|
||||
const auto high_found =
|
||||
Binary(state, spv::OpINotEqual, TypeBool(state), pair.high, ConstantU32(state, UINT32_MAX));
|
||||
return Select(state, TypeU32(state), high_found,
|
||||
Binary(state, spv::OpIAdd, TypeU32(state), pair.high, ConstantU32(state, 32)),
|
||||
pair.low);
|
||||
}
|
||||
|
||||
uint32_t EmitConvertU16U32(EmitterState& state, uint32_t arg0) {
|
||||
return EmitNative<spv::OpBitwiseAnd, IR::Type::U16>(state, arg0, ConstantU32(state, 0xffffu));
|
||||
}
|
||||
@@ -210,27 +198,14 @@ 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;
|
||||
return EmitNative<spv::OpBitFieldInsert, IR::Type::U32>(
|
||||
state, EmitF32ToF16RtzBits(state, arg0), EmitF32ToF16RtzBits(state, arg1),
|
||||
ConstantU32(state, 16), ConstantU32(state, 16));
|
||||
}
|
||||
|
||||
uint32_t EmitFPSaturate32(EmitterState& state, uint32_t arg0) {
|
||||
return EmitExt(state, TypeF32(state), GLSLstd450FClamp,
|
||||
// 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)});
|
||||
}
|
||||
|
||||
@@ -242,14 +217,6 @@ uint32_t EmitUMulHi(EmitterState& state, uint32_t arg0, uint32_t arg1) {
|
||||
return EmitMulHigh(state, arg0, arg1, false);
|
||||
}
|
||||
|
||||
uint32_t EmitBitCount64(EmitterState& state, uint32_t arg0) {
|
||||
// Vulkan bit-count operands remain 32-bit, including when counting a U64.
|
||||
const auto unpacked = Unary(state, spv::OpBitcast, TypeU32Vector(state, 2), arg0);
|
||||
const auto pair =
|
||||
ExtractPair(state, Unary(state, spv::OpBitCount, TypeU32Vector(state, 2), unpacked));
|
||||
return Binary(state, spv::OpIAdd, TypeU32(state), pair.low, pair.high);
|
||||
}
|
||||
|
||||
uint32_t EmitSMinTri32(EmitterState& state, uint32_t arg0, uint32_t arg1, uint32_t arg2) {
|
||||
return EmitSMin32(state, arg0, EmitSMin32(state, arg1, arg2));
|
||||
}
|
||||
@@ -326,8 +293,8 @@ uint32_t EmitFPSqrt(EmitterState& state, uint32_t arg0) {
|
||||
}
|
||||
|
||||
uint32_t EmitFPExp2(EmitterState& state, uint32_t arg0) {
|
||||
return EmitExt(state, TypeF32(state), GLSLstd450Exp2,
|
||||
{EmitFlushF32DenormToSignedZero(state, arg0)});
|
||||
// A denormal input already rounds to 1.0, the result for its flushed zero.
|
||||
return EmitExt(state, TypeF32(state), GLSLstd450Exp2, {arg0});
|
||||
}
|
||||
|
||||
uint32_t EmitFPLog2(EmitterState& state, uint32_t arg0) {
|
||||
|
||||
@@ -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,75 +93,88 @@ 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) {
|
||||
// PS5 V_MIN/V_MAX_F32 return the other source for a NaN source (S0 when both are NaN)
|
||||
// and order -0 below +0. An ordered compare takes S1 for a NaN S0 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::OpFOrdGreaterThanEqual : spv::OpFOrdLessThan,
|
||||
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), rhs), lhs,
|
||||
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);
|
||||
if (state.profile.native_float_min_max) {
|
||||
const auto result = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpExtInst, TypeF32(state), result, GlslStd450(state),
|
||||
max_value ? GLSLstd450NMax : GLSLstd450NMin, lhs, rhs);
|
||||
return result;
|
||||
}
|
||||
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);
|
||||
// The NaN path is RDNA2 MIN3, which keeps the later source when two are NaN.
|
||||
const auto min_ba = EmitMinMaxF32(state, b, a, b_bits, a_bits, false);
|
||||
const auto min3 =
|
||||
EmitMinMaxF32(state, c, min_ba, c_bits, EmitBitcastF32ToU32(state, min_ba), 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) {
|
||||
const auto bits = state.builder.AllocateId();
|
||||
const auto abs_bits = state.builder.AllocateId();
|
||||
const auto sign_bits = state.builder.AllocateId();
|
||||
const auto subnormal = state.builder.AllocateId();
|
||||
const auto selected = state.builder.AllocateId();
|
||||
const auto ret = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpBitcast, TypeU32(state), bits, value);
|
||||
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), abs_bits, bits,
|
||||
ConstantU32(state, 0x7fffffffu));
|
||||
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), sign_bits, bits,
|
||||
ConstantU32(state, 0x80000000u));
|
||||
state.builder.AddFunction(spv::OpULessThan, TypeBool(state), subnormal, abs_bits,
|
||||
ConstantU32(state, 0x00800000u));
|
||||
state.builder.AddFunction(spv::OpSelect, TypeU32(state), selected, subnormal, sign_bits, bits);
|
||||
state.builder.AddFunction(spv::OpBitcast, TypeF32(state), ret, selected);
|
||||
return ret;
|
||||
// x * 0 keeps the sign of a denormal; NaN and infinity fail the ordered compare.
|
||||
const auto magnitude = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpExtInst, TypeF32(state), magnitude, GlslStd450(state),
|
||||
GLSLstd450FAbs, value);
|
||||
const auto denormal = Binary(state, spv::OpFOrdLessThan, TypeBool(state), magnitude,
|
||||
ConstantF32(state, 0x00800000u));
|
||||
return Select(state, TypeF32(state), denormal,
|
||||
Binary(state, spv::OpFMul, TypeF32(state), value, ConstantF32(state, 0)), value);
|
||||
}
|
||||
|
||||
uint32_t EmitTrigCycleF32(EmitterState& state, uint32_t src, bool preserve_signed_zero) {
|
||||
const auto fract = state.builder.AllocateId();
|
||||
const auto bits = state.builder.AllocateId();
|
||||
const auto abs_bits = state.builder.AllocateId();
|
||||
const auto large = state.builder.AllocateId();
|
||||
const auto finite = state.builder.AllocateId();
|
||||
const auto large_finite = state.builder.AllocateId();
|
||||
const auto reduced = state.builder.AllocateId();
|
||||
// Finite values of magnitude 2^23 and above are integers, so fract already gives them +0.
|
||||
const auto fract = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpExtInst, TypeF32(state), fract, GlslStd450(state),
|
||||
GLSLstd450Fract, src);
|
||||
state.builder.AddFunction(spv::OpBitcast, TypeU32(state), bits, src);
|
||||
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), abs_bits, bits,
|
||||
ConstantU32(state, 0x7fffffffu));
|
||||
state.builder.AddFunction(spv::OpUGreaterThanEqual, TypeBool(state), large, abs_bits,
|
||||
ConstantU32(state, 0x4b000000u));
|
||||
state.builder.AddFunction(spv::OpULessThan, TypeBool(state), finite, abs_bits,
|
||||
ConstantU32(state, 0x7f800000u));
|
||||
state.builder.AddFunction(spv::OpLogicalAnd, TypeBool(state), large_finite, large, finite);
|
||||
state.builder.AddFunction(spv::OpSelect, TypeF32(state), reduced, large_finite,
|
||||
ConstantF32(state, 0), fract);
|
||||
if (!preserve_signed_zero) {
|
||||
return reduced;
|
||||
return fract;
|
||||
}
|
||||
const auto zero = state.builder.AllocateId();
|
||||
const auto ret = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpIEqual, TypeBool(state), zero, abs_bits,
|
||||
ConstantU32(state, 0));
|
||||
state.builder.AddFunction(spv::OpSelect, TypeF32(state), ret, zero, src, reduced);
|
||||
return ret;
|
||||
const auto abs_bits = Binary(state, spv::OpBitwiseAnd, TypeU32(state),
|
||||
Unary(state, spv::OpBitcast, TypeU32(state), src),
|
||||
ConstantU32(state, 0x7fffffffu));
|
||||
return Select(state, TypeF32(state),
|
||||
Binary(state, spv::OpIEqual, TypeBool(state), abs_bits, ConstantU32(state, 0)),
|
||||
src, fract);
|
||||
}
|
||||
|
||||
uint32_t EmitF16BitsToF32(EmitterState& state, uint32_t bits) {
|
||||
|
||||
@@ -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);
|
||||
@@ -115,51 +114,30 @@ uint32_t EmitBuiltinU32(EmitterState& state, IR::StageInputKind kind, uint32_t c
|
||||
|
||||
uint32_t EmitDppWriteCondition(ValueEmitContext& ctx, const IR::DppMoveFlags& flags,
|
||||
uint32_t exec) {
|
||||
auto& state = ctx.state;
|
||||
const auto lane = EmitSubgroupLocalInvocationId(state);
|
||||
const auto bank_shift = state.builder.AllocateId();
|
||||
const auto row_shift = state.builder.AllocateId();
|
||||
const auto bank = state.builder.AllocateId();
|
||||
const auto row = state.builder.AllocateId();
|
||||
const auto bank_bit = state.builder.AllocateId();
|
||||
const auto row_bit = state.builder.AllocateId();
|
||||
const auto bank_hit = state.builder.AllocateId();
|
||||
const auto row_hit = state.builder.AllocateId();
|
||||
const auto bank_ok = state.builder.AllocateId();
|
||||
const auto row_ok = state.builder.AllocateId();
|
||||
const auto masks_ok = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpShiftRightLogical, TypeU32(state), bank_shift, lane,
|
||||
ConstantU32(state, 2));
|
||||
state.builder.AddFunction(spv::OpShiftRightLogical, TypeU32(state), row_shift, lane,
|
||||
ConstantU32(state, 4));
|
||||
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), bank, bank_shift,
|
||||
ConstantU32(state, 3));
|
||||
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), row, row_shift,
|
||||
ConstantU32(state, 3));
|
||||
state.builder.AddFunction(spv::OpShiftLeftLogical, TypeU32(state), bank_bit,
|
||||
ConstantU32(state, 1), bank);
|
||||
state.builder.AddFunction(spv::OpShiftLeftLogical, TypeU32(state), row_bit,
|
||||
ConstantU32(state, 1), row);
|
||||
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), bank_hit,
|
||||
ConstantU32(state, flags.bank_mask), bank_bit);
|
||||
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), row_hit,
|
||||
ConstantU32(state, flags.row_mask), row_bit);
|
||||
state.builder.AddFunction(spv::OpINotEqual, TypeBool(state), bank_ok, bank_hit,
|
||||
ConstantU32(state, 0));
|
||||
state.builder.AddFunction(spv::OpINotEqual, TypeBool(state), row_ok, row_hit,
|
||||
ConstantU32(state, 0));
|
||||
state.builder.AddFunction(spv::OpLogicalAnd, TypeBool(state), masks_ok, bank_ok, row_ok);
|
||||
uint32_t writable = masks_ok;
|
||||
if (!flags.bound_control) {
|
||||
const auto target = EmitDppTargetLane(state, flags);
|
||||
const auto bounded = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpLogicalAnd, TypeBool(state), bounded, writable,
|
||||
target.valid);
|
||||
writable = bounded;
|
||||
auto& state = ctx.state;
|
||||
uint32_t writable = exec;
|
||||
uint32_t lane = 0;
|
||||
// Each mask bit enables writes to one bank (4 lanes) or row (16 lanes); a full mask enables all.
|
||||
const auto enable = [&](uint32_t mask, uint32_t group_shift) {
|
||||
if (mask == 0xfu) return;
|
||||
lane = lane != 0 ? lane : EmitSubgroupLocalInvocationId(state);
|
||||
const auto group = EmitNative<spv::OpBitFieldUExtract, IR::Type::U32>(
|
||||
state, lane, ConstantU32(state, group_shift), ConstantU32(state, 2));
|
||||
const auto bit = EmitNative<spv::OpBitFieldUExtract, IR::Type::U32>(
|
||||
state, ConstantU32(state, mask), group, ConstantU32(state, 1));
|
||||
writable = Binary(state, spv::OpLogicalAnd, TypeBool(state), writable,
|
||||
Binary(state, spv::OpINotEqual, TypeBool(state), bit, ConstantU32(state, 0)));
|
||||
};
|
||||
enable(flags.bank_mask, 2u);
|
||||
enable(flags.row_mask, 4u);
|
||||
// Only row shifts (0x101-0x10f, 0x111-0x11f) can vacate a lane; other controls always read one.
|
||||
const bool row_shift = !flags.dpp8 && flags.control > 0x100u && flags.control < 0x120u &&
|
||||
flags.control != 0x110u;
|
||||
if (!flags.bound_control && row_shift) {
|
||||
writable = Binary(state, spv::OpLogicalAnd, TypeBool(state), writable,
|
||||
EmitDppTargetLane(state, flags).valid);
|
||||
}
|
||||
const auto result = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpLogicalAnd, TypeBool(state), result, exec, writable);
|
||||
return result;
|
||||
return writable;
|
||||
}
|
||||
|
||||
uint32_t EmitAttribute(EmitterState& state, uint32_t attr, uint32_t chan) {
|
||||
@@ -350,6 +328,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 +400,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 +597,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 +612,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 +620,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 +656,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) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user