mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-29 00:33:06 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7974dc763 | ||
|
|
b899717900 | ||
|
|
d58aefde49 | ||
|
|
4c6be6cd1d | ||
|
|
584c2e8ca6 | ||
|
|
717b50155a | ||
|
|
57b161019d |
No files matched your search
@@ -407,12 +407,12 @@ jobs:
|
||||
- name: Verify artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
file _Build/linux/install/launcher
|
||||
file _Build/linux/install/kyty_launcher
|
||||
file _Build/linux/install/kyty_emulator
|
||||
test -x _Build/linux/install/kyty_updater
|
||||
file _Build/linux/install/kyty_emulator | grep -q "ELF 64-bit LSB .*x86-64"
|
||||
ldd _Build/linux/install/kyty_emulator > /dev/null
|
||||
readelf -d _Build/linux/install/launcher | grep -q 'RPATH.*\$ORIGIN/lib'
|
||||
readelf -d _Build/linux/install/kyty_launcher | grep -q 'RPATH.*\$ORIGIN/lib'
|
||||
while IFS= read -r -d '' binary; do
|
||||
while read -r dependency; do
|
||||
test -e "_Build/linux/install/lib/$dependency"
|
||||
@@ -421,8 +421,8 @@ jobs:
|
||||
sed -n 's/.*Shared library: \[\(libQt6[^]]*\|libicu[^]]*\)\].*/\1/p'
|
||||
)
|
||||
done < <(
|
||||
find _Build/linux/install/launcher _Build/linux/install/plugins \
|
||||
-type f \( -name launcher -o -name '*.so' \) -print0
|
||||
find _Build/linux/install/kyty_launcher _Build/linux/install/plugins \
|
||||
-type f \( -name kyty_launcher -o -name '*.so' \) -print0
|
||||
)
|
||||
_Build/linux/install/kyty_emulator --help > /dev/null
|
||||
|
||||
@@ -497,7 +497,7 @@ jobs:
|
||||
linux_dir="artifacts/KytyPS5-Linux-x86_64"
|
||||
test -d "$linux_dir"
|
||||
chmod a+x \
|
||||
"$linux_dir/launcher" \
|
||||
"$linux_dir/kyty_launcher" \
|
||||
"$linux_dir/kyty_emulator" \
|
||||
"$linux_dir/kyty_updater"
|
||||
tar -C "$linux_dir" -czf \
|
||||
|
||||
Vendored
+2
-2
@@ -5,7 +5,7 @@
|
||||
"name": "Debug launcher UI",
|
||||
"type": "cppvsdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}\\_Build\\vscode-clang\\launcher\\launcher.exe",
|
||||
"program": "${workspaceFolder}\\_Build\\vscode-clang\\launcher\\kyty_launcher.exe",
|
||||
"args": [],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}\\_Build\\vscode-clang\\launcher",
|
||||
@@ -13,7 +13,7 @@
|
||||
"externalConsole": false,
|
||||
"linux": {
|
||||
"type": "cppdbg",
|
||||
"program": "${workspaceFolder}/_Build/vscode-clang/launcher/launcher",
|
||||
"program": "${workspaceFolder}/_Build/vscode-clang/launcher/kyty_launcher",
|
||||
"cwd": "${workspaceFolder}/_Build/vscode-clang/launcher",
|
||||
"MIMode": "gdb",
|
||||
"miDebuggerPath": "/usr/bin/gdb"
|
||||
|
||||
@@ -275,11 +275,11 @@ From the repository root, follow the launcher configure, build and install steps
|
||||
[Building on Linux](#building-on-linux), omitting `-DCMAKE_PREFIX_PATH="$Qt6_DIR"`; CMake finds
|
||||
Fedora's packaged Qt 6 automatically.
|
||||
|
||||
Run `./_Build/linux/install/launcher` on regular Fedora or inside the container. To launch a
|
||||
Run `./_Build/linux/install/kyty_launcher` on regular Fedora or inside the container. To launch a
|
||||
container build from the host, leave the container with `exit`, then run from the repository root:
|
||||
|
||||
```bash
|
||||
distrobox enter kyty-build -- ./_Build/linux/install/launcher
|
||||
distrobox enter kyty-build -- ./_Build/linux/install/kyty_launcher
|
||||
```
|
||||
|
||||
Qt libraries and plugins are bundled. You can also launch directly on a Fedora Atomic host
|
||||
@@ -345,7 +345,7 @@ Use `_Build/linux` instead of `_Build/windows` for a Linux build.
|
||||
|
||||
A ready-made Visual Studio Code setup is included in [`.vscode`](.vscode). It configures CMake
|
||||
Tools to build the project with Ninja and `clang-cl` and provides launch profiles for both
|
||||
`launcher.exe` and `kyty_emulator.exe`. It is Windows-only: VS Code settings cannot select a
|
||||
`kyty_launcher.exe` and `kyty_emulator.exe`. It is Windows-only: VS Code settings cannot select a
|
||||
compiler per platform, so on Linux configure from the command line as shown above.
|
||||
|
||||
Before using it:
|
||||
@@ -365,11 +365,11 @@ Update your graphics driver before reporting rendering problems.
|
||||
To use the graphical launcher:
|
||||
|
||||
```powershell
|
||||
.\_Build\windows\install\launcher.exe
|
||||
.\_Build\windows\install\kyty_launcher.exe
|
||||
```
|
||||
|
||||
```bash
|
||||
./_Build/linux/install/launcher
|
||||
./_Build/linux/install/kyty_launcher
|
||||
```
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "تم كسب الكأس!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofej získána!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofæ optjent!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophäe erhalten!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Κερδίσατε ένα τρόπαιο!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophy earned!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophy earned!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "¡Ganaste un trofeo!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "¡Ganaste un trofeo!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Pokaali ansaittu!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophée obtenu !"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophée obtenu !"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trófea megszerezve!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofi diperoleh!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofeo ottenuto!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "トロフィーを獲得しました!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "트로피를 획득했습니다!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofee behaald!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofé opptjent!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Zdobyto trofeum!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Troféu conquistado!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Troféu conquistado!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofeu câștigat!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Трофей получен!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofé intjänad!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "ได้รับถ้วยรางวัลแล้ว!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Kupa kazanıldı!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Đã nhận cúp!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "已获得奖杯!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "已獲得獎盃!"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# Notification localization
|
||||
|
||||
Trophy notification subtitles use the configured console language. Trophy names still come
|
||||
from the game's metadata. The launcher trophy dialogs are unchanged.
|
||||
|
||||
Edit `assets/localization/<locale>.json`, using flat UTF-8 string values:
|
||||
|
||||
```json
|
||||
{
|
||||
"trophy.toast.earned": "Trophy earned!"
|
||||
}
|
||||
```
|
||||
|
||||
CMake converts the catalogs into C++ byte arrays in the build directory. The emulator parses
|
||||
the embedded catalogs once and falls back to `en-US.json` for missing translations. No language
|
||||
files or translation service are needed at runtime. Rebuild after editing a catalog.
|
||||
|
||||
For local testing, rebuild `kyty_emulator`, choose a game's Console Language in the launcher,
|
||||
and unlock a trophy with notifications enabled. Installed system fonts provide non-Latin
|
||||
glyphs; the selected language needs an appropriate font on the host.
|
||||
@@ -12,9 +12,23 @@ endif()
|
||||
|
||||
add_library(common STATIC ${common_src} ${common_headers})
|
||||
|
||||
set(localization_dir "${PROJECT_SOURCE_DIR}/assets/localization")
|
||||
file(GLOB localization_catalogs CONFIGURE_DEPENDS "${localization_dir}/*.json")
|
||||
set(localization_data "${CMAKE_CURRENT_BINARY_DIR}/localizationData.h")
|
||||
add_custom_command(
|
||||
OUTPUT "${localization_data}"
|
||||
COMMAND ${CMAKE_COMMAND} "-DINPUTS=${localization_catalogs}" "-DOUTPUT=${localization_data}"
|
||||
-P "${KYTY_SOURCE_DIR}/embed_localization.cmake"
|
||||
DEPENDS ${localization_catalogs} "${KYTY_SOURCE_DIR}/embed_localization.cmake"
|
||||
COMMENT "Embed notification localization catalogs"
|
||||
VERBATIM
|
||||
)
|
||||
target_sources(common PRIVATE "${localization_data}")
|
||||
|
||||
target_include_directories(common
|
||||
PUBLIC ${KYTY_SOURCE_DIR}
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
${KYTY_THIRD_PARTY_DIR}/cpuinfo/include
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "common/localization.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
#include "localizationData.h"
|
||||
|
||||
namespace Common::Localization {
|
||||
namespace {
|
||||
|
||||
constexpr std::array<std::string_view, 30> Locales = {
|
||||
"ja-JP", "en-US", "fr-FR", "es-ES", "de-DE", "it-IT", "nl-NL", "pt-PT",
|
||||
"ru-RU", "ko-KR", "zh-Hant", "zh-Hans", "fi-FI", "sv-SE", "da-DK", "no-NO",
|
||||
"pl-PL", "pt-BR", "en-GB", "tr-TR", "es-419", "ar-AE", "fr-CA", "cs-CZ",
|
||||
"hu-HU", "el-GR", "ro-RO", "th-TH", "vi-VN", "id-ID"};
|
||||
|
||||
size_t LanguageIndex(int console_language) {
|
||||
return console_language >= 0 && console_language < static_cast<int>(Locales.size())
|
||||
? static_cast<size_t>(console_language)
|
||||
: 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string_view LocaleName(int console_language) {
|
||||
return Locales[LanguageIndex(console_language)];
|
||||
}
|
||||
|
||||
std::string_view Get(int console_language, std::string_view key) {
|
||||
static const auto catalogs = [] {
|
||||
std::array<nlohmann::json, Locales.size()> result;
|
||||
for (size_t i = 0; i < Locales.size(); ++i) {
|
||||
const auto bytes = EmbeddedCatalog(Locales[i]);
|
||||
result[i] = nlohmann::json::parse(bytes.begin(), bytes.end(), nullptr, false);
|
||||
}
|
||||
return result;
|
||||
}();
|
||||
for (const auto index: std::array {LanguageIndex(console_language), size_t {1}}) {
|
||||
const auto& catalog = catalogs[index];
|
||||
const auto found = catalog.find(key);
|
||||
if (found != catalog.end() && found->is_string()) {
|
||||
const auto& text = found->get_ref<const std::string&>();
|
||||
if (!text.empty()) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
} // namespace Common::Localization
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace Common::Localization {
|
||||
|
||||
// Console language indices follow the system configuration; invalid indices use English.
|
||||
[[nodiscard]] std::string_view LocaleName(int console_language);
|
||||
|
||||
// Translations have static lifetime. Missing translations use English, then the supplied key.
|
||||
[[nodiscard]] std::string_view Get(int console_language, std::string_view key);
|
||||
|
||||
} // namespace Common::Localization
|
||||
@@ -1,9 +1,9 @@
|
||||
#include "common/trophies.h"
|
||||
|
||||
#include "common/file.h"
|
||||
#include "common/localization.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <fmt/format.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
@@ -105,13 +105,7 @@ Package ParsePackage(std::span<const std::byte> data, int console_language) {
|
||||
!conf["defaultLanguage"].is_string()) {
|
||||
return {};
|
||||
}
|
||||
static constexpr std::array<std::string_view, 30> locales = {
|
||||
"ja-JP", "en-US", "fr-FR", "es-ES", "de-DE", "it-IT", "nl-NL", "pt-PT",
|
||||
"ru-RU", "ko-KR", "zh-Hant", "zh-Hans", "fi-FI", "sv-SE", "da-DK", "no-NO",
|
||||
"pl-PL", "pt-BR", "en-GB", "tr-TR", "es-419", "ar-AE", "fr-CA", "cs-CZ",
|
||||
"hu-HU", "el-GR", "ro-RO", "th-TH", "vi-VN", "id-ID"};
|
||||
const auto locale =
|
||||
locales[console_language >= 0 && console_language < locales.size() ? console_language : 1];
|
||||
const auto locale = Localization::LocaleName(console_language);
|
||||
auto meta = ReadJson(files, fmt::format("tropmeta_{}.json", locale));
|
||||
if (meta.is_null()) {
|
||||
meta = ReadJson(files, fmt::format("tropmeta_{}.json",
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
if(NOT DEFINED INPUTS OR NOT DEFINED OUTPUT)
|
||||
message(FATAL_ERROR "embed_localization.cmake requires INPUTS and OUTPUT")
|
||||
endif()
|
||||
|
||||
set(contents "// Generated from assets/localization; do not edit.\n#pragma once\n#include <span>\n#include <string_view>\n\nnamespace {\n")
|
||||
set(lookup "std::span<const unsigned char> EmbeddedCatalog(std::string_view locale) {\n")
|
||||
set(continuation "[89ab][0-9a-f]")
|
||||
string(CONCAT utf8_character
|
||||
"[0-7][0-9a-f]|(c[2-9a-f]|d[0-9a-f])${continuation}"
|
||||
"|e0[ab][0-9a-f]${continuation}|e[1-9abcef]${continuation}${continuation}"
|
||||
"|ed[89][0-9a-f]${continuation}|f0[9ab][0-9a-f]${continuation}${continuation}"
|
||||
"|f[123]${continuation}${continuation}${continuation}|f48[0-9a-f]${continuation}${continuation}")
|
||||
foreach(catalog IN LISTS INPUTS)
|
||||
file(READ "${catalog}" bytes HEX)
|
||||
# Validate UTF-8, excluding overlong sequences, surrogates, and values above U+10FFFF.
|
||||
if(NOT bytes MATCHES "^(${utf8_character})*$")
|
||||
message(FATAL_ERROR "Localization must be UTF-8: ${catalog}")
|
||||
endif()
|
||||
file(READ "${catalog}" json)
|
||||
string(JSON type ERROR_VARIABLE error TYPE "${json}")
|
||||
if(error OR NOT type STREQUAL "OBJECT")
|
||||
message(FATAL_ERROR "Invalid localization object in ${catalog}: ${error}")
|
||||
endif()
|
||||
string(JSON count LENGTH "${json}")
|
||||
if(count GREATER 0)
|
||||
math(EXPR last "${count} - 1")
|
||||
foreach(index RANGE ${last})
|
||||
string(JSON key MEMBER "${json}" ${index})
|
||||
string(JSON type TYPE "${json}" "${key}")
|
||||
if(NOT type STREQUAL "STRING")
|
||||
message(FATAL_ERROR "Localization value '${key}' must be a string: ${catalog}")
|
||||
endif()
|
||||
string(JSON text GET "${json}" "${key}")
|
||||
if(key STREQUAL "" OR text STREQUAL "")
|
||||
message(FATAL_ERROR "Localization keys and values must not be empty: ${catalog}")
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
get_filename_component(locale "${catalog}" NAME_WE)
|
||||
if(locale STREQUAL "en-US")
|
||||
string(JSON earned ERROR_VARIABLE error GET "${json}" "trophy.toast.earned")
|
||||
if(error OR earned STREQUAL "")
|
||||
message(FATAL_ERROR "English localization requires trophy.toast.earned: ${catalog}")
|
||||
endif()
|
||||
set(english_found TRUE)
|
||||
endif()
|
||||
string(MAKE_C_IDENTIFIER "${locale}" symbol)
|
||||
string(REGEX REPLACE "(..)" "0x\\1," bytes "${bytes}")
|
||||
string(APPEND contents "constexpr unsigned char catalog_${symbol}[] = {${bytes}};\n")
|
||||
string(APPEND lookup "\tif (locale == \"${locale}\") return catalog_${symbol};\n")
|
||||
endforeach()
|
||||
if(NOT english_found)
|
||||
message(FATAL_ERROR "Localization requires an en-US.json fallback")
|
||||
endif()
|
||||
string(APPEND contents "\n${lookup}\treturn {};\n}\n} // namespace\n")
|
||||
|
||||
get_filename_component(output_dir "${OUTPUT}" DIRECTORY)
|
||||
file(MAKE_DIRECTORY "${output_dir}")
|
||||
file(WRITE "${OUTPUT}.tmp" "${contents}")
|
||||
execute_process(COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${OUTPUT}.tmp" "${OUTPUT}"
|
||||
COMMAND_ERROR_IS_FATAL ANY)
|
||||
file(REMOVE "${OUTPUT}.tmp")
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -126,6 +126,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);
|
||||
@@ -685,6 +686,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 {}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include "graphics/shader/recompiler/ShaderRecompiler.h"
|
||||
#include "graphics/shader/shaderCompiler.h"
|
||||
#include "kernel/memory.h"
|
||||
#include "kytyGitVersion.h"
|
||||
#include "loader/systemContent.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -81,7 +80,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);
|
||||
}
|
||||
|
||||
@@ -516,17 +516,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 +548,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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -536,7 +536,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)) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "common/assert.h"
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/file.h"
|
||||
#include "common/localization.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/stringUtils.h"
|
||||
#include "gpu_blit_shaders/gpu_imgui_hdr_spv.h"
|
||||
@@ -30,6 +31,7 @@
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <fmt/format.h>
|
||||
#include <initializer_list>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -280,6 +282,61 @@ std::vector<std::string_view> WrapTrophyTitle(std::string_view text, float font_
|
||||
}
|
||||
}
|
||||
|
||||
// ImGui's built-in font covers Latin only. Merge installed fonts for the selected console language.
|
||||
void AddSystemFontFallbacks(ImGuiIO& io) {
|
||||
io.Fonts->AddFontDefault();
|
||||
const auto add = [&](std::initializer_list<const char*> paths) {
|
||||
for (const char* path: paths) {
|
||||
std::error_code error;
|
||||
if (std::filesystem::is_regular_file(path, error)) {
|
||||
ImFontConfig config;
|
||||
config.MergeMode = true;
|
||||
config.Flags = ImFontFlags_NoLoadError;
|
||||
if (io.Fonts->AddFontFromFileTTF(path, 0.0f, &config) != nullptr) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
bool found =
|
||||
add({"C:/Windows/Fonts/segoeui.ttf", "/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
|
||||
"/Library/Fonts/Arial Unicode.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/TTF/DejaVuSans.ttf", "/usr/share/fonts/noto/NotoSans-Regular.ttf"});
|
||||
const auto locale = Common::Localization::LocaleName(Config::GetConsoleLanguage());
|
||||
const char* noto_cjk[] = {"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
||||
"/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
|
||||
"/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc"};
|
||||
if (locale == "ja-JP") {
|
||||
found = add({"C:/Windows/Fonts/meiryo.ttc", "C:/Windows/Fonts/YuGothM.ttc",
|
||||
"/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc", noto_cjk[0], noto_cjk[1],
|
||||
noto_cjk[2]});
|
||||
} else if (locale == "ko-KR") {
|
||||
found = add({"C:/Windows/Fonts/malgun.ttf", "/System/Library/Fonts/AppleSDGothicNeo.ttc",
|
||||
noto_cjk[0], noto_cjk[1], noto_cjk[2]});
|
||||
} else if (locale == "zh-Hans" || locale == "zh-Hant") {
|
||||
found =
|
||||
add({locale == "zh-Hans" ? "C:/Windows/Fonts/msyh.ttc" : "C:/Windows/Fonts/msjh.ttc",
|
||||
"/System/Library/Fonts/PingFang.ttc", noto_cjk[0], noto_cjk[1], noto_cjk[2]});
|
||||
} else if (locale == "th-TH") {
|
||||
found = add({"C:/Windows/Fonts/leelawui.ttf", "C:/Windows/Fonts/tahoma.ttf",
|
||||
"/System/Library/Fonts/Supplemental/Ayuthaya.ttf",
|
||||
"/usr/share/fonts/truetype/tlwg/Garuda.ttf",
|
||||
"/usr/share/fonts/truetype/noto/NotoSansThai-Regular.ttf",
|
||||
"/usr/share/fonts/noto/NotoSansThai-Regular.ttf"});
|
||||
} else if (locale == "ar-AE") {
|
||||
found = add({"C:/Windows/Fonts/tahoma.ttf", "/System/Library/Fonts/Supplemental/Arial.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/TTF/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/truetype/noto/NotoSansArabic-Regular.ttf",
|
||||
"/usr/share/fonts/noto/NotoSansArabic-Regular.ttf"});
|
||||
}
|
||||
if (!found) {
|
||||
LOGF("[Trophy] no installed fallback font for %.*s\n", static_cast<int>(locale.size()),
|
||||
locale.data());
|
||||
}
|
||||
}
|
||||
|
||||
void ClearInputEvents() {
|
||||
std::scoped_lock lock(g_input_mutex);
|
||||
g_input_events.clear();
|
||||
@@ -441,6 +498,63 @@ std::u16string Utf8ToUtf16(std::string_view text) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// The Arabic notification is a single RTL run. Use presentation forms because ImGui does not
|
||||
// shape Arabic. This intentionally applies only to the catalog subtitle, never game-provided names.
|
||||
std::string PrepareTrophySubtitle(int language) {
|
||||
const auto text = Common::Localization::Get(language, "trophy.toast.earned");
|
||||
if (Common::Localization::LocaleName(language) != "ar-AE") {
|
||||
return std::string(text);
|
||||
}
|
||||
struct ArabicForm {
|
||||
char16_t letter;
|
||||
char16_t isolated;
|
||||
bool joins_next;
|
||||
};
|
||||
static constexpr ArabicForm FORMS[] = {
|
||||
{0x0621, 0xfe80, false}, {0x0622, 0xfe81, false}, {0x0623, 0xfe83, false},
|
||||
{0x0624, 0xfe85, false}, {0x0625, 0xfe87, false}, {0x0626, 0xfe89, true},
|
||||
{0x0627, 0xfe8d, false}, {0x0628, 0xfe8f, true}, {0x0629, 0xfe93, false},
|
||||
{0x062a, 0xfe95, true}, {0x062b, 0xfe99, true}, {0x062c, 0xfe9d, true},
|
||||
{0x062d, 0xfea1, true}, {0x062e, 0xfea5, true}, {0x062f, 0xfea9, false},
|
||||
{0x0630, 0xfeab, false}, {0x0631, 0xfead, false}, {0x0632, 0xfeaf, false},
|
||||
{0x0633, 0xfeb1, true}, {0x0634, 0xfeb5, true}, {0x0635, 0xfeb9, true},
|
||||
{0x0636, 0xfebd, true}, {0x0637, 0xfec1, true}, {0x0638, 0xfec5, true},
|
||||
{0x0639, 0xfec9, true}, {0x063a, 0xfecd, true}, {0x0641, 0xfed1, true},
|
||||
{0x0642, 0xfed5, true}, {0x0643, 0xfed9, true}, {0x0644, 0xfedd, true},
|
||||
{0x0645, 0xfee1, true}, {0x0646, 0xfee5, true}, {0x0647, 0xfee9, true},
|
||||
{0x0648, 0xfeed, false}, {0x0649, 0xfeef, false}, {0x064a, 0xfef1, true},
|
||||
};
|
||||
const auto form = [](char16_t c) -> const ArabicForm* {
|
||||
for (const auto& value: FORMS) {
|
||||
if (value.letter == c) {
|
||||
return &value;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
const auto logical = Utf8ToUtf16(text);
|
||||
if (std::none_of(logical.begin(), logical.end(),
|
||||
[](char16_t c) { return c >= 0x0600 && c <= 0x06ff; })) {
|
||||
return std::string(text);
|
||||
}
|
||||
auto visual = logical;
|
||||
for (size_t i = 0; i < logical.size(); ++i) {
|
||||
const auto* current = form(logical[i]);
|
||||
if (current == nullptr) {
|
||||
continue;
|
||||
}
|
||||
const auto* previous = i > 0 ? form(logical[i - 1]) : nullptr;
|
||||
const auto* next = i + 1 < logical.size() ? form(logical[i + 1]) : nullptr;
|
||||
const bool joins_previous =
|
||||
previous != nullptr && previous->joins_next && current->letter != 0x0621;
|
||||
const bool joins_next = current->joins_next && next != nullptr && next->letter != 0x0621;
|
||||
visual[i] = static_cast<char16_t>(current->isolated + (joins_next ? 2 : 0) +
|
||||
(joins_previous ? 1 : 0));
|
||||
}
|
||||
std::reverse(visual.begin(), visual.end());
|
||||
return Common::Utf16ToUtf8(visual);
|
||||
}
|
||||
|
||||
std::string VisibleText(const Ime::HostSnapshot& snapshot, size_t max_units) {
|
||||
const size_t cursor = std::min<size_t>(snapshot.cursor, snapshot.text.size());
|
||||
const size_t payload_units = max_units > 4 ? max_units - 4 : 1;
|
||||
@@ -794,6 +908,7 @@ struct SystemOverlay::Impl {
|
||||
io.ConfigNavCursorVisibleAlways = true;
|
||||
io.BackendFlags |= ImGuiBackendFlags_HasGamepad;
|
||||
io.BackendPlatformName = "Kyty system overlay input";
|
||||
AddSystemFontFallbacks(io);
|
||||
ImGui::StyleColorsDark();
|
||||
auto& style = ImGui::GetStyle();
|
||||
style.WindowRounding = 10.0f;
|
||||
@@ -1578,8 +1693,9 @@ struct SystemOverlay::Impl {
|
||||
trophy_notification = std::move(g_trophy_notifications.front());
|
||||
g_trophy_notifications.pop_front();
|
||||
}
|
||||
trophy_notification_born = now;
|
||||
trophy_notification_image = LoadTrophyIcon(trophy_notification->icon_png);
|
||||
trophy_notification_subtitle = PrepareTrophySubtitle(Config::GetConsoleLanguage());
|
||||
trophy_notification_born = now;
|
||||
trophy_notification_image = LoadTrophyIcon(trophy_notification->icon_png);
|
||||
PlayTrophySound();
|
||||
}
|
||||
const float age = std::chrono::duration<float>(now - trophy_notification_born).count();
|
||||
@@ -1663,8 +1779,17 @@ struct SystemOverlay::Impl {
|
||||
line.data(), line.data() + line.size());
|
||||
line_y += line_height;
|
||||
}
|
||||
draw->AddText(nullptr, 20.0f * screen_scale, {text_left, line_y + 8.0f * screen_scale},
|
||||
fade(176, 180, 192, 255), "Trophy earned!");
|
||||
const float subtitle_size = 20.0f * screen_scale;
|
||||
const float subtitle_width =
|
||||
ImGui::GetFont()
|
||||
->CalcTextSizeA(subtitle_size, std::numeric_limits<float>::max(), 0.0f,
|
||||
trophy_notification_subtitle.c_str())
|
||||
.x;
|
||||
const float available_width = std::max(1.0f, width - text_offset - text_padding);
|
||||
const float fitted_size = std::max(
|
||||
1.0f, subtitle_size * std::min(1.0f, available_width / std::max(1.0f, subtitle_width)));
|
||||
draw->AddText(nullptr, fitted_size, {text_left, line_y + 8.0f * screen_scale},
|
||||
fade(176, 180, 192, 255), trophy_notification_subtitle.c_str());
|
||||
draw->PopClipRect();
|
||||
}
|
||||
|
||||
@@ -1782,6 +1907,7 @@ struct SystemOverlay::Impl {
|
||||
static constexpr std::size_t MAX_TOUCH_PARTICLES = 64;
|
||||
static constexpr std::size_t MAX_TOUCH_TRAIL_SEGMENTS = 128;
|
||||
std::optional<TrophyNotification> trophy_notification;
|
||||
std::string trophy_notification_subtitle;
|
||||
Clock::time_point trophy_notification_born;
|
||||
std::vector<std::unique_ptr<ImTextureData>> retired_trophy_images;
|
||||
std::array<std::unique_ptr<DdsImage>, 2> splash_dds;
|
||||
|
||||
@@ -945,7 +945,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 +952,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 +1036,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 && 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();
|
||||
|
||||
@@ -81,8 +81,7 @@ void ValidateNativeProgram(const IR::Program& program, bool lds_storage) {
|
||||
}
|
||||
uses_gds |= memory.kind == IR::ResourceKind::Gds;
|
||||
uses_lds |= memory.kind == IR::ResourceKind::Lds;
|
||||
} else if (memory.kind == IR::ResourceKind::Buffer ||
|
||||
memory.kind == IR::ResourceKind::ScalarBuffer) {
|
||||
} else if (memory.kind == IR::ResourceKind::Buffer) {
|
||||
if (memory.resource >= program.info.buffers.size()) {
|
||||
Fail(program, "typed shader contains an invalid buffer resource");
|
||||
}
|
||||
|
||||
@@ -52,9 +52,12 @@ uint32_t EmitBuiltinU32(EmitterState& state, IR::StageInputKind kind, uint32_t c
|
||||
const auto value = state.builder.AllocateId();
|
||||
const auto bits = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpLoad, TypeBool(state), value, variable);
|
||||
// PS5 initializes v_front_face with float +1.0/-1.0 bits.
|
||||
state.builder.AddFunction(spv::OpSelect, TypeU32(state), bits, value,
|
||||
ConstantU32(state, 0x3f800000u), ConstantU32(state, 0xbf800000u));
|
||||
// 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 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);
|
||||
return bits;
|
||||
}
|
||||
if (kind == IR::StageInputKind::VertexIndex || kind == IR::StageInputKind::InstanceIndex ||
|
||||
|
||||
@@ -867,7 +867,7 @@ void StoreFormattedPrepared(ValueEmitContext& ctx, const IR::Inst& inst,
|
||||
});
|
||||
}
|
||||
|
||||
uint32_t LoadIndirectScalarBuffer(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
uint32_t LoadScalarBuffer(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
auto& state = ctx.state;
|
||||
const auto& handle = *inst.Arg(0).ResolveInstruction();
|
||||
const auto word1 = ctx.Arg(handle, 1);
|
||||
@@ -1545,29 +1545,8 @@ uint32_t EmitReadConst(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
}
|
||||
|
||||
void EmitReadConstBuffer(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
auto mem = ctx.Memory(inst);
|
||||
if (mem.planning_only) return;
|
||||
if (mem.kind == IR::ResourceKind::IndirectBuffer) {
|
||||
ctx.Define(inst, LoadIndirectScalarBuffer(ctx, inst));
|
||||
return;
|
||||
}
|
||||
auto& state = ctx.state;
|
||||
mem.kind = IR::ResourceKind::ScalarBuffer;
|
||||
auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), ctx.Arg(inst, 1),
|
||||
ConstantU32(state, 2));
|
||||
if (mem.offset >= 4u) {
|
||||
index = Binary(state, spv::OpIAdd, TypeU32(state), index, ConstantU32(state, mem.offset >> 2u));
|
||||
}
|
||||
const auto access = PrepareMemoryResourceAccess(state, mem);
|
||||
// Scalar buffer addressing aligns the base and each offset independently.
|
||||
const auto element = Binary(
|
||||
state, spv::OpIAdd, TypeU32(state), index,
|
||||
Binary(state, spv::OpShiftRightLogical, TypeU32(state), access.byte_offset, ConstantU32(state, 2)));
|
||||
const auto condition = EmitMemoryElementInBounds(state, access, element);
|
||||
ctx.Define(inst, EmitValueOrZeroIfCondition(state, condition, [&]() {
|
||||
return EmitNative<spv::OpLoad, IR::Type::U32>(
|
||||
state, EmitMemoryElementPointer(state, access, element));
|
||||
}));
|
||||
if (ctx.Memory(inst).planning_only) return;
|
||||
ctx.Define(inst, LoadScalarBuffer(ctx, inst));
|
||||
}
|
||||
|
||||
uint32_t EmitGetBufferResourceIndex(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
|
||||
@@ -269,10 +269,8 @@ MemoryResourceAccess PrepareMemoryResourceAccess(EmitterState& state, const IR::
|
||||
case IR::ResourceKind::Flat:
|
||||
case IR::ResourceKind::Global:
|
||||
EXIT("physical address memory must use the BDA emitter\n");
|
||||
case IR::ResourceKind::ScalarBuffer:
|
||||
case IR::ResourceKind::Buffer: {
|
||||
const auto bits = mem.kind == IR::ResourceKind::Buffer
|
||||
? IR::StorageBufferElementBits(state.program, mem) : 32u;
|
||||
const auto bits = IR::StorageBufferElementBits(state.program, mem);
|
||||
auto* buffer = &state.storage_buffers[2];
|
||||
if (bits == 8u) {
|
||||
buffer = &state.storage_buffers[0];
|
||||
|
||||
@@ -128,7 +128,6 @@ struct BufferResource {
|
||||
bool written = false;
|
||||
bool atomic = false;
|
||||
bool formatted = false;
|
||||
bool scalar = false;
|
||||
bool live = false;
|
||||
uint32_t indirect_root = NoIndirectBuffer;
|
||||
uint32_t indirect_mapping_offset = 0;
|
||||
|
||||
@@ -608,13 +608,23 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
||||
ReplaceBinaryIdentity(inst, Type::U32, UINT32_MAX)) return;
|
||||
for (uint32_t arm = 0; arm < 2u; ++arm) {
|
||||
const auto mask = Arg(inst, arm);
|
||||
if (!IsImmediate(mask, Type::U32)) continue;
|
||||
if (mask.U32() == 0u) {
|
||||
Replace(inst, Value(0u));
|
||||
return;
|
||||
}
|
||||
const auto* shifted = Arg(inst, arm ^ 1u).TryInstruction();
|
||||
if (!IsImmediate(mask, Type::U32) || shifted == nullptr ||
|
||||
shifted->GetOpcode() != ValueOpcode::ShiftLeftLogical32 || !HasOneUse(*shifted)) continue;
|
||||
if (shifted == nullptr || shifted->GetOpcode() != ValueOpcode::ShiftLeftLogical32) continue;
|
||||
const auto shift = Arg(*shifted, 1);
|
||||
if (!IsImmediate(shift, Type::U32) || shift.U32() >= 32u) continue;
|
||||
if (!IsImmediate(shift, Type::U32)) continue;
|
||||
const auto shifted_mask = mask.U32() >> (shift.U32() & 31u);
|
||||
if (shifted_mask == 0u) {
|
||||
Replace(inst, Value(0u));
|
||||
return;
|
||||
}
|
||||
if (!HasOneUse(*shifted) || shift.U32() >= 32u) continue;
|
||||
const auto masked = worklist.Prepend(block, instruction, ValueOpcode::BitwiseAnd32,
|
||||
{Arg(*shifted, 0), Value(mask.U32() >> shift.U32())});
|
||||
{Arg(*shifted, 0), Value(shifted_mask)});
|
||||
Replace(inst, Value(&*worklist.Prepend(block, instruction, ValueOpcode::ShiftLeftLogical32,
|
||||
{Value(&*masked), shift})));
|
||||
return;
|
||||
|
||||
@@ -1178,7 +1178,7 @@ static UniformFillPlan AnalyzeUniformFill(const Program& program) {
|
||||
}
|
||||
if (store == nullptr || visited.size() != program.blocks.size()) return {};
|
||||
for (const auto& buffer: program.info.buffers) {
|
||||
if (buffer.read && (!buffer.scalar || buffer.written)) return {};
|
||||
if (buffer.read) return {};
|
||||
}
|
||||
const auto& memory = program.memory_info.at(store->Flags<MemoryFlags>().index);
|
||||
UniformFillPlan result;
|
||||
@@ -1589,7 +1589,6 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
base.formatted ? descriptor.Format() : Prospero::BufferFormat::kInvalid;
|
||||
buffer.descriptor_swizzle = base.formatted ? descriptor.DstSelXYZW() : DstSel(4, 5, 6, 7);
|
||||
buffer.zero_stride_oob = descriptor.OutOfBounds() == 0u && stride == 0u;
|
||||
buffer.scalar_empty = base.scalar && descriptor.GetSize() == 0u;
|
||||
}
|
||||
snapshot.images.resize(program.info.images.size());
|
||||
specialization.images.resize(program.info.images.size());
|
||||
@@ -1787,17 +1786,6 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
||||
}
|
||||
|
||||
auto& memory_info = program.memory_info;
|
||||
// Resolve empty scalar-table words before folding the indirect descriptors that use them.
|
||||
for (auto* block: program.blocks) {
|
||||
for (auto& inst: *block) {
|
||||
if (inst.GetOpcode() != ValueOpcode::ReadConstBuffer) continue;
|
||||
const auto& memory = memory_info[inst.Flags<MemoryFlags>().index];
|
||||
if (memory.kind == ResourceKind::ScalarBuffer &&
|
||||
specialization.buffers[memory.resource].scalar_empty &&
|
||||
buffers[memory.resource].indirect_root == BufferResource::NoIndirectBuffer)
|
||||
inst.ReplaceUsesWith(Value(0u));
|
||||
}
|
||||
}
|
||||
const ImageRemap image_remap(specialization);
|
||||
for (auto* block: program.blocks) {
|
||||
for (auto it = block->begin(); it != block->end(); ++it) {
|
||||
|
||||
@@ -13,7 +13,6 @@ struct ResourceSpecialization {
|
||||
Prospero::BufferFormat descriptor_format = Prospero::BufferFormat::kInvalid;
|
||||
uint32_t descriptor_swizzle = DstSel(4, 5, 6, 7);
|
||||
bool zero_stride_oob = false;
|
||||
bool scalar_empty = false;
|
||||
uint32_t indirect_root = BufferResource::NoIndirectBuffer;
|
||||
uint32_t indirect_mapping_offset = 0;
|
||||
uint32_t indirect_search_iterations = 0;
|
||||
|
||||
@@ -3735,8 +3735,6 @@ private:
|
||||
resource.written = resource.written || write;
|
||||
resource.atomic = resource.atomic || atomic;
|
||||
resource.formatted = resource.formatted || memory.formatted;
|
||||
resource.scalar = resource.scalar || op == ValueOpcode::ReadConstBuffer ||
|
||||
memory.kind == ResourceKind::ScalarBuffer;
|
||||
}
|
||||
|
||||
uint32_t AddImage(uint32_t source, const MemoryInfo& memory, ValueOpcode op, uint32_t pc) {
|
||||
@@ -3883,6 +3881,24 @@ private:
|
||||
if (memory.planning_only || IsIndirectPlanningMemory(flags.index)) {
|
||||
return;
|
||||
}
|
||||
if (op == ValueOpcode::ReadConstBuffer) {
|
||||
const auto* handle = inst.Arg(0).Resolve().TryInstruction();
|
||||
if (memory.kind != ResourceKind::ScalarBuffer || handle == nullptr ||
|
||||
handle->GetOpcode() != ValueOpcode::GetBufferResource || handle->NumArgs() != 4u) {
|
||||
Fail(flags.pc, "scalar buffer read requires a four-DWORD buffer descriptor");
|
||||
}
|
||||
// Scalar descriptors can span disjoint guest mappings. Keep their raw GPU operands
|
||||
// separate from handles that vector-resource planning may rewrite.
|
||||
auto* block = inst.Parent();
|
||||
const auto where = std::ranges::find_if(block->Instructions(),
|
||||
[&](const Inst& candidate) { return &candidate == &inst; });
|
||||
auto& scalar = *block->PrependNewInst(where, ValueOpcode::GetBufferResource,
|
||||
{handle->Arg(0), handle->Arg(1), handle->Arg(2), handle->Arg(3)});
|
||||
inst.SetArg(0, Value(&scalar));
|
||||
m_program.memory_info[flags.index].kind = ResourceKind::IndirectBuffer;
|
||||
m_info.uses_dma = true;
|
||||
return;
|
||||
}
|
||||
Inst* handle = nullptr;
|
||||
uint32_t source = 0;
|
||||
uint32_t resource = 0;
|
||||
@@ -3894,12 +3910,11 @@ private:
|
||||
source = indirect->source;
|
||||
} else if (!GetHandle(inst.Arg(0), ValueOpcode::GetBufferResource, 4, flags.pc,
|
||||
memory.resource * 4u, handle, source)) {
|
||||
if (memory.kind != (op == ValueOpcode::ReadConstBuffer ? ResourceKind::ScalarBuffer
|
||||
: ResourceKind::Buffer) ||
|
||||
if (memory.kind != ResourceKind::Buffer ||
|
||||
!memory.SupportsIndirectBufferLoad(op)) {
|
||||
Fail(flags.pc,
|
||||
"buffer descriptor is not a valid runtime value; GPU-selected access "
|
||||
"requires a scalar, raw 8/16-bit, raw DWORD x1/x2/x3/x4, or formatted X load");
|
||||
"requires a raw 8/16-bit, raw DWORD x1/x2/x3/x4, or formatted X load");
|
||||
}
|
||||
m_program.memory_info[flags.index].kind = ResourceKind::IndirectBuffer;
|
||||
m_info.uses_dma = true;
|
||||
|
||||
@@ -307,7 +307,7 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs
|
||||
if (shared_access != SharedAccess::None) {
|
||||
info.uses_lds |= kind == ResourceKind::Lds;
|
||||
info.uses_gds |= kind == ResourceKind::Gds;
|
||||
} else if (kind == ResourceKind::Buffer || kind == ResourceKind::ScalarBuffer) {
|
||||
} else if (kind == ResourceKind::Buffer) {
|
||||
if (memory.resource >= info.buffers.size()) {
|
||||
return Fail("buffer operation has invalid resource metadata");
|
||||
}
|
||||
|
||||
@@ -89,7 +89,8 @@ bool IsRawRead(const ResourcePlan& values, const Inst& inst) {
|
||||
const auto& memory = values.memory_info[index];
|
||||
if (op != ValueOpcode::LoadBufferU32) {
|
||||
return (op == ValueOpcode::LoadAddressU32 && memory.kind == ResourceKind::ScalarAddress) ||
|
||||
(op == ValueOpcode::ReadConstBuffer && memory.kind == ResourceKind::ScalarBuffer);
|
||||
(op == ValueOpcode::ReadConstBuffer &&
|
||||
(memory.kind == ResourceKind::ScalarBuffer || memory.kind == ResourceKind::IndirectBuffer));
|
||||
}
|
||||
if (inst.NumArgs() != 5u || memory.kind != ResourceKind::Buffer || memory.typed ||
|
||||
memory.formatted || memory.coherent || memory.data_bits != 32u ||
|
||||
|
||||
@@ -344,6 +344,8 @@ static void ShaderGetStaticInputInfoPS(
|
||||
ps_info.ps_pos_z = (active_inputs & 0x00000400u) != 0;
|
||||
ps_info.ps_pos_w = (active_inputs & 0x00000800u) != 0;
|
||||
ps_info.ps_front_face = (active_inputs & 0x00001000u) != 0;
|
||||
// SPI_BARYC_CNTL.FRONT_FACE_ALL_BITS selects integer masks instead of float +/-1.
|
||||
ps_info.ps_front_face_all_bits = (sh.baryc_cntl & 0x01000000u) != 0;
|
||||
ps_info.ps_ancillary = (active_inputs & 0x00002000u) != 0;
|
||||
ps_info.ps_sample_shading = (active_inputs & 0x00000011u) != 0;
|
||||
ps_info.ps_no_perspective = (sh.ps_input_ena & sh.ps_input_addr & 0x00000020u) != 0;
|
||||
@@ -464,6 +466,7 @@ void BuildStageStaticKey(const ShaderPixelInputInfo& info, std::vector<uint32_t>
|
||||
key.push_back(static_cast<uint32_t>(info.ps_pos_z));
|
||||
key.push_back(static_cast<uint32_t>(info.ps_pos_w));
|
||||
key.push_back(static_cast<uint32_t>(info.ps_front_face));
|
||||
key.push_back(static_cast<uint32_t>(info.ps_front_face_all_bits));
|
||||
key.push_back(static_cast<uint32_t>(info.ps_ancillary));
|
||||
key.push_back(static_cast<uint32_t>(info.ps_no_perspective));
|
||||
key.push_back(static_cast<uint32_t>(info.parameter_mode));
|
||||
@@ -737,6 +740,7 @@ void ShaderDbgDumpInputInfo(const ShaderPixelInputInfo& info) {
|
||||
"\t ps_pos_z = %s\n"
|
||||
"\t ps_pos_w = %s\n"
|
||||
"\t ps_front_face = %s\n"
|
||||
"\t ps_front_face_all_bits = %s\n"
|
||||
"\t ps_ancillary = %s\n"
|
||||
"\t ps_sample_shading = %s\n"
|
||||
"\t ps_no_perspective = %s\n"
|
||||
@@ -745,11 +749,10 @@ void ShaderDbgDumpInputInfo(const ShaderPixelInputInfo& info) {
|
||||
"\t ps_execute_on_noop = %s\n",
|
||||
info.input_num, info.ps_system_input_base, info.custom_interpolation_mask,
|
||||
info.ps_perspective_center_vgpr, info.ps_perspective_sample_vgpr,
|
||||
info.ps_perspective_centroid_vgpr,
|
||||
info.ps_pos_x ? "true" : "false",
|
||||
info.ps_perspective_centroid_vgpr, info.ps_pos_x ? "true" : "false",
|
||||
info.ps_pos_y ? "true" : "false", info.ps_pos_z ? "true" : "false",
|
||||
info.ps_pos_w ? "true" : "false", info.ps_front_face ? "true" : "false",
|
||||
info.ps_ancillary ? "true" : "false",
|
||||
info.ps_front_face_all_bits ? "true" : "false", info.ps_ancillary ? "true" : "false",
|
||||
info.ps_sample_shading ? "true" : "false", info.ps_no_perspective ? "true" : "false",
|
||||
info.ps_pixel_kill_enable ? "true" : "false", info.ps_early_z ? "true" : "false",
|
||||
info.ps_execute_on_noop ? "true" : "false");
|
||||
|
||||
@@ -196,6 +196,7 @@ struct ShaderPixelInputInfo {
|
||||
bool ps_pos_z = false;
|
||||
bool ps_pos_w = false;
|
||||
bool ps_front_face = false;
|
||||
bool ps_front_face_all_bits = false;
|
||||
bool ps_ancillary = false;
|
||||
bool ps_no_perspective = false;
|
||||
ShaderPixelParameterMode parameter_mode = ShaderPixelParameterMode::FirstVertex;
|
||||
|
||||
@@ -21,7 +21,8 @@ add_executable(launcher WIN32
|
||||
)
|
||||
|
||||
|
||||
target_link_libraries(launcher Qt6::Concurrent Qt6::Network Qt6::Widgets Vulkan::Headers SDL3::SDL3-static kyty_git_version common)
|
||||
target_link_libraries(launcher Qt6::Concurrent Qt6::Network Qt6::Widgets Vulkan::Headers SDL3::SDL3-static
|
||||
LibAtrac9 kyty_git_version common)
|
||||
if(LINUX)
|
||||
find_package(Qt6 COMPONENTS DBus REQUIRED)
|
||||
target_link_libraries(launcher Qt6::DBus)
|
||||
@@ -76,7 +77,7 @@ if(WIN32)
|
||||
)
|
||||
endif()
|
||||
|
||||
set(launcher_name "launcher")
|
||||
set(launcher_name "kyty_launcher")
|
||||
if(APPLE)
|
||||
set(launcher_name "KytyPS5")
|
||||
endif()
|
||||
@@ -101,6 +102,15 @@ install(TARGETS launcher
|
||||
BUNDLE DESTINATION .
|
||||
RUNTIME DESTINATION .)
|
||||
|
||||
if(NOT APPLE)
|
||||
# Older releases require the previous executable name when validating updates.
|
||||
install(CODE "
|
||||
file(INSTALL DESTINATION \"\${CMAKE_INSTALL_PREFIX}\"
|
||||
TYPE PROGRAM RENAME \"launcher${CMAKE_EXECUTABLE_SUFFIX}\"
|
||||
FILES \"\${CMAKE_INSTALL_PREFIX}/${launcher_name}${CMAKE_EXECUTABLE_SUFFIX}\")
|
||||
")
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
install(TARGETS kyty_emulator DESTINATION "KytyPS5.app/Contents/MacOS")
|
||||
install(TARGETS kyty_updater DESTINATION "KytyPS5.app/Contents/MacOS")
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QFutureWatcher>
|
||||
#include <QObject>
|
||||
#include <QPromise>
|
||||
#include <QString>
|
||||
#include <QThreadPool>
|
||||
|
||||
#include <vector>
|
||||
|
||||
struct SDL_AudioStream;
|
||||
|
||||
class TitleSound final: public QObject {
|
||||
public:
|
||||
explicit TitleSound(QObject* parent = nullptr);
|
||||
~TitleSound() override;
|
||||
|
||||
void SetTitle(const QString& base);
|
||||
|
||||
private:
|
||||
struct Sound {
|
||||
std::vector<float> pcm;
|
||||
int channels = 0;
|
||||
int rate = 0;
|
||||
};
|
||||
|
||||
static Sound Decode(const QByteArray& data, const QPromise<Sound>& promise);
|
||||
void Stop();
|
||||
|
||||
QThreadPool m_decode_pool;
|
||||
QFutureWatcher<Sound> m_decode;
|
||||
QString m_base;
|
||||
SDL_AudioStream* m_stream = nullptr;
|
||||
bool m_audio_initialized = false;
|
||||
};
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "configurationListWidget.h"
|
||||
#include "controllerPreview.h"
|
||||
#include "gameContent.h"
|
||||
#include "titleSound.h"
|
||||
#include "updateChecker.h"
|
||||
|
||||
#include <QApplication>
|
||||
@@ -90,6 +91,7 @@ private:
|
||||
MainDialog* m_main_dialog = nullptr;
|
||||
UpdateChecker* m_update_checker = nullptr;
|
||||
ControllerPreview m_controller_preview;
|
||||
TitleSound m_title_sound;
|
||||
QString m_interpreter;
|
||||
|
||||
QProcess m_process;
|
||||
@@ -124,7 +126,13 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
|
||||
|
||||
connect(main_dialog, &MainDialog::Start, this, &MainDialogPrivate::FindInterpreter,
|
||||
Qt::QueuedConnection);
|
||||
connect(m_ui->widget, &ConfigurationListWidget::Select, this, &MainDialogPrivate::Update);
|
||||
connect(m_ui->widget, &ConfigurationListWidget::Select, this, [this]() {
|
||||
Update();
|
||||
const auto* item = m_ui->widget->GetSelectedItem();
|
||||
m_title_sound.SetTitle(m_process.state() == QProcess::NotRunning && item != nullptr
|
||||
? item->GetInfo().basedir
|
||||
: QString());
|
||||
});
|
||||
connect(m_ui->widget, &ConfigurationListWidget::PreviewControllerColor, this,
|
||||
[this](const QString& color) {
|
||||
if (m_process.state() == QProcess::NotRunning) {
|
||||
@@ -530,6 +538,7 @@ void MainDialogPrivate::Run() {
|
||||
|
||||
m_running_item->SetRunning(true);
|
||||
m_controller_preview.SetSuspended(true);
|
||||
m_title_sound.SetTitle({});
|
||||
|
||||
auto info = m_ui->widget->CreateConfiguration(*m_running_item);
|
||||
m_main_dialog->RunInterpreter(&m_process, *info);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
#include "titleSound.h"
|
||||
|
||||
#include "gameContent.h"
|
||||
#include "libs/ajm/atrac9_decoder.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QtConcurrentRun>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <array>
|
||||
|
||||
TitleSound::Sound TitleSound::Decode(const QByteArray& data, const QPromise<Sound>& promise) {
|
||||
using namespace Libs::Audio::Ajm;
|
||||
AjmAt9Decoder decoder(0, 0, AjmSampleEncoding::Float,
|
||||
AJM_INSTANCE_FLAG_DEC_AT9_PARSE_RIFF_HEADER);
|
||||
AjmGaplessState gapless;
|
||||
std::array<float, 16 * 1024> batch;
|
||||
Sound sound;
|
||||
size_t offset = 0;
|
||||
while (offset < static_cast<size_t>(data.size()) && !gapless.IsEnd()) {
|
||||
if (promise.isCanceled()) {
|
||||
return {};
|
||||
}
|
||||
const auto result = decoder.Decode(data.constData() + offset, data.size() - offset,
|
||||
batch.data(), sizeof(batch), true, &gapless);
|
||||
if ((result.result != 0 && result.result != AJM_RESULT_NOT_ENOUGH_ROOM) ||
|
||||
result.input_consumed == 0) {
|
||||
return {};
|
||||
}
|
||||
const auto samples = result.output_written / sizeof(float);
|
||||
constexpr size_t MaxSamples = (128u << 20u) / sizeof(float);
|
||||
if (samples > MaxSamples - sound.pcm.size()) {
|
||||
return {};
|
||||
}
|
||||
sound.pcm.insert(sound.pcm.end(), batch.begin(), batch.begin() + samples);
|
||||
offset += result.input_consumed;
|
||||
}
|
||||
if (promise.isCanceled()) {
|
||||
return {};
|
||||
}
|
||||
const auto format = decoder.GetFormat();
|
||||
sound.channels = static_cast<int>(format.channel_num);
|
||||
sound.rate = static_cast<int>(format.sampling_frequency);
|
||||
return sound;
|
||||
}
|
||||
|
||||
TitleSound::TitleSound(QObject* parent): QObject(parent) {
|
||||
m_decode_pool.setMaxThreadCount(1);
|
||||
connect(&m_decode, &QFutureWatcher<Sound>::finished, this, [this] {
|
||||
if (m_decode.isCanceled() || m_decode.future().resultCount() == 0) {
|
||||
return;
|
||||
}
|
||||
auto sound = m_decode.future().takeResult();
|
||||
if (sound.pcm.empty()) {
|
||||
qWarning() << "Cannot decode title sound:" << m_base;
|
||||
return;
|
||||
}
|
||||
m_audio_initialized = SDL_InitSubSystem(SDL_INIT_AUDIO);
|
||||
if (m_audio_initialized) {
|
||||
const SDL_AudioSpec spec {SDL_AUDIO_F32, sound.channels, sound.rate};
|
||||
m_stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, nullptr,
|
||||
nullptr);
|
||||
}
|
||||
if (m_stream == nullptr ||
|
||||
!SDL_PutAudioStreamData(m_stream, sound.pcm.data(),
|
||||
static_cast<int>(sound.pcm.size() * sizeof(float))) ||
|
||||
!SDL_FlushAudioStream(m_stream) || !SDL_ResumeAudioStreamDevice(m_stream)) {
|
||||
qWarning() << "Cannot play title sound:" << SDL_GetError();
|
||||
Stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
TitleSound::~TitleSound() {
|
||||
m_decode.cancel();
|
||||
Stop();
|
||||
}
|
||||
|
||||
void TitleSound::SetTitle(const QString& base) {
|
||||
if (base == m_base) {
|
||||
return;
|
||||
}
|
||||
m_decode.cancel();
|
||||
Stop();
|
||||
m_base = base;
|
||||
if (!base.isEmpty()) {
|
||||
m_decode.setFuture(QtConcurrent::run(&m_decode_pool, [base](QPromise<Sound>& promise) {
|
||||
const auto data =
|
||||
GameContent::ReadFile(base, QStringLiteral("sce_sys/snd0.at9"), 32u << 20u);
|
||||
if (!data.isEmpty() && !promise.isCanceled()) {
|
||||
promise.addResult(Decode(data, promise));
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
void TitleSound::Stop() {
|
||||
if (m_stream != nullptr) {
|
||||
SDL_DestroyAudioStream(m_stream);
|
||||
m_stream = nullptr;
|
||||
}
|
||||
if (m_audio_initialized) {
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
m_audio_initialized = false;
|
||||
}
|
||||
}
|
||||
@@ -21,11 +21,11 @@ constexpr qint64 MAX_METADATA_SIZE = 2 * 1024 * 1024;
|
||||
|
||||
QString LauncherRelativePath() {
|
||||
#if defined(_WIN32)
|
||||
return QStringLiteral("launcher.exe");
|
||||
return QStringLiteral("kyty_launcher.exe");
|
||||
#elif defined(__APPLE__)
|
||||
return QStringLiteral("KytyPS5.app/Contents/MacOS/KytyPS5");
|
||||
#else
|
||||
return QStringLiteral("launcher");
|
||||
return QStringLiteral("kyty_launcher");
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -213,6 +213,12 @@ struct ProtectionCall {
|
||||
std::vector<ProtectionCall> g_protection_log;
|
||||
std::mutex g_protection_log_mutex;
|
||||
|
||||
struct ProtectionGate {
|
||||
std::binary_semaphore entered{0};
|
||||
std::binary_semaphore resume{0};
|
||||
};
|
||||
ProtectionGate *g_protection_gate = nullptr;
|
||||
|
||||
void ResetProtectionLog() {
|
||||
g_protection_calls = 0;
|
||||
g_protection_log.clear();
|
||||
@@ -220,6 +226,10 @@ void ResetProtectionLog() {
|
||||
|
||||
bool ProtectAddressSpace(uint64_t vaddr, uint64_t size,
|
||||
Common::VirtualMemory::Mode mode) {
|
||||
if (g_protection_gate != nullptr && mode == Common::VirtualMemory::Mode::ReadWrite) {
|
||||
g_protection_gate->entered.release();
|
||||
g_protection_gate->resume.acquire();
|
||||
}
|
||||
uint32_t protection = PAGE_NOACCESS;
|
||||
if (mode == Common::VirtualMemory::Mode::Read) {
|
||||
protection = PAGE_READONLY;
|
||||
@@ -407,6 +417,74 @@ void TestCpuDirtyUpload() {
|
||||
Release(memory);
|
||||
}
|
||||
|
||||
void TestBdaSyncDirtiness() {
|
||||
constexpr auto page = Libs::Graphics::TRACKER_PAGE_SIZE;
|
||||
TrackerHarness harness;
|
||||
auto &tracker = harness.tracker;
|
||||
auto *memory = Allocate(harness.page_manager, 3);
|
||||
const auto address = reinterpret_cast<uint64_t>(memory);
|
||||
Check(tracker.TakeBdaSync().Empty(), "new tracker queued unowned memory");
|
||||
tracker.QueueBdaSync(address, 3 * page);
|
||||
Check(tracker.IsRegionCpuModified(address, page), "new region was clean");
|
||||
auto pending = tracker.TakeBdaSync();
|
||||
Check(pending.Contains(address, 3 * page) && !pending.Intersects(address + 3 * page, page),
|
||||
"mapped or registered range did not bound initial BDA synchronization");
|
||||
tracker.ForEachUploadRange(address, 3 * page, false,
|
||||
[](uint64_t, uint64_t) noexcept {}, []() noexcept {});
|
||||
Check(tracker.TakeBdaSync().Empty(), "clean upload queued redundant BDA work");
|
||||
|
||||
tracker.MarkRegionAsCpuModified(address + 17, 1);
|
||||
pending = tracker.TakeBdaSync();
|
||||
Check(pending.Contains(address, page) && !pending.Intersects(address + page, page) &&
|
||||
tracker.TakeBdaSync().Empty(),
|
||||
"byte dirtiness did not drain exactly its whole tracker page");
|
||||
// A write after Take belongs to the next drain, even when its page was just taken.
|
||||
tracker.MarkRegionAsCpuModified(address + 31, 1);
|
||||
Check(tracker.TakeBdaSync().Contains(address, page), "re-dirty event was lost");
|
||||
tracker.ForEachUploadRange(address, page, true,
|
||||
[](uint64_t, uint64_t) noexcept {}, []() noexcept {});
|
||||
Check(tracker.TakeBdaSync().Empty(), "GPU ownership queued an unnecessary upload");
|
||||
tracker.InvalidateRegion(address + 7, 1, [&] {
|
||||
tracker.UnmarkRegionAsGpuModified(address, page);
|
||||
tracker.MarkRegionAsCpuModified(address, page);
|
||||
});
|
||||
Check(tracker.TakeBdaSync().Contains(address, page),
|
||||
"GPU-to-CPU invalidation did not queue BDA synchronization");
|
||||
tracker.InvalidateRegion(address + page + 7, 1, [] {});
|
||||
Check(tracker.TakeBdaSync().Contains(address + page, page),
|
||||
"CPU invalidation did not queue BDA synchronization");
|
||||
tracker.UntrackMemory(address, 3 * page);
|
||||
Check(tracker.TakeBdaSync().Contains(address, 3 * page),
|
||||
"untracked memory did not queue its newly dirty pages");
|
||||
Release(memory);
|
||||
}
|
||||
|
||||
void TestBdaSyncConcurrentDrain() {
|
||||
constexpr auto page = Libs::Graphics::TRACKER_PAGE_SIZE;
|
||||
TrackerHarness harness;
|
||||
auto &tracker = harness.tracker;
|
||||
auto *memory = Allocate(harness.page_manager, 1);
|
||||
const auto address = reinterpret_cast<uint64_t>(memory);
|
||||
tracker.ForEachUploadRange(address, page, false,
|
||||
[](uint64_t, uint64_t) noexcept {}, []() noexcept {});
|
||||
(void)tracker.TakeBdaSync();
|
||||
ProtectionGate gate;
|
||||
g_protection_gate = &gate;
|
||||
std::jthread writer([&] { tracker.MarkRegionAsCpuModified(address + 17, 1); });
|
||||
gate.entered.acquire();
|
||||
// Ownership is still changing. Publishing its event before this point would let
|
||||
// a drain consume it before the writer has finished releasing page protection.
|
||||
Check(tracker.TakeBdaSync().Empty(), "BDA event preceded its state transition");
|
||||
gate.resume.release();
|
||||
writer.join();
|
||||
g_protection_gate = nullptr;
|
||||
Check(tracker.TakeBdaSync().Contains(address, page) &&
|
||||
tracker.IsRegionCpuModified(address, page),
|
||||
"concurrent drain lost a completed CPU dirty transition");
|
||||
tracker.UntrackMemory(address, page);
|
||||
Release(memory);
|
||||
}
|
||||
|
||||
void TestCleanUploadPreservesOwnership() {
|
||||
constexpr auto page_size = Libs::Graphics::TRACKER_PAGE_SIZE;
|
||||
constexpr auto region_size = Libs::Graphics::TRACKER_REGION_SIZE;
|
||||
@@ -1202,6 +1280,8 @@ int main(int argc, char **argv) {
|
||||
TestQueriesDoNotRequireMappedOwnership();
|
||||
TestConcurrentRegionPublication();
|
||||
TestCpuDirtyUpload();
|
||||
TestBdaSyncDirtiness();
|
||||
TestBdaSyncConcurrentDrain();
|
||||
TestCleanUploadPreservesOwnership();
|
||||
TestRangeInvalidation();
|
||||
TestGpuReacquisitionAfterInvalidation();
|
||||
|
||||
@@ -1024,115 +1024,6 @@ void TestCyclicControlPredicatesRemainUnknown() {
|
||||
"cyclic control lost a possible branch during reachability");
|
||||
}
|
||||
|
||||
void TestEmptyScalarBufferSpecialization() {
|
||||
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
||||
struct Fixture {
|
||||
Program program;
|
||||
std::vector<std::pair<Inst *, Type>> uses;
|
||||
Inst *nonnull = nullptr;
|
||||
};
|
||||
const auto make_fixture = [] {
|
||||
Fixture fixture;
|
||||
auto &program = fixture.program;
|
||||
program.stage = Libs::Graphics::ShaderType::Compute;
|
||||
program.srt_plan_complete = program.resource_tracking_complete = true;
|
||||
program.user_data_count = 4u;
|
||||
auto &block = AddValueBlock(program);
|
||||
DescriptorSource source;
|
||||
source.dword_count = 4u;
|
||||
for (uint32_t word = 0; word < 4u; ++word)
|
||||
source.dwords[word] = Value(&block.AppendNewInst(ValueOpcode::GetUserData,
|
||||
{Value(static_cast<ScalarReg>(word))}));
|
||||
program.descriptor_sources.push_back(source);
|
||||
program.info.buffers.push_back({.source = 0u, .read = true, .scalar = true});
|
||||
const auto table = Value(&block.AppendNewInst(ValueOpcode::GetBufferResource,
|
||||
{source.dwords[0], source.dwords[1], source.dwords[2], source.dwords[3]}));
|
||||
std::array<Value, 4> words;
|
||||
for (uint32_t word = 0; word < 4u; ++word) {
|
||||
program.memory_info.push_back({.kind = ResourceKind::ScalarBuffer, .resource = 0u,
|
||||
.offset = word * 4u});
|
||||
auto &read = block.AppendNewInst(ValueOpcode::ReadConstBuffer, {table, Value(0u)});
|
||||
read.SetFlags(MemoryFlags{.index = word});
|
||||
words[word] = Value(&read);
|
||||
}
|
||||
const auto handle = Value(&block.AppendNewInst(ValueOpcode::GetBufferResource,
|
||||
{words[0], words[1], words[2], words[3]}));
|
||||
for (const auto opcode : {ValueOpcode::ReadConstBuffer, ValueOpcode::LoadBufferU8,
|
||||
ValueOpcode::LoadBufferU16, ValueOpcode::LoadBufferU32,
|
||||
ValueOpcode::LoadBufferU32x2, ValueOpcode::LoadBufferU32x3,
|
||||
ValueOpcode::LoadBufferU32x4}) {
|
||||
const auto index = static_cast<uint32_t>(program.memory_info.size());
|
||||
program.memory_info.push_back({.kind = ResourceKind::IndirectBuffer, .resource = UINT32_MAX,
|
||||
.data_dwords = BufferComponentCount(opcode),
|
||||
.data_bits = opcode == ValueOpcode::LoadBufferU8 ? 8u : opcode == ValueOpcode::LoadBufferU16 ? 16u : 32u,
|
||||
.formatted = opcode == ValueOpcode::LoadBufferU32});
|
||||
auto &read = opcode == ValueOpcode::ReadConstBuffer
|
||||
? block.AppendNewInst(opcode, {handle, Value(0u)})
|
||||
: block.AppendNewInst(opcode, {handle, Value(0u), Value(0u), Value(0u), Value(true)});
|
||||
read.SetFlags(MemoryFlags{.index = index});
|
||||
fixture.uses.emplace_back(&block.AppendNewInst(ValueOpcode::Identity, {Value(&read)}), read.GetType());
|
||||
}
|
||||
const auto nonnull = Value(&block.AppendNewInst(ValueOpcode::GetBufferResource,
|
||||
{words[0], words[1], words[2], Value(1u)}));
|
||||
auto &read = block.AppendNewInst(ValueOpcode::ReadConstBuffer, {nonnull, Value(0u)});
|
||||
read.SetFlags(MemoryFlags{.index = 4u});
|
||||
fixture.nonnull = &block.AppendNewInst(ValueOpcode::Identity, {Value(&read)});
|
||||
return fixture;
|
||||
};
|
||||
auto fixture = make_fixture();
|
||||
auto plan = ExtractResourcePlan(fixture.program);
|
||||
std::array<uint32_t, 4> descriptor{0x1000u, 8u << 16u, 0u, 3u << 28u};
|
||||
uint32_t reads = 0u;
|
||||
const SrtRuntime runtime{.user_data = descriptor, .read_memory = RejectSpecializationRead,
|
||||
.userdata = &reads, .read_specialization_memory = RejectSpecializationRead};
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization, empty, populated;
|
||||
for (const uint32_t records : {0u, 1u, 0x20000000u, 0u}) {
|
||||
descriptor[2] = records;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) && reads == 0u &&
|
||||
specialization.buffers[0].scalar_empty == (records == 0u),
|
||||
"scalar emptiness read table contents or truncated its 64-bit byte size");
|
||||
if (records == 0u) {
|
||||
if (empty.buffers.empty()) empty = specialization;
|
||||
else Check(specialization == empty, "returning to an empty scalar table changed its specialization");
|
||||
}
|
||||
else if (records == 1u) populated = specialization;
|
||||
else Check(specialization == populated, "nonempty scalar size created a shader permutation");
|
||||
auto current = make_fixture();
|
||||
ApplyResourceSpecialization(current.program, specialization);
|
||||
for (const auto &[use, type] : current.uses) {
|
||||
const auto value = use->Arg(0).Resolve();
|
||||
Check(value.GetType() == type, "empty scalar-table folding changed the load result type");
|
||||
if (records != 0u) {
|
||||
Check(value.TryInstruction() != nullptr && BufferAccessOf(value.Instruction()->GetOpcode()) == BufferAccess::Read,
|
||||
"nonempty scalar table lost a runtime buffer load");
|
||||
} else if (value.IsImmediate()) {
|
||||
Check(value == Value(uint8_t{0}) || value == Value(uint16_t{0}) || value == Value(0u),
|
||||
"empty scalar table did not produce a typed zero");
|
||||
} else {
|
||||
for (size_t component = 0; component < value.Instruction()->NumArgs(); ++component)
|
||||
Check(value.Instruction()->Arg(component) == Value(0u),
|
||||
"empty scalar table retained a nonzero vector component");
|
||||
}
|
||||
}
|
||||
Check(current.nonnull->Arg(0).Resolve().TryInstruction() != nullptr,
|
||||
"a partly nonzero indirect descriptor was treated as unbound");
|
||||
}
|
||||
Check(empty != populated, "empty-to-populated scalar transition reused the folded specialization");
|
||||
plan.info.buffers[0].scalar = false;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
!specialization.buffers[0].scalar_empty,
|
||||
"vector-only resource acquired scalar-empty specialization");
|
||||
empty.buffers[0].indirect_root = 0u;
|
||||
empty.buffers.push_back(empty.buffers[0]);
|
||||
empty.buffers[1].scalar_empty = false;
|
||||
ApplyResourceSpecialization(fixture.program, empty);
|
||||
for (const auto &[use, type] : fixture.uses)
|
||||
Check(use->Arg(0).GetType() == type && use->Arg(0).Resolve().TryInstruction() != nullptr &&
|
||||
BufferAccessOf(use->Arg(0).ResolveInstruction()->GetOpcode()) == BufferAccess::Read,
|
||||
"empty root folded a scalar read with nonempty candidates");
|
||||
}
|
||||
|
||||
void TestMixedSamplerVariantsShareRuntimeDescriptor() {
|
||||
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
||||
auto program = MixedSamplerProgram();
|
||||
@@ -1332,7 +1223,6 @@ int main() {
|
||||
for (const bool independent_mask : {false, true})
|
||||
TestEmptyTraversalSkipsUnmappedDescriptor(independent_mask);
|
||||
TestCyclicControlPredicatesRemainUnknown();
|
||||
TestEmptyScalarBufferSpecialization();
|
||||
TestMixedSamplerVariantsShareRuntimeDescriptor();
|
||||
TestSignedStorageSpecialization();
|
||||
for (const bool mixed : {false, true}) TestIndirectImageSamplerClasses(mixed);
|
||||
|
||||
@@ -312,7 +312,8 @@ void TestInvariantIndirectImageMaterialization(bool nested) {
|
||||
EliminateDeadCode(fixture->program.blocks);
|
||||
ValidateProgram(fixture->program, true);
|
||||
const auto &source = *plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor;
|
||||
Check(!source.selector && fixture->program.info.buffers.size() == (nested ? 3u : 2u) &&
|
||||
Check(!source.selector && fixture->program.info.buffers.empty() &&
|
||||
fixture->program.info.uses_dma &&
|
||||
std::ranges::any_of(*fixture->block, [](const Inst &inst) {
|
||||
return inst.GetOpcode() == ValueOpcode::ReadConstBuffer;
|
||||
}),
|
||||
@@ -3313,9 +3314,12 @@ void TestUniformScalarBufferImage() {
|
||||
fixture.PlanAndTrack();
|
||||
const auto plan = ExtractResourcePlan(fixture.program);
|
||||
|
||||
Check(std::ranges::none_of(plan.descriptor_sources, [](const DescriptorSource& source) {
|
||||
return source.indirect_descriptor.has_value();
|
||||
}), "draw-uniform scalar image was expanded to GPU-selected candidates");
|
||||
Check(plan.info.uses_dma && plan.info.buffers.empty() &&
|
||||
fixture.program.memory_info[key.Instruction()->Flags<MemoryFlags>().index].kind ==
|
||||
ResourceKind::IndirectBuffer &&
|
||||
std::ranges::none_of(plan.descriptor_sources, [](const DescriptorSource& source) {
|
||||
return source.indirect_descriptor.has_value();
|
||||
}), "draw-uniform scalar image lost its BDA read or expanded GPU-selected candidates");
|
||||
std::array<uint32_t, 9> user_data{0x1000u, 0u, 0x100u, 0u,
|
||||
0x2000u, 0u, 0x100u, 0u, 0u};
|
||||
LinearTestMemory memory;
|
||||
@@ -4642,7 +4646,6 @@ void TestComputeBufferFill() {
|
||||
bool conditional = false;
|
||||
bool shifted = false;
|
||||
bool extra_store = false;
|
||||
bool clean = false;
|
||||
bool branch = false;
|
||||
};
|
||||
const auto Run = [](Options options) {
|
||||
@@ -4670,12 +4673,9 @@ void TestComputeBufferFill() {
|
||||
if (options.shifted)
|
||||
index = fixture.Emit(ValueOpcode::IAdd32, {index, Value(1u)});
|
||||
Value value(0u);
|
||||
TestMemory memory;
|
||||
memory.words[0] = 0x40404040u;
|
||||
if (options.scalar) {
|
||||
const auto input =
|
||||
fixture.Buffer({Value(static_cast<uint32_t>(memory.base)),
|
||||
Value(4u << 16), Value(1u), Value(0x14204u)});
|
||||
fixture.Buffer({Value(0x1000u), Value(4u << 16), Value(1u), Value(0x14204u)});
|
||||
MemoryInfo load;
|
||||
load.kind = ResourceKind::ScalarBuffer;
|
||||
value = fixture.Emit(ValueOpcode::ReadConstBuffer, {input, Value(0u)},
|
||||
@@ -4703,48 +4703,26 @@ void TestComputeBufferFill() {
|
||||
std::array<uint32_t, 4> userdata{0x200000u, 4u << 16, 0x4000u, 0x14204u};
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
const auto Read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
|
||||
auto &memory = *static_cast<TestMemory *>(data);
|
||||
if (address != memory.base || words.size() != 1u)
|
||||
return false;
|
||||
++memory.reads;
|
||||
words[0] = memory.words[0];
|
||||
return true;
|
||||
};
|
||||
Check(MaterializeResources(
|
||||
plan,
|
||||
{.user_data = userdata,
|
||||
.read_memory = Read,
|
||||
.userdata = &memory,
|
||||
.read_specialization_memory = options.clean ? Read : nullptr},
|
||||
snapshot, specialization),
|
||||
Check(MaterializeResources(plan, {.user_data = userdata}, snapshot, specialization),
|
||||
"fill fixture did not materialize");
|
||||
const bool expected = !options.conditional && !options.shifted &&
|
||||
!options.extra_store && !options.branch &&
|
||||
(!options.scalar || options.clean);
|
||||
!options.scalar;
|
||||
Check((snapshot.uniform_fill.words != 0) == expected,
|
||||
"fill proof accepted an unsafe store or missed the real GTA3 clear");
|
||||
if (expected) {
|
||||
Check(snapshot.uniform_fill.words == 1 &&
|
||||
snapshot.uniform_fill.group_stride[0] == 64 &&
|
||||
snapshot.uniform_fill.value ==
|
||||
(options.scalar ? 0x40404040u : 0u),
|
||||
snapshot.uniform_fill.value == 0u,
|
||||
"fill proof lost address coverage or the actual stored scalar");
|
||||
if (options.scalar && options.clean) {
|
||||
Check(MaterializeResources(plan,
|
||||
{.user_data = userdata, .read_memory = Read, .userdata = &memory},
|
||||
snapshot, specialization) && snapshot.uniform_fill.words == 0,
|
||||
"an unavailable clean value retained a previous uniform fill");
|
||||
}
|
||||
}
|
||||
};
|
||||
Run({});
|
||||
Run({.scalar = true, .clean = true});
|
||||
Run({.scalar = true});
|
||||
Run({.conditional = true, .clean = true});
|
||||
Run({.shifted = true, .clean = true});
|
||||
Run({.extra_store = true, .clean = true});
|
||||
Run({.clean = true, .branch = true});
|
||||
Run({.conditional = true});
|
||||
Run({.shifted = true});
|
||||
Run({.extra_store = true});
|
||||
Run({.branch = true});
|
||||
}
|
||||
|
||||
void TestDenseBufferTracking() {
|
||||
@@ -4819,8 +4797,8 @@ void TestScalarAndVectorBufferAlias() {
|
||||
MemoryInfo scalar;
|
||||
scalar.kind = ResourceKind::ScalarBuffer;
|
||||
const auto scalar_flags = fixture.AddMemory(scalar, 4);
|
||||
fixture.Emit(ValueOpcode::ReadConstBuffer, {descriptor, fixture.UserData(4)},
|
||||
scalar_flags);
|
||||
const auto scalar_read = fixture.Emit(ValueOpcode::ReadConstBuffer,
|
||||
{descriptor, fixture.UserData(4)}, scalar_flags);
|
||||
MemoryInfo vector;
|
||||
vector.kind = ResourceKind::Buffer;
|
||||
const auto vector_flags = fixture.AddMemory(vector, 8);
|
||||
@@ -4829,12 +4807,18 @@ void TestScalarAndVectorBufferAlias() {
|
||||
vector_flags);
|
||||
fixture.PlanAndTrack();
|
||||
|
||||
Check(fixture.program.info.buffers.size() == 1 &&
|
||||
fixture.program.info.buffers[0].scalar,
|
||||
"typed scalar and vector uses of one descriptor were split");
|
||||
Check(fixture.program.memory_info[scalar_flags.index].resource == 0 &&
|
||||
Check(fixture.program.info.buffers.size() == 1 && fixture.program.info.uses_dma &&
|
||||
fixture.program.memory_info[scalar_flags.index].kind == ResourceKind::IndirectBuffer &&
|
||||
fixture.program.memory_info[vector_flags.index].resource == 0,
|
||||
"scalar/vector alias did not share a dense index");
|
||||
"scalar BDA access changed the shared vector descriptor binding");
|
||||
TestMemory memory;
|
||||
memory.words[1] = 0x12345678u;
|
||||
const std::array<uint32_t, 5> user_data{0x1000u, 0u, 32u, 0u, 4u};
|
||||
SrtWalker walker(fixture.program, {.user_data = user_data, .read_memory = ReadTestMemory,
|
||||
.userdata = &memory});
|
||||
uint32_t loaded = 0;
|
||||
Check(walker.Evaluate(scalar_read, loaded) && loaded == memory.words[1] && memory.reads == 1u,
|
||||
"scalar BDA read lost its raw descriptor or dynamic uniform offset");
|
||||
}
|
||||
|
||||
void TestRuntimeUnsignedMinDescriptor() {
|
||||
@@ -6333,7 +6317,7 @@ void TestGpuSelectedFormattedRecordStore() {
|
||||
}
|
||||
if (inst.GetOpcode() == ValueOpcode::ReadConstBuffer) {
|
||||
const auto& memory = fixture->program.memory_info[inst.Flags<MemoryFlags>().index];
|
||||
Check(memory.kind == ResourceKind::ScalarBuffer && !memory.planning_only,
|
||||
Check(memory.kind == ResourceKind::IndirectBuffer && !memory.planning_only,
|
||||
"store planning consumed descriptor words still used by the BDA load");
|
||||
++scalar_reads;
|
||||
}
|
||||
@@ -7765,9 +7749,9 @@ void TestConditionalBufferMaterialization() {
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.buffers.size() == 2 &&
|
||||
snapshot.buffers[1].dword_count == 4 &&
|
||||
snapshot.buffers[1].dwords == std::array<uint32_t, 8>{},
|
||||
snapshot.buffers.size() == 1 &&
|
||||
snapshot.buffers[0].dword_count == 4 &&
|
||||
snapshot.buffers[0].dwords == std::array<uint32_t, 8>{},
|
||||
"untaken scalar branch materialized stale buffer words");
|
||||
Check(snapshot.user_data == std::vector<uint32_t>(user_data.begin(), user_data.end()),
|
||||
"resource reachability changed native shader user data");
|
||||
@@ -7782,9 +7766,9 @@ void TestConditionalBufferMaterialization() {
|
||||
runtime.user_data = user_data;
|
||||
const auto CheckActive = [&] {
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.buffers.size() == 2 &&
|
||||
snapshot.buffers.size() == 1 &&
|
||||
std::equal(user_data.begin() + 4, user_data.end(),
|
||||
snapshot.buffers[1].dwords.begin()),
|
||||
snapshot.buffers[0].dwords.begin()),
|
||||
"potentially executed buffer descriptor was discarded");
|
||||
};
|
||||
CheckActive();
|
||||
@@ -7871,6 +7855,15 @@ void TestGuardedScalarDescriptorReads() {
|
||||
memory.data.words[0xb0 / 4] = 4u;
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
if (!exec) {
|
||||
for (const uint32_t enabled : {0u, 1u, 0u}) {
|
||||
memory.data.words[7] = enabled;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
memory.null_reads == 0 && snapshot.flattened_srt.empty(),
|
||||
"disabled GPU address loads introduced CPU descriptor reads");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!shared) {
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) && memory.null_reads == 0 &&
|
||||
std::ranges::all_of(snapshot.flattened_srt, [](auto word) { return word == 0; }),
|
||||
@@ -7883,11 +7876,8 @@ void TestGuardedScalarDescriptorReads() {
|
||||
memory.data.watched_address = 0x10a8u;
|
||||
memory.data.watched_reads = 0;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
std::ranges::any_of(snapshot.buffers, [](const auto& descriptor) {
|
||||
return descriptor.dwords[0] == 0x2000u;
|
||||
}) &&
|
||||
(!exec || (memory.data.watched_reads == 1 &&
|
||||
std::ranges::find(snapshot.flattened_srt, 0x2000u) != snapshot.flattened_srt.end())),
|
||||
memory.data.watched_reads == 1 &&
|
||||
std::ranges::find(snapshot.flattened_srt, 0x2000u) != snapshot.flattened_srt.end(),
|
||||
"active or shared descriptor read was omitted after refreshing the cached plan");
|
||||
if (!shared) {
|
||||
memory.data.words[7] = 0;
|
||||
@@ -7916,9 +7906,9 @@ void TestConservativeBufferReachability() {
|
||||
if (use != ConditionalBufferUse::Writable)
|
||||
std::copy_n(user_data.begin() + 4, expected.size(), expected.begin());
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.buffers.size() == 2 &&
|
||||
snapshot.buffers.size() == (use == ConditionalBufferUse::Writable ? 2u : 1u) &&
|
||||
std::equal(expected.begin(), expected.end(),
|
||||
snapshot.buffers[1].dwords.begin()),
|
||||
snapshot.buffers.back().dwords.begin()),
|
||||
"shared or loop-dependent resource was pruned, or inactive scalar branch retained");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1383,6 +1383,7 @@ struct GraphicsCase {
|
||||
std::vector<u32> vertices;
|
||||
bool pixel_ancillary = false;
|
||||
bool pixel_front_face = false;
|
||||
bool pixel_front_face_all_bits = false;
|
||||
u32 layers = 1;
|
||||
vk::SampleCountFlagBits samples = vk::SampleCountFlagBits::e1;
|
||||
bool pixel_position_w = false;
|
||||
@@ -1834,6 +1835,7 @@ CompiledShader CompileFragmentCase(const GraphicsCase &test) {
|
||||
: ShaderPixelParameterMode::FirstVertex;
|
||||
pixel_info.ps_ancillary = test.pixel_ancillary;
|
||||
pixel_info.ps_front_face = test.pixel_front_face;
|
||||
pixel_info.ps_front_face_all_bits = test.pixel_front_face_all_bits;
|
||||
pixel_info.ps_pos_w = test.pixel_position_w;
|
||||
pixel_info.ps_depth_export_enable = test.pixel_depth_export;
|
||||
pixel_info.ps_system_input_base = 2;
|
||||
@@ -5279,6 +5281,85 @@ public:
|
||||
std::printf("[gpu] %-32s ok\n", name);
|
||||
}
|
||||
|
||||
void CheckBdaSyncUploads() {
|
||||
constexpr const char *name = "BdaSyncUploads";
|
||||
constexpr auto page = BufferCache::CACHING_PAGESIZE;
|
||||
EnsureRuntimeContext();
|
||||
const auto base = LibKernel::Memory::AllocateRuntimeMemory(
|
||||
0, 3 * page, Common::VirtualMemory::Mode::ReadWrite, "bda_sync_uploads");
|
||||
Require(name, "allocation", base != 0, "guest allocation failed");
|
||||
auto *words = reinterpret_cast<u32 *>(base);
|
||||
std::fill_n(words, 3 * page / sizeof(u32), 0x12345678u);
|
||||
RenderContext context(m_runtime_context);
|
||||
auto &scheduler = context.GetCommandScheduler();
|
||||
auto &cache = context.GetBufferCache();
|
||||
HW::Context registers{};
|
||||
HW::UserConfig user_config{};
|
||||
HW::Shader shaders{};
|
||||
scheduler.Begin(registers, user_config, shaders);
|
||||
context.MapMemory(base, page);
|
||||
context.MapMemory(base + 2 * page, page);
|
||||
const auto id = cache.FindBuffer(base, 3 * page);
|
||||
auto &buffer = cache.GetBuffer(id);
|
||||
auto readback = CreateHostBuffer(name, 6 * sizeof(u32),
|
||||
vk::BufferUsageFlagBits::eTransferDst, {});
|
||||
const auto snapshot = [&](uint64_t offset, uint32_t index) {
|
||||
const vk::BufferCopy copy{offset, index * sizeof(u32), sizeof(u32)};
|
||||
scheduler.Current().Handle().copyBuffer(buffer.Handle(), readback.buffer, 1, ©);
|
||||
};
|
||||
context.PrepareBda();
|
||||
Require(name, "mapped intersection",
|
||||
!cache.IsRegionCpuModified(base, page) &&
|
||||
cache.IsRegionCpuModified(base + page, page) &&
|
||||
!cache.IsRegionCpuModified(base + 2 * page, page),
|
||||
"BDA upload missed a mapped range or uploaded an unmapped hole");
|
||||
snapshot(0, 0);
|
||||
snapshot(2 * page, 1);
|
||||
|
||||
cache.InvalidateMemory(base + 17, 1);
|
||||
words[4] = 0x87654321u;
|
||||
words[TRACKER_PAGE_SIZE / sizeof(u32) - 1] = 0xabcdef01u;
|
||||
context.PrepareBda();
|
||||
snapshot(16, 2);
|
||||
snapshot(TRACKER_PAGE_SIZE - sizeof(u32), 3);
|
||||
Require(name, "whole-page upload", !cache.IsRegionCpuModified(base, TRACKER_PAGE_SIZE),
|
||||
"one-byte invalidation did not upload and consume its whole page");
|
||||
|
||||
context.MapMemory(base + page, page);
|
||||
context.PrepareBda();
|
||||
snapshot(page, 4);
|
||||
Require(name, "new mapping", !cache.IsRegionCpuModified(base + page, page),
|
||||
"mapping into an existing buffer did not queue its dirty pages");
|
||||
context.UnmapMemory(base + 2 * page, page);
|
||||
words[2 * page / sizeof(u32)] = 0xfedcba98u;
|
||||
context.PrepareBda();
|
||||
Require(name, "unmapped dirty page", cache.IsRegionCpuModified(base + 2 * page, page),
|
||||
"BDA synchronization consumed dirtiness outside mapped memory");
|
||||
context.MapMemory(base + 2 * page, page);
|
||||
context.PrepareBda();
|
||||
snapshot(2 * page, 5);
|
||||
|
||||
vk::BufferMemoryBarrier barrier{};
|
||||
barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
|
||||
barrier.dstAccessMask = vk::AccessFlagBits::eHostRead;
|
||||
barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
barrier.buffer = readback.buffer;
|
||||
barrier.size = readback.size;
|
||||
scheduler.Current().Handle().pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
|
||||
vk::PipelineStageFlagBits::eHost, {}, 0, nullptr, 1, &barrier, 0, nullptr);
|
||||
scheduler.Finish();
|
||||
Require(name, "GPU contents", ReadBuffer(name, readback, 6) ==
|
||||
std::vector<u32>{0x12345678u, 0x12345678u, 0x87654321u, 0xabcdef01u,
|
||||
0x12345678u, 0xfedcba98u},
|
||||
"initial, re-dirtied, newly mapped or remapped bytes were stale on the GPU");
|
||||
context.UnmapMemory(base, 3 * page);
|
||||
scheduler.Finish();
|
||||
DestroyBuffer(&readback);
|
||||
Require(name, "release", LibKernel::Memory::FreeGuestMemory(base, 3 * page),
|
||||
"guest allocation release failed");
|
||||
std::printf("[gpu] %-32s ok\n", name);
|
||||
}
|
||||
|
||||
void CheckBdaPageTableUploads() {
|
||||
constexpr const char *name = "BdaPageTableUploads";
|
||||
constexpr uintptr_t base = 0x0000000207b00000ull;
|
||||
@@ -30436,7 +30517,7 @@ TestCase ScalarMemoryLoadVariants() {
|
||||
std::vector<u32> expected = initial;
|
||||
expected.insert(expected.end(), initial.begin(), initial.end());
|
||||
|
||||
return {"ScalarMemoryLoadVariants",
|
||||
TestCase test{"ScalarMemoryLoadVariants",
|
||||
code,
|
||||
initial,
|
||||
expected,
|
||||
@@ -30445,6 +30526,8 @@ TestCase ScalarMemoryLoadVariants() {
|
||||
O::S_BUFFER_LOAD_DWORDX2, O::S_BUFFER_LOAD_DWORDX4,
|
||||
O::S_BUFFER_LOAD_DWORDX8, O::S_BUFFER_LOAD_DWORDX16, O::V_MOV_B32,
|
||||
O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
|
||||
test.bda_mappings = {{0u, 0u}};
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase ScalarBufferOffsetAlignmentAndCarry() {
|
||||
@@ -30467,6 +30550,7 @@ TestCase ScalarBufferOffsetAlignmentAndCarry() {
|
||||
{O::S_MOV_B32, O::S_ADD_U32, O::S_BUFFER_LOAD_DWORD,
|
||||
O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
|
||||
test.forbidden_spirv = {"flattened_srt"};
|
||||
test.bda_mappings = {{0u, 0u}};
|
||||
test.compute_info.threads_num[0] = 1;
|
||||
test.compute_info.threads_num[1] = 1;
|
||||
test.compute_info.threads_num[2] = 1;
|
||||
@@ -31100,7 +31184,7 @@ TestCase BufferLoadsGpuSelectedDescriptors(u32 width, u32 wave = 32u) {
|
||||
}
|
||||
}
|
||||
AppendEnd(&test.code);
|
||||
test.bda_mappings = {{GuestBase, 0}};
|
||||
test.bda_mappings = {{0u, 0u}, {GuestBase, 0}};
|
||||
test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::BUFFER_LOAD_DWORD,
|
||||
O::V_READFIRSTLANE_B32, O::S_MUL_I32, O::S_BUFFER_LOAD_DWORDX4,
|
||||
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
@@ -31284,7 +31368,7 @@ TestCase BufferLoadGpuSelectedDescriptors(u32 wave, ShaderOpcode load) {
|
||||
code.push_back(EncodeSopc(0x0a, 28, 29));
|
||||
code.push_back(EncodeSopp(0x05, loop - static_cast<u32>(code.size()) - 1u));
|
||||
AppendEnd(&code);
|
||||
test.bda_mappings = {{GuestBase, 0}};
|
||||
test.bda_mappings = {{0u, 0u}, {GuestBase, 0}};
|
||||
test.fault_free_address = GuestBase + 0x100000u;
|
||||
test.compute_info.threads_num[0] = 1;
|
||||
test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1;
|
||||
@@ -31336,11 +31420,17 @@ void CheckSharedIndirectBufferLoads(VulkanHarness &vulkan) {
|
||||
auto compiled = CompileCase(test, vulkan.SubgroupSize());
|
||||
struct Profile { u32 components = 0, uses = 0, definitions = 0, calls = 0, reads = 0; };
|
||||
std::map<std::string, Profile> profiles;
|
||||
const u32 halves = wave == 64u && vulkan.SubgroupSize() == 32u ? 2u : 1u;
|
||||
u32 scalar_reads = 0;
|
||||
for (const auto *block : compiled.program.blocks)
|
||||
for (const auto &inst : *block) {
|
||||
const auto components = BufferComponentCount(inst.GetOpcode());
|
||||
if (components == 0u || inst.GetOpcode() == ValueOpcode::ReadConstBuffer) continue;
|
||||
if (components == 0u) continue;
|
||||
const auto &memory = compiled.program.memory_info[inst.Flags<MemoryFlags>().index];
|
||||
if (inst.GetOpcode() == ValueOpcode::ReadConstBuffer) {
|
||||
scalar_reads += !memory.planning_only;
|
||||
continue;
|
||||
}
|
||||
if (memory.kind != ResourceKind::IndirectBuffer || memory.formatted) continue;
|
||||
const auto name = "load_indirect_buffer_" + std::to_string(memory.data_bits) + "x" +
|
||||
std::to_string(components) + (memory.coherent ? "_coherent" : "");
|
||||
@@ -31371,11 +31461,11 @@ void CheckSharedIndirectBufferLoads(VulkanHarness &vulkan) {
|
||||
}
|
||||
offset += words.size();
|
||||
}
|
||||
Require(test.name, "raw load function scope", functions.size() == profiles.size() && inline_reads == 0u,
|
||||
Require(test.name, "raw load function scope", functions.size() == profiles.size() && inline_reads == scalar_reads * halves,
|
||||
"a raw load profile was unused, missing or still emitted inline");
|
||||
for (const auto &[name, profile] : profiles)
|
||||
Require(test.name, name.c_str(), profile.uses != 0u && profile.definitions == 1u &&
|
||||
profile.calls == profile.uses * (wave == 64u && vulkan.SubgroupSize() == 32u ? 2u : 1u) &&
|
||||
profile.calls == profile.uses * halves &&
|
||||
profile.reads == profile.components,
|
||||
"raw load helpers duplicated definitions, calls or component reads");
|
||||
auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size(), true);
|
||||
@@ -33679,6 +33769,48 @@ TestCase ScalarBufferFromLoopReadlane(u32 wave_size) {
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase ScalarBufferLoadsDisjointPages() {
|
||||
using O = ShaderOpcode;
|
||||
constexpr u32 GuestBase = 0x10000, PageSize = 0x10000, PageStride = 0x400000;
|
||||
TestCase test;
|
||||
test.name = "ScalarBufferLoadsDisjointPages";
|
||||
auto &code = test.code;
|
||||
code.push_back(EncodeSop2(0x1e, 12, 4, InlineU32(22)));
|
||||
code.push_back(EncodeSmem0(0x0a, 20, 4));
|
||||
code.push_back(EncodeSmem1(256, 12));
|
||||
code.push_back(EncodeVop1(0x01, 0, 4));
|
||||
code.push_back(EncodeVop2(0x1a, 0, InlineU32(2), 0));
|
||||
for (u32 component = 0; component < 4; ++component)
|
||||
AppendStoreSgprAtLaneDwordOffset(&code, 20 + component, 0, component);
|
||||
AppendEnd(&code);
|
||||
|
||||
test.initial.resize(2 * PageSize / sizeof(u32));
|
||||
for (u32 page = 0; page < 2; ++page) {
|
||||
for (u32 component = 0; component < 4; ++component) {
|
||||
const u32 value = 0x12340000u + page * 16 + component;
|
||||
test.initial[(page * PageSize + 256) / sizeof(u32) + component] = value;
|
||||
test.expected.push_back(value);
|
||||
}
|
||||
}
|
||||
test.user_data = MakeNativeUserData(nullptr);
|
||||
test.user_data[8] = GuestBase;
|
||||
test.user_data[10] = 32 * 1024 * 1024;
|
||||
test.has_user_data = true;
|
||||
// A large guest descriptor covers separate allocations, while its SSBO view ends at the first.
|
||||
test.storage_buffer_range_bytes = PageSize;
|
||||
test.bda_mappings = {{GuestBase, 0}, {GuestBase + PageStride, PageSize}};
|
||||
test.opcodes = {O::S_LSHL_B32, O::S_BUFFER_LOAD_DWORDX4, O::V_MOV_B32,
|
||||
O::V_LSHLREV_B32, O::V_ADD_NC_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
test.compute_info.wave_size = 32;
|
||||
test.compute_info.threads_num[0] = 1;
|
||||
test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1;
|
||||
test.compute_info.group_id[0] = true;
|
||||
test.compute_info.workgroup_register = 4;
|
||||
test.has_compute_info = true;
|
||||
test.dispatch_x = 2;
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase FlatVirtualAddressRebasesGuestAllocation() {
|
||||
using O = ShaderOpcode;
|
||||
|
||||
@@ -37950,7 +38082,7 @@ void CheckGpuSelectedRecordBufferAccess(VulkanHarness &vulkan) {
|
||||
}
|
||||
// Compile against poison selectors. A separate GPU dispatch writes the actual keys.
|
||||
auto compiled = CompileCase(test, vulkan.SubgroupSize());
|
||||
if (table_bytes == 0u || components != 0u)
|
||||
if (components != 0u)
|
||||
test.forbidden_spirv = {"convert_indirect_buffer_format", "load_indirect_buffer_",
|
||||
"indirect_buffer_format_geometry"};
|
||||
else
|
||||
@@ -37966,8 +38098,8 @@ void CheckGpuSelectedRecordBufferAccess(VulkanHarness &vulkan) {
|
||||
if (opcode == spv::OpFunctionCall && words[3] == bda_function) ++bda_calls;
|
||||
offset += words.size();
|
||||
}
|
||||
Require(test.name, "empty descriptor table specialization", (bda_calls == 0u) == (table_bytes == 0u || components != 0u),
|
||||
"empty table retained BDA reads or nonempty table lost GPU descriptor loads");
|
||||
Require(test.name, "GPU descriptor reads", (bda_calls == 0u) == (components != 0u),
|
||||
"CPU-planned descriptors retained BDA reads or runtime descriptors lost them");
|
||||
for (const auto &descriptor : compiled.resources.buffers) {
|
||||
ShaderBufferResource resource{};
|
||||
std::copy_n(descriptor.dwords.begin(), 4u, resource.fields);
|
||||
@@ -41299,6 +41431,83 @@ GraphicsCase GraphicsAncillarySampleId() {
|
||||
return test;
|
||||
}
|
||||
|
||||
// SPI_BARYC_CNTL[24] selects all-bits mode for uint/bool S_FRONT_FACE;
|
||||
// float S_FRONT_FACE and __getSpecialVgprFloat(__v_front_face) leave it clear.
|
||||
GraphicsCase GraphicsFrontFaceBits(bool reversed, bool all_bits) {
|
||||
GraphicsCase test;
|
||||
test.name = all_bits ? (reversed ? "GraphicsFrontFaceUintFront" : "GraphicsFrontFaceUintBack")
|
||||
: (reversed ? "GraphicsFrontFaceFloatFront" : "GraphicsFrontFaceFloatBack");
|
||||
test.pixel_front_face = true;
|
||||
test.pixel_front_face_all_bits = all_bits;
|
||||
// Preserve every raw bit as two finite, exactly representable integer-to-float
|
||||
// results. Directly exporting the integer front value would export a NaN.
|
||||
AppendVop3(&test.fragment_code, 0x148, 4, Vgpr(2), InlineU32(0), InlineU32(16));
|
||||
AppendVop3(&test.fragment_code, 0x148, 5, Vgpr(2), InlineU32(16), InlineU32(16));
|
||||
test.fragment_code.push_back(EncodeVop1(0x06, 0, Vgpr(4)));
|
||||
test.fragment_code.push_back(EncodeVop1(0x06, 1, Vgpr(5)));
|
||||
// Integer inputs test raw zero; float inputs test the sign.
|
||||
test.fragment_code.push_back(EncodeVopc(all_bits ? 0xc2 : 0x04, InlineU32(0), 2));
|
||||
AppendVMovLiteral(&test.fragment_code, 3, 0x3f800000u);
|
||||
test.fragment_code.push_back(EncodeVop2(0x01, 2, InlineU32(0), 3));
|
||||
test.fragment_code.push_back(EncodeExp0(0x00, 0xf));
|
||||
test.fragment_code.push_back(EncodeExp1(0, 1, 2, 3));
|
||||
AppendEnd(&test.fragment_code);
|
||||
test.opcodes = {ShaderOpcode::V_BFE_U32, ShaderOpcode::V_CVT_F32_U32,
|
||||
all_bits ? ShaderOpcode::V_CMP_EQ_U32 : ShaderOpcode::V_CMP_GT_F32,
|
||||
ShaderOpcode::V_CNDMASK_B32, ShaderOpcode::V_MOV_B32,
|
||||
ShaderOpcode::EXP, ShaderOpcode::S_ENDPGM};
|
||||
test.vertices = {
|
||||
0xbf800000u, 0xbf800000u, 0x3e800000u, 0x3f000000u, 0x3f400000u, 0x3f800000u,
|
||||
0x40400000u, 0xbf800000u, 0x3e800000u, 0x3f000000u, 0x3f400000u, 0x3f800000u,
|
||||
0xbf800000u, 0x40400000u, 0x3e800000u, 0x3f000000u, 0x3f400000u, 0x3f800000u};
|
||||
if (reversed) {
|
||||
std::swap_ranges(test.vertices.begin() + 6, test.vertices.begin() + 12,
|
||||
test.vertices.begin() + 12);
|
||||
}
|
||||
// With the harness's positive-height viewport, the default triangle faces back.
|
||||
const u32 bits = all_bits ? (reversed ? UINT32_MAX : 0u)
|
||||
: (reversed ? 0x3f800000u : 0xbf800000u);
|
||||
test.expected_pixel = {
|
||||
std::bit_cast<u32>(static_cast<float>(bits & 0xffffu)),
|
||||
std::bit_cast<u32>(static_cast<float>(bits >> 16u)),
|
||||
reversed ? 0u : 0x3f800000u, 0x3f800000u};
|
||||
return test;
|
||||
}
|
||||
|
||||
void CheckPixelFrontFaceModes() {
|
||||
constexpr const char *name = "PixelFrontFaceModes";
|
||||
static const std::array<u32, 1> code = {EncodeSopp(0x01)};
|
||||
HW::PixelShaderInfo regs{};
|
||||
regs.ps_regs.data_addr = reinterpret_cast<uint64_t>(code.data());
|
||||
ShaderMapUserData(regs.ps_regs.data_addr,
|
||||
{.type = Prospero::ShaderBinaryType::kPs,
|
||||
.code_size_bytes = static_cast<u32>(sizeof(code))});
|
||||
HW::ShaderRegisters sh{};
|
||||
sh.ps_input_ena = sh.ps_input_addr = 0x1002u;
|
||||
const std::array<Prospero::ColorComponentMapping, 8> export_mapping{};
|
||||
std::vector<u32> float_key;
|
||||
for (bool all_bits : {false, true}) {
|
||||
for (u32 other_bits : {0u, 0x120u}) {
|
||||
sh.baryc_cntl = (all_bits ? 0x01000000u : 0u) | other_bits;
|
||||
ShaderPixelInputInfo pixel{};
|
||||
(void)PrepareProgram(regs, sh, export_mapping, pixel);
|
||||
Require(name, "register format decoding",
|
||||
pixel.ps_front_face && pixel.ps_front_face_all_bits == all_bits &&
|
||||
pixel.ps_system_input_base == 2u,
|
||||
"SPI_BARYC_CNTL[24] did not select the front-face format");
|
||||
std::vector<u32> key;
|
||||
BuildStageStaticKey(pixel, key);
|
||||
if (!all_bits && other_bits == 0u) {
|
||||
float_key = key;
|
||||
}
|
||||
Require(name, "format specialization",
|
||||
(key != float_key) == all_bits,
|
||||
"front-face formats must have distinct shader keys and ignore unrelated bits");
|
||||
}
|
||||
}
|
||||
std::printf("[host] %-32s ok\n", name);
|
||||
}
|
||||
|
||||
GraphicsCase GraphicsFlatInterpolatorExport() {
|
||||
using O = ShaderOpcode;
|
||||
|
||||
@@ -41865,6 +42074,7 @@ std::vector<TestCase> MakeCases() {
|
||||
AddCase(ScalarLoadAlignsDynamicBase);
|
||||
cases.push_back(ScalarBufferFromLoopReadlane(32));
|
||||
cases.push_back(ScalarBufferFromLoopReadlane(64));
|
||||
AddCase(ScalarBufferLoadsDisjointPages);
|
||||
AddCase(BufferLoadStore);
|
||||
AddCase(BufferLoadDwordOffenIdxenUsesVaddrPlusOneOffset);
|
||||
AddCase(BufferStoreDwordOffenIdxenUsesVaddrPlusOneOffset);
|
||||
@@ -42179,6 +42389,10 @@ std::vector<GraphicsCase> MakeGraphicsCases() {
|
||||
GraphicsAncillaryLayer(false),
|
||||
GraphicsAncillaryLayer(true),
|
||||
GraphicsAncillarySampleId(),
|
||||
GraphicsFrontFaceBits(false, false),
|
||||
GraphicsFrontFaceBits(true, false),
|
||||
GraphicsFrontFaceBits(false, true),
|
||||
GraphicsFrontFaceBits(true, true),
|
||||
GraphicsFlatInterpolatorExport(),
|
||||
GraphicsDsAddtidScratchExport(),
|
||||
GraphicsDirectSgprPushConstantExport(),
|
||||
@@ -47858,6 +48072,9 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--indirect-buffer-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, ScalarBufferLoadsDisjointPages());
|
||||
RunCase(&vulkan, ScalarMemoryLoadVariants());
|
||||
RunCase(&vulkan, ScalarBufferOffsetAlignmentAndCarry());
|
||||
RunCase(&vulkan, ScalarBufferFromLoopReadlane(32));
|
||||
RunCase(&vulkan, ScalarBufferFromLoopReadlane(64));
|
||||
CheckBoundedFormattedBufferCopy(vulkan);
|
||||
@@ -47959,6 +48176,17 @@ int main(int argc, char **argv) {
|
||||
RunCase(&vulkan, SiblingSharedExitKeepsCapturedConditions());
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--front-face-only") == 0) {
|
||||
CheckPixelFrontFaceModes();
|
||||
VulkanHarness vulkan;
|
||||
for (bool all_bits : {false, true}) {
|
||||
for (bool reversed : {false, true}) {
|
||||
RunGraphicsCase(&vulkan, GraphicsFrontFaceBits(reversed, all_bits));
|
||||
}
|
||||
}
|
||||
RunGraphicsCase(&vulkan, GraphicsAncillaryLayer(true));
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--position-w-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunGraphicsCase(&vulkan, GraphicsPositionWExport());
|
||||
@@ -48378,6 +48606,7 @@ int main(int argc, char **argv) {
|
||||
if (argc == 2 && std::strcmp(argv[1], "--bda-page-table-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
vulkan.CheckBdaPageTableUploads();
|
||||
vulkan.CheckBdaSyncUploads();
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--buffer-cache-range-only") == 0) {
|
||||
@@ -48574,6 +48803,7 @@ int main(int argc, char **argv) {
|
||||
CheckEmbeddedFetchLaneSpill();
|
||||
CheckTessellationPrograms();
|
||||
CheckPixelParameterAliases();
|
||||
CheckPixelFrontFaceModes();
|
||||
CheckRectListShaders();
|
||||
CheckSigned16ImageStore(vulkan);
|
||||
CheckBoundedFormattedBufferCopy(vulkan);
|
||||
@@ -48607,6 +48837,7 @@ int main(int argc, char **argv) {
|
||||
CheckComputeLdsLimit(vulkan);
|
||||
vulkan.CheckNullTextureDescriptors();
|
||||
vulkan.CheckBdaPageTableUploads();
|
||||
vulkan.CheckBdaSyncUploads();
|
||||
vulkan.CheckLargeSparseImageUpload();
|
||||
vulkan.CheckUnifiedTextureCacheFlow();
|
||||
vulkan.CheckUnifiedImageViewCache();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "common/file.h"
|
||||
#include "common/localization.h"
|
||||
#include "common/trophies.h"
|
||||
|
||||
#include <chrono>
|
||||
@@ -8,6 +9,7 @@
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -20,6 +22,65 @@ void Check(bool value, const char *text) {
|
||||
}
|
||||
}
|
||||
|
||||
void TestLocalization() {
|
||||
struct Translation {
|
||||
std::string_view locale;
|
||||
std::string_view earned;
|
||||
};
|
||||
static constexpr Translation translations[] = {
|
||||
{"ja-JP", "トロフィーを獲得しました!"},
|
||||
{"en-US", "Trophy earned!"},
|
||||
{"fr-FR", "Trophée obtenu !"},
|
||||
{"es-ES", "¡Ganaste un trofeo!"},
|
||||
{"de-DE", "Trophäe erhalten!"},
|
||||
{"it-IT", "Trofeo ottenuto!"},
|
||||
{"nl-NL", "Trofee behaald!"},
|
||||
{"pt-PT", "Troféu conquistado!"},
|
||||
{"ru-RU", "Трофей получен!"},
|
||||
{"ko-KR", "트로피를 획득했습니다!"},
|
||||
{"zh-Hant", "已獲得獎盃!"},
|
||||
{"zh-Hans", "已获得奖杯!"},
|
||||
{"fi-FI", "Pokaali ansaittu!"},
|
||||
{"sv-SE", "Trofé intjänad!"},
|
||||
{"da-DK", "Trofæ optjent!"},
|
||||
{"no-NO", "Trofé opptjent!"},
|
||||
{"pl-PL", "Zdobyto trofeum!"},
|
||||
{"pt-BR", "Troféu conquistado!"},
|
||||
{"en-GB", "Trophy earned!"},
|
||||
{"tr-TR", "Kupa kazanıldı!"},
|
||||
{"es-419", "¡Ganaste un trofeo!"},
|
||||
{"ar-AE", "تم كسب الكأس!"},
|
||||
{"fr-CA", "Trophée obtenu !"},
|
||||
{"cs-CZ", "Trofej získána!"},
|
||||
{"hu-HU", "Trófea megszerezve!"},
|
||||
{"el-GR", "Κερδίσατε ένα τρόπαιο!"},
|
||||
{"ro-RO", "Trofeu câștigat!"},
|
||||
{"th-TH", "ได้รับถ้วยรางวัลแล้ว!"},
|
||||
{"vi-VN", "Đã nhận cúp!"},
|
||||
{"id-ID", "Trofi diperoleh!"},
|
||||
};
|
||||
constexpr std::string_view key = "trophy.toast.earned";
|
||||
const auto japanese = Common::Localization::Get(0, key);
|
||||
int language = 0;
|
||||
for (const auto &translation : translations) {
|
||||
Check(Common::Localization::LocaleName(language) == translation.locale,
|
||||
"console language selects its locale");
|
||||
Check(Common::Localization::Get(language, key) == translation.earned,
|
||||
"embedded notification preserves translated UTF-8 bytes");
|
||||
++language;
|
||||
}
|
||||
Check(japanese == translations[0].earned,
|
||||
"translation view survives other locale lookups");
|
||||
for (const int invalid : {-1, 30, 1000}) {
|
||||
Check(Common::Localization::LocaleName(invalid) == "en-US" &&
|
||||
Common::Localization::Get(invalid, key) == "Trophy earned!",
|
||||
"invalid console language falls back to English");
|
||||
}
|
||||
Check(Common::Localization::Get(0, "missing.translation") ==
|
||||
"missing.translation",
|
||||
"unknown translation returns its key");
|
||||
}
|
||||
|
||||
class TempDirectory {
|
||||
public:
|
||||
TempDirectory() {
|
||||
@@ -252,6 +313,7 @@ void TestUnlockPersistence(const std::filesystem::path &directory) {
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
TestLocalization();
|
||||
TempDirectory directory;
|
||||
TestEventExtraction(directory.Path());
|
||||
TestProgress(directory.Path());
|
||||
|
||||
+155
-62
@@ -5417,8 +5417,7 @@ void TestNewShaderRecompilerScalarMemoryBindingDomains() {
|
||||
Check(index < program.memory_info.size(),
|
||||
"typed scalar memory operation has invalid metadata");
|
||||
const auto &memory = program.memory_info[index];
|
||||
Check(memory.kind == kind && memory.resource == 0u &&
|
||||
!memory.planning_only,
|
||||
Check(memory.kind == kind && !memory.planning_only,
|
||||
"typed scalar memory operation has the wrong live domain");
|
||||
count++;
|
||||
}
|
||||
@@ -5478,17 +5477,15 @@ void TestNewShaderRecompilerScalarMemoryBindingDomains() {
|
||||
|
||||
auto buffer = RecompileForTest(buffer_shader, buffer_options);
|
||||
const auto& buffer_counts = buffer.program.bindings.descriptor_counts;
|
||||
Check(buffer.program.info.buffers.size() == 1u &&
|
||||
buffer.program.info.buffers[0].scalar &&
|
||||
buffer.program.info.buffers[0].descriptor_index == 0 &&
|
||||
!buffer.program.info.uses_dma &&
|
||||
buffer_counts[static_cast<size_t>(BindingKind::Buffers)] == 1 &&
|
||||
buffer_counts[static_cast<size_t>(BindingKind::BdaPagetable)] == 0 &&
|
||||
Check(buffer.program.info.buffers.empty() && buffer.program.info.uses_dma &&
|
||||
buffer_counts[static_cast<size_t>(BindingKind::Buffers)] == 0 &&
|
||||
buffer_counts[static_cast<size_t>(BindingKind::BdaPagetable)] == 1 &&
|
||||
buffer_counts[static_cast<size_t>(BindingKind::FaultBuffer)] == 1 &&
|
||||
buffer_counts[static_cast<size_t>(BindingKind::FlattenedSrt)] == 0,
|
||||
"descriptor scalar load did not use only the buffer domain");
|
||||
"descriptor scalar load did not use only the DMA domain");
|
||||
Check(count_live_memory_ops(
|
||||
buffer.program, ShaderRecompiler::IR::ValueOpcode::ReadConstBuffer,
|
||||
ShaderRecompiler::IR::ResourceKind::ScalarBuffer) == 1u,
|
||||
ShaderRecompiler::IR::ResourceKind::IndirectBuffer) == 1u,
|
||||
"descriptor scalar load did not remain a live typed buffer operation");
|
||||
CheckSpirvBinaryValidates(buffer.spirv);
|
||||
}
|
||||
@@ -9027,6 +9024,55 @@ void TestNewShaderRecompilerCfgExecSccSharedArm() {
|
||||
CheckSpirvBinaryValidates(result.spirv);
|
||||
}
|
||||
|
||||
std::vector<std::pair<uint32_t, std::array<uint32_t, 4>>> ScalarDescriptorsOnUniformPath(
|
||||
TestCompileResult& result, std::span<const uint32_t> user_data) {
|
||||
using namespace ShaderRecompiler::IR;
|
||||
using ShaderRecompiler::CFG::TerminatorKind;
|
||||
// These acyclic fixtures have uniform branches and descriptor loads already flattened.
|
||||
for (auto* block : result.program.blocks) {
|
||||
for (auto& inst : *block) {
|
||||
if (inst.GetOpcode() != ValueOpcode::ReadConst) continue;
|
||||
const auto slot = inst.Arg(1).Resolve().U32();
|
||||
Check(slot < result.resources.flattened_srt.size(), "scalar descriptor has an invalid SRT slot");
|
||||
inst.ReplaceUsesWith(Value(result.resources.flattened_srt[slot]));
|
||||
}
|
||||
}
|
||||
SrtWalker walker(result.program, {.user_data = user_data});
|
||||
std::vector<std::pair<uint32_t, std::array<uint32_t, 4>>> descriptors;
|
||||
Block* previous = nullptr;
|
||||
auto* block = result.program.blocks.front();
|
||||
for (size_t step = 0; step < result.program.blocks.size(); ++step) {
|
||||
for (auto& inst : *block) {
|
||||
if (inst.GetOpcode() == ValueOpcode::Phi) {
|
||||
size_t incoming = 0;
|
||||
while (incoming < inst.NumArgs() && inst.PhiBlock(incoming) != previous) ++incoming;
|
||||
Check(incoming < inst.NumArgs(), "descriptor Phi lost the executed predecessor");
|
||||
inst.ReplaceUsesWith(inst.Arg(incoming));
|
||||
} else if (inst.GetOpcode() == ValueOpcode::ReadConstBuffer) {
|
||||
const auto* handle = inst.Arg(0).ResolveInstruction();
|
||||
Check(handle && handle->GetOpcode() == ValueOpcode::GetBufferResource && handle->NumArgs() == 4u,
|
||||
"scalar read lost its raw buffer descriptor");
|
||||
auto& [pc, words] = descriptors.emplace_back();
|
||||
pc = inst.Flags<MemoryFlags>().pc;
|
||||
for (uint32_t word = 0; word < words.size(); ++word)
|
||||
Check(walker.Evaluate(handle->Arg(word), words[word]), "scalar descriptor word is not evaluable");
|
||||
}
|
||||
}
|
||||
const auto& term = block->terminator;
|
||||
if (term.kind == TerminatorKind::Return) return descriptors;
|
||||
Check(term.kind == TerminatorKind::Branch || term.kind == TerminatorKind::ConditionalBranch,
|
||||
"descriptor fixture has unsupported control flow");
|
||||
uint32_t condition = 1u;
|
||||
if (term.kind == TerminatorKind::ConditionalBranch)
|
||||
Check(walker.Evaluate(block->condition, condition), "descriptor branch is not uniform");
|
||||
previous = block;
|
||||
block = condition != 0u ? term.true_block : term.false_block;
|
||||
Check(block != nullptr, "descriptor fixture lost its branch target");
|
||||
}
|
||||
Check(false, "descriptor fixture unexpectedly contains a loop");
|
||||
return descriptors;
|
||||
}
|
||||
|
||||
void TestSharedReturnPreservesDescriptorDominance() {
|
||||
const uint32_t shader[] = {
|
||||
EncodeSmem0(0x02, 32, 14), (125u << 25u) | 0x60u,
|
||||
@@ -9072,9 +9118,12 @@ void TestSharedReturnPreservesDescriptorDominance() {
|
||||
auto options = MakeCompileOptions(ShaderType::Pixel);
|
||||
options.user_data = user_data;
|
||||
auto result = RecompileForTest(shader, options, ReadHostTestMemory);
|
||||
Check(!result.program.dispatcher_fallback &&
|
||||
result.resources.buffers.size() == 1u &&
|
||||
result.resources.buffers[0].dwords[0] == 0x2000u,
|
||||
Check(!result.program.dispatcher_fallback && result.resources.buffers.empty() &&
|
||||
result.program.info.uses_dma,
|
||||
"surviving scalar operation did not retain structured BDA access");
|
||||
const auto descriptors = ScalarDescriptorsOnUniformPath(result, user_data);
|
||||
Check(descriptors.size() == 1u && descriptors[0].first == 0x20u &&
|
||||
std::equal(descriptors[0].second.begin(), descriptors[0].second.end(), table.begin() + 0x50u / 4u),
|
||||
"return-only descriptor reached the surviving buffer operation");
|
||||
CheckSpirvBinaryValidates(result.spirv);
|
||||
}
|
||||
@@ -9131,24 +9180,18 @@ void TestNestedSelectionPreservesDescriptorSources() {
|
||||
auto options = MakeCompileOptions(ShaderType::Compute);
|
||||
options.user_data_base = 8;
|
||||
options.user_data = user_data;
|
||||
const auto result = RecompileForTest(shader, options, ReadHostTestMemory);
|
||||
Check(!result.program.dispatcher_fallback && result.resources.buffers.size() == 4u,
|
||||
"nested descriptor selection lost its structured resource bindings");
|
||||
for (size_t i = 0; i < result.program.info.buffers.size(); ++i) {
|
||||
const auto pc = result.program.info.buffers[i].first_use_pc;
|
||||
Check(pc == 8u * 4u || pc == 14u * 4u || pc == direct_arm * 4u || pc == join * 4u,
|
||||
"nested descriptor selection lost a native source location");
|
||||
const auto selected_pc = (mode == 0u ? 8u : mode == 1u ? 14u : direct_arm) * 4u;
|
||||
const auto *expected = pc == 8u * 4u ? table.data()
|
||||
: pc == 14u * 4u ? table.data() + 4
|
||||
: pc == direct_arm * 4u ? user_data.data() + 8 : user_data.data() + 4;
|
||||
const auto &descriptor = result.resources.buffers[i];
|
||||
const bool unused = pc != selected_pc && pc != join * 4u;
|
||||
Check(descriptor.dword_count == 4 &&
|
||||
(std::equal(expected, expected + 4, descriptor.dwords.begin()) ||
|
||||
(unused && std::ranges::all_of(descriptor.dwords, [](auto word) { return word == 0; }))),
|
||||
"nested descriptor selection chose another arm's descriptor");
|
||||
}
|
||||
auto result = RecompileForTest(shader, options, ReadHostTestMemory);
|
||||
Check(!result.program.dispatcher_fallback && result.resources.buffers.size() == 1u &&
|
||||
result.program.info.buffers[0].first_use_pc == join * 4u &&
|
||||
std::equal(user_data.begin() + 4, user_data.begin() + 8,
|
||||
result.resources.buffers[0].dwords.begin()),
|
||||
"nested descriptor selection lost its structured output binding");
|
||||
const auto descriptors = ScalarDescriptorsOnUniformPath(result, user_data);
|
||||
const auto selected_pc = (mode == 0u ? 8u : mode == 1u ? 14u : direct_arm) * 4u;
|
||||
const auto* expected = mode == 0u ? table.data() : mode == 1u ? table.data() + 4 : user_data.data() + 8;
|
||||
Check(descriptors.size() == 1u && descriptors[0].first == selected_pc &&
|
||||
std::equal(expected, expected + 4, descriptors[0].second.begin()),
|
||||
"nested descriptor selection chose another arm's descriptor");
|
||||
CheckSpirvBinaryValidates(result.spirv);
|
||||
}
|
||||
}
|
||||
@@ -9207,16 +9250,16 @@ void TestSharedExitPreservesNativeDescriptorSources() {
|
||||
user_data[2] = arm;
|
||||
auto options = MakeCompileOptions(ShaderType::Pixel);
|
||||
options.user_data = user_data;
|
||||
const auto result = RecompileForTest(shader, options, ReadHostTestMemory);
|
||||
Check(!result.program.dispatcher_fallback && result.resources.buffers.size() == 2u,
|
||||
"shared-exit descriptor selection lost its structured resource bindings");
|
||||
for (size_t i = 0; i < result.program.info.buffers.size(); ++i) {
|
||||
const auto pc = result.program.info.buffers[i].first_use_pc;
|
||||
const uint32_t expected = pc == 14u * 4u ? table[8]
|
||||
: pc == 22u * 4u ? table[12] : 0u;
|
||||
Check(expected != 0u && result.resources.buffers[i].dwords[0] == expected,
|
||||
"shared-exit descriptor selection retained an overwritten native source");
|
||||
}
|
||||
auto result = RecompileForTest(shader, options, ReadHostTestMemory);
|
||||
Check(!result.program.dispatcher_fallback && result.resources.buffers.empty() &&
|
||||
result.program.info.uses_dma,
|
||||
"shared-exit descriptor selection lost its structured scalar reads");
|
||||
const auto descriptors = ScalarDescriptorsOnUniformPath(result, user_data);
|
||||
const auto selected_pc = (arm == 0u ? 22u : 14u) * 4u;
|
||||
const auto* expected = table.data() + (arm == 0u ? 12u : 8u);
|
||||
Check(descriptors.size() == 1u && descriptors[0].first == selected_pc &&
|
||||
std::equal(expected, expected + 4, descriptors[0].second.begin()),
|
||||
"shared-exit descriptor selection retained an overwritten native source");
|
||||
CheckSpirvBinaryValidates(result.spirv);
|
||||
}
|
||||
}
|
||||
@@ -12874,6 +12917,51 @@ void TestLateConstantPropagation() {
|
||||
"an ungrounded Phi cycle acquired a constant");
|
||||
}
|
||||
|
||||
void TestMaskedShiftConstantPropagation() {
|
||||
using namespace ShaderRecompiler::IR;
|
||||
for (const uint32_t count : {0u, 1u, 8u, 31u, 32u, 33u, 40u, 63u, 64u, UINT32_MAX}) {
|
||||
for (const uint32_t mask : {0u, 1u, 0xffu, 0x7fffffffu, 0x80000000u, UINT32_MAX}) {
|
||||
for (const bool swapped : {false, true}) {
|
||||
for (const bool shared : {false, true}) {
|
||||
Block block;
|
||||
IREmitter ir(&block);
|
||||
const auto input = ir.GetUserData(static_cast<ScalarReg>(0));
|
||||
const auto shifted = ir.Emit(ValueOpcode::ShiftLeftLogical32, {input, Value(count)});
|
||||
const auto result = ir.Emit(ValueOpcode::BitwiseAnd32,
|
||||
swapped ? std::initializer_list<Value>{Value(mask), shifted}
|
||||
: std::initializer_list<Value>{shifted, Value(mask)});
|
||||
const auto other = shared ? ir.Emit(ValueOpcode::IAdd32, {shifted, Value(1u)}) : Value{};
|
||||
ConstantPropagationPass({&block});
|
||||
const bool zero = ((UINT32_MAX << (count & 31u)) & mask) == 0u;
|
||||
Check(zero ? result.Resolve() == Value(0u) : !result.Resolve().IsImmediate(),
|
||||
"masked shift retained impossible bits or discarded possible bits");
|
||||
if (shared) {
|
||||
Check(other.Resolve().Instruction()->Arg(0).Resolve() == shifted.Resolve(),
|
||||
"folding a masked shift changed its other consumer");
|
||||
}
|
||||
constexpr uint32_t source = 0xa5f03c69u;
|
||||
input.Instruction()->ReplaceUsesWith(Value(source));
|
||||
ConstantPropagationPass({&block});
|
||||
const uint32_t expected = source << (count & 31u);
|
||||
Check(result.Resolve() == Value(expected & mask) &&
|
||||
(!shared || other.Resolve() == Value(expected + 1u)),
|
||||
"masked shift folding changed a runtime result");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Block block;
|
||||
IREmitter ir(&block);
|
||||
const auto input = ir.GetUserData(static_cast<ScalarReg>(0));
|
||||
const auto count = ir.GetUserData(static_cast<ScalarReg>(1));
|
||||
const auto shifted = ir.Emit(ValueOpcode::ShiftLeftLogical32, {input, count});
|
||||
const auto result = ir.Emit(ValueOpcode::BitwiseAnd32, {shifted, Value(0xffu)});
|
||||
ConstantPropagationPass({&block});
|
||||
Check(result.Resolve().Instruction()->GetOpcode() == ValueOpcode::BitwiseAnd32,
|
||||
"a variable shift count was treated as a constant");
|
||||
}
|
||||
|
||||
void TestU64ShiftConstantPropagation() {
|
||||
using namespace ShaderRecompiler::IR;
|
||||
|
||||
@@ -14076,10 +14164,12 @@ void TestNewShaderRecompilerNativeBindingPlan() {
|
||||
storage = kind;
|
||||
}
|
||||
}
|
||||
Check(counts[static_cast<size_t>(BindingKind::Buffers)] == 2 &&
|
||||
Check(counts[static_cast<size_t>(BindingKind::Buffers)] == 1 &&
|
||||
result.program.info.buffers.size() == 1u &&
|
||||
result.program.info.buffers[0].descriptor_index == 0 &&
|
||||
result.program.info.buffers[1].descriptor_index == 1,
|
||||
"native emission did not allocate scalar/vector buffer slots");
|
||||
counts[static_cast<size_t>(BindingKind::BdaPagetable)] == 1 &&
|
||||
counts[static_cast<size_t>(BindingKind::FaultBuffer)] == 1,
|
||||
"native emission did not allocate the vector buffer and scalar BDA slots");
|
||||
Check(sampled && counts[static_cast<size_t>(*sampled)] == 1,
|
||||
"native emission did not allocate the sampled image");
|
||||
Check(storage && counts[static_cast<size_t>(*storage)] == 1,
|
||||
@@ -14090,7 +14180,8 @@ void TestNewShaderRecompilerNativeBindingPlan() {
|
||||
"native emission did not allocate the sampler");
|
||||
Check(SpirvContainsOpcode(result.spirv, 86),
|
||||
"SPIR-V binary does not combine separate image/sampler descriptors");
|
||||
for (const auto kind : {BindingKind::Buffers, *sampled, BindingKind::Samplers}) {
|
||||
for (const auto kind : {BindingKind::Buffers, BindingKind::BdaPagetable,
|
||||
BindingKind::FaultBuffer, *sampled, BindingKind::Samplers}) {
|
||||
Check(SpirvHasDecorationValue(result.spirv, 33u,
|
||||
ShaderRecompiler::IR::NativeBinding(result.program.stage, kind)),
|
||||
"SPIR-V lacks a resource Binding decoration");
|
||||
@@ -14113,10 +14204,10 @@ void TestNewShaderRecompilerNativeBindingPlan() {
|
||||
for (auto *block : result.program.blocks) {
|
||||
const auto found = std::ranges::find_if(*block, [](const auto &inst) {
|
||||
return inst.GetOpcode() ==
|
||||
ShaderRecompiler::IR::ValueOpcode::GetBufferResource;
|
||||
ShaderRecompiler::IR::ValueOpcode::StoreBufferU32;
|
||||
});
|
||||
if (found != block->end()) {
|
||||
buffer_handle = &*found;
|
||||
buffer_handle = found->Arg(0).ResolveInstruction();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -14290,13 +14381,11 @@ void TestComputeImageFill() {
|
||||
0xbf8cc07fu, 0x7e06026au, 0xd7460001u, 0x0405060du,
|
||||
0x7e04020eu, 0xf0200128u, 0x00000300u, 0xbf810000u,
|
||||
};
|
||||
uint32_t value = 0x7bu;
|
||||
const auto address = reinterpret_cast<uint64_t>(&value);
|
||||
constexpr uint32_t value = 0x7bu;
|
||||
const std::array<uint32_t, 12> userdata = {
|
||||
0x20021000u, 0xc0500000u, 0x01d8c347u, 0xd1800204u,
|
||||
0u, 0x00700000u, 0u, 0u,
|
||||
static_cast<uint32_t>(address),
|
||||
static_cast<uint32_t>(address >> 32) | (4u << 16), 1u, 0x14204u,
|
||||
0x1000u, 4u << 16u, 1u, 0x14204u,
|
||||
};
|
||||
ShaderComputeInputInfo compute{};
|
||||
compute.threads_num[0] = compute.threads_num[1] = 8;
|
||||
@@ -14307,9 +14396,14 @@ void TestComputeImageFill() {
|
||||
auto options = MakeCompileOptions(ShaderType::Compute);
|
||||
options.input_info.compute = &compute;
|
||||
options.user_data = userdata;
|
||||
enum class Mutation { None, Offset, WrongAxis, VaryingValue, Predicate, ExtraStore, A16 };
|
||||
const auto Run = [&](Mutation mutation, bool clean = true) {
|
||||
auto translated = ShaderRecompiler::TranslateProgram(shader, options);
|
||||
enum class Mutation { None, RuntimeScalar, Offset, WrongAxis, VaryingValue, Predicate, ExtraStore, A16 };
|
||||
const auto Run = [&](Mutation mutation) {
|
||||
auto code = std::to_array(shader);
|
||||
if (mutation != Mutation::RuntimeScalar) {
|
||||
code[2] = EncodeSMovB32(106, 255);
|
||||
code[3] = value;
|
||||
}
|
||||
auto translated = ShaderRecompiler::TranslateProgram(code, options);
|
||||
auto &program = translated.program;
|
||||
for (auto *block : program.blocks) {
|
||||
const auto found = std::ranges::find(*block, ValueOpcode::ImageWrite, &Inst::GetOpcode);
|
||||
@@ -14320,7 +14414,8 @@ void TestComputeImageFill() {
|
||||
Check(coordinates && values, "stencil fill fixture lost its image operands");
|
||||
const auto x = coordinates->Arg(0);
|
||||
switch (mutation) {
|
||||
case Mutation::None: break;
|
||||
case Mutation::None:
|
||||
case Mutation::RuntimeScalar: break;
|
||||
case Mutation::Offset:
|
||||
coordinates->SetArg(0, Value(&*block->PrependNewInst(found, ValueOpcode::IAdd32,
|
||||
{x, Value(1u)})));
|
||||
@@ -14345,15 +14440,12 @@ void TestComputeImageFill() {
|
||||
auto plan = ExtractResourcePlan(program);
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
Check(MaterializeResources(plan,
|
||||
{.user_data = userdata, .read_memory = ReadHostTestMemory,
|
||||
.read_specialization_memory = clean ? ReadHostTestMemory : nullptr},
|
||||
snapshot, specialization),
|
||||
Check(MaterializeResources(plan, {.user_data = userdata}, snapshot, specialization),
|
||||
"captured stencil clear did not materialize");
|
||||
const auto &fill = snapshot.uniform_fill;
|
||||
const bool expected = mutation == Mutation::None && clean;
|
||||
const bool expected = mutation == Mutation::None;
|
||||
Check((fill.kind == UniformFillKind::Image) == expected,
|
||||
"image fill proof accepted an unsafe store or missed the captured clear");
|
||||
"image fill proof accepted a runtime read or unsafe store, or missed a constant clear");
|
||||
if (expected) {
|
||||
Check(fill.resource == 0 && fill.words == 1 && fill.value == value &&
|
||||
fill.group_stride == std::array<uint32_t, 3>{8, 8, 1},
|
||||
@@ -14361,7 +14453,7 @@ void TestComputeImageFill() {
|
||||
}
|
||||
};
|
||||
Run(Mutation::None);
|
||||
Run(Mutation::None, false);
|
||||
Run(Mutation::RuntimeScalar);
|
||||
for (const auto mutation : {Mutation::Offset, Mutation::WrongAxis, Mutation::VaryingValue,
|
||||
Mutation::Predicate, Mutation::ExtraStore, Mutation::A16})
|
||||
Run(mutation);
|
||||
@@ -16364,6 +16456,7 @@ int main() {
|
||||
TestValuePhiValidation();
|
||||
TestLaneMaskProjection();
|
||||
TestLateConstantPropagation();
|
||||
TestMaskedShiftConstantPropagation();
|
||||
TestU64ShiftConstantPropagation();
|
||||
TestU64RegisterPairRoundTrip();
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
||||
|
||||
Reference in new issue
Block a user