Compare commits

..
Author SHA1 Message Date
crevant 8b095caefe perf(files): move directory-entry names instead of copying them (#758) 2026-09-22 12:26:06 +04:00
nmzik 8dc9b9d532 Persist save allocations for search and mount capacity queries 2026-09-22 11:38:50 +04:00
e40438f95e Decode ATRAC9 streams after complete RIFF headers
Co-authored-by: Techx3 <56268269+Techx3@users.noreply.github.com>
Co-authored-by: Ali Almohaya <17254073+Almo7aya@users.noreply.github.com>
2026-09-22 11:38:50 +04:00
nmzik a6a56c8a44 Remove obsolete partial indirect draw warnings 2026-09-22 11:38:50 +04:00
nmzik 4b074cf9e9 Avoid heap allocation when compacting materialized shader images 2026-09-22 11:38:50 +04:00
nmzikandAli Almohaya 7c9e18e73b Ignore inactive metadata address bits in sampled depth descriptors
Co-authored-by: Ali Almohaya <17254073+Almo7aya@users.noreply.github.com>
2026-09-22 11:38:50 +04:00
nmzik d2df2cad74 Resolve draw-uniform image descriptors through scalar buffer chains 2026-09-22 11:38:50 +04:00
nmzikandBipin b2a80a3a96 Resolve GPU-selected raw buffer descriptors through shared RDNA2 addressing
Co-authored-by: Bipin <87369440+bipinkrish@users.noreply.github.com>
2026-09-22 11:38:50 +04:00
nmzik cfb8f7b246 Decode unsigned byte sources for SDWA half-float conversion 2026-09-22 11:11:36 +04:00
nmzik d792dfa885 Reject AvPlayer pause and seek before playback 2026-09-22 11:11:36 +04:00
nmzik 12ff571609 Preserve AvPlayer sync mode before source creation 2026-09-22 11:11:36 +04:00
nmzik d3c182f385 Centralize attachment sample validation before acquisition 2026-09-22 11:11:36 +04:00
nmzik bf4dcdce52 Accept resolved FMASK metadata with native MSAA 2026-09-22 11:11:36 +04:00
nmzik 476204cd4b Initialize native NGG vertex launch state and wave size 2026-09-22 11:11:36 +04:00
nmzik f9e56ef8a0 Scope native barriers to shaders with shared execution 2026-09-22 11:11:36 +04:00
nmzik 61761b65f2 Honor PS5 title output-resolution detection permission 2026-09-22 11:11:36 +04:00
nmzik ba55ba5dc5 Allow storage access to PS5 cubemap faces 2026-09-21 00:49:54 +04:00
nmzik c997cdb837 Skip zero-valued indirect CX 0x24a 2026-09-21 00:49:54 +04:00
Kody Schram fe4f9423e3 Significantly Reduce Linux Release Size (#738)
Added symbol stripping for linux build, drops fs from 120 mb to 20mb
2026-09-20 21:26:24 +04:00
nmzik 81af130147 Avoid game list rescans on settings changes/Remove Space pause shortcut 2026-09-20 21:07:36 +04:00
Saadiandnmzik ada0514084 launcher: add a game list refresh action (#683)
* launcher: add a game list refresh action

Dropping new games into an already-configured game folder was only
picked up after restarting the launcher or re-saving the global
settings dialog. Add a refresh toolbar button, an F5 shortcut, and a
"Refresh game list" context-menu entry, all triggering a rescan of the
game folders.

* launcher: do not rescan the game list while a game is running

Clearing cfgs_list destroys the running ConfigurationItem: the running
state is lost and the replacement items allow editing or deleting a
game whose process is still active. Skip the rescan while any item is
running (CodeRabbit review finding for the refresh action).

* launcher: preserve game state when refreshing the list

Share one refresh action across the toolbar, keyboard shortcut, and context menu. Retain running items during rescans, update their settings for the next launch, and preserve selection, filtering, and sort order.

Commit active inline editors before rebuilding rows to avoid losing comments or accessing destroyed configuration data. Tie editor callbacks to item lifetime and guard the context-menu item reference.

Validation: launcher build and startup check passed; 32 Qt interaction checks passed.

* launcher: remove refresh from the game context menu

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-20 20:44:40 +04:00
Ilyasandnmzik d7da244475 Add automatic sccache/ccache detection to CMake and remove deprecated Ninja setup action (#734)
* remove outdated ninja setup action

* Remove deprecated ninja setup action

The seanmiddleditch/gha-setup-ninja action has been archived by its
maintainer, who explicitly states that Ninja is now pre-installed on
all GitHub-hosted runners. This removes the action from all three
build jobs (Windows, macOS, Linux) and removes ninja from the package
install lists on macOS and Linux.

* Add automatic ccache/sccache detection to CMakeLists.txt

Detects sccache (preferred on Windows/MSVC) or ccache at configure time
and sets CMAKE_C_COMPILER_LAUNCHER / CMAKE_CXX_COMPILER_LAUNCHER
automatically. Works for local builds without manual flags.

Priority: sccache > ccache (sccache handles MSVC PDBs better).

* Delete github-actions-version-research.md

* Respect user-specified CMAKE_C_COMPILER_LAUNCHER / CMAKE_CXX_COMPILER_LAUNCHER

Only auto-detect sccache/ccache if no launcher was explicitly provided
via -DCMAKE_C_COMPILER_LAUNCHER=... or -DCMAKE_CXX_COMPILER_LAUNCHER=...

* Preserve explicit compiler launcher settings per language

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-20 19:49:59 +04:00
Elijaandnmzik 549495575c shader: add S_BITCMP0_B64 and S_BITCMP1_B64 (#688)
* shader: add S_BITCMP0_B64 and S_BITCMP1_B64

* test: move S_BITCMP_B64 coverage to compute suite

* test: use the existing compute CTest entry for bit comparisons

* test: remove the dedicated S_BITCMP_B64 command-line selector

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-20 19:36:12 +04:00
Maayan080andnmzik c3cc8454b9 common: remove unused recursive file enumeration (#681)
common: don't follow reparse points in the Windows recursive file walk

SysFileFindFiles recurses into every entry carrying
FILE_ATTRIBUTE_DIRECTORY, which on Windows includes directory symlinks
and junctions. A junction pointing at one of its own ancestors makes the
walk recurse until the stack runs out.

The Linux implementation already guards against this: it uses lstat
rather than stat specifically so a symlink is never followed into a
cycle. Skip reparse-point entries on Windows for the same reason.

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-20 18:50:57 +04:00
Saadi b8ad1d8c4b common: return real Win32 cache flags from GetCacheAccessType (#673)
GetCacheAccessType returned the sys_file_cache_type_t enum values as
CreateFileW flags: SYS_FILE_CACHE_SEQUENTIAL_SCAN (2) became
FILE_ATTRIBUTE_HIDDEN and SYS_FILE_CACHE_AUTO (0) lost the normal
attributes, and no FILE_FLAG_SEQUENTIAL_SCAN hint was ever applied.
Return FILE_FLAG_SEQUENTIAL_SCAN / FILE_ATTRIBUTE_NORMAL instead,
mirroring the POSIX fadvise handling on Linux.
2026-09-20 18:47:25 +04:00
Elija cfb110e0aa shader: allow partial SDWA destinations for V_NOT_B32 (#684) 2026-09-20 18:21:40 +04:00
nmzik e930657bf6 tesselation support in GUI 2026-09-20 17:16:19 +04:00
nmzik 0ad9149c90 Fix three-vertex rectangle rendering 2026-09-20 17:16:19 +04:00
nmzik 25f2699523 Fix video aspect ratio 2026-09-20 17:16:19 +04:00
nmzik a849f25376 Make tessellation optional 2026-09-20 17:16:19 +04:00
nmzik fa51da4d76 Fix AvPlayer end-of-file handling 2026-09-20 17:16:19 +04:00
nmzik 6b79b7a003 Fix AvPlayer startup state 2026-09-20 17:16:19 +04:00
nmzik 102a10f0fe Fix signed sampler comparisons 2026-09-20 17:16:19 +04:00
nmzik 8d66c4ef6d user service: return logged-out error for unavailable user names 2026-09-20 14:03:08 +04:00
nmzikandAugusto Domingos 8cecaf4e30 Merge PR #716: Fix Unicode paths on Windows
Preserve native Windows paths for filesystem operations, command-line options,
and game icons. Complete Unicode handling in recursive file listings and file
logging, restore unmounting by host directory, and extend regression coverage.

Original implementation by Augusto Domingos (@AghastyGD); retain all five of
his PR commits and include the reviewed follow-up fixes.

Closes #714

Co-authored-by: Augusto Domingos <augustodomingosvasco@gmail.com>
2026-09-20 02:14:22 +04:00
nmzik 82b2711cdf audio: play ATRAC9 sampler blocks 2026-09-20 01:47:43 +04:00
nmzik 35a275e97d avplayer: signal when playback ends 2026-09-20 01:47:43 +04:00
nmzik 303a0203b6 audio: play NGS2 PCM buffers 2026-09-20 01:47:43 +04:00
cfbaee07f0 Persist SaveDataMemory2 save data to disk (fixes Astro Bot progress not saving) (#659)
* Persist save-data memory across restarts

* Fix SetSaveDataMemory2 data loss, split saves per user, add atomic writes everywhere

- SaveDataSetSaveDataMemory2 treated data_num=0 as "no entries" even when
  a valid data pointer was supplied. Astro Bot calls this with data_num=0
  meaning "one entry, at data[0]" (an implicit-count convention), so every
  real save write from it was silently dropped as a no-op. Default
  data_num to 1 when data is non-null, matching that convention.
- SaveDataSetupSaveDataMemory2/Get/SetSaveDataMemory2 rejected any call
  that included a non-null icon pointer. Icon persistence isn't
  implemented, but titles are allowed to pass one anyway; rejecting the
  whole call over it made every title that does so fail outright. Ignore
  icon instead of rejecting.
- SaveDataMount3/SaveDataTransferringMount/SaveDataDelete built their
  save-data path from title+dir_name only, dropping the user_id PS5
  local users are keyed by (unlike the newer SetSaveDataMemory2 layout,
  which already nests by user_id). Nest these under user_id too so saves
  from different local users can no longer collide. No migration for the
  old flat layout - the project is still pre-release/experimental.
- Classic SaveDataMount3-mounted titles (Blasphemous, Dead Cells, ...)
  write their save files directly through the guest's normal file I/O
  once mounted, bypassing SaveData's own write path entirely, so they
  had none of the crash-safety SetSaveDataMemory2 already had. Added the
  same protection at the file-system layer: a guest file opened under a
  /savedataN mount that recreates the file from scratch (O_CREAT and/or
  O_TRUNC) is now written to a .tmp shadow file and atomically replaces
  the real one on close, instead of being truncated in place.
- Both write paths now back up the file they're about to replace into a
  sibling "backups" folder (same filename, no added suffix) before the
  atomic replace, so a manual recovery copy of the previous save always
  survives a write - even one that "succeeds" but wasn't wanted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Fix save-memory compatibility and preserve existing saves

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 00:51:22 +04:00
Augusto Domingos fe45265fcd preserve native paths when creating directories 2026-09-19 17:53:15 +02:00
Augusto Domingos 8b294eb5b8 Add unicode path regression coverage 2026-09-19 15:48:30 +02:00
Augusto Domingos 07c9978ac6 keep game icon paths native in windows 2026-09-19 14:45:24 +02:00
Augusto Domingos b61b3f2a40 handle UTF-8 command-line paths 2026-09-19 14:12:35 +02:00
Augusto Domingos 001dfea501 Preserve native windows paths in filesystem operations 2026-09-19 13:11:55 +02:00
66 changed files with 2954 additions and 865 deletions

No files matched your search

+1 -5
View File
@@ -43,9 +43,6 @@ jobs:
- name: Setup Visual Studio environment
uses: ilammy/msvc-dev-cmd@v1
- name: Setup Ninja
uses: seanmiddleditch/gha-setup-ninja@v5
- name: Locate vcpkg
id: vcpkg
shell: pwsh
@@ -155,7 +152,7 @@ jobs:
- name: Install build dependencies
shell: bash
run: brew install glslang ninja
run: brew install glslang
- name: Install Qt 6.10.3
uses: jurplel/install-qt-action@v4
@@ -318,7 +315,6 @@ jobs:
libxrandr-dev \
libxss-dev \
lld \
ninja-build \
pkg-config \
wayland-protocols
+3
View File
@@ -86,6 +86,9 @@ set(SPDLOG_BUILD_WARNINGS OFF CACHE BOOL "" FORCE)
set(SPDLOG_INSTALL OFF CACHE BOOL "" FORCE)
set(SPDLOG_FMT_EXTERNAL ON CACHE BOOL "" FORCE)
set(SPDLOG_NO_EXCEPTIONS ON CACHE BOOL "" FORCE)
if(WIN32)
set(SPDLOG_WCHAR_FILENAMES ON CACHE BOOL "" FORCE)
endif()
add_subdirectory(spdlog EXCLUDE_FROM_ALL)
if (NOT TARGET FFmpeg::ffmpeg)
+29
View File
@@ -12,6 +12,22 @@ endif()
project(Kyty)
find_program(SCCACHE_PROGRAM sccache)
find_program(CCACHE_PROGRAM ccache)
foreach(lang IN ITEMS C CXX)
# An explicitly empty launcher disables caching for this language.
if(NOT DEFINED CMAKE_${lang}_COMPILER_LAUNCHER)
if(SCCACHE_PROGRAM)
set(CMAKE_${lang}_COMPILER_LAUNCHER "${SCCACHE_PROGRAM}")
message(STATUS "sccache enabled for ${lang}: ${SCCACHE_PROGRAM}")
elseif(CCACHE_PROGRAM)
set(CMAKE_${lang}_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
message(STATUS "ccache enabled for ${lang}: ${CCACHE_PROGRAM}")
endif()
endif()
endforeach()
set(KYTY_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src")
set(KYTY_TESTS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/tests")
set(KYTY_THIRD_PARTY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty")
@@ -450,6 +466,12 @@ add_executable(audio_out2_port_tests EXCLUDE_FROM_ALL
target_link_libraries(audio_out2_port_tests common fmt::fmt)
target_include_directories(audio_out2_port_tests PRIVATE ${inc_headers})
add_executable(save_data_memory_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/SaveDataMemoryTests.cpp"
)
target_link_libraries(save_data_memory_tests common fmt::fmt)
target_include_directories(save_data_memory_tests PRIVATE ${inc_headers})
add_executable(event_queue_lifetime_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/EventQueueLifetimeTests.cpp"
"${KYTY_SOURCE_DIR}/kernel/eventQueue.cpp"
@@ -534,6 +556,7 @@ if(BUILD_TESTING)
add_test(NAME kernel_file_system COMMAND $<TARGET_FILE:kernel_file_system_tests>)
add_test(NAME sync_on_address COMMAND $<TARGET_FILE:sync_on_address_tests>)
add_test(NAME audio_out2_port COMMAND $<TARGET_FILE:audio_out2_port_tests>)
add_test(NAME save_data_memory COMMAND $<TARGET_FILE:save_data_memory_tests>)
add_test(NAME shader_recompiler_compute COMMAND $<TARGET_FILE:shader_recompiler_compute_tests>)
add_test(NAME shader_recompiler_alignbyte
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --alignbyte-only)
@@ -658,6 +681,12 @@ elseif(WIN32 OR LINUX)
set_target_properties(kyty_emulator PROPERTIES LINK_FLAGS "${KYTY_LD_OPTIONS} -Wl,-Map=${KYTY_EMULATOR_MAP_LINK_PATH}")
endif()
if(LINUX)
target_link_options(kyty_emulator PRIVATE
$<$<CONFIG:Release>:-Wl,--strip-all>
)
endif()
if(APPLE)
# The emulator writes x86-64 trampolines/PLT stubs into memory and executes them, and
# runs guest code under Rosetta. That requires the JIT / unsigned-executable-memory
+4
View File
@@ -135,6 +135,10 @@ bool ReadbackLinearImagesEnabled() {
return g_config->readback_linear_images;
}
bool TessellationEnabled() {
return g_config->tessellation_enabled;
}
bool PlayGoHackEnabled() {
return g_config->playgo_hack_enabled;
}
+2
View File
@@ -66,6 +66,7 @@ struct ConfigOptions {
bool gpu_assisted_validation_enabled = false;
bool renderdoc_enabled = false;
bool readback_linear_images = false;
bool tessellation_enabled = false;
bool playgo_hack_enabled = false;
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool red_zone_protection_enabled = false;
@@ -110,6 +111,7 @@ bool GpuAssistedValidationEnabled();
bool RenderDocEnabled();
bool ReadbackLinearImagesEnabled();
bool TessellationEnabled();
bool PlayGoHackEnabled();
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool RedZoneProtectionEnabled();
+49 -109
View File
@@ -245,88 +245,78 @@ void File::Printf(const char* format, ...) {
Write(s.data(), static_cast<uint32_t>(s.size()));
}
static std::filesystem::path WithoutTrailingSeparator(
const std::filesystem::path& path) {
if (!path.empty() && !path.has_filename() &&
path != path.root_path()) {
return path.parent_path();
}
return path;
}
bool File::IsDirectoryExisting(const std::filesystem::path& path) {
auto path_str = PathToGenericString(path);
return SysFileIsDirectoryExisting(path_str.ends_with("/") ? Common::RemoveLast(path_str, 1)
: path_str);
return SysFileIsDirectoryExisting(WithoutTrailingSeparator(path));
}
bool File::IsFileExisting(const std::filesystem::path& name) {
return SysFileIsFileExisting(name);
}
bool File::CreateDirectory(
const std::filesystem::path& path) // @suppress("Member declaration not found")
{
auto path_str = PathToGenericString(path);
return SysFileCreateDirectory(path_str.ends_with("/") ? Common::RemoveLast(path_str, 1)
: path_str);
bool File::CreateDirectory(const std::filesystem::path& path) {
return SysFileCreateDirectory(
WithoutTrailingSeparator(path));
}
bool File::DeleteDirectory(const std::filesystem::path& path) {
auto path_str = PathToGenericString(path);
return SysFileDeleteDirectory(path_str.ends_with("/") ? Common::RemoveLast(path_str, 1)
: path_str);
return SysFileDeleteDirectory(
WithoutTrailingSeparator(path));
}
bool File::CreateDirectories(const std::filesystem::path& path) {
std::string real_path = Common::ReplaceChar(PathToGenericString(path), '\\', '/');
const auto normalized_path =
WithoutTrailingSeparator(path);
std::vector<std::string> list = Common::Split(real_path, "/");
auto current_path = normalized_path.root_path();
std::string p;
for (const auto& component:
normalized_path.relative_path()) {
current_path /= component;
for (uint32_t si = 0; si < list.size(); si++) {
const std::string& s = list[si];
if (IsDirectoryExisting(current_path)) {
continue;
}
if (si != 0 || real_path.starts_with("/")) {
p += "/";
}
if (!CreateDirectory(current_path)) {
return false;
}
}
p += s;
if (IsDirectoryExisting(p)) {
continue;
}
if (!CreateDirectory(p)) // @suppress("Invalid arguments")
{
return false;
}
}
return true;
return true;
}
bool File::DeleteDirectories(const std::filesystem::path& path) {
std::string real_path = Common::ReplaceChar(PathToGenericString(path), '\\', '/');
const auto normalized_path =
WithoutTrailingSeparator(path);
std::vector<std::string> list = Common::Split(real_path, "/");
std::vector<std::filesystem::path> directories;
auto current_path = normalized_path.root_path();
std::string p;
std::vector<std::string> list2;
for (const auto& component:
normalized_path.relative_path()) {
current_path /= component;
directories.push_back(current_path);
}
for (uint32_t si = 0; si < list.size(); si++) {
const std::string& s = list[si];
for (auto it = directories.rbegin();
it != directories.rend(); ++it) {
if (!DeleteDirectory(*it)) {
return false;
}
}
if (si != 0 || real_path.starts_with("/")) {
p += "/";
}
p += s;
list2.push_back(p);
}
uint32_t num = list2.size();
for (uint32_t si = num - 1; si < num; si--) {
if (!DeleteDirectory(list2[si])) {
return false;
}
}
return true;
return true;
}
bool File::DeleteFile(
@@ -455,51 +445,6 @@ bool File::SetLastAccessAndWriteTimeUTC(const std::filesystem::path& name, const
return SysFileSetLastAccessAndWriteTimeUtc(name, af, wf);
}
std::vector<File::FindInfo> File::FindFiles(const std::filesystem::path& path) {
std::vector<sys_file_find_t> files;
SysFileFindFiles(path, files);
auto path_str = PathToGenericString(path);
uint32_t len = static_cast<uint32_t>(path_str.size());
if (!path_str.ends_with("/") && !path_str.ends_with("\\")) {
len++;
}
std::vector<File::FindInfo> ret;
ret.reserve(files.size());
for (const auto& f: files) {
File::FindInfo r {};
r.path_with_name = f.path_with_name;
r.rel_path_with_name = Common::Mid(PathToGenericString(f.path_with_name), len);
r.size = f.size;
SysTimeStruct at {};
SysTimeStruct wt {};
SysFileToSystemTimeUtc(f.last_access_time, at);
SysFileToSystemTimeUtc(f.last_write_time, wt);
if (!at.is_invalid && !wt.is_invalid) {
r.last_access_time = DateTime(Date(at.Year, at.Month, at.Day),
Time(at.Hour, at.Minute, at.Second, at.Milliseconds));
r.last_write_time = DateTime(Date(wt.Year, wt.Month, wt.Day),
Time(wt.Hour, wt.Minute, wt.Second, wt.Milliseconds));
}
// printf("%s, %s, %" PRIu64", %s, %s\n", r.path_with_name.c_str(),
// r.rel_path_with_name.c_str(), r.size, r.last_access_time.ToString().c_str(),
// r.last_write_time.ToString().c_str());
ret.push_back(std::move(r));
}
return ret;
}
std::vector<File::DirEntry> File::GetDirEntries(const std::filesystem::path& path) {
std::vector<sys_dir_entry_t> files;
@@ -508,13 +453,8 @@ std::vector<File::DirEntry> File::GetDirEntries(const std::filesystem::path& pat
std::vector<File::DirEntry> ret;
ret.reserve(files.size());
for (const auto& f: files) {
File::DirEntry r {};
r.name = f.name;
r.is_file = f.is_file;
ret.push_back(std::move(r));
for (auto& f: files) {
ret.emplace_back(std::move(f.name), f.is_file);
}
return ret;
-9
View File
@@ -30,14 +30,6 @@ class File {
public:
enum class Mode { Read, Write, ReadWrite, WriteRead };
struct FindInfo {
std::filesystem::path path_with_name;
std::filesystem::path rel_path_with_name;
DateTime last_access_time;
DateTime last_write_time;
uint64_t size;
};
struct DirEntry {
std::string name;
bool is_file;
@@ -98,7 +90,6 @@ public:
static bool SetLastAccessAndWriteTimeUTC(const std::filesystem::path& name,
const DateTime& access, const DateTime& write);
static std::vector<FindInfo> FindFiles(const std::filesystem::path& path);
static std::vector<DirEntry> GetDirEntries(const std::filesystem::path& path);
static bool CopyFile(const std::filesystem::path& src, const std::filesystem::path& dst);
static bool RenameFile(const std::filesystem::path& src, const std::filesystem::path& dst);
+1 -2
View File
@@ -3,7 +3,6 @@
#include "common/assert.h"
#include "common/emulatorConfig.h"
#include "common/stringUtils.h"
#include <cstdio>
#include <filesystem>
@@ -47,7 +46,7 @@ std::shared_ptr<spdlog::logger> MakeFileLogger(std::string name
}
auto sink =
std::make_shared<spdlog::sinks::basic_file_sink_mt>(Common::PathToString(path), true);
std::make_shared<spdlog::sinks::basic_file_sink_mt>(path.native(), true);
return MakeLogger(std::move(name), std::move(sink));
}
-10
View File
@@ -18,14 +18,6 @@ enum sys_file_cache_type_t {
SYS_FILE_CACHE_SEQUENTIAL_SCAN = 2 // NOLINT(readability-identifier-naming)
};
// NOLINTNEXTLINE(readability-identifier-naming)
struct sys_file_find_t {
std::filesystem::path path_with_name;
SysFileTimeStruct last_access_time;
SysFileTimeStruct last_write_time;
uint64_t size;
};
// NOLINTNEXTLINE(readability-identifier-naming)
struct sys_dir_entry_t {
std::string name;
@@ -75,8 +67,6 @@ bool SysFileSetLastWriteTimeUtc(const std::filesystem::path& name, SysFileTimeSt
bool SysFileSetLastAccessAndWriteTimeUtc(const std::filesystem::path& name,
SysFileTimeStruct& access, SysFileTimeStruct& write);
// NOLINTNEXTLINE(google-runtime-references)
void SysFileFindFiles(const std::filesystem::path& path, std::vector<sys_file_find_t>& out);
// NOLINTNEXTLINE(google-runtime-references)
void SysFileGetDents(const std::filesystem::path& path, std::vector<sys_dir_entry_t>& out);
bool SysFileCopyFile(const std::filesystem::path& src, const std::filesystem::path& dst);
bool SysFileRenameFile(const std::filesystem::path& src, const std::filesystem::path& dst);
-45
View File
@@ -610,51 +610,6 @@ bool SysFileSetLastAccessAndWriteTimeUtc(const std::filesystem::path& name,
// }
}
// Recursively collect regular files.
void SysFileFindFiles(const std::filesystem::path& path, std::vector<sys_file_find_t>& out) {
auto real_path = get_internal_name(path);
DIR* dir = opendir(real_path.string().c_str());
if (dir == nullptr) {
return;
}
for (const dirent* entry = readdir(dir); entry != nullptr; entry = readdir(dir)) {
const std::string file_name(entry->d_name);
if (file_name == "." || file_name == "..") {
continue;
}
auto child = real_path / file_name;
struct stat s {};
// lstat, so a symlink is never followed into a cycle during the recursive walk.
if (0 != lstat(child.string().c_str(), &s)) {
continue;
}
if (S_ISDIR(s.st_mode)) {
SysFileFindFiles(child, out);
} else if (S_ISREG(s.st_mode)) {
sys_file_find_t r {};
r.path_with_name = child;
r.size = static_cast<uint64_t>(s.st_size);
r.last_access_time.is_invalid = false;
r.last_access_time.time = s.st_atime;
r.last_access_time.nanos = KYTY_STAT_ATIME_NS(s);
r.last_write_time.is_invalid = false;
r.last_write_time.time = s.st_mtime;
r.last_write_time.nanos = KYTY_STAT_MTIME_NS(s);
out.push_back(r);
}
}
closedir(dir);
}
// Keep "." and ".." to match FindFirstFileW.
void SysFileGetDents(const std::filesystem::path& path, std::vector<sys_dir_entry_t>& out) {
auto real_path = get_internal_name(path);
+6 -61
View File
@@ -57,10 +57,10 @@ static DWORD GetCacheAccessType(sys_file_cache_type_t t) {
}
if (t == SYS_FILE_CACHE_SEQUENTIAL_SCAN) {
return SYS_FILE_CACHE_SEQUENTIAL_SCAN;
return FILE_FLAG_SEQUENTIAL_SCAN;
}
return SYS_FILE_CACHE_AUTO;
return FILE_ATTRIBUTE_NORMAL;
}
void SysFileRead(void* data, uint32_t size, sys_file_t& f, uint32_t* bytes_read) {
@@ -476,66 +476,11 @@ bool SysFileSetLastAccessAndWriteTimeUtc(const std::filesystem::path& name,
return ok;
}
void SysFileFindFiles(const std::filesystem::path& path, std::vector<sys_file_find_t>& out) {
std::string real_path = Common::ReplaceChar(Common::PathToGenericString(path), '\\', '/');
if (!real_path.ends_with("/")) {
real_path += "/";
}
std::string pattern = real_path + "*";
HANDLE h = nullptr;
WIN32_FIND_DATAW data;
auto wide_pattern = std::filesystem::path(pattern).wstring();
h = FindFirstFileW(wide_pattern.c_str(), &data);
if (h == INVALID_HANDLE_VALUE) {
return;
}
do {
std::filesystem::path file_name(data.cFileName);
auto file_name_str = Common::PathToString(file_name);
if (file_name_str == "." || file_name_str == "..") {
continue;
}
if ((data.dwFileAttributes & static_cast<DWORD>(FILE_ATTRIBUTE_DIRECTORY)) != 0u) {
SysFileFindFiles(std::filesystem::path(real_path) / file_name, out);
} else {
sys_file_find_t r {};
r.path_with_name = std::filesystem::path(real_path) / file_name;
r.size = (static_cast<uint64_t>(data.nFileSizeHigh) << 32u) +
static_cast<uint64_t>(data.nFileSizeLow);
r.last_access_time.is_invalid = false;
r.last_access_time.time = data.ftLastAccessTime;
r.last_write_time.is_invalid = false;
r.last_write_time.time = data.ftLastWriteTime;
out.push_back(r);
}
} while (FindNextFileW(h, &data) != 0);
FindClose(h);
}
void SysFileGetDents(const std::filesystem::path& path, std::vector<sys_dir_entry_t>& out) {
std::string real_path = Common::ReplaceChar(Common::PathToGenericString(path), '\\', '/');
if (!real_path.ends_with("/")) {
real_path += "/";
}
const auto pattern = path / L"*";
std::string pattern = real_path + "*";
HANDLE h = nullptr;
WIN32_FIND_DATAW data;
auto wide_pattern = std::filesystem::path(pattern).wstring();
h = FindFirstFileW(wide_pattern.c_str(), &data);
WIN32_FIND_DATAW data {};
HANDLE h = FindFirstFileW(pattern.c_str(), &data);
if (h == INVALID_HANDLE_VALUE) {
return;
@@ -549,7 +494,7 @@ void SysFileGetDents(const std::filesystem::path& path, std::vector<sys_dir_entr
r.is_file = ((data.dwFileAttributes & static_cast<DWORD>(FILE_ATTRIBUTE_DIRECTORY)) == 0u);
r.name = Common::PathToString(file_name);
out.push_back(r);
out.push_back(std::move(r));
} while (FindNextFileW(h, &data) != 0);
+8
View File
@@ -34,6 +34,14 @@ inline std::string PathToGenericString(const std::filesystem::path& path) {
#endif
}
inline std::filesystem::path PathFromUtf8(std::string_view text) {
#if defined(__cpp_char8_t)
return std::filesystem::path(std::u8string(text.begin(), text.end()));
#else
return std::filesystem::u8path(text.begin(), text.end());
#endif
}
inline bool EqualNoCase(std::string_view lhs, std::string_view rhs) {
if (lhs.size() != rhs.size()) {
return false;
@@ -2017,6 +2017,14 @@ KYTY_CP_OP_PARSER(CpOpIndirectCxRegs) {
auto pfunc = g_hw_ctx_indirect_func[cmd_offset & (Pm4::CX_NUM - 1)];
if (pfunc == nullptr) {
if (raw_cmd_offset == 0x24au && value == 0u) {
static std::atomic_flag logged = ATOMIC_FLAG_INIT;
if (!logged.test_and_set(std::memory_order_relaxed)) {
Log::WriteToConsoleAndLog(
"\t diagnostic: ignoring indirect CX {0x24a, 0}; hardware effect unresolved\n");
}
continue;
}
EXIT("unknown cx reg at %05" PRIx32 ": 0x%" PRIx32 "\n", num_dw - dw, cmd_offset);
}
+4
View File
@@ -26,6 +26,10 @@ enum class PrimitiveType : uint32_t {
kPolygon = 21,
};
constexpr bool IsRectList(PrimitiveType type) {
return type == PrimitiveType::kRectList || type == PrimitiveType::kRectListLegacy;
}
enum class IndexType : uint32_t {
kIndex16 = 0,
kIndex32 = 1,
-43
View File
@@ -912,17 +912,6 @@ void CommandProcessor::DrawIndirect(uint32_t data_offset, uint32_t draw_initiato
if (!indexed) {
DrawIndirectArgs args {};
std::memcpy(&args, args_addr, sizeof(args));
if (args.instance_count != 1u || args.start_vertex_location != 0u ||
args.start_instance_location != 0u) {
static std::atomic<uint32_t> log_count {0};
if (log_count.fetch_add(1) < 64) {
LOGF("\t warning: partial DrawIndirect args: vertex_count=%" PRIu32
", instance_count=%" PRIu32 ", start_vertex=%" PRIu32
", start_instance=%" PRIu32 "\n",
args.vertex_count_per_instance, args.instance_count,
args.start_vertex_location, args.start_instance_location);
}
}
m_num_instances = args.instance_count;
DrawIndexAuto({.vertex_count = args.vertex_count_per_instance,
.instance_count = args.instance_count,
@@ -934,16 +923,6 @@ void CommandProcessor::DrawIndirect(uint32_t data_offset, uint32_t draw_initiato
DrawIndexedIndirectArgs args {};
std::memcpy(&args, args_addr, sizeof(args));
if (args.base_vertex_location != 0u || args.start_instance_location != 0u) {
static std::atomic<uint32_t> log_count {0};
if (log_count.fetch_add(1) < 64) {
LOGF("\t warning: partial DrawIndexIndirect args: index_count=%" PRIu32
", instance_count=%" PRIu32 ", start_index=%" PRIu32 ", base_vertex=%" PRIu32
", start_instance=%" PRIu32 "\n",
args.index_count_per_instance, args.instance_count, args.start_index_location,
args.base_vertex_location, args.start_instance_location);
}
}
uint64_t index_size = 0;
switch (m_index_type_and_size) {
@@ -1005,17 +984,6 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
if (!indexed) {
auto* args = reinterpret_cast<const DrawIndirectArgs*>(args_addr);
if (args->instance_count != 1u || args->start_vertex_location != 0u ||
args->start_instance_location != 0u) {
static std::atomic<uint32_t> log_count {0};
if (log_count.fetch_add(1) < 64) {
LOGF("\t warning: partial DrawIndirectMulti args[%u]: vertex_count=%" PRIu32
", instance_count=%" PRIu32 ", start_vertex=%" PRIu32
", start_instance=%" PRIu32 "\n",
i, args->vertex_count_per_instance, args->instance_count,
args->start_vertex_location, args->start_instance_location);
}
}
m_num_instances = args->instance_count;
DrawIndexAuto({.vertex_count = args->vertex_count_per_instance,
.instance_count = args->instance_count,
@@ -1026,17 +994,6 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
}
auto* args = reinterpret_cast<const DrawIndexedIndirectArgs*>(args_addr);
if (args->base_vertex_location != 0u || args->start_instance_location != 0u) {
static std::atomic<uint32_t> log_count {0};
if (log_count.fetch_add(1) < 64) {
LOGF("\t warning: partial DrawIndexIndirectMulti args[%u]: index_count=%" PRIu32
", instance_count=%" PRIu32 ", start_index=%" PRIu32 ", base_vertex=%" PRIu32
", start_instance=%" PRIu32 "\n",
i, args->index_count_per_instance, args->instance_count,
args->start_index_location, args->base_vertex_location,
args->start_instance_location);
}
}
uint64_t index_size = 0;
switch (m_index_type_and_size) {
+2 -3
View File
@@ -209,17 +209,16 @@ static void RtCheck(const HW::RenderTarget& rt) {
}
if (rt.info.fmask_compression_enable) {
EXIT_NOT_IMPLEMENTED(rt.attrib.num_samples == 0 && rt.attrib.num_fragments == 0);
// Native MSAA stores expanded samples, independent of FMASK metadata compression.
static bool logged = false;
if (!logged) {
LOGF("RenderTarget: using native Vulkan MSAA without guest FMASK metadata, "
LOGF("RenderTarget: using expanded native Vulkan MSAA samples, "
"fmask=0x%016" PRIx64 "\n",
rt.fmask.addr);
logged = true;
}
}
EXIT_NOT_IMPLEMENTED(rt.info.fmask_data_compression_disable != false);
if (rt.info.cmask_fast_clear_enable || rt.info.dcc_compression_enable) {
static bool logged = false;
if (!logged) {
@@ -184,17 +184,18 @@ bool IsSupportedDepthTextureEncoding(const ShaderTextureResource& descriptor, bo
}
const bool full = common && descriptor.fields[4] == field4_expected &&
descriptor.fields[5] == field5_expected;
if (!full || (descriptor.fields[6] == 0 && descriptor.fields[7] != 0) ||
if (!full ||
(descriptor.MsaaDepth() && !IsMultisampledTexture(descriptor.Type()))) {
return false;
}
if (descriptor.fields[6] == 0) {
const auto metadata_control = descriptor.fields[6] & 0x00ffffffu;
if (metadata_control == 0) {
return true;
}
constexpr uint32_t htile_control = 0x00280000u;
const uint32_t expected_control = htile_control | (descriptor.MsaaDepth() ? (1u << 10u) : 0u);
const auto metadata_addr = descriptor.MetaAddr() << 8u;
return (descriptor.fields[6] & 0x00ffffffu) == expected_control && metadata_addr != 0 &&
return metadata_control == expected_control && metadata_addr != 0 &&
metadata_addr < TRACKER_ADDRESS_SIZE && (metadata_addr & 0x7fffu) == 0 &&
descriptor.TileMode() == Prospero::TileMode::kDepth;
}
@@ -252,10 +253,10 @@ static bool IsSupportedStorageTextureDescriptor(const ShaderRecompiler::IR::Imag
(is_color_2d_array && descriptor.BaseArray5() <= descriptor.Depth());
const bool is_2d =
resource.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2D && valid_2d_slice;
// Storage cube coordinates address individual faces, including partial cube views.
const bool is_cube = resource.cube && descriptor.Type() == Prospero::ImageType::kCube &&
descriptor.Width5() == descriptor.Height5() &&
descriptor.BaseArray5() <= descriptor.Depth() &&
(descriptor.Depth() - descriptor.BaseArray5() + 1u) % 6u == 0;
descriptor.BaseArray5() <= descriptor.Depth();
const bool is_2d_array =
resource.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2DArray &&
((!resource.cube && is_color_2d_array && descriptor.BaseArray5() <= descriptor.Depth()) ||
@@ -337,6 +337,7 @@ struct PipelineCache::ProgramCache {
if constexpr (std::is_same_v<InputInfo, ShaderVertexInputInfo>) {
options.user_data_base = 8;
options.wave_size = input_info.wave_size;
if (stage == ShaderType::Mesh || stage == ShaderType::TessellationControl) {
options.user_data_base = 0;
options.wave_size = stage == ShaderType::Mesh ? input_info.mesh.wave_size : 64u;
@@ -755,8 +756,7 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(
static_params.depth_bounds_test_enable = depth.depth_bounds_test_enable;
static_params.depth_min_bounds = depth.depth_min_bounds;
static_params.depth_max_bounds = depth.depth_max_bounds;
const bool rect_list =
command.GetUserConfig().GetPrimType() == Prospero::PrimitiveType::kRectList;
const bool rect_list = Prospero::IsRectList(command.GetUserConfig().GetPrimType());
static_params.cull_back = !rect_list && mc.cull_back;
static_params.cull_front = !rect_list && mc.cull_front;
static_params.face = mc.face;
+15 -44
View File
@@ -2,6 +2,7 @@
#include "common/assert.h"
#include "common/common.h"
#include "common/emulatorConfig.h"
#include "common/file.h"
#include "common/logging/log.h"
#include "common/profiler.h"
@@ -460,7 +461,6 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
state.height = std::numeric_limits<uint32_t>::max();
state.num_layers = std::numeric_limits<uint32_t>::max();
state.num_color_attachments = 0;
uint32_t attachment_samples = 0;
for (uint32_t i = 0; i < color_count; i++) {
auto& target = colors[i];
EXIT_IF(!target.image_id);
@@ -481,12 +481,6 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
target.desc.view_info.base_level, target.desc.view_info.base_layer,
target.desc.view_info.layer_count);
EXIT_IF(image.backing.samples != target.desc.info.samples || image_view == nullptr);
if (attachment_samples == 0) {
attachment_samples = target.desc.info.samples;
} else if (attachment_samples != target.desc.info.samples) {
EXIT("mixed color attachment sample counts are unsupported: %u and %u\n",
attachment_samples, target.desc.info.samples);
}
const auto& view = target.desc.view_info;
const auto layout = image.binding.is_bound ? vk::ImageLayout::eGeneral
: vk::ImageLayout::eColorAttachmentOptimal;
@@ -535,12 +529,6 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
image.info.data.address, depth.desc.view_info.base_layer,
depth.desc.view_info.layer_count);
EXIT_IF(image_view == nullptr || image.backing.samples != depth.desc.info.samples);
if (attachment_samples == 0) {
attachment_samples = depth.desc.info.samples;
} else if (attachment_samples != depth.desc.info.samples) {
EXIT("mixed color/depth sample counts are unsupported: %u and %u\n", attachment_samples,
depth.desc.info.samples);
}
const bool feedback = depth.depth_write_enable && pixel &&
std::ranges::any_of(pixel->images, [&](const TextureBinding& binding) {
if (binding.image_id != depth.image_id ||
@@ -591,9 +579,6 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
const auto& limits = buffer.GetGraphics().GetPhysicalDeviceProperties().limits;
state.width = limits.maxFramebufferWidth;
state.height = limits.maxFramebufferHeight;
} else if (attachment_samples == 0 ||
vulkan_sample_count(attachment_samples) == vk::SampleCountFlagBits {}) {
EXIT("render state has no valid attachments\n");
}
if (state.num_layers == std::numeric_limits<uint32_t>::max()) {
state.num_layers = 1;
@@ -789,8 +774,7 @@ static void SetDrawDebugPhase(CommandBuffer& buffer, uint64_t submit_id, const D
draw.instance_count, draw.first_instance);
}
static bool GetDrawTopology(const HW::UserConfig& ucfg, bool auto_draw,
vk::PrimitiveTopology& topology) {
static bool GetDrawTopology(const HW::UserConfig& ucfg, vk::PrimitiveTopology& topology) {
topology = vk::PrimitiveTopology::ePointList;
@@ -813,14 +797,11 @@ static bool GetDrawTopology(const HW::UserConfig& ucfg, bool auto_draw,
topology = vk::PrimitiveTopology::eTriangleStrip;
break;
case Prospero::PrimitiveType::kPatch:
if (!Config::TessellationEnabled()) return false;
[[fallthrough]];
case Prospero::PrimitiveType::kRectList:
topology = vk::PrimitiveTopology::ePatchList;
break;
case Prospero::PrimitiveType::kRectListLegacy:
if (!auto_draw) {
EXIT("unknown primitive type: %u\n", static_cast<uint32_t>(ucfg.GetPrimType()));
}
topology = vk::PrimitiveTopology::eTriangleStrip;
topology = vk::PrimitiveTopology::ePatchList;
break;
case Prospero::PrimitiveType::kQuadListLegacy:
topology = vk::PrimitiveTopology::eTriangleFan;
@@ -999,8 +980,7 @@ static void LogDrawStateIfNeeded(const CommandBuffer& buffer, const DrawCallInfo
return;
}
if (!draw.IsIndexed() &&
buffer.GetUserConfig().GetPrimType() != Prospero::PrimitiveType::kRectListLegacy) {
if (!draw.IsIndexed() && !Prospero::IsRectList(buffer.GetUserConfig().GetPrimType())) {
return;
}
@@ -1013,8 +993,7 @@ static void LogDrawStateIfNeeded(const CommandBuffer& buffer, const DrawCallInfo
}
static void EmitDrawPrimitives(const HW::UserConfig& ucfg, vk::CommandBuffer vk_buffer,
const ShaderVertexInputInfo& vs_input_info, const DrawCallInfo& draw,
const DrawEmitInfo& emit) {
const DrawCallInfo& draw, const DrawEmitInfo& emit) {
switch (ucfg.GetPrimType()) {
case Prospero::PrimitiveType::kPointList:
case Prospero::PrimitiveType::kLineList:
@@ -1023,6 +1002,7 @@ static void EmitDrawPrimitives(const HW::UserConfig& ucfg, vk::CommandBuffer vk_
case Prospero::PrimitiveType::kTriFan:
case Prospero::PrimitiveType::kTriStrip:
case Prospero::PrimitiveType::kRectList:
case Prospero::PrimitiveType::kRectListLegacy:
case Prospero::PrimitiveType::kPatch:
if (draw.IsIndexed()) {
vk_buffer.drawIndexed(draw.index_count, draw.instance_count, 0, emit.vertex_offset,
@@ -1032,14 +1012,6 @@ static void EmitDrawPrimitives(const HW::UserConfig& ucfg, vk::CommandBuffer vk_
emit.first_instance);
}
break;
case Prospero::PrimitiveType::kRectListLegacy:
if (draw.IsIndexed()) {
EXIT("unknown primitive type: %u\n", static_cast<uint32_t>(ucfg.GetPrimType()));
}
// Sarah
EXIT_NOT_IMPLEMENTED(!(draw.index_count == 3 && vs_input_info.buffers_num == 0));
vk_buffer.draw(4, draw.instance_count, emit.first_vertex, emit.first_instance);
break;
case Prospero::PrimitiveType::kQuadListLegacy:
EXIT_NOT_IMPLEMENTED((draw.index_count & 0x3u) != 0);
for (uint32_t i = 0; i < draw.index_count; i += 4) {
@@ -1121,10 +1093,6 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
vertex_bindings = AcquireVertexBuffers(buffer, state.vertex_info[0]);
index_binding = PrepareIndexBuffer(buffer, index_source);
}
const auto rendering =
AcquireRenderTargets(buffer, state.color_info, state.color_count, state.depth_info,
bindings.pixel);
if (draw.IsIndexed()) {
LogDrawPhase(draw.Name(), "CreatePipeline");
}
@@ -1132,6 +1100,9 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
std::span {state.color_info, state.color_count}, state.depth_info, vertex_stages, buffer,
state.ps_active ? &state.ps_input_info : nullptr, topology, primitive_restart_enable,
state.programs);
const auto rendering =
AcquireRenderTargets(buffer, state.color_info, state.color_count, state.depth_info,
bindings.pixel);
// Resource preparation above may synchronously finish and restart the scheduler. From this
// point onward, every operation targets the current command buffer and cannot touch guest
@@ -1182,7 +1153,7 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
if (mesh_active) {
vk_buffer.drawMeshTasksEXT(mesh_groups, draw.instance_count, 1);
} else {
EmitDrawPrimitives(ucfg, vk_buffer, state.vertex_info[0], draw, emit);
EmitDrawPrimitives(ucfg, vk_buffer, draw, emit);
}
if (!draw.IsIndexed()) {
@@ -1258,7 +1229,7 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
hw_check(buffer);
vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList;
if (!GetDrawTopology(ucfg, false, topology)) {
if (!GetDrawTopology(ucfg, topology)) {
return;
}
@@ -1368,7 +1339,7 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
args.vertex_count, args.instance_count, args.first_instance};
vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList;
if (!GetDrawTopology(ucfg, true, topology)) {
if (!GetDrawTopology(ucfg, topology)) {
ResetBindings();
return;
}
@@ -1378,7 +1349,7 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
return;
}
const bool rect_list = ucfg.GetPrimType() == Prospero::PrimitiveType::kRectList;
const bool rect_list = Prospero::IsRectList(ucfg.GetPrimType());
if (rect_list && state.vertex_info[0].buffers_num == 0 &&
state.vertex_info[0].stage.program->param_export_mask == 0 &&
state.ps_input_info.input_num != 0) {
+6 -1
View File
@@ -20,6 +20,7 @@
#include "kernel/pthread.h"
#include "libs/errno.h"
#include "libs/libs.h"
#include "loader/systemContent.h"
#include <algorithm>
#include <array>
@@ -1654,8 +1655,12 @@ KYTY_SYSV_ABI int VideoOutGetOutputStatus(int handle, VideoOutOutputStatus* stat
return VIDEO_OUT_ERROR_INVALID_HANDLE;
}
int32_t attribute3 = 0;
Loader::SystemContentParamSfoGetInt("ATTRIBUTE3", &attribute3);
ctx->mutex.Lock();
status->resolution = (ctx->width >= 3840 || ctx->height >= 2160 ? 2u : 1u);
// Primary output reports 4K unless param.json Video-out Info enables resolution detection.
status->resolution =
((attribute3 & 4) != 0 && ctx->width < 3840 && ctx->height < 2160 ? 1u : 2u);
status->dynamicRange = 1;
status->refreshRate =
(ctx->output_mode == VIDEO_OUT_OUTPUT_MODE_119_88HZ || Config::GetVblankFrequency() >= 119
@@ -141,9 +141,8 @@ public:
}
}
const bool reserved = binding.key == SDLK_ESCAPE || binding.key == SDLK_SPACE ||
binding.key == SDLK_F1 || binding.key == SDLK_F7 ||
binding.key == SDLK_F11;
const bool reserved = binding.key == SDLK_ESCAPE || binding.key == SDLK_F1 ||
binding.key == SDLK_F7 || binding.key == SDLK_F11;
if (binding.control == INVALID_CONTROL || reserved ||
(binding.key == SDLK_UNKNOWN && binding.mouse_button == 0)) {
EXIT("Invalid input mapping: %s\n", value.c_str());
+7 -6
View File
@@ -27,6 +27,7 @@
#include "common/systemInfo.h"
#include "common/threads.h"
#include "common/timer.h"
#include "common/stringUtils.h"
#include "graphics/host_gpu/graphicContext.h"
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
@@ -45,6 +46,7 @@
#include <string>
#include <vector>
#include <vulkan/vk_platform.h>
#include <filesystem>
#define STB_IMAGE_IMPLEMENTATION
#define STBI_NO_SIMD
@@ -220,7 +222,7 @@ static void ToggleDesktopFullscreen() {
}
}
static void GameEventKeyboard(WindowLoopState& game, const EventKeyboard& key) {
static void GameEventKeyboard(const EventKeyboard& key) {
static SDL_Keycode fullscreen_key = SDLK_UNKNOWN;
#ifdef KYTY_DBG_INPUT
@@ -233,7 +235,6 @@ static void GameEventKeyboard(WindowLoopState& game, const EventKeyboard& key) {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS || KYTY_PLATFORM == KYTY_PLATFORM_LINUX
if (key.down) {
switch (key.key_code) {
case SDLK_SPACE: SetPause(game, !game.paused.load(std::memory_order_acquire)); break;
case SDLK_F1:
if (!key.repeat) {
RenderDocRequestCapture();
@@ -537,7 +538,7 @@ void WindowContext::ProcessEvent(double time_s) {
key.mod = event->key.keysym.mod;
key.timestamp_seconds = time_s;
GameEventKeyboard(game, key);
GameEventKeyboard(key);
break;
}
@@ -945,10 +946,10 @@ struct WindowIcon {
}
};
static void WindowLoadPngIcon(const std::string& path, WindowIcon* icon) {
static void WindowLoadPngIcon(const std::filesystem::path& path, WindowIcon* icon) {
Common::File f;
if (!f.Open(path, Common::File::Mode::Read)) {
EXIT("Can't open icon file %s\n", path.c_str());
EXIT("Can't open icon file %s\n", Common::PathToString(path).c_str());
}
int width = 0;
@@ -974,7 +975,7 @@ void WindowContext::UpdateIcon() {
static bool icon_loaded = false;
if (!icon_loaded) {
std::string icon_path;
std::filesystem::path icon_path;
if (Loader::SystemContentGetIconPath(&icon_path)) {
WindowLoadPngIcon(icon_path, &icon);
}
@@ -132,12 +132,23 @@ void ValidateNativeProgram(const IR::Program& program) {
program.memory_info[index].planning_only;
});
};
const auto indirect_buffer_handle = [&](const IR::Inst& handle) {
return program.info.uses_dma && handle.NumArgs() == 4u && !handle.Uses().empty() &&
std::ranges::all_of(handle.Uses(), [&](const IR::Use& use) {
if (IR::BufferAccessOf(use.user->GetOpcode()) != IR::BufferAccess::Read) {
return false;
}
const auto index = use.user->Flags<IR::MemoryFlags>().index;
return index < program.memory_info.size() &&
program.memory_info[index].kind == IR::ResourceKind::IndirectBuffer;
});
};
for (const auto* block: program.blocks) {
for (const auto& inst: *block) {
const auto dense = inst.Flags<uint32_t>();
switch (inst.GetOpcode()) {
case IR::ValueOpcode::GetBufferResource:
if (planning_only_handle(inst)) {
if (planning_only_handle(inst) || indirect_buffer_handle(inst)) {
break;
}
if (dense >= program.info.buffers.size()) {
@@ -212,6 +223,9 @@ Emitter::SpirvRequirements Emitter::AnalyzeProgramRequirements(const IR::Program
Fail(program, "buffer operation has invalid memory metadata");
}
const auto& memory = program.memory_info[memory_index];
if (memory.kind == IR::ResourceKind::IndirectBuffer) {
requirements.subgroup_local_invocation_id = true;
}
if (memory.kind == IR::ResourceKind::Buffer) {
if (memory.resource >= program.info.buffers.size()) {
Fail(program, "buffer operation has invalid resource metadata");
@@ -559,6 +559,10 @@ void EmitVoid(ValueEmitContext&) {}
void EmitBarrier(EmitterState& state) {
const auto tessellation = state.program.stage == ShaderType::TessellationControl;
if (!tessellation && ShaderWorkgroupInput(state.program.stage, state.input_info) == nullptr) {
// Independent graphics invocations have no native workgroup left to synchronize.
return;
}
const auto memory_scope = tessellation ? spv::ScopeInvocation : spv::ScopeWorkgroup;
const auto semantics = tessellation ? spv::MemorySemanticsMaskNone
: spv::MemorySemanticsAcquireReleaseMask |
@@ -27,61 +27,66 @@ uint32_t EmitDsMaskedLaneRead(EmitterState& state, uint32_t source, uint32_t tar
return Select(state, TypeU32(state), source_active, shuffled, ConstantU32(state, 0));
}
uint32_t BufferByteAddress(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem,
uint32_t index, uint32_t offset, uint32_t soffset) {
auto& state = ctx.state;
const uint32_t packed = StorageBufferPackedStride(state, mem);
const uint32_t stride = packed & 0x3fffu;
const bool swizzle = stride != 0u && ((packed >> 14u) & 1u) != 0u;
if (((packed >> 20u) & 1u) != 0u) {
const auto lane = Binary(state, spv::OpBitwiseAnd, TypeU32(state),
EmitSubgroupLocalInvocationId(state), ConstantU32(state, 63));
index = Binary(state, spv::OpIAdd, TypeU32(state), index, lane);
}
if (mem.offset != 0u) {
offset = Binary(state, spv::OpIAdd, TypeU32(state), offset, ConstantU32(state, mem.offset));
}
struct BufferAddress {
uint32_t offset;
uint32_t byte;
};
uint32_t address = 0;
if (!swizzle) {
if (stride == 0u) {
address = offset;
} else {
const auto indexed = stride == 1u ? index
: Binary(state, spv::OpIMul, TypeU32(state), index,
ConstantU32(state, stride));
address = Binary(state, spv::OpIAdd, TypeU32(state), indexed, offset);
}
} else {
const uint32_t stride_enum = (packed >> 16u) & 3u;
const uint32_t index_stride = 8u << stride_enum;
const auto index_msb = Binary(state, spv::OpShiftRightLogical, TypeU32(state), index,
ConstantU32(state, stride_enum + 3u));
const auto index_lsb = Binary(state, spv::OpBitwiseAnd, TypeU32(state), index,
ConstantU32(state, index_stride - 1u));
const auto offset_msb =
BufferAddress CalculateBufferAddress(EmitterState& state, uint32_t index, uint32_t offset,
uint32_t soffset, uint32_t immediate, uint32_t stride,
uint32_t swizzle, uint32_t index_stride) {
if (immediate != 0u) {
offset = Binary(state, spv::OpIAdd, TypeU32(state), offset, ConstantU32(state, immediate));
}
const auto indexed = Binary(state, spv::OpIMul, TypeU32(state), index, stride);
auto address = Binary(state, spv::OpIAdd, TypeU32(state), indexed, offset);
if (swizzle != 0u) {
const auto index_shift =
Binary(state, spv::OpIAdd, TypeU32(state), index_stride, ConstantU32(state, 3));
const auto indices = Binary(state, spv::OpShiftLeftLogical, TypeU32(state),
ConstantU32(state, 1), index_shift);
const auto index_msb =
Binary(state, spv::OpShiftRightLogical, TypeU32(state), index, index_shift);
const auto index_lsb =
Binary(state, spv::OpBitwiseAnd, TypeU32(state), index,
Binary(state, spv::OpISub, TypeU32(state), indices, ConstantU32(state, 1)));
const auto offset_msb =
Binary(state, spv::OpBitwiseAnd, TypeU32(state), offset, ConstantU32(state, ~3u));
const auto offset_lsb =
Binary(state, spv::OpBitwiseAnd, TypeU32(state), offset, ConstantU32(state, 3u));
const auto indexed_msb = stride == 1u ? index_msb
: Binary(state, spv::OpIMul, TypeU32(state),
index_msb, ConstantU32(state, stride));
const auto msb = Binary(state, spv::OpIMul, TypeU32(state),
Binary(state, spv::OpIAdd, TypeU32(state), indexed_msb, offset_msb),
ConstantU32(state, index_stride));
const auto indexed_msb = Binary(state, spv::OpIMul, TypeU32(state), index_msb, stride);
const auto msb =
Binary(state, spv::OpIMul, TypeU32(state),
Binary(state, spv::OpIAdd, TypeU32(state), indexed_msb, offset_msb), indices);
const auto lsb = Binary(state, spv::OpIAdd, TypeU32(state),
Binary(state, spv::OpShiftLeftLogical, TypeU32(state), index_lsb,
ConstantU32(state, 2u)),
offset_lsb);
address = Binary(state, spv::OpIAdd, TypeU32(state), msb, lsb);
address = Select(state, TypeU32(state), swizzle,
Binary(state, spv::OpIAdd, TypeU32(state), msb, lsb), address);
}
return {offset, Binary(state, spv::OpIAdd, TypeU32(state), address, soffset)};
}
const auto soffset_value = inst.Arg(3).Resolve();
if (soffset_value.IsImmediate() && soffset_value.GetType() == IR::Type::U32 &&
soffset_value.U32() == 0u) {
return address;
uint32_t BufferLane(EmitterState& state) {
return Binary(state, spv::OpBitwiseAnd, TypeU32(state), EmitSubgroupLocalInvocationId(state),
ConstantU32(state, 63));
}
uint32_t BufferByteAddress(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem) {
auto& state = ctx.state;
const auto packed = StorageBufferPackedStride(state, mem);
const auto stride = packed & 0x3fffu;
auto index = ctx.Arg(inst, 1);
if ((packed & (1u << 20u)) != 0u) {
index = Binary(state, spv::OpIAdd, TypeU32(state), index, BufferLane(state));
}
return Binary(state, spv::OpIAdd, TypeU32(state), address, soffset);
const bool swizzle = stride != 0u && (packed & (1u << 14u)) != 0u;
return CalculateBufferAddress(state, index, ctx.Arg(inst, 2), ctx.Arg(inst, 3), mem.offset,
ConstantU32(state, stride),
swizzle ? ConstantBool(state, true) : 0u,
ConstantU32(state, (packed >> 16u) & 3u))
.byte;
}
uint32_t AddU64Low(EmitterState& state, uint32_t low, uint32_t high, uint32_t add_low,
@@ -196,11 +201,8 @@ uint32_t LoadBdaDword(ValueEmitContext& ctx, uint32_t address) {
});
}
uint32_t LoadBda(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem,
uint32_t bits) {
auto& state = ctx.state;
const auto address = GuestAddress(ctx, inst, mem);
const auto active = ctx.Arg(inst, inst.NumArgs() - 1);
uint32_t LoadBda(ValueEmitContext& ctx, uint32_t address, uint32_t active, uint32_t bits) {
auto& state = ctx.state;
return EmitValueOrZeroIfCondition(state, active, [&]() {
const auto aligned = Binary(state, spv::OpBitwiseAnd, TypeScalarU64(state), address,
ConstantDeviceAddress(state, ~uint64_t {3}));
@@ -236,8 +238,7 @@ uint32_t LoadBda(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryIn
uint32_t ByteAddress(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem) {
if (mem.kind == IR::ResourceKind::Buffer) {
return BufferByteAddress(ctx, inst, mem, ctx.Arg(inst, 1), ctx.Arg(inst, 2),
ctx.Arg(inst, 3));
return BufferByteAddress(ctx, inst, mem);
}
if (mem.kind == IR::ResourceKind::Lds || mem.kind == IR::ResourceKind::Gds) {
if (mem.offset == 0u) {
@@ -734,12 +735,86 @@ void FormattedStore(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Memor
});
}
uint32_t LoadIndirectBuffer(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t components) {
auto& state = ctx.state;
const auto& handle = *inst.Arg(0).ResolveInstruction();
const auto word1 = ctx.Arg(handle, 1);
const auto records = ctx.Arg(handle, 2);
const auto word3 = ctx.Arg(handle, 3);
const auto field = [&](uint32_t word, uint32_t first, uint32_t count) {
return EmitBitFieldUExtract(state, word, ConstantU32(state, first),
ConstantU32(state, count));
};
const auto nonzero = [&](uint32_t value) {
return Binary(state, spv::OpINotEqual, TypeBool(state), value, ConstantU32(state, 0));
};
const auto has_dword = [&](uint32_t offset, uint32_t size) {
return AndCondition(
state,
Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), size, ConstantU32(state, 4)),
Binary(state, spv::OpULessThanEqual, TypeBool(state), offset,
Binary(state, spv::OpISub, TypeU32(state), size, ConstantU32(state, 4))));
};
const auto stride = field(word1, 16, 14);
const auto swizzle = AndCondition(state, nonzero(stride), nonzero(field(word1, 31, 1)));
const auto index_stride = field(word3, 21, 2);
const auto add_tid = nonzero(field(word3, 23, 1));
const auto index =
Binary(state, spv::OpIAdd, TypeU32(state), ctx.Arg(inst, 1),
Select(state, TypeU32(state), add_tid, BufferLane(state), ConstantU32(state, 0)));
const auto soffset = ctx.Arg(inst, 3);
const auto base = DeviceAddressFromWords(state, ctx.Arg(handle, 0), field(word1, 0, 16));
const auto valid_format = nonzero(field(word3, 12, 7));
const auto mode = field(word3, 28, 2);
const auto index_in_bounds = Binary(state, spv::OpULessThan, TypeBool(state), index, records);
const auto scalar_in_bounds =
Binary(state, spv::OpULessThanEqual, TypeBool(state), soffset, records);
const auto raw_records = Binary(state, spv::OpISub, TypeU32(state), records, soffset);
const auto raw_index_in_bounds =
Binary(state, spv::OpULessThan, TypeBool(state), index, raw_records);
std::array<uint32_t, 4> values {};
for (uint32_t component = 0; component < components; component++) {
const auto address = CalculateBufferAddress(state, index, ctx.Arg(inst, 2), soffset,
ctx.Memory(inst).offset + component * 4u,
stride, swizzle, index_stride);
// RDNA2 raw DWORD vectors check each component, unlike formatted accesses.
const auto structured_bounds =
AndCondition(state, index_in_bounds,
Binary(state, spv::OpULessThan, TypeBool(state), address.offset, stride));
// OOB_SELECT=3 reduces NUM_RECORDS by SOFFSET before the offset/index check.
const auto raw_bounds = AndCondition(
state, scalar_in_bounds,
Select(state, TypeBool(state), swizzle,
AndCondition(state, raw_index_in_bounds, has_dword(address.offset, stride)),
has_dword(address.offset, raw_records)));
auto in_bounds =
Select(state, TypeBool(state),
Binary(state, spv::OpIEqual, TypeBool(state), mode, ConstantU32(state, 0)),
structured_bounds, index_in_bounds);
in_bounds = Select(
state, TypeBool(state),
Binary(state, spv::OpULessThan, TypeBool(state), mode, ConstantU32(state, 2)),
in_bounds,
Select(state, TypeBool(state),
Binary(state, spv::OpIEqual, TypeBool(state), mode, ConstantU32(state, 2)),
nonzero(records), raw_bounds));
const auto guest =
Binary(state, spv::OpIAdd, TypeScalarU64(state), base,
Unary(state, spv::OpUConvert, TypeScalarU64(state), address.byte));
values[component] = LoadBda(ctx, guest, AndCondition(state, valid_format, in_bounds), 32u);
}
return ConstructU32Composite(state, components, values);
}
uint32_t LoadWideBuffer(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t components) {
auto& state = ctx.state;
return EmitValueOrDefaultIfCondition(
state, ctx.Arg(inst, inst.NumArgs() - 1), TypeU32Composite(state, components),
ConstantU32CompositeZero(state, components), [&]() {
const auto mem = ctx.Memory(inst);
if (mem.kind == IR::ResourceKind::IndirectBuffer) {
return LoadIndirectBuffer(ctx, inst, components);
}
const auto resource = PrepareMemoryResourceAccess(state, mem);
const auto info = Format::GetFormatInfo(
mem.formatted ? BufferFormat(ctx, mem) : Prospero::BufferFormat::kInvalid);
@@ -1088,7 +1163,8 @@ void EmitLoadMemory(ValueEmitContext& ctx, const IR::Inst& inst) {
value = LoadWideShared(ctx, inst, shared_components);
else if (address_info.access == IR::AddressAccess::Read &&
mem.kind != IR::ResourceKind::Scratch)
value = LoadBda(ctx, inst, mem, address_info.data_bits);
value = LoadBda(ctx, GuestAddress(ctx, inst, mem), ctx.Arg(inst, inst.NumArgs() - 1),
address_info.data_bits);
else if (op == IR::ValueOpcode::LoadBufferU32 && mem.formatted)
value = FormattedLoad(ctx, inst, mem);
else if (inst.GetType() == IR::Type::U8)
@@ -1,4 +1,4 @@
#include "graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h"
#include "graphics/shader/recompiler/backend/spirv/spirvEmitterInstructions.h"
namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
namespace {
@@ -127,10 +127,7 @@ void EmitMeshEntryPoint(EmitterState& state) {
state.builder.AddFunction(spv::OpFunctionCall, TypeVoid(state), state.builder.AllocateId(),
state.mesh_guest_func);
// All guest waves finish before the uniform Vulkan allocation and output stores.
state.builder.AddFunction(spv::OpControlBarrier, ConstantU32(state, spv::ScopeWorkgroup),
ConstantU32(state, spv::ScopeWorkgroup),
ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask |
spv::MemorySemanticsWorkgroupMemoryMask));
EmitBarrier(state);
const auto vertices = MeshLoad(state, state.mesh_allocation, spv::StorageClassWorkgroup,
TypeU32(state), ConstantU32(state, 0));
const auto primitives = MeshLoad(state, state.mesh_allocation, spv::StorageClassWorkgroup,
@@ -74,7 +74,8 @@ constexpr OpcodeMap SOPC_OPCODE_LIST[] = {
{0x03u, Opcode::S_CMP_GE_I32}, {0x04u, Opcode::S_CMP_LT_I32}, {0x05u, Opcode::S_CMP_LE_I32},
{0x06u, Opcode::S_CMP_EQ_U32}, {0x07u, Opcode::S_CMP_LG_U32}, {0x08u, Opcode::S_CMP_GT_U32},
{0x09u, Opcode::S_CMP_GE_U32}, {0x0au, Opcode::S_CMP_LT_U32}, {0x0bu, Opcode::S_CMP_LE_U32},
{0x0cu, Opcode::S_BITCMP0_B32}, {0x0du, Opcode::S_BITCMP1_B32}, {0x12u, Opcode::S_CMP_EQ_U64},
{0x0cu, Opcode::S_BITCMP0_B32}, {0x0du, Opcode::S_BITCMP1_B32},
{0x0eu, Opcode::S_BITCMP0_B64}, {0x0fu, Opcode::S_BITCMP1_B64}, {0x12u, Opcode::S_CMP_EQ_U64},
{0x13u, Opcode::S_CMP_LG_U64},
};
@@ -73,6 +73,8 @@ enum class Opcode {
S_SUB_I32,
S_BITCMP0_B32,
S_BITCMP1_B32,
S_BITCMP0_B64,
S_BITCMP1_B64,
S_BITSET0_B32,
S_BITSET1_B32,
S_BITSET0_B64,
@@ -521,13 +521,14 @@ constexpr Vop1SdwaRule VOP1_SDWA_RULES[] = {
{Opcode::V_CVT_F32_U32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_CVT_F32_I32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_CVT_F32_UBYTE0, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_NOT_B32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_NOT_B32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), SdwaSelBytes() | SdwaSelWords(),
SdwaSelFull(), false},
{Opcode::V_FFBL_B32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_CVT_F32_F16, SdwaSelWords() | SdwaSelFull(), 0, 0, true},
{Opcode::V_CVT_F16_F32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), true},
{Opcode::V_CVT_F16_U16, SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
SdwaSelWords() | SdwaSelFull(), false},
{Opcode::V_CVT_F16_U16, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), false},
{Opcode::V_CVT_U16_F16, SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
SdwaSelWords() | SdwaSelFull(), true},
{Opcode::V_CVT_F16_I16, SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
@@ -617,13 +618,14 @@ bool SupportsVop1Clamp(Opcode opcode) {
}
bool ValidateVop1Sdwa(Instruction& inst, uint32_t opcode, uint32_t modifier) {
const auto dst_sel = (modifier >> 8u) & 0x7u;
const auto dst_u = (modifier >> 11u) & 0x3u;
const auto clamp = (modifier >> 13u) & 0x1u;
const auto omod = (modifier >> 14u) & 0x3u;
const auto src0_sel = (modifier >> 16u) & 0x7u;
const auto src0_neg = (modifier >> 20u) & 0x1u;
const auto src0_abs = (modifier >> 21u) & 0x1u;
const auto dst_sel = (modifier >> 8u) & 0x7u;
const auto dst_u = (modifier >> 11u) & 0x3u;
const auto clamp = (modifier >> 13u) & 0x1u;
const auto omod = (modifier >> 14u) & 0x3u;
const auto src0_sel = (modifier >> 16u) & 0x7u;
const auto src0_sext = (modifier >> 19u) & 0x1u;
const auto src0_neg = (modifier >> 20u) & 0x1u;
const auto src0_abs = (modifier >> 21u) & 0x1u;
if (src0_sel > 6u || dst_sel > 6u) {
SetUnsupported(inst, Family::VOP1, opcode, "VOP1 SDWA selector is invalid");
@@ -639,7 +641,8 @@ bool ValidateVop1Sdwa(Instruction& inst, uint32_t opcode, uint32_t modifier) {
"VOP1 SDWA destination selector is not supported");
return false;
}
if (!IsVop1SdwaSourceSupported(inst.opcode, src0_sel, src0_neg != 0u, src0_abs != 0u)) {
if (!IsVop1SdwaSourceSupported(inst.opcode, src0_sel, src0_neg != 0u, src0_abs != 0u) ||
(inst.opcode == Opcode::V_CVT_F16_U16 && src0_sel <= 3u && src0_sext != 0u)) {
SetUnsupported(inst, Family::VOP1, opcode, "VOP1 SDWA source selector is not supported");
return false;
}
@@ -507,6 +507,18 @@ bool Translator::S_BITCMP_B32(const Decoder::Instruction& inst, bool expected) {
return true;
}
bool Translator::S_BITCMP_B64(const Decoder::Instruction& inst, bool expected) {
const auto value = ReadU32Pair(inst.src0);
const auto offset = ir.BitwiseAnd(ReadU32(inst.src1), IR::U32(IR::Value(63u)));
const auto word_bit = ir.BitwiseAnd(offset, IR::U32(IR::Value(31u)));
const auto word =
ir.Select(ir.ULessThan(offset, IR::U32(IR::Value(32u))), value[0], value[1]);
const auto bit =
IR::U32(ir.Emit(IR::ValueOpcode::BitFieldUExtract, {word, word_bit, IR::Value(1u)}));
WriteCompareResult(inst.dst, ir.IEqual(bit, IR::U32(IR::Value(expected ? 1u : 0u))));
return true;
}
bool Translator::V_ALIGNBIT_B32(const Decoder::Instruction& inst) {
const auto hi = ReadU32(inst.src0);
const auto lo = ReadU32(inst.src1);
@@ -208,6 +208,8 @@ bool Translator::EmitScalar(const Decoder::Instruction& inst) {
case O::S_BFE_U64: return S_BFE_U64(inst);
case O::S_BITCMP0_B32: return S_BITCMP_B32(inst, false);
case O::S_BITCMP1_B32: return S_BITCMP_B32(inst, true);
case O::S_BITCMP0_B64: return S_BITCMP_B64(inst, false);
case O::S_BITCMP1_B64: return S_BITCMP_B64(inst, true);
case O::S_PACK_LL_B32_B16: return PackB16(inst, false, false);
case O::S_PACK_LH_B32_B16: return PackB16(inst, false, true);
case O::S_PACK_HH_B32_B16: return PackB16(inst, true, true);
@@ -1225,6 +1225,12 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
builtin(IR::StageInputKind::PackedAncillary));
}
} else if (options.stage == ShaderType::Vertex) {
// Vulkan owns primitive assembly; each vertex subgroup is one NGG wave.
// Keep its full lane extent: mbcnt(-1) uses lane ordinals, not active counts.
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(2),
IR::U32(IR::Value(options.wave_size << 12u)));
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(3),
IR::U32(IR::Value((1u << 28u) | options.wave_size)));
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(5),
builtin(IR::StageInputKind::VertexIndex));
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(8),
@@ -205,6 +205,7 @@ private:
bool V_BFE_U32(const Decoder::Instruction& inst, bool sign);
bool V_BFI_B32(const Decoder::Instruction& inst);
bool S_BITCMP_B32(const Decoder::Instruction& inst, bool expected);
bool S_BITCMP_B64(const Decoder::Instruction& inst, bool expected);
bool V_ALIGNBIT_B32(const Decoder::Instruction& inst);
bool V_ALIGNBYTE_B32(const Decoder::Instruction& inst);
bool V_LSHL_ADD_U32(const Decoder::Instruction& inst);
+10 -3
View File
@@ -447,13 +447,20 @@ void ValidateProgram(const Program& program, bool require_ssa) {
ValueOpcodeName(inst.GetOpcode())));
}
const auto& memory = program.memory_info[memory_index];
if (memory.kind != ResourceKind::Buffer &&
memory.kind != ResourceKind::ScalarBuffer) {
const bool vector_buffer = memory.kind == ResourceKind::Buffer ||
memory.kind == ResourceKind::IndirectBuffer;
if (!vector_buffer && memory.kind != ResourceKind::ScalarBuffer) {
return Fail(fmt::format("{} has a non-buffer resource kind",
ValueOpcodeName(inst.GetOpcode())));
}
if (memory.kind == ResourceKind::IndirectBuffer &&
(memory.formatted || memory.typed ||
(inst.GetOpcode() != ValueOpcode::LoadBufferU32x2 &&
inst.GetOpcode() != ValueOpcode::LoadBufferU32x4))) {
return Fail("indirect buffer requires a raw DWORD x2/x4 load");
}
if (buffer_components > 1u &&
(memory.kind != ResourceKind::Buffer || memory.data_bits != 32u ||
(!vector_buffer || memory.data_bits != 32u ||
memory.data_dwords != buffer_components || memory.component_index != 0u)) {
return Fail(fmt::format("{} has inconsistent native-wide metadata",
ValueOpcodeName(inst.GetOpcode())));
@@ -27,6 +27,7 @@ enum class ResourceKind {
ScalarBuffer,
ScalarAddress,
Buffer,
IndirectBuffer,
Flat,
Global,
Scratch,
@@ -357,16 +357,23 @@ struct SamplerPlan {
};
struct ImageRemap {
explicit ImageRemap(const ResourceSpecialization& specialization) {
for (const auto& image: specialization.images) {
indices.push_back(image.fmask ? UINT32_MAX : count++);
explicit ImageRemap(const ResourceSpecialization& specialization)
: source_count(static_cast<uint32_t>(specialization.images.size())) {
EXIT_IF(specialization.images.size() > indices.size());
for (uint32_t index = 0; index < source_count; index++) {
indices[index] = specialization.images[index].fmask ? UINT32_MAX : count++;
}
}
uint32_t operator[](uint32_t index) const {
EXIT_IF(index >= source_count);
return indices[index];
}
template <typename T>
void Apply(std::vector<T>& images) const {
EXIT_IF(images.size() != indices.size());
for (uint32_t index = 0; index < indices.size(); index++) {
EXIT_IF(images.size() != source_count);
for (uint32_t index = 0; index < source_count; index++) {
if (indices[index] != UINT32_MAX && indices[index] != index) {
images[indices[index]] = std::move(images[index]);
}
@@ -374,8 +381,10 @@ struct ImageRemap {
images.resize(count);
}
std::vector<uint32_t> indices;
uint32_t count = 0;
private:
std::array<uint32_t, ShaderInfo::MaxImages> indices;
uint32_t source_count;
uint32_t count = 0;
};
template <typename Images>
@@ -1075,29 +1084,29 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
for (auto* block: program.blocks) {
for (auto& inst: *block) {
if (inst.GetOpcode() == ValueOpcode::GetImageResource) {
inst.SetFlags(image_remap.indices.at(inst.Flags<uint32_t>()));
inst.SetFlags(image_remap[inst.Flags<uint32_t>()]);
}
}
}
for (auto& memory: memory_info) {
if (memory.kind == ResourceKind::Image && !memory.planning_only) {
memory.resource = image_remap.indices.at(memory.resource);
memory.resource = image_remap[memory.resource];
}
}
for (auto& buffer: buffers) {
if (buffer.image_alias != BufferResource::NoImageAlias) {
buffer.image_alias = image_remap.indices.at(buffer.image_alias);
buffer.image_alias = image_remap[buffer.image_alias];
}
}
for (auto& pair: sampled_pairs) {
pair.image = image_remap.indices.at(pair.image);
pair.image = image_remap[pair.image];
}
for (auto& image: images) {
if (image.indirect_root != ImageResource::NoIndirectImage) {
image.indirect_root = image_remap.indices.at(image.indirect_root);
image.indirect_root = image_remap[image.indirect_root];
}
for (auto& resource: image.indirect_resources) {
resource = image_remap.indices.at(resource);
resource = image_remap[resource];
}
}
image_remap.Apply(images);
@@ -526,7 +526,7 @@ private:
}
}
void GetHandle(Value value, ValueOpcode expected, uint32_t width, uint32_t pc, Inst*& handle,
bool GetHandle(Value value, ValueOpcode expected, uint32_t width, uint32_t pc, Inst*& handle,
uint32_t& source, bool sampler = false, bool sample_adjust = false) {
handle = value.Resolve().TryInstruction();
if (handle == nullptr || handle->GetOpcode() != expected) {
@@ -535,21 +535,17 @@ private:
DescriptorSource descriptor;
MakeSource(*handle, width, sampler, sample_adjust, descriptor, pc);
uint32_t bad_dword = 0;
if (expected == ValueOpcode::GetImageResource) {
for (; bad_dword < descriptor.dword_count; bad_dword++) {
const auto* value = descriptor.dwords[bad_dword].Resolve().TryInstruction();
if (value != nullptr && value->GetOpcode() == ValueOpcode::ReadConstBuffer) {
Fail(pc, fmt::format("{} dword {} is not a valid runtime value",
ValueOpcodeName(expected), bad_dword));
}
}
bad_dword = 0;
}
if (!ValidateSource(descriptor, bad_dword)) {
if (expected == ValueOpcode::GetBufferResource &&
std::all_of(descriptor.dwords.begin(), descriptor.dwords.begin() + width,
[](Value word) { return word.Resolve().GetType() == Type::U32; })) {
return false;
}
Fail(pc, fmt::format("{} dword {} is not a valid runtime value",
ValueOpcodeName(expected), bad_dword));
}
source = InternSource(descriptor);
return true;
}
void ValidateAddressHandle(Value value, uint32_t pc) const {
@@ -720,7 +716,18 @@ private:
uint32_t resource = 0;
if (buffer != BufferAccess::None) {
GetHandle(inst.Arg(0), ValueOpcode::GetBufferResource, 4, flags.pc, handle, source);
if (!GetHandle(inst.Arg(0), ValueOpcode::GetBufferResource, 4, flags.pc, handle,
source)) {
if (memory.kind != ResourceKind::Buffer || memory.formatted || memory.typed ||
(op != ValueOpcode::LoadBufferU32x2 && op != ValueOpcode::LoadBufferU32x4)) {
Fail(flags.pc,
"buffer descriptor is not a valid runtime value; GPU-selected access "
"requires a raw DWORD x2/x4 load");
}
m_program.memory_info[flags.index].kind = ResourceKind::IndirectBuffer;
m_info.uses_dma = true;
return;
}
resource = AddBuffer(source, memory, op, flags.pc);
if (resource == UINT32_MAX) {
Fail(flags.pc, "buffer resource limit exceeded");
@@ -125,6 +125,7 @@ bool IsRuntimeUniformOp(ValueOpcode op) {
case ValueOpcode::ULessThan32:
case ValueOpcode::IEqual32:
case ValueOpcode::UGreaterThan32:
case ValueOpcode::SGreaterThanEqual32:
case ValueOpcode::INotEqual32:
case ValueOpcode::LogicalOr:
case ValueOpcode::LogicalAnd:
@@ -930,6 +931,13 @@ private:
return true;
}
return false;
case ValueOpcode::SGreaterThanEqual32:
if (binary()) {
result = std::bit_cast<int32_t>(static_cast<uint32_t>(a)) >=
std::bit_cast<int32_t>(static_cast<uint32_t>(b));
return true;
}
return false;
case ValueOpcode::LogicalAnd:
if (binary()) {
result = (a != 0u) && (b != 0u);
+2
View File
@@ -651,6 +651,7 @@ void BuildStageStaticKey(const ShaderVertexInputInfo& info, std::vector<uint32_t
key.push_back(static_cast<uint32_t>(info.fetch_attrib_reg));
key.push_back(static_cast<uint32_t>(info.fetch_buffer_reg));
key.push_back(info.resources_num);
key.push_back(info.wave_size);
key.push_back(info.scratch_size_dwords);
key.push_back(info.pa_cl_vs_out_cntl);
key.push_back(static_cast<uint32_t>(info.clip_space.enabled));
@@ -764,6 +765,7 @@ ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context&
regs.gs_regs.rsrc2.user_sgpr, sh, data, info)) {
EXIT("failed to prepare vertex shader program\n");
}
info.wave_size = (context.GetShaderStages() & 0x00400000u) != 0 ? 32u : 64u;
return params;
}
// NGG user SGPRs start at s8; a separately compiled GS back half also receives
+1
View File
@@ -134,6 +134,7 @@ struct ShaderVertexInputInfo {
int fetch_attrib_reg = 0;
int fetch_buffer_reg = 0;
int buffers_num = 0;
uint32_t wave_size = 64;
uint32_t scratch_size_dwords = 0;
uint32_t pa_cl_vs_out_cntl = 0;
ShaderClipSpaceTransform clip_space;
+8 -6
View File
@@ -251,16 +251,15 @@ void FileDescriptors::CloseAll() {
void MountPoints::Mount(const std::filesystem::path& folder, const std::string& point) {
Common::LockGuard lock(m_mutex);
auto folder_str = Common::FixDirectorySlash(Common::PathToGenericString(folder));
auto point_str = Common::FixDirectorySlash(point);
Umount(point_str);
MountPair p;
p.dir = folder_str;
p.dir = folder;
p.point = point_str;
m_mount_pairs.push_back(p);
m_mount_pairs.push_back(std::move(p));
}
void MountPoints::Umount(const std::string& folder_or_point) {
@@ -270,7 +269,7 @@ void MountPoints::Umount(const std::string& folder_or_point) {
const auto it = std::find_if(
m_mount_pairs.begin(), m_mount_pairs.end(), [&folder_or_point_str](const MountPair& p) {
return Common::PathToGenericString(p.dir) == folder_or_point_str ||
return Common::FixDirectorySlash(Common::PathToGenericString(p.dir)) == folder_or_point_str ||
p.point == folder_or_point_str;
});
if (it != m_mount_pairs.end()) {
@@ -343,14 +342,17 @@ std::filesystem::path MountPoints::ResolvePath(const std::string& mounted_name)
while (rel_path.starts_with('/')) {
rel_path = Common::RemoveFirst(rel_path, 1);
}
const auto native_rel_path = Common::PathFromUtf8(rel_path);
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
if (HasWindowsForbiddenFilenameCharacter(rel_path)) {
::printf("FileSystem: Windows-incompatible guest filename: %s\n",
mounted_name.c_str());
}
return p.dir / rel_path;
return p.dir / native_rel_path;
#else
return ResolvePathIgnoringCase(p.dir / rel_path);
return ResolvePathIgnoringCase(p.dir / native_rel_path);
#endif
}
@@ -167,6 +167,12 @@
<property name="text"><string>Enable readback</string></property>
</widget>
</item>
<item row="7" column="1">
<widget class="QCheckBox" name="checkBox_tessellation">
<property name="toolTip"><string>Draw tessellated geometry. Disabled by default; unsupported tessellation may cause crashes.</string></property>
<property name="text"><string>Enable tessellation support (experimental)</string></property>
</widget>
</item>
</layout>
</widget>
</item>
@@ -275,13 +281,13 @@
<item>
<widget class="QCheckBox" name="checkBox_amd_cpu">
<property name="toolTip"><string>Patch CPU instructions that differ between AMD and the host CPU.</string></property>
<property name="text"><string>AMD CPU patch (EXPERIMENTAL)</string></property>
<property name="text"><string>AMD CPU patch (experimental)</string></property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_red_zone_protection">
<property name="toolTip"><string>Helps prevent crashes in some games on Windows</string></property>
<property name="text"><string>Windows SysV red zone crash protection (Experimental)</string></property>
<property name="text"><string>Windows SysV red zone crash protection (experimental)</string></property>
</widget>
</item>
</layout>
@@ -44,6 +44,28 @@
<property name="spacing">
<number>4</number>
</property>
<item>
<widget class="QToolButton" name="refresh_button">
<property name="minimumSize">
<size>
<width>36</width>
<height>36</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="global_settings_button">
<property name="minimumSize">
@@ -245,6 +267,14 @@
</layout>
</item>
</layout>
<action name="refresh_action">
<property name="text">
<string>Refresh game list</string>
</property>
<property name="toolTip">
<string>Rescan game folders for new games</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="#000" d="M17.65 6.35A7.96 7.96 0 0 0 12 4a8 8 0 1 0 7.73 10h-2.08A6 6 0 1 1 12 6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>

After

Width:  |  Height:  |  Size: 232 B

+5 -3
View File
@@ -92,6 +92,7 @@ public:
int gpu_index = -1;
bool fullscreen_enabled = false;
bool readback_linear_images = false;
bool tessellation_enabled = false;
int vblank_frequency = 60;
int console_language = DEFAULT_CONSOLE_LANGUAGE;
bool vulkan_validation_enabled = false;
@@ -122,6 +123,7 @@ public:
gpu_index = other.gpu_index;
fullscreen_enabled = other.fullscreen_enabled;
readback_linear_images = other.readback_linear_images;
tessellation_enabled = other.tessellation_enabled;
vblank_frequency = other.vblank_frequency;
console_language = other.console_language;
vulkan_validation_enabled = other.vulkan_validation_enabled;
@@ -142,7 +144,7 @@ public:
host_input_mapping = other.host_input_mapping;
}
void CopyFrom(const Configuration& other) {
void CopyGameInfoFrom(const Configuration& other) {
name = other.name;
title_id = other.title_id;
gameVersion = other.gameVersion;
@@ -152,8 +154,6 @@ public:
custom_settings = other.custom_settings;
game_status = other.game_status;
game_comment = other.game_comment;
CopyEmulatorSettingsFrom(other);
elf = other.elf;
}
void WriteSettings(QSettings* s) const {
@@ -169,6 +169,7 @@ public:
KYTY_CFG_SET(gpu_index);
KYTY_CFG_SET(fullscreen_enabled);
KYTY_CFG_SET(readback_linear_images);
KYTY_CFG_SET(tessellation_enabled);
KYTY_CFG_SET(vblank_frequency);
KYTY_CFG_SET(console_language);
KYTY_CFG_SET(vulkan_validation_enabled);
@@ -210,6 +211,7 @@ public:
}
KYTY_CFG_GET(fullscreen_enabled);
KYTY_CFG_GET(readback_linear_images);
KYTY_CFG_GET(tessellation_enabled);
vblank_frequency = s->value("vblank_frequency", vblank_frequency).toInt();
console_language = s->value("console_language", console_language).toInt();
if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) {
@@ -8,6 +8,8 @@
#include <QStringList>
#include <QWidget>
#include <memory>
class ConfigurationItem;
class CompatibilityDatabase;
class QEvent;
@@ -30,10 +32,9 @@ public:
[[nodiscard]] const ConfigurationItem* GetSelectedItem() const { return m_selected_item; }
ConfigurationItem* GetSelectedItem() { return m_selected_item; }
[[nodiscard]] const QString& GetSettingsFile() const { return m_settings_file; }
[[nodiscard]] const QStringList& GetHostInputMapping() const {
return m_global_info.host_input_mapping;
}
[[nodiscard]] const QString& GetSettingsFile() const { return m_settings_file; }
[[nodiscard]] std::unique_ptr<Configuration>
CreateConfiguration(const ConfigurationItem& item) const;
bool EnsureGameDirectory();
void ScanGameDirectory();
@@ -64,7 +65,6 @@ protected slots:
void filter_configurations(const QString& text);
private:
void ClearCustomSettings(ConfigurationItem* item);
void SelectItem(QTreeWidgetItem* witem);
void ApplyCompatibility();
void UpdateToolbarIcons();
+1
View File
@@ -3,6 +3,7 @@
<file alias="edit-configuration.svg">icons/edit-configuration.svg</file>
<file alias="global-settings.svg">icons/global-settings.svg</file>
<file alias="input-mapping.svg">icons/input-mapping.svg</file>
<file alias="refresh.svg">icons/refresh.svg</file>
<file alias="remove-configuration.svg">icons/remove-configuration.svg</file>
</qresource>
<qresource prefix="/styles">
@@ -230,6 +230,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
: 0);
m_ui->checkBox_fullscreen->setChecked(info.fullscreen_enabled);
m_ui->checkBox_readback->setChecked(info.readback_linear_images);
m_ui->checkBox_tessellation->setChecked(info.tessellation_enabled);
m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency);
m_ui->comboBox_console_language->clear();
m_ui->comboBox_console_language->addItems(CONSOLE_LANGUAGE_NAMES);
@@ -375,6 +376,7 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui) {
info.gpu_index = ui.comboBox_gpu->currentIndex() - 1;
info.fullscreen_enabled = ui.checkBox_fullscreen->isChecked();
info.readback_linear_images = ui.checkBox_readback->isChecked();
info.tessellation_enabled = ui.checkBox_tessellation->isChecked();
info.vblank_frequency = ui.spinBox_vblank_frequency->value();
info.console_language = ui.comboBox_console_language->currentIndex();
info.vulkan_validation_enabled = ui.checkBox_vulkan_validation->isChecked();
+102 -42
View File
@@ -13,6 +13,7 @@
#include <QAbstractItemModel>
#include <QAbstractItemView>
#include <QAction>
#include <QApplication>
#include <QComboBox>
#include <QCoreApplication>
#include <QCursor>
@@ -26,11 +27,13 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QKeySequence>
#include <QLineEdit>
#include <QMenu>
#include <QMessageBox>
#include <QPainter>
#include <QPalette>
#include <QPointer>
#include <QPushButton>
#include <QRegularExpression>
#include <QSet>
@@ -201,6 +204,10 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
m_ui->setupUi(this);
ConfigureGameList(m_ui);
m_ui->refresh_action->setShortcuts(QKeySequence::Refresh);
m_ui->refresh_button->setDefaultAction(m_ui->refresh_action);
addAction(m_ui->refresh_action);
UpdateToolbarIcons();
m_ui->global_settings_button->setToolTip(tr("Edit global settings and game folders"));
m_ui->input_mapping_button->setToolTip(tr("Edit global input mapping"));
@@ -221,7 +228,10 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
m_ui->cfgs_list->setColumnWidth(GAME_PATH_COLUMN, 320);
m_ui->cfgs_list->setColumnWidth(GAME_STATUS_COLUMN, 150);
m_ui->cfgs_list->setColumnWidth(GAME_COMMENT_COLUMN, 240);
m_ui->cfgs_list->sortItems(GAME_NAME_COLUMN, Qt::AscendingOrder);
connect(m_ui->refresh_action, &QAction::triggered, this,
&ConfigurationListWidget::ScanGameDirectory);
connect(m_ui->global_settings_button, &QToolButton::clicked, this,
&ConfigurationListWidget::edit_global_settings);
connect(m_ui->input_mapping_button, &QToolButton::clicked, this,
@@ -276,6 +286,8 @@ void ConfigurationListWidget::UpdateToolbarIcons() {
button->setIcon(QIcon(pixmap));
};
set_icon(m_ui->refresh_button, QStringLiteral(":/icons/refresh.svg"));
m_ui->refresh_action->setIcon(m_ui->refresh_button->icon());
set_icon(m_ui->global_settings_button, QStringLiteral(":/icons/global-settings.svg"));
set_icon(m_ui->input_mapping_button, QStringLiteral(":/icons/input-mapping.svg"));
set_icon(m_ui->edit_button, QStringLiteral(":/icons/edit-configuration.svg"));
@@ -398,10 +410,17 @@ void ConfigurationListWidget::ApplyCompatibility() {
}
}
static Configuration* CloneConfiguration(const Configuration& source) {
auto* ret = new Configuration;
ret->CopyFrom(source);
return ret;
std::unique_ptr<Configuration>
ConfigurationListWidget::CreateConfiguration(const ConfigurationItem& item) const {
auto info = std::make_unique<Configuration>();
const auto* custom = m_custom_infos.value(item.GetInfo().game_path);
info->CopyGameInfoFrom(item.GetInfo());
info->CopyEmulatorSettingsFrom(custom != nullptr ? *custom : m_global_info);
if (custom != nullptr && !custom->elf.isEmpty()) {
info->elf = custom->elf;
}
info->host_input_mapping = m_global_info.host_input_mapping;
return info;
}
struct GameMetadata {
@@ -516,9 +535,6 @@ static void SetGameFiles(Configuration& info, const QString& game_dir, const QSt
if (info.name.isEmpty()) {
info.name = game.dirName();
}
if (info.elf.isEmpty()) {
info.elf = QStringLiteral("eboot.bin");
}
}
static Configuration* FindCustomInfo(QMap<QString, Configuration*>* custom_infos,
@@ -564,11 +580,32 @@ bool ConfigurationListWidget::HasValidGameDirectory() const {
}
void ConfigurationListWidget::ScanGameDirectory() {
// Commit an active inline editor before destroying its row or replacing its data.
if (auto* focused = QApplication::focusWidget();
focused != nullptr && m_ui->cfgs_list->isAncestorOf(focused)) {
m_ui->cfgs_list->setFocus();
}
const auto selected_path =
m_selected_item != nullptr ? m_selected_item->GetInfo().game_path : QString();
const bool sorting_enabled = m_ui->cfgs_list->isSortingEnabled();
const int sort_column = m_ui->cfgs_list->sortColumn();
const auto sort_order = m_ui->cfgs_list->header()->sortIndicatorOrder();
const QSignalBlocker blocker(m_ui->cfgs_list);
m_ui->cfgs_list->setSortingEnabled(false);
m_selected_item = nullptr;
m_ui->cfgs_list->clear();
m_ui->cfgs_list->SetBackgroundImage({});
m_ui->edit_button->setEnabled(false);
m_ui->delete_button->setEnabled(false);
// MainDialog tracks the running item, so keep its identity even if its
// directory is no longer present. Other rows can be rebuilt from disk.
QMap<QString, ConfigurationItem*> running_items;
for (int index = m_ui->cfgs_list->topLevelItemCount() - 1; index >= 0; index--) {
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->topLevelItem(index));
if (item->IsRunning()) {
running_items.insert(PathKey(item->GetInfo().game_path), item);
} else {
delete item;
}
}
const QString eboot_name = QStringLiteral("eboot.bin");
QSet<QString> found_games;
@@ -601,16 +638,9 @@ void ConfigurationListWidget::ScanGameDirectory() {
auto metadata = GetGameMetadata(
game_dir.filePath(QStringLiteral("sce_sys/param.json")), game_dir.dirName());
auto info = std::make_unique<Configuration>();
auto* custom = FindCustomInfo(&m_custom_infos, game_path, legacy_game_path);
if (custom != nullptr) {
info->CopyFrom(*custom);
info->custom_settings = true;
} else {
info->CopyEmulatorSettingsFrom(m_global_info);
info->custom_settings = false;
}
auto info = std::make_unique<Configuration>();
info->custom_settings =
FindCustomInfo(&m_custom_infos, game_path, legacy_game_path) != nullptr;
SetGameFiles(*info, game_dir.absolutePath(), game_path, metadata);
const auto* compatibility = m_compatibility->Find(info->title_id);
if (compatibility != nullptr) {
@@ -618,10 +648,20 @@ void ConfigurationListWidget::ScanGameDirectory() {
info->game_comment = compatibility->comment;
}
if (auto* item = running_items.value(game_key); item != nullptr) {
// Refresh metadata without losing the running row's identity.
const QSignalBlocker status_blocker(item->GetStatusCombo());
item->GetInfo().CopyGameInfoFrom(*info);
item->Update();
item->SetCompatibilityEditable(m_compatibility->IsLocal() &&
!item->GetInfo().title_id.trimmed().isEmpty());
continue;
}
auto* item = new ConfigurationItem(std::move(info), m_ui->cfgs_list);
item->SetCompatibilityEditable(m_compatibility->IsLocal() &&
!item->GetInfo().title_id.trimmed().isEmpty());
connect(item->GetStatusCombo(), &QComboBox::currentIndexChanged, this,
connect(item->GetStatusCombo(), &QComboBox::currentIndexChanged, item,
[this, item](int /*index*/) {
if (!m_compatibility->IsLocal()) {
return;
@@ -638,7 +678,7 @@ void ConfigurationListWidget::ScanGameDirectory() {
m_ui->cfgs_list->setCurrentItem(item);
SelectItem(item);
});
connect(item->GetCommentEdit(), &QLineEdit::editingFinished, this, [this, item]() {
connect(item->GetCommentEdit(), &QLineEdit::editingFinished, item, [this, item]() {
if (!m_compatibility->IsLocal()) {
return;
}
@@ -664,8 +704,22 @@ void ConfigurationListWidget::ScanGameDirectory() {
}
}
m_ui->cfgs_list->sortItems(GAME_NAME_COLUMN, Qt::AscendingOrder);
m_ui->cfgs_list->setSortingEnabled(sorting_enabled);
if (sorting_enabled) {
m_ui->cfgs_list->sortItems(sort_column, sort_order);
}
filter_configurations(m_ui->search_line_edit->text());
ConfigurationItem* selected_item = nullptr;
for (int index = 0; index < m_ui->cfgs_list->topLevelItemCount(); index++) {
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->topLevelItem(index));
if (!item->isHidden() && item->GetInfo().game_path == selected_path) {
selected_item = item;
break;
}
}
m_ui->cfgs_list->setCurrentItem(selected_item);
SelectItem(selected_item);
}
void ConfigurationListWidget::edit_configuration() {
@@ -674,16 +728,19 @@ void ConfigurationListWidget::edit_configuration() {
return;
}
ConfigurationEditDialog dlg(item->GetInfo(), this);
auto info = CreateConfiguration(*item);
ConfigurationEditDialog dlg(*info, this);
dlg.setWindowTitle(tr("Edit game settings"));
if (dlg.exec() == QDialog::Accepted) {
info->custom_settings = true;
item->GetInfo().custom_settings = true;
auto game_path = item->GetInfo().game_path;
auto game_path = info->game_path;
delete m_custom_infos.take(game_path);
m_custom_infos.insert(game_path, CloneConfiguration(item->GetInfo()));
m_custom_infos.insert(game_path, info.release());
WriteSettings();
ScanGameDirectory();
item->Update();
SelectItem(item);
}
}
@@ -695,9 +752,11 @@ void ConfigurationListWidget::delete_configuartion() {
if (QMessageBox::Yes == QMessageBox::question(this, tr("Clear custom settings"),
tr("Do you want to clear custom settings?"))) {
ClearCustomSettings(item);
delete m_custom_infos.take(item->GetInfo().game_path);
item->GetInfo().custom_settings = false;
WriteSettings();
ScanGameDirectory();
item->Update();
SelectItem(item);
}
}
@@ -712,9 +771,13 @@ void ConfigurationListWidget::edit_global_settings() {
if (dlg.exec() == QDialog::Accepted) {
m_global_info.CopyEmulatorSettingsFrom(info);
m_game_dirs = NormalizeGameDirectories(dlg.GetGameDirectories());
const auto game_dirs = NormalizeGameDirectories(dlg.GetGameDirectories());
const bool game_dirs_changed = game_dirs != m_game_dirs;
m_game_dirs = game_dirs;
WriteSettings();
ScanGameDirectory();
if (game_dirs_changed) {
ScanGameDirectory();
}
}
}
@@ -726,10 +789,6 @@ void ConfigurationListWidget::edit_input_mapping() {
}
}
void ConfigurationListWidget::ClearCustomSettings(ConfigurationItem* item) {
delete m_custom_infos.take(item->GetInfo().game_path);
}
void ConfigurationListWidget::ViewTrophies() {
auto* current = static_cast<ConfigurationItem*>(m_ui->cfgs_list->currentItem());
auto* item = current != nullptr ? current : m_selected_item;
@@ -871,12 +930,13 @@ void ConfigurationListWidget::show_context_menu(const QPoint& pos) {
connect(action_view_trophies, &QAction::triggered, this,
&ConfigurationListWidget::ViewTrophies);
QAction* action_patches = menu.addAction(tr("Cheats (experimental)..."));
connect(action_patches, &QAction::triggered, this, [this, item]() {
if (item != nullptr) {
auto* dialog = new PatchesDialog(item->GetInfo(), this);
dialog->show();
}
});
connect(action_patches, &QAction::triggered, this,
[this, item = QPointer<ConfigurationItem>(item)]() {
if (item != nullptr) {
auto* dialog = new PatchesDialog(item->GetInfo(), this);
dialog->show();
}
});
action_patches->setVisible(item != nullptr &&
PatchesDialog::IsSupportedTitleId(item->GetInfo().title_id));
QAction* action_remove_save_data =
+4 -4
View File
@@ -90,6 +90,7 @@ QString KeyName(const QKeyEvent& event) {
}
switch (key) {
case Qt::Key_Space: return QStringLiteral("Space");
case Qt::Key_Return:
case Qt::Key_Enter: return QStringLiteral("Return");
case Qt::Key_Backspace: return QStringLiteral("Backspace");
@@ -132,8 +133,7 @@ public:
auto* layout = new QVBoxLayout(this);
m_label = new QLabel(
tr("Press a key or mouse button.\nSpace, F1, F7, and F11 are reserved; Esc cancels."),
this);
tr("Press a key or mouse button.\nF1, F7, and F11 are reserved; Esc cancels."), this);
m_label->setAlignment(Qt::AlignCenter);
layout->addWidget(m_label);
}
@@ -149,8 +149,8 @@ protected:
reject();
return;
}
if (event->key() == Qt::Key_Space || event->key() == Qt::Key_F1 ||
event->key() == Qt::Key_F7 || event->key() == Qt::Key_F11) {
if (event->key() == Qt::Key_F1 || event->key() == Qt::Key_F7 ||
event->key() == Qt::Key_F11) {
m_label->setText(tr("That key is reserved by the emulator."));
return;
}
+5 -4
View File
@@ -226,6 +226,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
args << "--fullscreen";
}
args << "--readback-linear-images" << BoolArg(info.readback_linear_images);
if (info.tessellation_enabled) {
args << "--tessellation";
}
args << "--vblank-frequency" << QString::number(info.vblank_frequency);
args << "--console-language" << QString::number(info.console_language);
args << "--vulkan-validation" << BoolArg(info.vulkan_validation_enabled);
@@ -490,10 +493,8 @@ void MainDialogPrivate::Run() {
m_running_item->SetRunning(true);
Configuration info;
info.CopyFrom(m_running_item->GetInfo());
info.host_input_mapping = m_ui->widget->GetHostInputMapping();
m_main_dialog->RunInterpreter(&m_process, info);
auto info = m_ui->widget->CreateConfiguration(*m_running_item);
m_main_dialog->RunInterpreter(&m_process, *info);
Update();
}
+1 -1
View File
@@ -19,7 +19,7 @@
PatchesDialog::PatchesDialog(const Configuration& game, QWidget* parent)
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()) {
setAttribute(Qt::WA_DeleteOnClose);
setWindowTitle(tr("Cheats (Experimental) - %1").arg(game.name));
setWindowTitle(tr("Cheats (experimental) - %1").arg(game.name));
resize(640, 480);
auto* layout = new QVBoxLayout(this);
+8 -6
View File
@@ -464,6 +464,14 @@ private:
const auto* chunk = input + offset;
const auto chunk_size = static_cast<size_t>(AjmReadLe32(chunk + 4));
const auto payload = offset + 8;
// The data chunk describes the whole stream; this job may contain only its prefix.
if (AjmFourCcEquals(chunk, 'd', 'a', 't', 'a')) {
*data_offset = payload;
result->input_consumed = payload;
result->format = GetFormat();
result->total_decoded_samples = m_total_decoded_samples;
return true;
}
if (payload > input_size || chunk_size > input_size - payload) {
result->result = AJM_RESULT_PARTIAL_INPUT;
return false;
@@ -496,12 +504,6 @@ private:
LOGF("AJM ATRAC9 gapless: total=%" PRIu32 ", skip=%" PRIu16 "\n",
gapless_decode.total_samples, gapless_decode.skip_samples);
}
} else if (AjmFourCcEquals(chunk, 'd', 'a', 't', 'a')) {
*data_offset = payload;
result->input_consumed = payload;
result->format = GetFormat();
result->total_decoded_samples = m_total_decoded_samples;
return true;
}
const auto padded_size = chunk_size + (chunk_size & 1u);
+542 -130
View File
@@ -9,6 +9,7 @@
#include "common/threads.h"
#include "kernel/pthread.h"
#include "kernel/semaphore.h"
#include "libs/ajm/atrac9_decoder.h"
#include "libs/audio_internal.h"
#include "libs/errno.h"
#include "libs/libs.h"
@@ -18,8 +19,10 @@
#include <atomic>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <limits>
#include <magic_enum.hpp>
#include <memory>
#include <vector>
#include "libatrac9.h"
@@ -1824,26 +1827,146 @@ enum class Ngs2RackType {
CustomSampler,
};
struct Ngs2UserFxContext {
void* common;
void* param;
void* work;
uintptr_t user_data;
uint32_t max_voices;
uint32_t voice_index;
uint64_t reserved[4] {};
};
struct Ngs2UserFxProcessContext {
float** channels;
void* common;
const void* param;
void* work;
void* state;
uintptr_t user_data;
uint32_t flags;
uint32_t input_channels;
uint32_t output_channels;
uint32_t grain_samples;
uint32_t sample_rate;
uint32_t reserved {};
uint64_t reserved2[4] {};
};
struct Ngs2UserFxOption {
Ngs2CustomModuleOption header;
int KYTY_SYSV_ABI (*setup)(Ngs2UserFxContext*);
int KYTY_SYSV_ABI (*cleanup)(Ngs2UserFxContext*);
uintptr_t control;
int KYTY_SYSV_ABI (*process)(Ngs2UserFxProcessContext*);
size_t common_size;
size_t param_size;
size_t work_size;
uintptr_t user_data;
};
struct Ngs2RackInternal {
Ngs2Internal* ngs = nullptr;
Ngs2RackInternal* next = nullptr;
Ngs2RackType type = Ngs2RackType::Sampler;
Ngs2RackOptionUnion option;
Ngs2ContextBufferInfo buffer_info;
Ngs2BufferAllocator allocator;
Ngs2Internal* ngs = nullptr;
Ngs2RackInternal* next = nullptr;
Ngs2RackType type = Ngs2RackType::Sampler;
Ngs2RackOptionUnion option;
Ngs2ContextBufferInfo buffer_info;
Ngs2BufferAllocator allocator;
std::array<std::vector<uint8_t>, 24> common;
std::array<Ngs2UserFxOption, 24> fx {};
};
enum class Ngs2VoicePlayState { Empty, Playing, Paused, Stopped };
enum class Ngs2VoicePlayEvent { None, Play, Pause, Resume, Stop, StopImm, Kill };
struct Ngs2VoiceInternal {
Ngs2VoicePlayEvent event = Ngs2VoicePlayEvent::None;
Ngs2VoicePlayState state = Ngs2VoicePlayState::Empty;
Ngs2RackInternal* rack = nullptr;
uintptr_t callback = 0;
uintptr_t callback_data = 0;
uint32_t callback_flags = 0;
struct Port {
Ngs2VoiceInternal* dest = nullptr;
uint32_t input = 0;
float volume = 1.0f;
int32_t matrix = -1;
};
struct Block {
const uint8_t* data;
Ngs2WaveformBlock info;
uint32_t cursor = 0;
size_t data_cursor = 0;
uint32_t skip_remaining = info.num_skip_samples;
};
struct Module {
std::vector<uint8_t> param, work, state;
uint32_t flags = 1;
};
Ngs2VoicePlayEvent event = Ngs2VoicePlayEvent::None;
Ngs2VoicePlayState state = Ngs2VoicePlayState::Empty;
Ngs2RackInternal* rack = nullptr;
uintptr_t callback = 0;
uintptr_t callback_data = 0;
uint32_t callback_flags = 0;
std::deque<Block> blocks;
std::vector<Port> ports;
std::vector<std::vector<float>> matrices;
std::vector<Module> modules;
std::vector<float> samples;
std::vector<float> output_matrix;
uint32_t channels = 0;
uint32_t output_id = 0;
bool rendering = false;
bool rendered = false;
bool has_samples = false;
std::unique_ptr<Ajm::AjmAt9Decoder> decoder;
std::vector<float> decoded_frame;
bool accepts_blocks = true;
void SetupSampler(const Ngs2WaveformFormat& format) {
EXIT_NOT_IMPLEMENTED(format.sample_rate != rack->ngs->option.sample_rate ||
format.frame_margin != 0 || format.frame_offset != 0);
channels = format.num_channels;
blocks.clear();
decoder.reset();
decoded_frame.clear();
accepts_blocks = true;
if (format.waveform_type == NGS2_WAVEFORM_TYPE_ATRAC9) {
decoder = std::make_unique<Ajm::AjmAt9Decoder>(channels, format.sample_rate,
Ajm::AjmSampleEncoding::Float, 0);
const auto result =
decoder->Initialize(&format.config_data, sizeof(format.config_data));
EXIT_NOT_IMPLEMENTED(result.result != OK);
EXIT_NOT_IMPLEMENTED(result.format.channel_num != channels ||
result.format.sampling_frequency != format.sample_rate);
Ajm::AjmSidebandDecAt9CodecInfo info {};
decoder->WriteCodecInfo(&info, sizeof(info), result);
EXIT_NOT_IMPLEMENTED(info.frame_samples > std::numeric_limits<uint16_t>::max());
decoded_frame.resize(info.frame_samples * channels);
} else {
EXIT_NOT_IMPLEMENTED(format.waveform_type != 0x12);
}
}
void SetMatrix(uint32_t index, const float* levels, uint32_t count) {
EXIT_NOT_IMPLEMENTED(index >= matrices.size());
matrices[index].assign(levels, levels + count);
}
void SetVolume(uint32_t index, float volume) {
EXIT_NOT_IMPLEMENTED(index >= ports.size());
ports[index].volume = volume;
}
void SetPortMatrix(uint32_t index, int32_t matrix) {
EXIT_NOT_IMPLEMENTED(index >= ports.size());
ports[index].matrix = matrix;
}
void SetEvent(uint32_t id) {
switch (id) {
case 1: event = Ngs2VoicePlayEvent::Play; break;
case 2: event = Ngs2VoicePlayEvent::Stop; break;
case 4: event = Ngs2VoicePlayEvent::StopImm; break;
case 8: event = Ngs2VoicePlayEvent::Kill; break;
case 16: event = Ngs2VoicePlayEvent::Pause; break;
case 32: event = Ngs2VoicePlayEvent::Resume; break;
default: EXIT("unknown event_id: 0x%08" PRIx32 "\n", id);
}
}
};
struct Ngs2VoiceParamHeader {
@@ -1931,6 +2054,9 @@ static Ngs2RackInternal* g_racks_list = nullptr;
static std::atomic_uint32_t g_next_ngs_uid = 1;
static Common::Mutex g_racks_mutex;
static_assert(sizeof(Ngs2UserFxContext) == 72);
static_assert(sizeof(Ngs2UserFxProcessContext) == 104);
static_assert(sizeof(Ngs2UserFxOption) == 72);
static_assert(sizeof(Ngs2SystemOption) == 144);
static_assert(sizeof(Ngs2SystemInfo) == 184);
static_assert(sizeof(Ngs2RackOption) == 176);
@@ -2272,6 +2398,35 @@ int KYTY_SYSV_ABI Ngs2SystemDestroy(uintptr_t system_handle, Ngs2ContextBufferIn
return OK;
}
static void Ngs2InitModules(Ngs2RackInternal& rack) {
if (!Ngs2RackIsCustom(rack.type)) {
return;
}
const auto& custom = rack.option.custom_sampler.custom_rack_option;
EXIT_NOT_IMPLEMENTED(custom.num_buffers != 1);
auto* voices = reinterpret_cast<Ngs2VoiceInternal*>(&rack + 1);
for (uint32_t m = 0; m < custom.num_modules; ++m) {
if (custom.module[m].module_id != 0x1f) {
continue;
}
auto& fx = rack.fx[m];
fx = *reinterpret_cast<const Ngs2UserFxOption*>(custom.module[m].option);
rack.common[m].resize(fx.common_size);
for (uint32_t i = 0; i < rack.option.common.max_voices; ++i) {
auto& module = voices[i].modules[m];
module.param.resize(fx.param_size);
module.work.resize(fx.work_size);
module.state.resize(custom.module[m].state_size);
if (fx.setup != nullptr) {
Ngs2UserFxContext context {
rack.common[m].data(), module.param.data(), module.work.data(),
fx.user_data, rack.option.common.max_voices, i};
EXIT_NOT_IMPLEMENTED(fx.setup(&context) != OK);
}
}
}
}
int KYTY_SYSV_ABI Ngs2RackCreate(uintptr_t system_handle, uint32_t rack_id,
const Ngs2RackOption* option,
const Ngs2ContextBufferInfo* buffer_info, uintptr_t* handle) {
@@ -2313,7 +2468,7 @@ int KYTY_SYSV_ABI Ngs2RackCreate(uintptr_t system_handle, uint32_t rack_id,
static_cast<uint64_t>(buffer_info->host_buffer_size));
auto* ngs = reinterpret_cast<Ngs2Internal*>(system_handle);
auto* rack = static_cast<Ngs2RackInternal*>(buffer_info->host_buffer);
auto* rack = new (buffer_info->host_buffer) Ngs2RackInternal {};
auto* voices = reinterpret_cast<Ngs2VoiceInternal*>(rack + 1);
Common::LockGuard lock(ngs->mutex);
@@ -2373,10 +2528,15 @@ int KYTY_SYSV_ABI Ngs2RackCreate(uintptr_t system_handle, uint32_t rack_id,
}
for (uint32_t i = 0; i < option->max_voices; i++) {
voices[i].rack = rack;
voices[i].event = Ngs2VoicePlayEvent::None;
voices[i].state = Ngs2VoicePlayState::Empty;
auto& voice = *std::construct_at(voices + i);
voice.rack = rack;
voice.ports.resize(option->max_ports);
voice.matrices.resize(option->max_matrices);
if (Ngs2RackIsCustom(rack->type)) {
voice.modules.resize(rack->option.custom_sampler.custom_rack_option.num_modules);
}
}
Ngs2InitModules(*rack);
*handle = reinterpret_cast<uintptr_t>(rack);
@@ -2406,22 +2566,9 @@ int KYTY_SYSV_ABI Ngs2RackCreateWithAllocator(uintptr_t system_handle, uint32_t
EXIT_NOT_IMPLEMENTED(option->size < sizeof(Ngs2RackOption));
LOGF("\t rack_id = 0x%" PRIx32 "\n"
"\t name = %.64s\n"
"\t flags = %u\n"
"\t max_grain_samples = %u\n"
"\t max_voices = %u\n"
"\t max_input_delay_blocks = %u\n"
"\t max_matrices = %u\n"
"\t max_ports = %u\n"
"\t max_voice_channels = %u\n"
"\t max_output_channels = %u\n"
"\t alloc_handler = 0x%016" PRIx64 "\n"
LOGF("\t alloc_handler = 0x%016" PRIx64 "\n"
"\t free_handler = 0x%016" PRIx64 "\n"
"\t user_data = 0x%016" PRIx64 "\n",
rack_id, option->name, option->flags, option->max_grain_samples, option->max_voices,
option->max_input_delay_blocks, option->max_matrices, option->max_ports,
option->max_voice_channels, option->max_output_channels,
reinterpret_cast<uint64_t>(allocator->alloc_handler),
reinterpret_cast<uint64_t>(allocator->free_handler),
static_cast<uint64_t>(allocator->user_data));
@@ -2477,26 +2624,41 @@ int KYTY_SYSV_ABI Ngs2RackDestroy(uintptr_t rack_handle, Ngs2ContextBufferInfo*
if (ngs == nullptr) {
return ERROR_INVALID_RACK_HANDLE;
}
Ngs2ContextBufferInfo context_buffer;
Ngs2BufferAllocator allocator;
{
Common::LockGuard lock(ngs->mutex);
Common::LockGuard racks_lock(g_racks_mutex);
auto** link = &g_racks_list;
while (*link != nullptr && *link != rack) {
link = &(*link)->next;
{
Common::LockGuard racks_lock(g_racks_mutex);
auto** link = &g_racks_list;
while (*link != nullptr && *link != rack) {
link = &(*link)->next;
}
if (*link == nullptr) {
return ERROR_INVALID_RACK_HANDLE;
}
*link = rack->next;
}
if (*link == nullptr) {
return ERROR_INVALID_RACK_HANDLE;
auto* voices = reinterpret_cast<Ngs2VoiceInternal*>(rack + 1);
for (uint32_t i = 0; i < rack->option.common.max_voices; ++i) {
for (size_t m = 0; m < voices[i].modules.size(); ++m) {
const auto& fx = rack->fx[m];
if (fx.cleanup != nullptr) {
auto& module = voices[i].modules[m];
Ngs2UserFxContext context {rack->common[m].data(),
module.param.data(),
module.work.data(),
fx.user_data,
rack->option.common.max_voices,
i};
EXIT_NOT_IMPLEMENTED(fx.cleanup(&context) != OK);
}
}
std::destroy_at(voices + i);
}
*link = rack->next;
context_buffer = rack->buffer_info;
allocator = rack->allocator;
rack->ngs = nullptr;
rack->next = nullptr;
std::destroy_at(rack);
}
if (allocator.free_handler != nullptr) {
@@ -2539,75 +2701,231 @@ int KYTY_SYSV_ABI Ngs2RackUnlock(uintptr_t rack_handle) {
return OK;
}
static void Ngs2ApplyEvent(Ngs2VoiceInternal& voice) {
switch (voice.event) {
case Ngs2VoicePlayEvent::None: break;
case Ngs2VoicePlayEvent::Play:
if (voice.state == Ngs2VoicePlayState::Empty) {
voice.state = Ngs2VoicePlayState::Playing;
}
break;
case Ngs2VoicePlayEvent::Pause:
if (voice.state == Ngs2VoicePlayState::Playing) {
voice.state = Ngs2VoicePlayState::Paused;
}
break;
case Ngs2VoicePlayEvent::Resume:
if (voice.state == Ngs2VoicePlayState::Paused) {
voice.state = Ngs2VoicePlayState::Playing;
}
break;
case Ngs2VoicePlayEvent::Stop:
if (voice.state == Ngs2VoicePlayState::Playing) {
voice.state = Ngs2VoicePlayState::Stopped;
}
break;
case Ngs2VoicePlayEvent::StopImm:
case Ngs2VoicePlayEvent::Kill: voice.state = Ngs2VoicePlayState::Empty; break;
}
voice.event = Ngs2VoicePlayEvent::None;
}
static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
uint32_t output = 0;
while (output < grain && !voice.blocks.empty()) {
auto& block = voice.blocks.front();
auto count = std::min(grain - output, block.info.num_samples - block.cursor);
const int16_t* pcm = nullptr;
const float* decoded = nullptr;
if (voice.decoder != nullptr) {
const auto skip =
std::min(block.skip_remaining,
static_cast<uint32_t>(voice.decoded_frame.size() / voice.channels));
Ajm::AjmGaplessState gapless;
gapless.Set({block.info.num_samples - block.cursor, static_cast<uint16_t>(skip), 0},
true);
const auto result = voice.decoder->Decode(
block.data + block.data_cursor, block.info.data_size - block.data_cursor,
voice.decoded_frame.data(), voice.decoded_frame.size() * sizeof(float), false,
&gapless);
EXIT_NOT_IMPLEMENTED(result.result != OK || result.input_consumed == 0);
block.data_cursor += result.input_consumed;
block.skip_remaining -= skip - gapless.current.skip_samples;
const auto decoded_samples =
static_cast<uint32_t>(result.output_written / (sizeof(float) * voice.channels));
if (decoded_samples == 0) {
continue;
}
EXIT_NOT_IMPLEMENTED(decoded_samples > count);
count = decoded_samples;
decoded = voice.decoded_frame.data();
} else {
pcm = reinterpret_cast<const int16_t*>(block.data) +
(block.info.num_skip_samples + block.cursor) * voice.channels;
}
for (uint32_t c = 0; c < voice.channels; ++c) {
for (uint32_t i = 0; i < count; ++i) {
const auto index = i * voice.channels + c;
voice.samples[c * grain + output + i] =
decoded != nullptr ? decoded[index] : pcm[index] / 32768.0f;
}
}
output += count;
block.cursor += count;
if (block.cursor == block.info.num_samples) {
struct CallbackInfo {
uintptr_t data, voice;
uint32_t flag, reserved;
uintptr_t user;
const void* block_data;
size_t size;
uint32_t repeats, attributes;
} info {voice.callback_data,
reinterpret_cast<uintptr_t>(&voice),
1,
0,
block.info.user_data,
block.data,
block.info.data_size,
0,
0};
static_assert(sizeof(CallbackInfo) == 56);
voice.blocks.pop_front();
if (voice.blocks.empty() && !voice.accepts_blocks) {
voice.state = Ngs2VoicePlayState::Empty;
}
if (voice.callback != 0 && (voice.callback_flags & 1u) != 0) {
reinterpret_cast<void KYTY_SYSV_ABI (*)(const CallbackInfo*)>(voice.callback)(
&info);
}
}
}
voice.has_samples = output != 0;
}
static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2VoiceInternal*>& voices,
uint32_t grain) {
if (voice.rendered) {
return;
}
EXIT_NOT_IMPLEMENTED(voice.rendering);
voice.rendering = true;
voice.samples.assign(grain * voice.channels, 0.0f);
voice.has_samples = false;
if (voice.state == Ngs2VoicePlayState::Playing) {
if (voice.rack->type == Ngs2RackType::CustomSampler) {
Ngs2ConsumeSamples(voice, grain);
}
for (auto* source: voices) {
for (const auto& port: source->ports) {
if (port.dest != &voice || port.volume == 0.0f) {
continue;
}
Ngs2RenderVoice(*source, voices, grain);
if (!source->has_samples) {
continue;
}
EXIT_NOT_IMPLEMENTED(port.input != 0);
const auto* matrix = port.matrix < 0 ? nullptr : &source->matrices.at(port.matrix);
for (uint32_t dst = 0; dst < voice.channels; ++dst) {
for (uint32_t src = 0; src < source->channels; ++src) {
const float level =
port.volume * (matrix == nullptr
? (src == dst ? 1.0f : 0.0f)
: matrix->at(dst * source->channels + src));
for (uint32_t i = 0; i < grain; ++i) {
voice.samples[dst * grain + i] +=
source->samples[src * grain + i] * level;
}
}
}
voice.has_samples = true;
}
}
if (voice.has_samples) {
const auto& custom = voice.rack->option.custom_sampler.custom_rack_option;
for (size_t m = 0; m < voice.modules.size(); ++m) {
EXIT_NOT_IMPLEMENTED(custom.module[m].module_id != 0x1f);
EXIT_NOT_IMPLEMENTED(custom.module[m].source_buffer_id != 0 ||
custom.module[m].dest_buffer_id != 0);
auto& module = voice.modules[m];
const auto& fx = voice.rack->fx[m];
std::vector<float*> channels(voice.channels);
for (uint32_t c = 0; c < voice.channels; ++c) {
channels[c] = voice.samples.data() + c * grain;
}
Ngs2UserFxProcessContext context {channels.data(),
voice.rack->common[m].data(),
module.param.data(),
module.work.data(),
module.state.data(),
fx.user_data,
module.flags,
voice.channels,
voice.channels,
grain,
voice.rack->ngs->option.sample_rate};
EXIT_NOT_IMPLEMENTED(fx.process(&context) != OK);
module.flags = 0;
}
}
}
voice.rendering = false;
voice.rendered = true;
}
int KYTY_SYSV_ABI Ngs2SystemRender(uintptr_t system_handle, const Ngs2RenderBufferInfo* buffer_info,
uint32_t num_buffer_info) {
static std::atomic_uint32_t render_log_count = 0;
const auto log_index = render_log_count.fetch_add(1, std::memory_order_relaxed);
const bool log_this_call = (log_index < 16 || (log_index % 600) == 0);
if (log_this_call) {
PRINT_NAME();
LOGF("\t call_count = %" PRIu32 "\n", log_index + 1);
}
EXIT_NOT_IMPLEMENTED(buffer_info == nullptr);
EXIT_NOT_IMPLEMENTED(system_handle == 0);
EXIT_NOT_IMPLEMENTED(num_buffer_info == 0);
EXIT_NOT_IMPLEMENTED(buffer_info == nullptr || system_handle == 0 || num_buffer_info == 0);
auto* ngs = reinterpret_cast<Ngs2Internal*>(system_handle);
Common::LockGuard lock(ngs->mutex);
for (uint32_t i = 0; i < num_buffer_info; i++) {
std::vector<Ngs2VoiceInternal*> voices;
{
Common::LockGuard racks_lock(g_racks_mutex);
for (auto* rack = g_racks_list; rack != nullptr; rack = rack->next) {
if (rack->ngs != ngs) {
continue;
}
auto* items = reinterpret_cast<Ngs2VoiceInternal*>(rack + 1);
for (uint32_t i = 0; i < rack->option.common.max_voices; ++i) {
Ngs2ApplyEvent(items[i]);
items[i].rendered = false;
if (items[i].channels != 0) {
voices.push_back(items + i);
}
}
}
}
for (uint32_t i = 0; i < num_buffer_info; ++i) {
if (buffer_info[i].buffer != nullptr && buffer_info[i].buffer_size != 0) {
std::memset(buffer_info[i].buffer, 0, buffer_info[i].buffer_size);
}
}
Common::LockGuard racks_lock(g_racks_mutex);
for (auto* rack = g_racks_list; rack != nullptr; rack = rack->next) {
if (rack->ngs == ngs) {
auto* voices = reinterpret_cast<Ngs2VoiceInternal*>(rack + 1);
for (uint32_t i = 0; i < rack->option.common.max_voices; i++) {
auto& voice = voices[i];
switch (voice.event) {
case Ngs2VoicePlayEvent::None:
if (voice.state == Ngs2VoicePlayState::Playing ||
voice.state == Ngs2VoicePlayState::Stopped) {
voice.state = Ngs2VoicePlayState::Empty;
}
break;
case Ngs2VoicePlayEvent::Play:
if (voice.state == Ngs2VoicePlayState::Empty) {
voice.state = Ngs2VoicePlayState::Playing;
}
break;
case Ngs2VoicePlayEvent::Pause:
if (voice.state == Ngs2VoicePlayState::Playing) {
voice.state = Ngs2VoicePlayState::Paused;
}
break;
case Ngs2VoicePlayEvent::Resume:
if (voice.state == Ngs2VoicePlayState::Paused) {
voice.state = Ngs2VoicePlayState::Playing;
}
break;
case Ngs2VoicePlayEvent::Stop:
if (voice.state == Ngs2VoicePlayState::Playing) {
voice.state = Ngs2VoicePlayState::Stopped;
}
break;
case Ngs2VoicePlayEvent::StopImm:
case Ngs2VoicePlayEvent::Kill: voice.state = Ngs2VoicePlayState::Empty; break;
const auto grain = ngs->option.num_grain_samples;
for (auto* voice: voices) {
Ngs2RenderVoice(*voice, voices, grain);
if (voice->rack->type != Ngs2RackType::Mastering || !voice->has_samples) {
continue;
}
EXIT_NOT_IMPLEMENTED(voice->output_id >= num_buffer_info);
const auto& output = buffer_info[voice->output_id];
EXIT_NOT_IMPLEMENTED(output.buffer == nullptr);
EXIT_NOT_IMPLEMENTED(output.waveform_type != 0x18 ||
output.num_channels != voice->channels);
EXIT_NOT_IMPLEMENTED(output.buffer_size < grain * output.num_channels * sizeof(float));
auto* pcm = static_cast<float*>(output.buffer);
for (uint32_t dst = 0; dst < output.num_channels; ++dst) {
for (uint32_t src = 0; src < voice->channels; ++src) {
const float level = voice->output_matrix.empty()
? (src == dst ? 1.0f : 0.0f)
: voice->output_matrix.at(src * output.num_channels + dst);
for (uint32_t i = 0; i < grain; ++i) {
pcm[i * output.num_channels + dst] += voice->samples[src * grain + i] * level;
}
voice.event = Ngs2VoicePlayEvent::None;
}
}
}
ngs->render_count++;
++ngs->render_count;
return OK;
}
@@ -2944,6 +3262,7 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
case 0x0001: {
EXIT_NOT_IMPLEMENTED(param->size != sizeof(Ngs2VoiceMatrixLevelsParam));
const auto* ml = reinterpret_cast<const Ngs2VoiceMatrixLevelsParam*>(param);
voice->SetMatrix(ml->matrix_id, ml->levels, ml->num_levels);
LOGF("\t matrix_id = %u\n"
"\t num_levels = %u\n"
"\t levels = 0x%016" PRIx64 "\n",
@@ -2954,6 +3273,7 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
EXIT_NOT_IMPLEMENTED(param->size != sizeof(Ngs2VoicePortVolumeParam));
const auto* volume =
reinterpret_cast<const Ngs2VoicePortVolumeParam*>(param);
voice->SetVolume(volume->port, volume->level);
LOGF("\t port = %u\n"
"\t level = %f\n",
volume->port, volume->level);
@@ -2962,6 +3282,7 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
case 0x0003: {
EXIT_NOT_IMPLEMENTED(param->size != sizeof(Ngs2VoicePortMatrixParam));
const auto* pm = reinterpret_cast<const Ngs2VoicePortMatrixParam*>(param);
voice->SetPortMatrix(pm->port, pm->matrix_id);
LOGF("\t port = %u\n"
"\t matrix_id = %d\n",
pm->port, pm->matrix_id);
@@ -2978,6 +3299,10 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
case 0x0005: {
EXIT_NOT_IMPLEMENTED(param->size != sizeof(Ngs2VoicePatchParam));
const auto* patch = reinterpret_cast<const Ngs2VoicePatchParam*>(param);
EXIT_NOT_IMPLEMENTED(patch->port >= voice->ports.size());
voice->ports[patch->port].dest =
reinterpret_cast<Ngs2VoiceInternal*>(patch->dest_handle);
voice->ports[patch->port].input = patch->dest_input_id;
LOGF("\t connect->port = %u\n"
"\t connect->dest_input_id = %u\n"
"\t connect->dest_handle = 0x%016" PRIx64 "\n",
@@ -2987,15 +3312,7 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
case 0x0006: {
EXIT_NOT_IMPLEMENTED(param->size != sizeof(Ngs2VoiceEventParam));
const auto* event = reinterpret_cast<const Ngs2VoiceEventParam*>(param);
switch (event->event_id) {
case 0x0001: voice->event = Ngs2VoicePlayEvent::Play; break;
case 0x0002: voice->event = Ngs2VoicePlayEvent::Stop; break;
case 0x0004: voice->event = Ngs2VoicePlayEvent::StopImm; break;
case 0x0008: voice->event = Ngs2VoicePlayEvent::Kill; break;
case 0x0010: voice->event = Ngs2VoicePlayEvent::Pause; break;
case 0x0020: voice->event = Ngs2VoicePlayEvent::Resume; break;
default: EXIT("unknown event_id: 0x%08" PRIx32 "\n", event->event_id);
}
voice->SetEvent(event->event_id);
LOGF("\t event = %u\n", event->event_id);
break;
}
@@ -3020,24 +3337,89 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
case 0x1000: EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Sampler); break;
case 0x2000: EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Submixer); break;
case 0x2001: EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Reverb); break;
case 0x3000: EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Mastering); break;
case 0x4000: {
EXIT_NOT_IMPLEMENTED(!Ngs2RackIsCustom(voice->rack->type));
auto cid = param->id & 0xffffu;
auto module_id = (cid >> 8u) & 0xffu;
auto ctl_id = (cid >> 5u) & 0x7u;
auto module_no = cid & 0x1fu;
LOGF("\t custom module_id = 0x%02" PRIx32 ", ctl_id = 0x%" PRIx32
", module_no = %" PRIu32 "\n",
module_id, ctl_id, module_no);
case 0x3000: {
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Mastering);
const auto* values = reinterpret_cast<const uint32_t*>(param + 1);
switch (param->id & 0xffffu) {
case 0: voice->channels = values[0]; break;
case 1: {
const auto* matrix =
reinterpret_cast<const Ngs2VoiceMatrixLevelsParam*>(param);
EXIT_NOT_IMPLEMENTED(matrix->matrix_id != 5);
voice->output_matrix.assign(matrix->levels,
matrix->levels + matrix->num_levels);
break;
}
case 5: voice->output_id = values[0]; break;
default: EXIT("unsupported mastering control: 0x%08" PRIx32 "\n", param->id);
}
break;
}
case 0x4001:
case 0x4000: {
EXIT_NOT_IMPLEMENTED(!Ngs2RackIsCustom(voice->rack->type));
const auto index = param->id & 0x1fu;
EXIT_NOT_IMPLEMENTED((param->id & 0xffffffe0u) != 0x40001f00u ||
index >= voice->modules.size());
struct FxParam {
Ngs2VoiceParamHeader header;
const void* data;
size_t size;
};
const auto& fx = *reinterpret_cast<const FxParam*>(param);
auto& module = voice->modules[index];
EXIT_NOT_IMPLEMENTED(voice->rack->fx[index].control != 0 ||
fx.size != module.param.size());
std::memcpy(module.param.data(), fx.data, fx.size);
module.flags |= 2;
break;
}
case 0x4001: {
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::CustomSampler);
switch (param->id & 0xffffu) {
case 0: {
const auto& format =
*reinterpret_cast<const Ngs2WaveformFormat*>(param + 1);
voice->SetupSampler(format);
break;
}
case 1: {
struct BlocksParam {
Ngs2VoiceParamHeader header;
const uint8_t* data;
uint32_t flags, count;
const Ngs2WaveformBlock* blocks;
};
const auto& blocks = *reinterpret_cast<const BlocksParam*>(param);
EXIT_NOT_IMPLEMENTED(!voice->accepts_blocks ||
(blocks.flags != 0 && blocks.flags != 0x11));
voice->accepts_blocks = (blocks.flags & 1u) != 0;
for (uint32_t i = 0; i < blocks.count; ++i) {
const auto& block = blocks.blocks[i];
EXIT_NOT_IMPLEMENTED(block.num_repeats != 0 || block.num_samples == 0);
EXIT_NOT_IMPLEMENTED(
voice->decoder == nullptr &&
(uint64_t(block.num_skip_samples) + block.num_samples) *
voice->channels * 2 >
block.data_size);
voice->blocks.push_back({blocks.data + block.data_offset, block});
}
break;
}
case 5:
EXIT_NOT_IMPLEMENTED(*reinterpret_cast<const float*>(param + 1) != 1.0f);
break;
default: EXIT("unsupported sampler control: 0x%08" PRIx32 "\n", param->id);
}
break;
case 0x4002:
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::CustomSubmixer);
}
case 0x4002: {
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::CustomSubmixer ||
(param->id & 0xffffu) != 0);
const auto* channels = reinterpret_cast<const uint32_t*>(param + 1);
EXIT_NOT_IMPLEMENTED(channels[0] != channels[1]);
voice->channels = channels[0];
break;
}
case 0x4003:
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::CustomMastering);
break;
@@ -3055,14 +3437,44 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
}
int KYTY_SYSV_ABI Ngs2VoiceRunCommands(uintptr_t voice_handle, const void* commands,
uint32_t num_commands, uint32_t flags) {
PRINT_NAME();
(void)voice_handle;
(void)commands;
(void)num_commands;
(void)flags;
size_t num_commands) {
struct Command {
uint32_t id;
uint8_t flags, type;
uint16_t count;
union {
uint32_t u;
int32_t i;
float f;
const float* levels;
} value;
};
auto* voice = reinterpret_cast<Ngs2VoiceInternal*>(voice_handle);
Common::LockGuard lock(voice->rack->ngs->mutex);
const auto* params = static_cast<const Command*>(commands);
for (size_t i = 0; i < num_commands; ++i) {
const auto& p = params[i];
const auto index = p.id >> 24;
switch (p.id & 0xffffffu) {
case 2:
EXIT_NOT_IMPLEMENTED(p.type != 4);
voice->SetEvent(p.value.u);
break;
case 5:
EXIT_NOT_IMPLEMENTED(p.type != 0x11);
voice->SetMatrix(index, p.value.levels, p.count);
break;
case 6:
EXIT_NOT_IMPLEMENTED(p.type != 1);
voice->SetVolume(index, p.value.f);
break;
case 7:
EXIT_NOT_IMPLEMENTED(p.type != 3);
voice->SetPortMatrix(index, p.value.i);
break;
default: EXIT("unknown command: 0x%08" PRIx32 "\n", p.id);
}
}
return OK;
}
+1 -1
View File
@@ -336,7 +336,7 @@ int KYTY_SYSV_ABI Ngs2RackGetVoiceHandle(uintptr_t rack_handle, uint32_t voice_i
uintptr_t* handle);
int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamHeader* param_list);
int KYTY_SYSV_ABI Ngs2VoiceRunCommands(uintptr_t voice_handle, const void* commands,
uint32_t num_commands, uint32_t flags);
size_t num_commands);
int KYTY_SYSV_ABI Ngs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* state,
size_t state_size);
int KYTY_SYSV_ABI Ngs2VoiceGetStateFlags(uintptr_t voice_handle, uint32_t* state_flags);
+108 -84
View File
@@ -117,6 +117,13 @@ struct AvPlayerEventReplacement {
void* object_pointer = nullptr;
AvPlayerEventCallback event_callback = nullptr;
};
static void emit_event(AvPlayerEventReplacement event, int32_t id, void* data = nullptr) {
if (event.event_callback != nullptr) {
LOGF("\t event_id = %d\n", id);
event.event_callback(event.object_pointer, id, 0, data);
}
}
struct AvPlayerUri {
const char* name = nullptr;
uint32_t length = 0;
@@ -639,9 +646,9 @@ private:
class Source {
public:
Source(AvPlayerMemAllocator m, AvPlayerFileReplacement f, int32_t video_buffers, bool vdec2,
uint32_t demux_buffer_size)
: mem(m), file(f),
Source(AvPlayerMemAllocator m, AvPlayerFileReplacement f, AvPlayerEventReplacement e,
int32_t video_buffers, bool vdec2, uint32_t demux_buffer_size)
: mem(m), file(f), event(e),
max_video_buffers(std::clamp(video_buffers <= 0 ? 2 : video_buffers, 2, 16)),
use_vdec2(vdec2),
video_packets(
@@ -699,7 +706,7 @@ public:
int Enable(uint32_t id) {
std::scoped_lock lifecycle_lock(lifecycle_mutex);
std::lock_guard state_lock(mutex);
if (!stopped) {
if (state == State::Playing) {
return AVPLAYER_ERROR_OPERATION_FAILED;
}
if (fmt == nullptr || id >= fmt->nb_streams) {
@@ -717,7 +724,7 @@ public:
int Disable(uint32_t id) {
std::scoped_lock lifecycle_lock(lifecycle_mutex);
std::lock_guard state_lock(mutex);
if (!stopped) {
if (state == State::Playing) {
return AVPLAYER_ERROR_OPERATION_FAILED;
}
if (video_id == static_cast<int>(id)) {
@@ -748,9 +755,11 @@ public:
return AVPLAYER_ERROR_OPERATION_FAILED;
}
time = CurrentTimeNoLock();
restart = !stopped;
restart = state == State::Playing;
was_paused = paused;
stopped = true;
if (restart) {
state = State::Ready;
}
}
if (restart) {
StopWorkers();
@@ -771,7 +780,7 @@ public:
int StartImpl(uint64_t ms, bool paused_after_start = false, bool show_seek_frame = false) {
{
std::lock_guard lock(mutex);
stopped = true;
state = State::Ready;
}
StopWorkers();
interrupt_io = false;
@@ -784,14 +793,17 @@ public:
AutoEnable();
}
if (!video_id && !audio_id) {
state = State::Stopped;
return AVPLAYER_ERROR_OPERATION_FAILED;
}
if (!OpenCodecs()) {
ResetNoLock(true);
state = State::Stopped;
return AVPLAYER_ERROR_OPERATION_FAILED;
}
if (!AllocateBuffers()) {
ResetNoLock(true);
state = State::Stopped;
return AVPLAYER_ERROR_NO_MEMORY;
}
SeekNoLock(ms);
@@ -802,12 +814,11 @@ public:
pause_time = clock_start;
seek_video_frame_pending =
paused_after_start && show_seek_frame && video_id.has_value();
stopped = false;
}
if (!StartWorkers()) {
std::lock_guard lock(mutex);
stopped = true;
state = State::Stopped;
ResetNoLock(true);
result = AVPLAYER_ERROR_OPERATION_FAILED;
}
@@ -817,28 +828,39 @@ public:
return result;
}
int Stop() {
std::scoped_lock lifecycle_lock(lifecycle_mutex);
bool was_playing_video = false;
bool was_playing_video = false;
AvPlayerEventReplacement stop_event {};
{
std::lock_guard lock(mutex);
was_playing_video = !stopped && video_id.has_value();
stopped = true;
}
StopWorkers();
{
std::lock_guard lock(mutex);
ResetNoLock();
std::scoped_lock lifecycle_lock(lifecycle_mutex);
{
std::lock_guard lock(mutex);
if (state == State::Playing) {
was_playing_video = video_id.has_value();
stop_event = event;
}
state = State::Stopped;
}
StopWorkers();
{
std::lock_guard lock(mutex);
ResetNoLock();
}
}
if (was_playing_video) {
::printf("AvPlayer video stopped\n");
}
emit_event(stop_event, AVPLAYER_EVENT_STATE_STOP);
return 0;
}
void Pause() {
int Pause() {
std::scoped_lock lifecycle_lock(lifecycle_mutex);
std::lock_guard lock(mutex);
if (state != State::Playing) {
return AVPLAYER_ERROR_OPERATION_FAILED;
}
paused = true;
pause_time = std::chrono::steady_clock::now();
return 0;
}
void Resume() {
std::scoped_lock lifecycle_lock(lifecycle_mutex);
@@ -860,9 +882,8 @@ public:
}
bool Active() const {
std::lock_guard lock(mutex);
return !stopped && !pipeline_failed &&
(!demux_eof || !video_done || !audio_done || !video_frames.Empty() ||
!audio_frames.Empty());
return state == State::Ready ||
(state == State::Playing && !pipeline_failed && !DrainedNoLock());
}
uint64_t CurrentTime() const {
std::lock_guard lock(mutex);
@@ -873,12 +894,15 @@ public:
bool was_paused = false;
{
std::lock_guard lock(mutex);
if (state != State::Playing) {
return AVPLAYER_ERROR_OPERATION_FAILED;
}
was_paused = paused;
}
return StartImpl(ms, was_paused, true);
}
uint64_t CurrentTimeNoLock() const {
if (stopped) {
if (state != State::Playing) {
return 0;
}
using namespace std::chrono;
@@ -915,7 +939,7 @@ public:
return false;
}
std::lock_guard lock(mutex);
if (stopped || !video_id) {
if (state != State::Playing || !video_id) {
return false;
}
const bool deliver_seek_frame = paused && seek_video_frame_pending;
@@ -955,7 +979,8 @@ public:
return false;
}
std::lock_guard lock(mutex);
if (paused || stopped || !audio_id || trick_speed != AVPLAYER_TRICK_SPEED_NORMAL) {
if (paused || state != State::Playing || !audio_id ||
trick_speed != AVPLAYER_TRICK_SPEED_NORMAL) {
return false;
}
auto frame = audio_frames.TryPop();
@@ -988,6 +1013,12 @@ public:
}
private:
enum class State { Ready, Playing, Stopped };
bool DrainedNoLock() const {
return demux_eof && video_done && audio_done && video_frames.Empty() &&
audio_frames.Empty();
}
void RecordLoopBoundary(const ReadyFrame& frame) {
if (frame.timestamp_offset > last_output_loop_offset) {
last_output_loop_offset = frame.timestamp_offset;
@@ -1002,23 +1033,17 @@ private:
streamer.reset();
}
void ResetNoLock(bool retain_output_buffers = false) {
std::optional<ReadyFrame> delivered_video;
std::optional<ReadyFrame> delivered_audio;
std::deque<std::unique_ptr<GuestBuffer>> in_flight_video;
if (retain_output_buffers) {
delivered_video = std::move(current_video);
delivered_audio = std::move(current_audio);
in_flight_video = std::move(retired_video);
}
video_packets.Clear();
audio_packets.Clear();
video_frames.Clear();
audio_frames.Clear();
video_buffers.Clear();
audio_buffers.Clear();
retired_video.clear();
current_video.reset();
current_audio.reset();
if (!retain_output_buffers) {
retired_video.clear();
current_video.reset();
current_audio.reset();
}
if (sws != nullptr) {
sws_freeContext(sws);
sws = nullptr;
@@ -1042,9 +1067,6 @@ private:
last_audio_ts = 0;
last_output_loop_offset = 0;
pending_loop_warnings = 0;
current_video = std::move(delivered_video);
current_audio = std::move(delivered_audio);
retired_video = std::move(in_flight_video);
}
void AutoEnable() {
for (uint32_t i = 0; i < fmt->nb_streams; i++) {
@@ -1174,6 +1196,7 @@ private:
return true;
}
bool StartWorkers() {
std::unique_lock lock(mutex);
worker_stop = false;
interrupt_io = false;
pipeline_failed = false;
@@ -1183,9 +1206,11 @@ private:
if ((video_id && !CreateWorker(video_thread, VideoDecoderEntry, "AvPlayerVideoDecoder")) ||
(audio_id && !CreateWorker(audio_thread, AudioDecoderEntry, "AvPlayerAudioDecoder")) ||
!CreateWorker(demux_thread, DemuxEntry, "AvPlayerDemuxer")) {
lock.unlock();
StopWorkers();
return false;
}
state = State::Playing;
return true;
}
void NotifyWorkers() {
@@ -1195,8 +1220,11 @@ private:
audio_buffers.Notify();
}
void StopWorkers() {
worker_stop = true;
interrupt_io = true;
{
std::lock_guard lock(mutex);
worker_stop = true;
interrupt_io = true;
}
NotifyWorkers();
if (demux_thread != nullptr) {
LibKernel::PthreadJoin(demux_thread, nullptr);
@@ -1212,9 +1240,12 @@ private:
}
}
void FailPipeline() {
pipeline_failed = true;
worker_stop = true;
interrupt_io = true;
{
std::lock_guard lock(mutex);
pipeline_failed = true;
worker_stop = true;
interrupt_io = true;
}
NotifyWorkers();
}
void Demux() {
@@ -1381,15 +1412,8 @@ private:
return use_vdec2 ? static_cast<uint32_t>(s->codecpar->height)
: align_up(static_cast<uint32_t>(s->codecpar->height), 16);
}
float Aspect(AVStream* s) const {
if (s->codecpar->height == 0) {
return 0.0f;
}
double r = static_cast<double>(s->codecpar->width) / s->codecpar->height;
if (s->sample_aspect_ratio.num > 0 && s->sample_aspect_ratio.den > 0) {
r *= av_q2d(s->sample_aspect_ratio);
}
return static_cast<float>(r);
float Aspect(AVRational ratio) const {
return ratio.num > 0 && ratio.den > 0 ? static_cast<float>(av_q2d(ratio)) : 0.0f;
}
uint64_t Duration(AVStream* s) const {
return s->duration != AV_NOPTS_VALUE
@@ -1402,14 +1426,14 @@ private:
std::memset(v, 0, sizeof(*v));
v->width = Width(s);
v->height = Height(s);
v->aspect_ratio = Aspect(s);
v->aspect_ratio = Aspect(s->sample_aspect_ratio);
lang(v->language_code, s->metadata);
}
void FillVideoEx(AVStream* s, AvPlayerVideoEx* v) const {
void FillVideoEx(AVStream* s, AvPlayerVideoEx* v, AVRational aspect) const {
std::memset(v, 0, sizeof(*v));
v->width = Width(s);
v->height = Height(s);
v->aspect_ratio = Aspect(s);
v->aspect_ratio = Aspect(aspect);
v->pitch = VideoPitch(s);
v->luma_bit_depth = 8;
v->chroma_bit_depth = 8;
@@ -1445,7 +1469,7 @@ private:
FillVideo(s, &d->video);
}
if (ex) {
FillVideoEx(s, &ex->video);
FillVideoEx(s, &ex->video, s->sample_aspect_ratio);
}
break;
case AVMEDIA_TYPE_AUDIO:
@@ -1530,7 +1554,7 @@ private:
info->time_stamp = to_ms(
src->best_effort_timestamp != AV_NOPTS_VALUE ? src->best_effort_timestamp : src->pts,
s->time_base);
FillVideoEx(s, &info->details.video);
FillVideoEx(s, &info->details.video, src->sample_aspect_ratio);
info->details.video.crop_left_offset = static_cast<uint32_t>(src->crop_left);
info->details.video.crop_right_offset =
static_cast<uint32_t>(src->crop_right + (pitch - src->width));
@@ -1607,6 +1631,7 @@ private:
}
AvPlayerMemAllocator mem;
AvPlayerFileReplacement file;
AvPlayerEventReplacement event;
int max_video_buffers = 2;
bool use_vdec2 = false;
AvPlayerSourceType source_type = AvPlayerSourceUnknown;
@@ -1642,7 +1667,7 @@ private:
std::atomic_bool audio_done {true};
std::mutex lifecycle_mutex;
mutable std::mutex mutex;
bool stopped = true;
State state = State::Ready;
bool paused = false;
bool seek_video_frame_pending = false;
std::atomic_bool loop {false};
@@ -1664,6 +1689,7 @@ struct AvPlayerInternal {
AvPlayerPostInitData post_init {};
bool auto_start = false;
int32_t video_buffers = 2;
uint32_t sync_mode = 0;
uint32_t start_bandwidth = 0;
uint32_t minimum_bandwidth = 0;
uint32_t maximum_bandwidth = 0;
@@ -1673,19 +1699,13 @@ struct AvPlayerInternal {
static bool valid_allocators(const AvPlayerMemAllocator& m) {
return m.allocate && m.deallocate && m.allocate_texture && m.deallocate_texture;
}
static void emit_event(AvPlayerInternal* h, int32_t id, void* data = nullptr) {
if (h != nullptr && h->event.event_callback != nullptr) {
LOGF("\t event_id = %d\n", id);
h->event.event_callback(h->event.object_pointer, id, 0, data);
}
}
static void pump_warnings(AvPlayerInternal* h) {
if (h == nullptr || h->source == nullptr) {
return;
}
while (auto w = h->source->TakeWarning()) {
int32_t warning = *w;
emit_event(h, AVPLAYER_EVENT_WARNING_ID, &warning);
emit_event(h->event, AVPLAYER_EVENT_WARNING_ID, &warning);
}
}
static AvPlayerInternal* create_player(const AvPlayerMemAllocator& mem,
@@ -1713,17 +1733,18 @@ static int add_source(AvPlayerInternal* h, const std::string& filename, AvPlayer
}
bool vdec2 =
h->post_init.video_decoder_init.decoder_type.video_type == AvPlayerVideoDecoderSoftware2;
auto s = std::make_unique<Source>(h->mem, h->file, h->video_buffers, vdec2,
auto s = std::make_unique<Source>(h->mem, h->file, h->event, h->video_buffers, vdec2,
h->post_init.demux_video_buffer_size);
if (auto rc = s->Init(filename, type); rc < 0) {
return rc;
}
s->SetSync(h->sync_mode);
h->source = std::move(s);
emit_event(h, AVPLAYER_EVENT_STATE_READY);
emit_event(h->event, AVPLAYER_EVENT_STATE_READY);
if (h->auto_start) {
auto rc = h->source->Start();
if (rc == 0) {
emit_event(h, AVPLAYER_EVENT_STATE_PLAY);
emit_event(h->event, AVPLAYER_EVENT_STATE_PLAY);
}
return rc;
}
@@ -1872,7 +1893,7 @@ int KYTY_SYSV_ABI AvPlayerStart(AvPlayerInternal* h) {
}
auto rc = h->source->Start();
if (rc == 0) {
emit_event(h, AVPLAYER_EVENT_STATE_PLAY);
emit_event(h->event, AVPLAYER_EVENT_STATE_PLAY);
}
return rc;
}
@@ -1887,7 +1908,7 @@ int KYTY_SYSV_ABI AvPlayerStartEx(AvPlayerInternal* h, const void* start_info_ex
: static_cast<const AvPlayerStartInfoEx*>(start_info_ex)->start_time_milliseconds;
auto rc = h->source->Start(ms);
if (rc == 0) {
emit_event(h, AVPLAYER_EVENT_STATE_PLAY);
emit_event(h->event, AVPLAYER_EVENT_STATE_PLAY);
}
return rc;
}
@@ -1896,18 +1917,18 @@ int KYTY_SYSV_ABI AvPlayerStop(AvPlayerInternal* h) {
if (h == nullptr || h->source == nullptr) {
return AVPLAYER_ERROR_INVALID_PARAMS;
}
auto rc = h->source->Stop();
emit_event(h, AVPLAYER_EVENT_STATE_STOP);
return rc;
return h->source->Stop();
}
int KYTY_SYSV_ABI AvPlayerPause(AvPlayerInternal* h) {
PRINT_NAME();
if (h == nullptr || h->source == nullptr) {
return AVPLAYER_ERROR_INVALID_PARAMS;
}
h->source->Pause();
emit_event(h, AVPLAYER_EVENT_STATE_PAUSE);
return 0;
auto rc = h->source->Pause();
if (rc == 0) {
emit_event(h->event, AVPLAYER_EVENT_STATE_PAUSE);
}
return rc;
}
int KYTY_SYSV_ABI AvPlayerResume(AvPlayerInternal* h) {
PRINT_NAME();
@@ -1915,7 +1936,7 @@ int KYTY_SYSV_ABI AvPlayerResume(AvPlayerInternal* h) {
return AVPLAYER_ERROR_INVALID_PARAMS;
}
h->source->Resume();
emit_event(h, AVPLAYER_EVENT_STATE_PLAY);
emit_event(h->event, AVPLAYER_EVENT_STATE_PLAY);
return 0;
}
int KYTY_SYSV_ABI AvPlayerSetLooping(AvPlayerInternal* h, Bool loop) {
@@ -1941,13 +1962,16 @@ int KYTY_SYSV_ABI AvPlayerSetTrickSpeed(AvPlayerInternal* h, int32_t trick_speed
}
int KYTY_SYSV_ABI AvPlayerSetAvSyncMode(AvPlayerInternal* h, uint32_t sync_mode) {
PRINT_NAME();
if (h == nullptr || h->source == nullptr) {
if (h == nullptr) {
return AVPLAYER_ERROR_INVALID_PARAMS;
}
if (sync_mode > 1) {
return AVPLAYER_ERROR_NOT_SUPPORTED;
}
h->source->SetSync(sync_mode);
h->sync_mode = sync_mode;
if (h->source != nullptr) {
h->source->SetSync(sync_mode);
}
return 0;
}
int KYTY_SYSV_ABI AvPlayerSetAvailableBandwidth(AvPlayerInternal* h, uint32_t start_bandwidth,
@@ -2003,7 +2027,7 @@ Bool KYTY_SYSV_ABI AvPlayerGetAudioData(AvPlayerInternal* h, AvPlayerFrameInfo*
}
Bool KYTY_SYSV_ABI AvPlayerIsActive(AvPlayerInternal* h) {
PRINT_NAME();
return h != nullptr && h->source != nullptr && h->source->Active() ? 1 : 0;
return h != nullptr && (h->source == nullptr || h->source->Active());
}
uint64_t KYTY_SYSV_ABI AvPlayerCurrentTime(AvPlayerInternal* h) {
PRINT_NAME();
@@ -2017,7 +2041,7 @@ int KYTY_SYSV_ABI AvPlayerJumpToTime(AvPlayerInternal* h, uint64_t time_ms) {
auto rc = h->source->Jump(time_ms);
if (rc == 0) {
int32_t w = AVPLAYER_WARNING_JUMP_COMPLETE;
emit_event(h, AVPLAYER_EVENT_WARNING_ID, &w);
emit_event(h->event, AVPLAYER_EVENT_WARNING_ID, &w);
}
return rc;
}
+361 -65
View File
@@ -14,7 +14,9 @@
#include <algorithm>
#include <cstring>
#include <ctime>
#include <deque>
#include <map>
#include <vector>
namespace Libs {
@@ -25,6 +27,7 @@ namespace SaveData {
// TODO(): specify dir at launcher
static constexpr char SAVE_DATA_DIR[] = "_SaveData";
static constexpr uint64_t SAVE_DATA_BLOCKS_MIN = 48;
static constexpr uint64_t SAVE_DATA_BLOCKS_MAX = 16384;
struct SceSaveDataDirName {
@@ -176,6 +179,13 @@ struct SaveDataMemorySet2 {
uint8_t reserved[24];
};
struct SaveDataMemorySync {
int32_t user_id;
uint32_t slot_id;
uint32_t option;
uint8_t reserved[28];
};
struct SaveDataTransferringMount {
int32_t user_id;
const SceSaveDataTitleId* title_id;
@@ -230,23 +240,170 @@ static constexpr uint32_t SAVE_DATA_COMMIT_MODE_BACKUP_ASYNC = 1u;
static constexpr uint32_t SAVE_DATA_EVENT_TYPE_UMOUNT_BACKUP_END = 1u;
static constexpr uint32_t SAVE_DATA_EVENT_TYPE_BACKUP_END = 2u;
static constexpr uint32_t SAVE_DATA_EVENT_TYPE_MEMORY_SYNC_END = 3u;
static constexpr uint32_t SAVE_DATA_EVENT_TYPE_COMMIT_BACKUP_END = 4u;
static std::vector<uint8_t> g_save_data_memory(0x10000);
static constexpr size_t SAVE_DATA_MEMORY_MAX_SIZE = 32 * 1024 * 1024;
static constexpr uint32_t SAVE_DATA_MEMORY_SET_PARAM = 1u;
static constexpr uint32_t SAVE_DATA_MEMORY_DOUBLE_BUFFER = 2u;
struct SaveDataMemory {
std::vector<uint8_t> data;
SaveDataParam param {};
uint32_t option = 0;
};
static std::map<std::filesystem::path, SaveDataMemory> g_save_data_memory;
static int32_t g_next_transaction_resource = 1;
static std::deque<SaveDataEvent> g_save_data_events;
static SaveDataMountSlots g_mount_slots;
static Common::Mutex g_mount_mutex;
static bool valid_path_component(std::string_view name) {
return !name.empty() && name != "." && name != ".." &&
name.find_first_not_of(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.@") ==
std::string_view::npos;
}
template <size_t N>
static bool valid_path_component(const char (&name)[N]) {
const auto end = std::find(name, name + N, '\0');
return end != name + N && valid_path_component(std::string_view(name, end - name));
}
static std::string get_title_id() {
std::string title_id;
if (!Loader::SystemContentParamSfoGetString("TITLE_ID", &title_id) || title_id.empty()) {
if (!Loader::SystemContentParamSfoGetString("TITLE_ID", &title_id) ||
!valid_path_component(title_id)) {
title_id = "UNKNOWN";
}
return title_id;
}
static std::string memory_dir_name(uint32_t slot_id) {
return "sce_sdmemory" + (slot_id == 0 ? std::string() : std::to_string(slot_id));
}
static std::filesystem::path save_directory(std::string_view title_id, std::string_view dir_name,
int32_t user_id) {
const auto directory = std::filesystem::path(SAVE_DATA_DIR) / title_id / dir_name;
for (uint32_t slot = 0; slot < 4; slot++) {
if (dir_name == memory_dir_name(slot)) {
return directory / std::to_string(user_id);
}
}
return directory;
}
static std::filesystem::path memory_directory(int32_t user_id, uint32_t slot_id) {
return save_directory(get_title_id(), memory_dir_name(slot_id), user_id);
}
static bool valid_memory_range(const SaveDataMemoryData& data, size_t size) {
return data.buf != nullptr && data.offset >= 0 && static_cast<uint64_t>(data.offset) <= size &&
data.buf_size <= size - static_cast<size_t>(data.offset);
}
// Replacing a flushed temporary file protects the
// previous save if writing fails or the emulator exits partway through an update.
static int write_save_file(const std::filesystem::path& path, const void* data, uint32_t size) {
const auto temporary = std::filesystem::path(path.string() + ".tmp");
Common::File file;
if (!file.Create(temporary)) {
return SAVE_DATA_ERROR_INTERNAL;
}
uint32_t written = 0;
file.Write(data, size, &written);
const bool flushed = file.Flush();
file.Close();
std::error_code error;
if (written == size && flushed) {
// Common::File::RenameFile deliberately refuses to replace an existing file.
std::filesystem::rename(temporary, path, error);
if (!error) {
return OK;
}
}
std::filesystem::remove(temporary, error);
return SAVE_DATA_ERROR_INTERNAL;
}
static int read_save_blocks(const std::filesystem::path& directory, uint64_t* blocks) {
Common::File file(directory / "sce_sys" / "blocks.bin", Common::File::Mode::Read);
if (file.IsInvalid() || file.Size() != sizeof(*blocks)) {
return SAVE_DATA_ERROR_BROKEN;
}
uint64_t value = 0;
uint32_t read = 0;
file.Read(&value, sizeof(value), &read);
if (read != sizeof(value) || value < SAVE_DATA_BLOCKS_MIN || value > SAVE_DATA_BLOCKS_MAX) {
return SAVE_DATA_ERROR_BROKEN;
}
*blocks = value;
return OK;
}
static int save_memory(const std::filesystem::path& directory, const SaveDataMemory& memory) {
std::error_code error;
std::filesystem::create_directories(directory, error);
if (error) {
return SAVE_DATA_ERROR_INTERNAL;
}
// Display metadata is a separate host sidecar, leaving memory.dat as raw guest bytes.
int result = write_save_file(directory / "param.bin", &memory.param, sizeof(memory.param));
if (result == OK) {
result =
write_save_file(directory / "memory.dat", memory.data.data(), memory.data.size());
}
return result;
}
static int load_memory(const std::filesystem::path& directory, SaveDataMemory* memory,
size_t* existed_size) {
std::error_code error;
const bool exists = std::filesystem::exists(directory / "memory.dat", error);
if (error) {
return SAVE_DATA_ERROR_INTERNAL;
}
if (!exists) {
return OK;
}
Common::File file(directory / "memory.dat", Common::File::Mode::Read);
if (file.IsInvalid()) {
return SAVE_DATA_ERROR_INTERNAL;
}
*existed_size = file.Size();
if (*existed_size == 0 || *existed_size > SAVE_DATA_MEMORY_MAX_SIZE) {
return SAVE_DATA_ERROR_BROKEN;
}
const auto size = static_cast<uint32_t>(std::min(*existed_size, memory->data.size()));
uint32_t read = 0;
file.Read(memory->data.data(), size, &read);
if (read != size) {
return SAVE_DATA_ERROR_BROKEN;
}
file.Close();
const bool has_param = std::filesystem::exists(directory / "param.bin", error);
if (error) {
return SAVE_DATA_ERROR_INTERNAL;
}
if (has_param) {
if (!file.Open(directory / "param.bin", Common::File::Mode::Read)) {
return SAVE_DATA_ERROR_INTERNAL;
}
if (file.Size() != sizeof(memory->param)) {
return SAVE_DATA_ERROR_BROKEN;
}
file.Read(&memory->param, sizeof(memory->param), &read);
if (read != sizeof(memory->param)) {
return SAVE_DATA_ERROR_BROKEN;
}
}
return OK;
}
static void queue_save_data_event(uint32_t type, int32_t user_id,
const SceSaveDataTitleId* title_id,
const SceSaveDataDirName* dir_name, int32_t error_code = OK) {
@@ -322,6 +479,7 @@ int KYTY_SYSV_ABI SaveDataTerminate() {
return SAVE_DATA_ERROR_BUSY;
}
g_save_data_events.clear();
g_save_data_memory.clear();
return OK;
}
@@ -347,10 +505,17 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
PRINT_NAME();
if (cond == nullptr || result == nullptr ||
(cond->title_id != nullptr && !valid_path_component(cond->title_id->data)) ||
(result->dir_names_num != 0 && result->dir_names == nullptr) ||
(cond->dir_name != nullptr &&
std::memchr(cond->dir_name->data, 0, sizeof(cond->dir_name->data)) == nullptr) ||
static_cast<uint32_t>(cond->key) > static_cast<uint32_t>(SaveDataSortKey::FreeBlocks) ||
static_cast<uint32_t>(cond->order) > static_cast<uint32_t>(SaveDataSortOrder::Descent)) {
return SAVE_DATA_ERROR_PARAMETER;
}
if (cond->user_id < 0) {
return SAVE_DATA_ERROR_INVALID_LOGIN_USER;
}
LOGF("\t user_id = %d\n"
"\t title_id = %s\n"
@@ -369,14 +534,15 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
std::memset(result->reserved, 0, sizeof(result->reserved));
result->pad2 = 0;
Common::LockGuard lock(g_mount_mutex);
const std::string title_id = cond->title_id != nullptr ? cond->title_id->data : get_title_id();
const auto root = std::filesystem::path(SAVE_DATA_DIR) / title_id;
std::vector<std::string> dir_list;
std::string root =
Common::FixDirectorySlash((std::string(SAVE_DATA_DIR) + "/" + get_title_id()));
if (Common::File::IsDirectoryExisting(root)) {
for (const auto& entry: Common::File::GetDirEntries(root)) {
if (!entry.is_file && entry.name != "." && entry.name != ".." &&
!entry.name.starts_with("sce_")) {
!entry.name.starts_with("sce_") && valid_path_component(entry.name)) {
if (cond->dir_name == nullptr || cond->dir_name->data[0] == '\0' ||
dir_name_match(Common::ToLower(entry.name).c_str(),
Common::ToLower(std::string(cond->dir_name->data)).c_str())) {
@@ -411,9 +577,14 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
result->params[i] = {};
}
if (result->infos != nullptr) {
result->infos[i] = {};
result->infos[i].blocks = SAVE_DATA_BLOCKS_MAX;
result->infos[i].free_blocks = SAVE_DATA_BLOCKS_MAX;
auto& info = result->infos[i];
info = {};
const int status = read_save_blocks(root / dir_list[i], &info.blocks);
if (status != OK) {
return status;
}
// Host-directory saves report their allocation as free space.
info.free_blocks = info.blocks;
}
}
@@ -423,9 +594,13 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
int KYTY_SYSV_ABI SaveDataMount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result) {
PRINT_NAME();
EXIT_NOT_IMPLEMENTED(mount == nullptr);
EXIT_NOT_IMPLEMENTED(mount_result == nullptr);
EXIT_NOT_IMPLEMENTED(mount->dir_name == nullptr);
if (mount == nullptr || mount_result == nullptr || mount->dir_name == nullptr ||
!valid_path_component(mount->dir_name->data)) {
return SAVE_DATA_ERROR_PARAMETER;
}
if (mount->user_id < 0) {
return SAVE_DATA_ERROR_INVALID_LOGIN_USER;
}
LOGF("\t user_id = %d\n"
"\t dir_name = %s\n"
@@ -439,9 +614,8 @@ int KYTY_SYSV_ABI SaveDataMount3(const SaveDataMount3* mount, SaveDataMountResul
*mount_result = {};
Common::LockGuard lock(g_mount_mutex);
const std::string dir_name = mount->dir_name->data;
const std::string mount_dir =
std::string(SAVE_DATA_DIR) + "/" + get_title_id() + "/" + dir_name;
const std::string dir_name = mount->dir_name->data;
const auto mount_dir = save_directory(get_title_id(), dir_name, mount->user_id).string();
const bool create = ((mount->mount_mode & 4u) != 0);
const bool create2 = ((mount->mount_mode & 32u) != 0);
const bool open = (!create && !create2 && ((mount->mount_mode & 3u) != 0));
@@ -468,10 +642,22 @@ int KYTY_SYSV_ABI SaveDataMount3(const SaveDataMount3* mount, SaveDataMountResul
bool created = false;
if ((create || create2) && !Common::File::IsDirectoryExisting(mount_dir)) {
Common::File::CreateDirectories(mount_dir);
if (mount->blocks < SAVE_DATA_BLOCKS_MIN || mount->blocks > SAVE_DATA_BLOCKS_MAX) {
return SAVE_DATA_ERROR_PARAMETER;
}
const auto metadata = std::filesystem::path(mount_dir) / "sce_sys";
std::error_code error;
std::filesystem::create_directories(metadata, error);
if (error) {
return SAVE_DATA_ERROR_INTERNAL;
}
const int status = write_save_file(metadata / "blocks.bin", &mount->blocks,
sizeof(mount->blocks));
if (status != OK) {
std::filesystem::remove_all(mount_dir, error);
return status;
}
created = true;
EXIT_NOT_IMPLEMENTED((!Common::File::IsDirectoryExisting(mount_dir)));
}
return mount_save_data(slot, dir_name, mount_dir, created ? 1u : 0u, mount_result);
@@ -481,9 +667,13 @@ int KYTY_SYSV_ABI SaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2* setup
SaveDataMemorySetupResult* result) {
PRINT_NAME();
if (setup_param == nullptr) {
if (setup_param == nullptr || setup_param->slot_id >= 4 || setup_param->memory_size == 0 ||
setup_param->memory_size > SAVE_DATA_MEMORY_MAX_SIZE || (setup_param->option & ~3u) != 0) {
return SAVE_DATA_ERROR_PARAMETER;
}
if (setup_param->user_id < 0) {
return SAVE_DATA_ERROR_INVALID_LOGIN_USER;
}
LOGF("\t option = 0x%08" PRIx32 "\n"
"\t user_id = %" PRId32 "\n"
@@ -493,13 +683,41 @@ int KYTY_SYSV_ABI SaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2* setup
setup_param->option, setup_param->user_id, static_cast<uint64_t>(setup_param->memory_size),
static_cast<uint64_t>(setup_param->icon_memory_size), setup_param->slot_id);
if (setup_param->memory_size > g_save_data_memory.size()) {
g_save_data_memory.resize(setup_param->memory_size);
Common::LockGuard lock(g_mount_mutex);
const auto directory = memory_directory(setup_param->user_id, setup_param->slot_id);
if (g_save_data_memory.contains(directory) ||
g_mount_slots.FindAvailable(memory_dir_name(setup_param->slot_id)) ==
SaveDataMountSlots::BUSY) {
return SAVE_DATA_ERROR_BUSY;
}
size_t total = setup_param->memory_size *
((setup_param->option & SAVE_DATA_MEMORY_DOUBLE_BUFFER) != 0 ? 2 : 1);
for (const auto& [path, memory]: g_save_data_memory) {
total +=
memory.data.size() * ((memory.option & SAVE_DATA_MEMORY_DOUBLE_BUFFER) != 0 ? 2 : 1);
}
if (g_save_data_memory.size() >= 4 || total > SAVE_DATA_MEMORY_MAX_SIZE) {
return SAVE_DATA_ERROR_LIMITATION_OVER;
}
SaveDataMemory memory;
memory.data.resize(setup_param->memory_size);
memory.option = setup_param->option;
std::snprintf(memory.param.title, sizeof(memory.param.title), "Saved Data");
size_t existed_size = 0;
const int status = load_memory(directory, &memory, &existed_size);
if (status != OK) {
return status;
}
if (existed_size == 0 && setup_param->init_param != nullptr &&
(memory.option & SAVE_DATA_MEMORY_SET_PARAM) != 0) {
memory.param = *setup_param->init_param;
memory.param.mtime = 0;
}
g_save_data_memory.emplace(directory, std::move(memory));
if (result != nullptr) {
*result = {};
result->existed_memory_size = g_save_data_memory.size();
result->existed_memory_size = existed_size;
}
return OK;
@@ -508,9 +726,12 @@ int KYTY_SYSV_ABI SaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2* setup
int KYTY_SYSV_ABI SaveDataGetSaveDataMemory2(SaveDataMemoryGet2* get_param) {
PRINT_NAME();
if (get_param == nullptr) {
if (get_param == nullptr || get_param->slot_id >= 4) {
return SAVE_DATA_ERROR_PARAMETER;
}
if (get_param->user_id < 0) {
return SAVE_DATA_ERROR_INVALID_LOGIN_USER;
}
LOGF("\t user_id = %" PRId32 "\n"
"\t data = 0x%016" PRIx64 "\n"
@@ -521,20 +742,23 @@ int KYTY_SYSV_ABI SaveDataGetSaveDataMemory2(SaveDataMemoryGet2* get_param) {
reinterpret_cast<uint64_t>(get_param->param), reinterpret_cast<uint64_t>(get_param->icon),
get_param->slot_id);
Common::LockGuard lock(g_mount_mutex);
const auto it =
g_save_data_memory.find(memory_directory(get_param->user_id, get_param->slot_id));
if (it == g_save_data_memory.end()) {
return SAVE_DATA_ERROR_MEMORY_NOT_READY;
}
const auto& memory = it->second;
if ((get_param->param != nullptr && (memory.option & SAVE_DATA_MEMORY_SET_PARAM) == 0) ||
(get_param->data != nullptr && !valid_memory_range(*get_param->data, memory.data.size()))) {
return SAVE_DATA_ERROR_PARAMETER;
}
if (get_param->data != nullptr) {
auto* data = get_param->data;
if (data->buf == nullptr || data->offset < 0) {
return SAVE_DATA_ERROR_PARAMETER;
}
const auto offset = static_cast<size_t>(data->offset);
if (offset + data->buf_size > g_save_data_memory.size()) {
g_save_data_memory.resize(offset + data->buf_size);
}
std::memcpy(data->buf, g_save_data_memory.data() + offset, data->buf_size);
const auto& data = *get_param->data;
std::memcpy(data.buf, memory.data.data() + data.offset, data.buf_size);
}
if (get_param->param != nullptr) {
std::memset(get_param->param, 0, sizeof(*get_param->param));
*get_param->param = memory.param;
}
if (get_param->icon != nullptr) {
get_param->icon->data_size = 0;
@@ -546,9 +770,13 @@ int KYTY_SYSV_ABI SaveDataGetSaveDataMemory2(SaveDataMemoryGet2* get_param) {
int KYTY_SYSV_ABI SaveDataSetSaveDataMemory2(const SaveDataMemorySet2* set_param) {
PRINT_NAME();
if (set_param == nullptr) {
if (set_param == nullptr || set_param->slot_id >= 4 || set_param->data_num > 5 ||
(set_param->data == nullptr && set_param->data_num != 0)) {
return SAVE_DATA_ERROR_PARAMETER;
}
if (set_param->user_id < 0) {
return SAVE_DATA_ERROR_INVALID_LOGIN_USER;
}
LOGF("\t user_id = %" PRId32 "\n"
"\t data = 0x%016" PRIx64 "\n"
@@ -560,23 +788,42 @@ int KYTY_SYSV_ABI SaveDataSetSaveDataMemory2(const SaveDataMemorySet2* set_param
reinterpret_cast<uint64_t>(set_param->param), reinterpret_cast<uint64_t>(set_param->icon),
set_param->data_num, set_param->slot_id);
const uint32_t data_num = (set_param->data_num == 0 ? 1 : set_param->data_num);
if (set_param->data != nullptr) {
for (uint32_t i = 0; i < data_num; i++) {
const auto& data = set_param->data[i];
if (data.buf == nullptr || data.offset < 0) {
return SAVE_DATA_ERROR_PARAMETER;
}
const auto offset = static_cast<size_t>(data.offset);
if (offset + data.buf_size > g_save_data_memory.size()) {
g_save_data_memory.resize(offset + data.buf_size);
}
std::memcpy(g_save_data_memory.data() + offset, data.buf, data.buf_size);
Common::LockGuard lock(g_mount_mutex);
const auto it =
g_save_data_memory.find(memory_directory(set_param->user_id, set_param->slot_id));
if (it == g_save_data_memory.end()) {
return SAVE_DATA_ERROR_MEMORY_NOT_READY;
}
if (set_param->param != nullptr && (it->second.option & SAVE_DATA_MEMORY_SET_PARAM) == 0) {
return SAVE_DATA_ERROR_PARAMETER;
}
// Some games supply one descriptor with an implicit zero count.
const uint32_t data_num =
set_param->data != nullptr && set_param->data_num == 0 ? 1 : set_param->data_num;
for (uint32_t i = 0; i < data_num; i++) {
if (!valid_memory_range(set_param->data[i], it->second.data.size())) {
return SAVE_DATA_ERROR_PARAMETER;
}
}
if (data_num == 0 && set_param->param == nullptr) {
return OK;
}
return OK;
// Save the complete update before publishing it so progress survives process exit.
auto updated = it->second;
for (uint32_t i = 0; i < data_num; i++) {
const auto& data = set_param->data[i];
std::memcpy(updated.data.data() + data.offset, data.buf, data.buf_size);
}
if (set_param->param != nullptr) {
updated.param = *set_param->param;
}
updated.param.mtime = static_cast<int64_t>(std::time(nullptr));
const int status = save_memory(it->first, updated);
if (status == OK) {
it->second = std::move(updated);
}
return status;
}
int KYTY_SYSV_ABI SaveDataTransferringMount(const SaveDataTransferringMount* mount,
@@ -584,9 +831,13 @@ int KYTY_SYSV_ABI SaveDataTransferringMount(const SaveDataTransferringMount* mou
PRINT_NAME();
if (mount == nullptr || mount_result == nullptr || mount->title_id == nullptr ||
mount->dir_name == nullptr) {
mount->dir_name == nullptr || !valid_path_component(mount->title_id->data) ||
!valid_path_component(mount->dir_name->data)) {
return SAVE_DATA_ERROR_PARAMETER;
}
if (mount->user_id < 0) {
return SAVE_DATA_ERROR_INVALID_LOGIN_USER;
}
LOGF("\t user_id = %" PRId32 "\n"
"\t title_id = %s\n"
@@ -597,8 +848,7 @@ int KYTY_SYSV_ABI SaveDataTransferringMount(const SaveDataTransferringMount* mou
Common::LockGuard lock(g_mount_mutex);
const std::string dir_name = mount->dir_name->data;
const std::string mount_dir =
std::string(SAVE_DATA_DIR) + "/" + mount->title_id->data + "/" + dir_name;
const auto mount_dir = save_directory(mount->title_id->data, dir_name, mount->user_id).string();
const int slot = g_mount_slots.FindAvailable(dir_name);
if (slot == SaveDataMountSlots::BUSY) {
return SAVE_DATA_ERROR_BUSY;
@@ -669,6 +919,7 @@ int KYTY_SYSV_ABI SaveDataCommit(const SaveDataCommitParam* param) {
param->resource, param->commit_mode);
if ((param->commit_mode & SAVE_DATA_COMMIT_MODE_BACKUP_ASYNC) != 0) {
Common::LockGuard lock(g_mount_mutex);
queue_save_data_event(SAVE_DATA_EVENT_TYPE_COMMIT_BACKUP_END, 0, nullptr, nullptr);
}
@@ -678,9 +929,13 @@ int KYTY_SYSV_ABI SaveDataCommit(const SaveDataCommitParam* param) {
int KYTY_SYSV_ABI SaveDataDelete(const SaveDataDelete* del) {
PRINT_NAME();
if (del == nullptr || del->dir_name == nullptr) {
if (del == nullptr || del->dir_name == nullptr || !valid_path_component(del->dir_name->data) ||
(del->title_id != nullptr && !valid_path_component(del->title_id->data))) {
return SAVE_DATA_ERROR_PARAMETER;
}
if (del->user_id < 0) {
return SAVE_DATA_ERROR_INVALID_LOGIN_USER;
}
LOGF("\t user_id = %" PRId32 "\n"
"\t title_id = %s\n"
@@ -688,13 +943,17 @@ int KYTY_SYSV_ABI SaveDataDelete(const SaveDataDelete* del) {
del->user_id, del->title_id != nullptr ? del->title_id->data : "<default>",
del->dir_name->data);
std::string dir =
std::string(SAVE_DATA_DIR) + "/" + get_title_id() + "/" + std::string(del->dir_name->data);
if (Common::File::IsDirectoryExisting(dir)) {
Common::File::DeleteDirectory(dir);
Common::LockGuard lock(g_mount_mutex);
const auto dir = save_directory(del->title_id != nullptr ? del->title_id->data : get_title_id(),
del->dir_name->data, del->user_id);
if (g_mount_slots.FindAvailable(del->dir_name->data) == SaveDataMountSlots::BUSY ||
g_save_data_memory.contains(dir)) {
return SAVE_DATA_ERROR_BUSY;
}
std::error_code error;
std::filesystem::remove_all(dir, error);
return OK;
return error ? SAVE_DATA_ERROR_INTERNAL : OK;
}
int KYTY_SYSV_ABI SaveDataGetParam(const SaveDataMountPoint* mount_point, uint32_t param_type,
@@ -760,10 +1019,35 @@ int KYTY_SYSV_ABI SaveDataSaveIconByPath(const SaveDataMountPoint* mount_point,
return OK;
}
int KYTY_SYSV_ABI SaveDataSyncSaveDataMemory(const void* sync_param) {
int KYTY_SYSV_ABI SaveDataSyncSaveDataMemory(const SaveDataMemorySync* sync_param) {
PRINT_NAME();
LOGF("\t sync_param = 0x%016" PRIx64 "\n", reinterpret_cast<uint64_t>(sync_param));
if (sync_param == nullptr || sync_param->slot_id >= 4 || sync_param->option > 1) {
return SAVE_DATA_ERROR_PARAMETER;
}
if (sync_param->user_id < 0) {
return SAVE_DATA_ERROR_INVALID_LOGIN_USER;
}
Common::LockGuard lock(g_mount_mutex);
const auto it =
g_save_data_memory.find(memory_directory(sync_param->user_id, sync_param->slot_id));
if (it == g_save_data_memory.end()) {
return SAVE_DATA_ERROR_MEMORY_NOT_READY;
}
const auto dir_name = memory_dir_name(sync_param->slot_id);
if (g_mount_slots.FindAvailable(dir_name) == SaveDataMountSlots::BUSY) {
return SAVE_DATA_ERROR_BUSY;
}
const int status = save_memory(it->first, it->second);
if (sync_param->option == 1) {
return status;
}
SceSaveDataTitleId title {};
SceSaveDataDirName directory {};
std::snprintf(title.data, sizeof(title.data), "%s", get_title_id().c_str());
std::snprintf(directory.data, sizeof(directory.data), "%s", dir_name.c_str());
queue_save_data_event(SAVE_DATA_EVENT_TYPE_MEMORY_SYNC_END, sync_param->user_id, &title,
&directory, status);
return OK;
}
@@ -779,6 +1063,7 @@ int KYTY_SYSV_ABI SaveDataGetEventResult(const void* event_param, SaveDataEvent*
return SAVE_DATA_ERROR_PARAMETER;
}
Common::LockGuard lock(g_mount_mutex);
if (g_save_data_events.empty()) {
return SAVE_DATA_ERROR_NOT_FOUND;
}
@@ -812,6 +1097,7 @@ int KYTY_SYSV_ABI SaveDataBackup(const SaveDataBackup* backup) {
backup->user_id, backup->title_id != nullptr ? backup->title_id->data : "<default>",
backup->dir_name->data);
Common::LockGuard lock(g_mount_mutex);
queue_save_data_event(SAVE_DATA_EVENT_TYPE_BACKUP_END, backup->user_id, backup->title_id,
backup->dir_name);
@@ -848,13 +1134,23 @@ int KYTY_SYSV_ABI SaveDataGetMountInfo(const SaveDataMountPoint* mount_point,
SaveDataMountInfo* info) {
PRINT_NAME();
EXIT_NOT_IMPLEMENTED(mount_point == nullptr);
EXIT_NOT_IMPLEMENTED(info == nullptr);
if (mount_point == nullptr || info == nullptr ||
std::memchr(mount_point->data, 0, sizeof(mount_point->data)) == nullptr) {
return SAVE_DATA_ERROR_PARAMETER;
}
Common::LockGuard lock(g_mount_mutex);
if (g_mount_slots.Find(mount_point->data) == SaveDataMountSlots::FULL) {
return SAVE_DATA_ERROR_NOT_MOUNTED;
}
uint64_t blocks = 0;
const int status = read_save_blocks(
LibKernel::FileSystem::GetRealFilename(mount_point->data), &blocks);
if (status != OK) {
return status;
}
*info = {};
info->blocks = SAVE_DATA_BLOCKS_MAX;
info->free_blocks = SAVE_DATA_BLOCKS_MAX;
info->blocks = blocks;
info->free_blocks = blocks;
return OK;
}
+3 -1
View File
@@ -92,7 +92,9 @@ static KYTY_SYSV_ABI int UserServiceGetLoginUserIdList(UserServiceLoginUserIdLis
}
static KYTY_SYSV_ABI int UserServiceGetUserName(int user_id, char* name, size_t size) {
EXIT_NOT_IMPLEMENTED(user_id != Config::GetUserId());
if (user_id != Config::GetUserId()) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
}
EXIT_NOT_IMPLEMENTED(size < 5);
const auto& user_name = Config::GetUserName();
+11 -10
View File
@@ -164,6 +164,7 @@ void Psf::OpenJson(const std::filesystem::path& file_name) {
add_string_param("contentId", "CONTENT_ID");
add_string_param("contentVersion", "APP_VER");
add_string_param("appVersion", "APP_VER");
add_int_param("attribute3", "ATTRIBUTE3");
add_int_param("userDefinedParam1", "USER_DEFINED_PARAM_1");
add_int_param("userDefinedParam2", "USER_DEFINED_PARAM_2");
add_int_param("userDefinedParam3", "USER_DEFINED_PARAM_3");
@@ -647,20 +648,20 @@ uint64_t SystemContentGetFlexibleMemorySize() {
return sc->psf.GetFlexibleMemorySize();
}
bool SystemContentGetIconPath(std::string* path) {
if (path == nullptr) {
return false;
}
bool SystemContentGetIconPath(std::filesystem::path* path) {
if (path == nullptr) {
return false;
}
auto* sc = Common::Singleton<SystemContent>::Instance();
auto* sc = Common::Singleton<SystemContent>::Instance();
if (sc->icon_path.empty()) {
return false;
}
if (sc->icon_path.empty()) {
return false;
}
*path = Common::PathToString(sc->icon_path);
*path = sc->icon_path;
return true;
return true;
}
bool SystemContentGetChunksNum(uint32_t* num) {
+1 -1
View File
@@ -15,7 +15,7 @@ bool SystemContentParamSfoGetInt(const char* name, int32_t* value);
bool SystemContentParamSfoGetString(const char* name, std::string* value);
bool SystemContentParamSfoGetString(const char* name, char* value, size_t value_size);
uint64_t SystemContentGetFlexibleMemorySize();
bool SystemContentGetIconPath(std::string* path);
bool SystemContentGetIconPath(std::filesystem::path* path);
bool SystemContentGetChunksNum(uint32_t* num);
} // namespace Loader
+56 -11
View File
@@ -10,6 +10,8 @@
#include <charconv>
#include <cstdio>
#include <string_view>
#include <vector>
#include <fmt/format.h>
#include <magic_enum.hpp>
@@ -62,6 +64,7 @@ static void PrintUsage() {
::printf(" --gpu-assisted-validation <t|f> Bounds-check shader accesses on the GPU.\n"
" Implies --vulkan-validation; very slow.\n");
::printf(" --shader-validation <true|false> Enable shader validation.\n");
::printf(" --tessellation Draw tessellation patches; skipped by default.\n");
::printf(" --shader-optimization-type <value> None, Size, or Performance.\n");
::printf(" --shader-log-direction <value> Silent, Console, or File.\n");
::printf(" --shader-log-folder <path> Shader log output folder.\n");
@@ -178,6 +181,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
continue;
}
if (arg == "--tessellation") {
options.config.tessellation_enabled = true;
continue;
}
if (arg == "--profile") {
options.config.profiler_enabled = true;
continue;
@@ -207,15 +215,19 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
}
value = Common::FixFilenameSlash(value);
if (Common::File::IsDirectoryExisting(value)) {
options.app0_dir = value;
const auto path = Common::PathFromUtf8(value);
if (Common::File::IsDirectoryExisting(path)) {
options.app0_dir = path;
options.elf = "/app0/eboot.bin";
} else if (Common::File::IsFileExisting(value)) {
options.app0_dir = Common::DirectoryWithoutFilename(value);
} else if (Common::File::IsFileExisting(path)) {
options.app0_dir = path.parent_path();
if (options.app0_dir.empty()) {
options.app0_dir = ".";
}
options.elf = "/app0/" + Common::FilenameWithoutDirectory(value);
options.elf = std::filesystem::path("/app0") / path.filename();
} else {
::printf("--game must point to an existing directory or ELF: %s\n", value.c_str());
return false;
@@ -226,11 +238,13 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
return false;
}
value = Common::FixFilenameSlash(value);
if (!Common::File::IsFileExisting(value)) {
const auto path = Common::PathFromUtf8(value);
if (!Common::File::IsFileExisting(path)) {
::printf("--game-patch must point to an existing file: %s\n", value.c_str());
return false;
}
options.game_patch = value;
options.game_patch = path;
} else if (arg == "--screen-width") {
options.config.screen_width = static_cast<uint32_t>(Common::ToInt32(value));
} else if (arg == "--screen-height") {
@@ -291,14 +305,14 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
return false;
}
} else if (arg == "--shader-log-folder") {
options.config.shader_log_folder = value;
options.config.shader_log_folder = Common::PathFromUtf8(value);
} else if (arg == "--command-buffer-dump") {
if (!ParseBool(value, options.config.command_buffer_dump_enabled)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--command-buffer-dump-folder") {
options.config.command_buffer_dump_folder = value;
options.config.command_buffer_dump_folder = Common::PathFromUtf8(value);
} else if (arg == "--graphics-debug-dump") {
if (!ParseBool(value, options.config.graphics_debug_dump_enabled)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
@@ -310,7 +324,7 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
return false;
}
} else if (arg == "--printf-output-file") {
options.config.printf_output_file = value;
options.config.printf_output_file = Common::PathFromUtf8(value);
} else if (arg == "--spirv-debug-printf") {
if (!ParseBool(value, options.config.spirv_debug_printf_enabled)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
@@ -341,7 +355,7 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
return show_help || (!options.app0_dir.empty() && !options.elf.empty());
}
int main(int argc, char* argv[]) {
static int Main(int argc, char* argv[]) {
VirtualMemory::Init();
InitializeThreads();
@@ -367,3 +381,34 @@ int main(int argc, char* argv[]) {
return 0;
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
int wmain(int argc, wchar_t* argv[]) {
std::vector<std::string> utf8_args;
utf8_args.reserve(static_cast<size_t>(argc));
for (int index = 0; index < argc; index++) {
const std::wstring_view wide(argv[index]);
const std::u16string utf16(wide.begin(), wide.end());
utf8_args.push_back(Common::Utf16ToUtf8(utf16));
}
std::vector<char*> utf8_argv;
utf8_argv.reserve(utf8_args.size());
for (auto& argument: utf8_args) {
utf8_argv.push_back(argument.data());
}
return Main(argc, utf8_argv.data());
}
#else
int main(int argc, char* argv[]) {
return Main(argc, argv);
}
#endif
+106
View File
@@ -4,12 +4,14 @@
#include "common/logging/log.h"
#include "common/subsystems.h"
#include "common/threads.h"
#include "common/stringUtils.h"
#include "graphics/presentation/window/windowInternal.h"
#include "kernel/fileSystem.h"
#include "libs/errno.h"
#include "libs/network.h"
#include "loader/symbolDatabase.h"
#include <algorithm>
#include <array>
#include <chrono>
@@ -89,6 +91,33 @@ void CheckSaveRename(const std::filesystem::path &root,
"renamed save contents");
}
void TestSaveOpenVisibility() {
constexpr char Path[] = "/savedata0/visible-save.dat";
constexpr char Payload[] = "saved progress";
const int fd = FileSystem::KernelOpen(Path, 0xa01, 0777);
Check(fd >= 3, "create save file exclusively");
FileSystem::FileStat stat {};
Check(FileSystem::KernelStat(Path, &stat) == OK && stat.st_size == 0,
"created save file is visible before close");
Check(FileSystem::KernelOpen(Path, 0xa01, 0777) ==
Libs::LibKernel::KERNEL_ERROR_EEXIST,
"exclusive creation detects an open save file");
Check(FileSystem::KernelWrite(fd, Payload, sizeof(Payload) - 1) ==
sizeof(Payload) - 1,
"populate save file before truncation");
Check(FileSystem::KernelClose(fd) == OK, "close populated save file");
Check(FileSystem::KernelStat(Path, &stat) == OK &&
stat.st_size == sizeof(Payload) - 1,
"save file contains the truncation fixture");
const int truncated = FileSystem::KernelOpen(Path, 0x401, 0777);
Check(truncated >= 3, "truncate existing save file");
Check(FileSystem::KernelStat(Path, &stat) == OK && stat.st_size == 0,
"save truncation is visible before close");
Check(FileSystem::KernelClose(truncated) == OK, "close truncated save file");
}
void CheckMountRoot(const std::filesystem::path &root) {
Common::File cache;
Check(cache.Create(root / "rpf.cache"), "create directory listing fixture");
@@ -126,6 +155,80 @@ void CheckMountRoot(const std::filesystem::path &root) {
}
}
FileSystem::Umount("/app0");
Check(FileSystem::GetRealFilename("/app0/rpf.cache") == "/app0/rpf.cache",
"unmount by guest path");
for (const auto &folder : {root, root / ""}) {
for (const auto &host : {root, root / ""}) {
FileSystem::Mount(folder, "/app0");
FileSystem::Umount(Common::PathToGenericString(host));
Check(FileSystem::GetRealFilename("/app0/rpf.cache") == "/app0/rpf.cache",
"unmount by host path with or without trailing separator");
}
}
}
void CheckUnicodePaths(const std::filesystem::path &root) {
constexpr std::string_view HostDirectory =
"Test\xc3\xa9-\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e";
constexpr std::string_view GuestFilename =
"asset-\xc3\xa9-\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e.bin";
const auto unicode_root =
root / Common::PathFromUtf8(HostDirectory);
const auto nested_root = unicode_root / "nested";
Check(Common::File::CreateDirectories(nested_root),
"create nested Unicode host directory");
CheckMountRoot(nested_root);
const auto native_file =
nested_root / Common::PathFromUtf8(GuestFilename);
Common::File fixture;
Check(fixture.Create(native_file), "create Unicode filename");
fixture.Close();
const auto entries = Common::File::GetDirEntries(nested_root);
Check(std::any_of(entries.begin(), entries.end(), [&](const auto &entry) {
return entry.is_file && entry.name == GuestFilename;
}), "directory enumeration returns UTF-8 filenames");
FileSystem::Mount(nested_root, "/app0");
const auto guest_file =
std::string("/app0/") + std::string(GuestFilename);
Check(FileSystem::GetRealFilename(guest_file) == native_file,
"resolve Unicode guest path");
const int fd = FileSystem::KernelOpen(guest_file.c_str(), 0, 0);
Check(fd >= 3, "open Unicode guest path");
Check(FileSystem::KernelClose(fd) == OK,
"close Unicode guest path");
FileSystem::Umount("/app0");
}
void CheckUnicodeLogPath(const std::filesystem::path &root) {
Config::ConfigOptions options;
options.printf_direction = Config::LogDirection::File;
options.printf_output_file = root / u8"logs-\u65e5\u672c\u8a9e-\U0001f600" / u8"log-\u00e9.txt";
Config::Load(options);
Log::Initialize();
constexpr std::string_view Payload = "Unicode log path\n";
Log::Write(Payload);
Log::Shutdown();
Common::File result(options.printf_output_file, Common::File::Mode::Read);
Check(!result.IsInvalid(), "open Unicode log file");
const auto data = result.ReadWholeBuffer();
Check(data.size() == Payload.size() &&
std::memcmp(data.data(), Payload.data(), data.size()) == 0,
"Unicode log file contains output");
options.printf_direction = Config::LogDirection::Silent;
Config::Load(options);
Log::Initialize();
}
void CheckDirectoryStream(const std::filesystem::path &root) {
@@ -387,9 +490,12 @@ int main() {
TempDirectory temporary;
FileSystem::Initialize();
CheckMountRoot(temporary.Path());
CheckUnicodePaths(temporary.Path());
CheckUnicodeLogPath(temporary.Path());
CheckDirectoryStream(temporary.Path());
CheckAprPaths(temporary.Path());
FileSystem::Mount(temporary.Path(), "/savedata0");
TestSaveOpenVisibility();
CheckSaveRename(temporary.Path(), "first-save");
CheckSaveRename(temporary.Path(), "replacement-save");
FileSystem::Shutdown();
+87 -5
View File
@@ -213,8 +213,11 @@ MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 0,
fixture->AddMemory(shared_buffer, 0x10d8));
fixture->Emit(ValueOpcode::ReferenceU32, {load});
}
const auto invocation = fixture->Emit(
ValueOpcode::GetBuiltin,
{Value(static_cast<uint32_t>(StageInputKind::GlobalInvocationId)), Value(0u)});
const auto selector = fixture->Emit(ValueOpcode::ReadFirstLane,
{fixture->UserData(8), Value(true)});
{invocation, Value(true)});
const auto record =
fixture->Emit(ValueOpcode::IMul32, {selector, Value(224u)});
const auto member = fixture->Emit(ValueOpcode::IAdd32, {record, Value(4u)});
@@ -457,6 +460,83 @@ void TestInvariantIndirectImageMaterialization() {
"wrapped scalar immediate entered the invariant image proof");
}
void TestUniformScalarBufferImage() {
Fixture fixture(ShaderType::Pixel);
std::array<Value, 4> material_words;
std::array<Value, 4> heap_words;
for (uint32_t dword = 0; dword < 4; dword++) {
material_words[dword] = fixture.UserData(dword);
heap_words[dword] = fixture.UserData(dword + 4u);
}
const auto material = fixture.Buffer(material_words);
const auto heap = fixture.Buffer(heap_words);
const auto selector = fixture.Emit(ValueOpcode::ShiftLeftLogical32,
{fixture.UserData(8), Value(2u)});
MemoryInfo scalar;
scalar.kind = ResourceKind::ScalarBuffer;
scalar.offset = 0x40u;
const auto key = fixture.Emit(ValueOpcode::ReadConstBuffer,
{material, selector}, fixture.AddMemory(scalar, 0x100));
const auto record = fixture.Emit(ValueOpcode::IMul32, {key, Value(48u)});
std::array<Value, 8> image_words;
for (uint32_t dword = 0; dword < image_words.size(); dword++) {
scalar.offset = 0x10u + dword * sizeof(uint32_t);
image_words[dword] = fixture.Emit(ValueOpcode::ReadConstBuffer,
{heap, record}, fixture.AddMemory(scalar, 0x200));
}
const auto image = fixture.Image(image_words);
const auto sampler = fixture.Sampler({Value(0u), Value(0u), Value(0u), Value(0u)});
MemoryInfo sample;
sample.kind = ResourceKind::Image;
sample.image_dimension = Decoder::ImageDimension::Dim2D;
fixture.Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, fixture.ImageAddress()}, fixture.AddMemory(sample, 0x228));
fixture.PlanAndTrack();
const auto plan = ExtractResourcePlan(fixture.program);
std::array<uint32_t, 9> user_data{0x1000u, 0u, 0x100u, 0u,
0x2000u, 0u, 0x100u, 0u, 0u};
LinearTestMemory memory;
memory.words[0x44u / 4u] = 1u;
std::array<uint32_t, 8> descriptor{};
descriptor[0] = 0x20u;
descriptor[1] = static_cast<uint32_t>(
Libs::Graphics::Prospero::BufferFormat::k32_32_32_32Float)
<< 20u;
descriptor[2] = 3u | (3u << 14u);
descriptor[3] = Libs::Graphics::DstSel(4, 5, 6, 7) |
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D)
<< 28u);
for (uint32_t index = 0; index < 2; index++) {
std::copy(descriptor.begin(), descriptor.end(),
memory.words.begin() + (0x1010u + index * 48u) / 4u);
memory.words[(0x1010u + index * 48u) / 4u] += index;
}
SrtRuntime runtime{.user_data = user_data,
.read_memory = ReadLinearTestMemory,
.userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const auto materializes = [&](uint32_t address) {
return MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.images.size() == 1 && snapshot.images[0].dwords[0] == address;
};
Check(materializes(0x20u), "nested draw-uniform image descriptor did not materialize");
user_data[8] = 1u;
Check(materializes(0x21u), "changed draw selector reused the previous image descriptor");
memory.words[0x44u / 4u] = 0u;
memory.words[0x1010u / 4u] = 0x30u;
Check(materializes(0x30u), "changed scalar table memory reused the previous image descriptor");
const auto prior_snapshot = snapshot;
const auto prior_specialization = specialization;
memory.fail_address = 0x1044u;
Check(!MaterializeResources(plan, runtime, snapshot, specialization) &&
SameResourceSnapshot(snapshot, prior_snapshot) &&
specialization == prior_specialization,
"unavailable nested image table partially changed the resource snapshot");
}
void TestComputeBufferFill() {
struct Options {
bool scalar = false;
@@ -1246,7 +1326,7 @@ ResourcePlan ConditionalSamplerPlan(bool diamond, bool reverse, bool reverse_phi
flag = fixture.Emit(ValueOpcode::LaneId);
}
fixture.program.block_info[0].condition =
fixture.Emit(ValueOpcode::IEqual32, {flag, Value(0u)});
fixture.Emit(ValueOpcode::SGreaterThanEqual32, {flag, Value(0u)});
if (writable) {
MemoryInfo memory;
memory.kind = ResourceKind::Buffer;
@@ -1324,9 +1404,10 @@ void TestConditionalSamplerPhi() {
.userdata = &memory,
.read_specialization_memory =
ReadLinearTestMemory};
for (uint32_t flag = 0; flag < 2; ++flag) {
memory.words[(0x1000 + 196) / 4] = flag;
const uint32_t first = (flag != 0u) != reverse ? 480 / 4 : 448 / 4;
// Sampler selection compares a signed value against zero.
for (const auto flag : {-1, 0, 1, INT32_MIN, INT32_MAX}) {
memory.words[(0x1000 + 196) / 4] = std::bit_cast<uint32_t>(flag);
const uint32_t first = (flag < 0) != reverse ? 480 / 4 : 448 / 4;
memory.fail_address = 0x1000 + (first == 448 / 4 ? 480u : 448u);
DescriptorValue selected;
Check(EvaluateDescriptorSource(plan, source, runtime, selected),
@@ -2012,6 +2093,7 @@ int main() {
Run("FMASK load specialization", TestFmaskLoadSpecialization);
Run("dynamic storage mips", TestDynamicStorageMipTracking);
Run("invariant indirect images", TestInvariantIndirectImageMaterialization);
Run("draw-uniform scalar image", TestUniformScalarBufferImage);
Run("SRT runtime", TestSrtFlatteningAndRuntimeMemoization);
Run("dynamic SRT", TestDynamicSrtReadRemainsExplicit);
Run("phi validation", TestPhiValidation);
+503
View File
@@ -0,0 +1,503 @@
// Exercise the guest ABI without linking unrelated emulator subsystems.
#include "libs/libSaveData.cpp"
#include <array>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <limits>
#ifdef _WIN32
#include <process.h>
#else
#include <sys/wait.h>
#include <unistd.h>
#endif
namespace {
std::string g_test_title = "PPSA21564";
std::filesystem::path g_mounted_directory;
std::map<std::string, std::filesystem::path> g_test_mounts;
} // namespace
namespace Loader {
bool SystemContentParamSfoGetString(const char*, std::string* value) {
*value = g_test_title;
return true;
}
void SymbolDatabase::Add(const SymbolResolve&, uint64_t, const std::string&) {}
namespace Timer {
Common::Time GetTime() {
return Common::Time(0);
}
} // namespace Timer
} // namespace Loader
namespace Libs::LibKernel::FileSystem {
void Mount(const std::filesystem::path& directory, const std::string& point) {
g_mounted_directory = directory;
g_test_mounts[point] = directory;
}
void Umount(const std::string& point) { g_test_mounts.erase(point); }
std::filesystem::path GetRealFilename(const std::string& point) {
return g_test_mounts.at(point);
}
} // namespace Libs::LibKernel::FileSystem
namespace {
using namespace Libs::SaveData;
namespace fs = std::filesystem;
#define CHECK(condition) \
do { \
if (!(condition)) { \
std::fprintf(stderr, "SaveDataMemoryTests:%d: %s\n", __LINE__, #condition); \
std::abort(); \
} \
} while (false)
SaveDataMemorySetup2 SetupParam(int32_t user = 1, uint32_t slot = 0, size_t size = 16) {
SaveDataMemorySetup2 setup {};
setup.user_id = user;
setup.slot_id = slot;
setup.memory_size = size;
return setup;
}
size_t Setup(int32_t user = 1, uint32_t slot = 0, size_t size = 16, uint32_t option = 0) {
auto setup = SetupParam(user, slot, size);
setup.option = option;
SaveDataMemorySetupResult result {};
CHECK(SaveDataSetupSaveDataMemory2(&setup, &result) == OK);
return result.existed_memory_size;
}
int Set(void* bytes, size_t size, int64_t offset = 0, int32_t user = 1, uint32_t slot = 0) {
SaveDataMemoryData data {bytes, size, offset, {}};
SaveDataMemorySet2 set {};
set.user_id = user;
set.slot_id = slot;
set.data = &data;
set.data_num = 1;
return SaveDataSetSaveDataMemory2(&set);
}
std::vector<uint8_t> Get(size_t size = 16, int32_t user = 1, uint32_t slot = 0) {
std::vector<uint8_t> bytes(size);
SaveDataMemoryData data {bytes.data(), size, 0, {}};
SaveDataMemoryGet2 get {};
get.user_id = user;
get.slot_id = slot;
get.data = &data;
CHECK(SaveDataGetSaveDataMemory2(&get) == OK);
return bytes;
}
void Reset(const char* title) {
CHECK(SaveDataTerminate() == OK);
g_test_title = title;
}
void TestValidationAndScatter() {
Reset("VALIDATE");
uint8_t byte = 0x55;
SaveDataMemoryGet2 get {};
get.user_id = 1;
SaveDataMemorySync sync {};
sync.user_id = 1;
CHECK(SaveDataGetSaveDataMemory2(&get) == SAVE_DATA_ERROR_MEMORY_NOT_READY);
CHECK(Set(&byte, 1) == SAVE_DATA_ERROR_MEMORY_NOT_READY);
CHECK(SaveDataSyncSaveDataMemory(&sync) == SAVE_DATA_ERROR_MEMORY_NOT_READY);
CHECK(SaveDataSetupSaveDataMemory2(nullptr, nullptr) == SAVE_DATA_ERROR_PARAMETER);
auto invalid_user = SetupParam(-1);
CHECK(SaveDataSetupSaveDataMemory2(&invalid_user, nullptr) ==
SAVE_DATA_ERROR_INVALID_LOGIN_USER);
for (auto invalid:
{SetupParam(1, 4), SetupParam(1, 0, 0), SetupParam(1, 0, 32 * 1024 * 1024 + 1)}) {
CHECK(SaveDataSetupSaveDataMemory2(&invalid, nullptr) == SAVE_DATA_ERROR_PARAMETER);
}
CHECK(Setup() == 0);
auto setup = SetupParam();
CHECK(SaveDataSetupSaveDataMemory2(&setup, nullptr) == SAVE_DATA_ERROR_BUSY);
std::array<uint8_t, 16> original {};
for (size_t i = 0; i < original.size(); ++i) {
original[i] = static_cast<uint8_t>(i);
}
CHECK(Set(original.data(), original.size()) == OK);
SaveDataMemoryData data[] = {{&byte, 1, 2, {}}, {&byte, 1, 16, {}}};
SaveDataMemorySet2 set {};
set.user_id = 1;
set.data = data;
set.data_num = 2;
CHECK(SaveDataSetSaveDataMemory2(&set) == SAVE_DATA_ERROR_PARAMETER);
CHECK(Get() == std::vector<uint8_t>(original.begin(), original.end()));
data[1].offset = 12;
CHECK(SaveDataSetSaveDataMemory2(&set) == OK);
original[2] = original[12] = byte;
CHECK(Get() == std::vector<uint8_t>(original.begin(), original.end()));
byte = 0xaa;
set.data_num = 0;
CHECK(SaveDataSetSaveDataMemory2(&set) == OK);
original[2] = byte;
CHECK(Get() == std::vector<uint8_t>(original.begin(), original.end()));
set.data = nullptr;
CHECK(SaveDataSetSaveDataMemory2(&set) == OK);
CHECK(Get() == std::vector<uint8_t>(original.begin(), original.end()));
set.data_num = 6;
CHECK(SaveDataSetSaveDataMemory2(&set) == SAVE_DATA_ERROR_PARAMETER);
set.data_num = 1;
set.data = nullptr;
CHECK(SaveDataSetSaveDataMemory2(&set) == SAVE_DATA_ERROR_PARAMETER);
CHECK(Set(&byte, 1, -1) == SAVE_DATA_ERROR_PARAMETER);
CHECK(Set(&byte, 1, std::numeric_limits<int64_t>::max()) == SAVE_DATA_ERROR_PARAMETER);
CHECK(Set(&byte, std::numeric_limits<size_t>::max(), 1) == SAVE_DATA_ERROR_PARAMETER);
SaveDataMemoryData invalid {&byte, std::numeric_limits<size_t>::max(), 1, {}};
get.data = &invalid;
CHECK(SaveDataGetSaveDataMemory2(&get) == SAVE_DATA_ERROR_PARAMETER);
CHECK(byte == 0xaa);
CHECK(Get() == std::vector<uint8_t>(original.begin(), original.end()));
CHECK(SaveDataTerminate() == OK);
CHECK(Setup() == original.size());
CHECK(Get() == std::vector<uint8_t>(original.begin(), original.end()));
}
void TestIsolationAndSync() {
Reset("ISOLATE");
uint8_t byte = 11;
CHECK(Setup() == 0);
CHECK(Set(&byte, 1) == OK);
CHECK(Setup(2) == 0);
CHECK(Setup(1, 1) == 0);
CHECK(Get(16, 2) == std::vector<uint8_t>(16));
CHECK(Get(16, 1, 1) == std::vector<uint8_t>(16));
byte = 22;
CHECK(Set(&byte, 1, 0, 2) == OK);
byte = 33;
CHECK(Set(&byte, 1, 0, 1, 1) == OK);
CHECK(Get()[0] == 11);
CHECK(Get(16, 2)[0] == 22);
CHECK(Get(16, 1, 1)[0] == 33);
SaveDataMemorySync sync {};
sync.user_id = 1;
sync.slot_id = 1;
CHECK(SaveDataSyncSaveDataMemory(&sync) == OK);
SaveDataEvent event {};
CHECK(SaveDataGetEventResult(nullptr, &event) == OK);
CHECK(event.type == 3 && event.error_code == OK && event.user_id == 1);
CHECK(std::string(event.dir_name.data) == "sce_sdmemory1");
CHECK(SaveDataGetEventResult(nullptr, &event) == SAVE_DATA_ERROR_NOT_FOUND);
sync.option = 1;
CHECK(SaveDataSyncSaveDataMemory(&sync) == OK);
CHECK(SaveDataGetEventResult(nullptr, &event) == SAVE_DATA_ERROR_NOT_FOUND);
sync.option = 2;
CHECK(SaveDataSyncSaveDataMemory(&sync) == SAVE_DATA_ERROR_PARAMETER);
Reset("OTHER");
CHECK(Setup() == 0);
CHECK(Get() == std::vector<uint8_t>(16));
Reset("ISOLATE");
CHECK(Setup() == 16);
CHECK(Setup(2) == 16);
CHECK(Setup(1, 1) == 16);
CHECK(Get()[0] == 11 && Get(16, 2)[0] == 22 && Get(16, 1, 1)[0] == 33);
Reset("OTHER");
SceSaveDataTitleId title {};
std::strcpy(title.data, "ISOLATE");
SceSaveDataDirName name {};
std::strcpy(name.data, "sce_sdmemory");
struct SaveDataTransferringMount mount {};
mount.user_id = 2;
mount.title_id = &title;
mount.dir_name = &name;
SaveDataMountResult result {};
CHECK(SaveDataTransferringMount(&mount, &result) == OK);
CHECK(g_mounted_directory == fs::path("_SaveData/ISOLATE/sce_sdmemory/2"));
CHECK(std::ifstream(g_mounted_directory / "memory.dat", std::ios::binary).get() == 22);
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
struct SaveDataDelete del {};
del.user_id = 1;
del.title_id = &title;
del.dir_name = &name;
CHECK(SaveDataDelete(&del) == OK);
Reset("ISOLATE");
CHECK(Setup() == 0);
CHECK(Setup(2) == 16);
CHECK(Get(16, 2)[0] == 22);
}
void TestFailedWritePreservesSave() {
Reset("IOFAIL");
CHECK(Setup() == 0);
uint8_t byte = 42;
CHECK(Set(&byte, 1) == OK);
fs::rename("_SaveData", "save-backup");
{
std::ofstream blocker("_SaveData");
blocker << "not a directory";
}
byte = 99;
CHECK(Set(&byte, 1) < 0);
CHECK(Get()[0] == 42);
CHECK(fs::remove("_SaveData"));
fs::rename("save-backup", "_SaveData");
CHECK(SaveDataTerminate() == OK);
CHECK(Setup() == 16);
CHECK(Get()[0] == 42);
}
void WriteRestartFixture() {
auto setup = SetupParam(7, 3);
setup.option = 1;
std::array<uint8_t, 4> icon_bytes {1, 2, 3, 4};
SaveDataIcon icon {icon_bytes.data(), icon_bytes.size(), icon_bytes.size(), {}};
setup.init_icon = &icon;
setup.icon_memory_size = icon_bytes.size();
SaveDataParam param {};
std::strcpy(param.title, "Astro progress");
param.user_param = 17;
setup.init_param = &param;
CHECK(SaveDataSetupSaveDataMemory2(&setup, nullptr) == OK);
std::array<uint8_t, 4> progress {11, 22, 33, 44};
SaveDataMemoryData data {progress.data(), progress.size(), 4, {}};
SaveDataMemorySet2 set {};
set.user_id = 7;
set.slot_id = 3;
set.data = &data;
set.icon = &icon;
CHECK(SaveDataSetSaveDataMemory2(&set) == OK);
set.data = nullptr;
param.user_param = 18;
std::strcpy(param.detail, "Coins, level state, rescued bots");
set.param = &param;
CHECK(SaveDataSetSaveDataMemory2(&set) == OK);
// No Sync or Terminate: Set must survive process exit on its own.
}
void ReadRestartFixture() {
CHECK(Setup(7, 3, 24, 1) == 16);
auto expected = std::vector<uint8_t>(24);
expected[4] = 11;
expected[5] = 22;
expected[6] = 33;
expected[7] = 44;
CHECK(Get(24, 7, 3) == expected);
SaveDataParam param {};
std::array<uint8_t, 4> icon_bytes {9, 9, 9, 9};
SaveDataIcon icon {icon_bytes.data(), icon_bytes.size(), icon_bytes.size(), {}};
SaveDataMemoryGet2 get {};
get.user_id = 7;
get.slot_id = 3;
get.param = &param;
get.icon = &icon;
CHECK(SaveDataGetSaveDataMemory2(&get) == OK);
CHECK(icon.data_size == 0);
CHECK(
std::all_of(icon_bytes.begin(), icon_bytes.end(), [](uint8_t byte) { return byte == 9; }));
CHECK(std::string(param.title) == "Astro progress");
CHECK(std::string(param.detail) == "Coins, level state, rescued bots");
CHECK(param.user_param == 18);
CHECK(SaveDataTerminate() == OK);
CHECK(Setup(7, 3, 6) == 16);
expected.resize(6);
CHECK(Get(6, 7, 3) == expected);
}
SceSaveDataDirName DirName(const char* text) {
SceSaveDataDirName name {};
std::snprintf(name.data, sizeof(name.data), "%s", text);
return name;
}
std::vector<std::string> Search(int32_t user, const SceSaveDataTitleId* title = nullptr) {
SaveDataDirNameSearchCond cond {};
cond.user_id = user;
cond.title_id = title;
std::array<SceSaveDataDirName, 8> names {};
SaveDataDirNameSearchResult result {};
result.dir_names = names.data();
result.dir_names_num = names.size();
CHECK(SaveDataDirNameSearch(&cond, &result) == OK);
CHECK(result.hit_num == result.set_num && result.set_num <= names.size());
std::vector<std::string> found;
for (uint32_t i = 0; i < result.set_num; ++i) {
found.emplace_back(names[i].data);
}
return found;
}
void TestClassicSavePaths() {
Reset("CLASSIC");
const std::vector<std::string> names {"save.1", "save1", "slot@A", "slotA"};
fs::create_directories("_SaveData/CLASSIC/save.1");
{
std::ofstream saved("_SaveData/CLASSIC/save.1/progress");
saved << "save.1";
}
CHECK(Search(1) == std::vector<std::string> {"save.1"});
for (const auto& text: names) {
auto name = DirName(text.c_str());
struct SaveDataMount3 mount {};
mount.user_id = 1;
mount.dir_name = &name;
mount.mount_mode = text == "save.1" ? 1 : 4;
mount.blocks = 48;
SaveDataMountResult result {};
CHECK(SaveDataMount3(&mount, &result) == OK);
CHECK(g_mounted_directory == fs::path("_SaveData") / "CLASSIC" / text);
if (text != "save.1") {
std::ofstream saved(g_mounted_directory / "progress");
saved << text;
}
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
}
CHECK(Search(1) == names);
SceSaveDataTitleId title {};
std::strcpy(title.data, "CLASSIC");
Reset("OTHER");
CHECK(Search(1).empty());
CHECK(Search(1, &title) == names);
for (const auto& text: names) {
auto name = DirName(text.c_str());
struct SaveDataTransferringMount mount {};
mount.user_id = 1;
mount.title_id = &title;
mount.dir_name = &name;
SaveDataMountResult result {};
CHECK(SaveDataTransferringMount(&mount, &result) == OK);
std::ifstream saved(g_mounted_directory / "progress");
std::string progress;
saved >> progress;
CHECK(progress == text);
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
}
Reset("CLASSIC");
for (const char* text: {"", ".", "..", "../save1", "save/1", "save\\1"}) {
auto name = DirName(text);
struct SaveDataMount3 mount {};
mount.user_id = 1;
mount.dir_name = &name;
mount.mount_mode = 32;
SaveDataMountResult result {};
g_mounted_directory.clear();
CHECK(SaveDataMount3(&mount, &result) == SAVE_DATA_ERROR_PARAMETER);
CHECK(g_mounted_directory.empty());
struct SaveDataDelete del {};
del.user_id = 1;
del.dir_name = &name;
CHECK(SaveDataDelete(&del) == SAVE_DATA_ERROR_PARAMETER);
}
CHECK(Search(1) == names);
}
void TestSaveAllocations() {
Reset("CAPACITY");
std::array<SceSaveDataDirName, 2> names {DirName("Options"), DirName("Player")};
const std::array<uint64_t, 2> allocations {48, 96};
for (const auto blocks : {0u, 47u, 16385u}) {
struct SaveDataMount3 invalid {};
invalid.user_id = 1;
invalid.dir_name = &names[0];
invalid.mount_mode = 4;
invalid.blocks = blocks;
SaveDataMountResult result {};
CHECK(SaveDataMount3(&invalid, &result) == SAVE_DATA_ERROR_PARAMETER);
CHECK(!fs::exists("_SaveData/CAPACITY/Options"));
}
for (size_t i = 0; i < names.size(); i++) {
struct SaveDataMount3 mount {};
mount.user_id = 1;
mount.dir_name = &names[i];
mount.mount_mode = 4;
mount.blocks = allocations[i];
SaveDataMountResult result {};
CHECK(SaveDataMount3(&mount, &result) == OK);
CHECK(result.mount_status == 1);
std::ofstream(g_mounted_directory / "USR-DATA") << "saved payload";
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
SaveDataMountInfo info {};
CHECK(SaveDataGetMountInfo(&result.mount_point, &info) == SAVE_DATA_ERROR_NOT_MOUNTED);
}
Reset("CAPACITY");
std::array<SceSaveDataDirName, 2> found {};
std::array<SaveDataSearchInfo, 2> infos {};
SaveDataDirNameSearchCond cond {};
cond.user_id = 1;
SaveDataDirNameSearchResult search {};
search.dir_names = found.data();
search.dir_names_num = found.size();
search.infos = infos.data();
CHECK(SaveDataDirNameSearch(&cond, &search) == OK && search.set_num == 2);
for (size_t i = 0; i < names.size(); i++) {
CHECK(std::string(found[i].data) == names[i].data);
CHECK(infos[i].blocks == allocations[i] && infos[i].free_blocks == allocations[i]);
struct SaveDataMount3 mount {};
mount.user_id = 1;
mount.dir_name = &names[i];
mount.mount_mode = 34;
mount.blocks = 16384;
SaveDataMountResult result {};
CHECK(SaveDataMount3(&mount, &result) == OK && result.mount_status == 0);
std::ofstream(g_mounted_directory / "USR-DATA", std::ios::app) << "more data";
SaveDataMountInfo info {};
CHECK(SaveDataGetMountInfo(&result.mount_point, &info) == OK);
CHECK(info.blocks == allocations[i] && info.free_blocks == allocations[i]);
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
}
CHECK(infos[0].blocks + infos[1].blocks == 144);
const fs::path metadata = "_SaveData/CAPACITY/Options/sce_sys/blocks.bin";
fs::resize_file(metadata, 7);
CHECK(SaveDataDirNameSearch(&cond, &search) == SAVE_DATA_ERROR_BROKEN);
CHECK(fs::file_size(metadata) == 7);
CHECK(fs::remove(metadata));
CHECK(SaveDataDirNameSearch(&cond, &search) == SAVE_DATA_ERROR_BROKEN);
CHECK(!fs::exists(metadata));
}
void RunChild(const fs::path& executable, const char* mode) {
#ifdef _WIN32
CHECK(_spawnl(_P_WAIT, executable.string().c_str(), executable.string().c_str(), mode,
static_cast<char*>(nullptr)) == 0);
#else
const pid_t pid = fork();
CHECK(pid >= 0);
if (pid == 0) {
execl(executable.c_str(), executable.c_str(), mode, static_cast<char*>(nullptr));
_exit(127);
}
int status = 0;
CHECK(waitpid(pid, &status, 0) == pid);
CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0);
#endif
}
} // namespace
int main(int argc, char** argv) {
if (argc == 2) {
if (std::string_view(argv[1]) == "write") {
WriteRestartFixture();
} else {
CHECK(std::string_view(argv[1]) == "read");
ReadRestartFixture();
}
return 0;
}
const auto executable = fs::absolute(argv[0]);
const auto previous = fs::current_path();
const auto unique = std::chrono::steady_clock::now().time_since_epoch().count();
const auto temp = fs::temp_directory_path() / ("kyty_save_memory_" + std::to_string(unique));
CHECK(fs::create_directories(temp));
fs::current_path(temp);
RunChild(executable, "write");
RunChild(executable, "read");
TestValidationAndScatter();
TestIsolationAndSync();
TestFailedWritePreservesSave();
TestClassicSavePaths();
TestSaveAllocations();
CHECK(SaveDataTerminate() == OK);
fs::current_path(previous);
fs::remove_all(temp);
return 0;
}
+456 -4
View File
@@ -9405,6 +9405,114 @@ public:
}
void CheckCubeFaceStorageExpansion() {
constexpr const char *name = "CubeFaceStorageExpansion";
constexpr uintptr_t base = 0x0000000204700000ull;
constexpr uint64_t allocation_size = 0x10000;
constexpr std::array<uint32_t, 6> half_red{0x3c00, 0x4000, 0x4200,
0x4400, 0x4500, 0x4600};
EnsureRuntimeContext();
int64_t direct_offset = -1;
Require(name, "direct allocation",
Libs::LibKernel::Memory::KernelAllocateDirectMemory(
0, Libs::LibKernel::Memory::KernelGetDirectMemorySize(),
allocation_size, allocation_size, 0, &direct_offset) == 0,
"cube-face allocation failed");
void *mapped = reinterpret_cast<void *>(base);
Require(name, "direct mapping",
Libs::LibKernel::Memory::KernelMapDirectMemory(
&mapped, allocation_size, 0x3, 0x10, direct_offset,
allocation_size) == 0 && mapped == reinterpret_cast<void *>(base),
"cube-face mapping failed");
std::memset(mapped, 0, allocation_size);
{
RenderContext context(m_runtime_context);
auto &scheduler = context.GetCommandScheduler();
HW::Context registers{};
HW::UserConfig user_config{};
HW::Shader shaders{};
scheduler.Begin(registers, user_config, shaders);
context.MapMemory(base, allocation_size);
auto &cache = context.GetTextureCache();
auto &executor = context.GetRenderExecutor();
for (uint32_t face = 0; face < half_red.size(); ++face) {
// PPSA25380 clears six single-face cube views at one guest address.
ShaderTextureResource descriptor{{static_cast<uint32_t>(base >> 8u),
0xc4700000u, 0x0001c001u, 0xb0000facu,
face | (face << 16u), 0x00700000u, 0, 0}};
TestCase test;
test.name = name;
test.has_user_data = true;
std::copy_n(descriptor.fields, 8, test.user_data.begin());
test.has_compute_info = true;
test.compute_info.threads_num[0] = test.compute_info.threads_num[1] = 8;
test.compute_info.threads_num[2] = 1;
test.compute_info.thread_ids_num = 2;
test.opcodes = {ShaderOpcode::V_MOV_B32, ShaderOpcode::IMAGE_STORE,
ShaderOpcode::S_ENDPGM};
test.code = {EncodeVop1(0x01, 20, 256), EncodeVop1(0x01, 21, 257)};
AppendVMovU32(&test.code, 22, 0);
AppendVMovLiteral(&test.code, 0,
std::bit_cast<uint32_t>(static_cast<float>(face + 1)));
AppendVMovLiteral(&test.code, 1, std::bit_cast<uint32_t>(0.5f));
AppendVMovLiteral(&test.code, 2, std::bit_cast<uint32_t>(0.25f));
AppendVMovLiteral(&test.code, 3, std::bit_cast<uint32_t>(1.0f));
test.code.push_back(EncodeMimg0(0x08, 0xf, 0, false, 5));
test.code.push_back(EncodeMimg1(0, 20));
AppendEnd(&test.code);
const auto compiled = CompileCase(test, SubgroupSize());
const auto &resource = compiled.program.info.images.at(0);
Require(name, "cube storage specialization", resource.cube &&
resource.written && resource.dimension ==
ShaderRecompiler::Decoder::ImageDimension::Dim2DArray,
"single-face cube store lost its array coordinates");
ShaderRecompiler::IR::DescriptorValue value{};
value.dword_count = 8;
std::copy_n(descriptor.fields, 8, value.dwords.begin());
const auto binding = RenderExecutorTestAccess::ResolveTexture(executor, resource, value);
Image storage;
storage.view = cache.FindTexture(binding.image_id, binding.desc);
auto &image = cache.GetImage(binding.image_id);
Require(name, "face view and backing", storage.view != nullptr &&
image.info.pixel_format == vk::Format::eR16G16B16A16Sfloat &&
image.info.bytes_per_block == 8 &&
image.info.resources.layers == face + 1 &&
image.backing.layers == face + 1 &&
binding.desc.view_info.type == vk::ImageViewType::e2DArray &&
binding.desc.view_info.base_layer == face &&
binding.desc.view_info.layer_count == 1,
"cube face did not expand the array or select its own layer");
image.Transit(vk::ImageLayout::eGeneral, vk::AccessFlagBits2::eShaderWrite,
{}, scheduler.Current().Handle());
storage.layout = image.backing.state.layout;
scheduler.Finish();
Dispatch(test, compiled, {}, nullptr, nullptr, &storage);
cache.MarkGpuWritten(binding.image_id);
for (uint32_t prior = 0; prior <= face; ++prior) {
const auto pixels = ReadCachedTexel(name, context, binding.image_id,
{}, {8, 8, 1}, prior);
for (uint32_t pixel = 0; pixel < 64; ++pixel) {
Require(name, "stored and preserved faces",
pixels[pixel * 2] == (0x38000000u | half_red[prior]) &&
pixels[pixel * 2 + 1] == 0x3c003400u,
"face " + std::to_string(prior) + " pixel " +
std::to_string(pixel) + " changed after writing face " +
std::to_string(face));
}
}
}
RenderExecutorTestAccess::ResetBindings(executor);
context.UnmapMemory(base, allocation_size);
scheduler.Finish();
}
Require(name, "release",
Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0 &&
Libs::LibKernel::Memory::KernelReleaseDirectMemory(
direct_offset, allocation_size) == 0,
"cube-face backing release failed");
std::printf("[gpu] %-32s ok\n", name);
}
void CheckRenderExecutorColorStandardTileDiscovery() {
constexpr const char *name = "RenderExecutorColorStandardTile";
constexpr uintptr_t base = 0x0000000203b00000ull;
@@ -12389,15 +12497,14 @@ public:
const auto &image =
compiled.program.info.images.at(binding.resources.front());
bool supported =
image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2D;
image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2D ||
image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2DArray;
if (resource_class == ShaderRecompiler::IR::ImageResourceClass::Sampled) {
supported = supported ||
image.dimension ==
ShaderRecompiler::Decoder::ImageDimension::Dim1D ||
image.dimension ==
ShaderRecompiler::Decoder::ImageDimension::Dim1DArray ||
image.dimension ==
ShaderRecompiler::Decoder::ImageDimension::Dim2DArray;
ShaderRecompiler::Decoder::ImageDimension::Dim1DArray;
}
Require(test.name, "dispatch", supported,
"unsupported image dimension needs a matching Vulkan test view");
@@ -12831,6 +12938,7 @@ public:
const uint32_t extent = depth_feedback ? 8 : 32;
constexpr uintptr_t depth_address = 0x0000000204400000ull;
constexpr uint64_t allocation_size = 0x40000;
constexpr uint64_t rect_address = depth_address + 0x8000;
EnsureRuntimeContext();
RenderContext context(m_runtime_context);
auto &scheduler = context.GetCommandScheduler();
@@ -12865,6 +12973,15 @@ public:
static_cast<float *>(mapped)[y * 64 + x] = initial_depth(x, y);
}
}
if (!depth_feedback && !packed_vertex_color) {
// Rectangle input contains three position/UV records.
constexpr std::array<float, 24> rect_vertices{
-0.75f, -0.75f, 0, 0, 0, 0, 0, 0,
0.75f, -0.75f, 0, 0, 1, 0, 0, 0,
-0.75f, 0.75f, 0, 0, 0, 1, 0, 0};
std::memcpy(reinterpret_cast<void *>(rect_address), rect_vertices.data(),
sizeof(rect_vertices));
}
resources.MapMemory(depth_address, allocation_size);
if (depth_feedback) {
depth.desc.type = BindingType::DepthTarget;
@@ -13521,6 +13638,82 @@ public:
Require(name, "sparse MRT3 output", sparse_pixels[component] == expected,
"MRT3 was compacted to a different slot or clipped by unused MRT0");
}
// A fourth VS invocation reads beyond the descriptor instead of reconstructing the corner.
const ShaderBufferResource rect_buffer{{
static_cast<u32>(rect_address),
static_cast<u32>(rect_address >> 32u) | (32u << 16u), 3,
DstSel(4, 5, 6, 7) |
(static_cast<u32>(Prospero::BufferFormat::k32Float) << 12u)}};
static const auto rect_vertex_code = [] {
std::vector<u32> code{
EncodeMubuf0(0x0d, 0, true, false), EncodeMubuf1(0, 2, 5),
EncodeMubuf0(0x0d, 16, true, false), EncodeMubuf1(2, 2, 5)};
AppendVMovU32(&code, 4, 0);
AppendVMovLiteral(&code, 6, 0x3f800000u);
code.insert(code.end(), {EncodeExp0(0x0c, 0xf), EncodeExp1(0, 1, 4, 6),
EncodeExp0(0x20, 0xf), EncodeExp1(2, 3, 4, 6)});
AppendEnd(&code);
return code;
}();
static const auto rect_pixel_code = [] {
std::vector<u32> code;
for (u32 component = 0; component < 2; component++) {
code.push_back(EncodeVintrp(0, 20 + component, 0, component, 0));
code.push_back(EncodeVintrp(1, 20 + component, 0, component, 1));
}
AppendVMovU32(&code, 22, 0);
AppendVMovLiteral(&code, 23, 0x3f800000u);
code.insert(code.end(), {EncodeExp0(0x03, 0xf), EncodeExp1(20, 21, 22, 23)});
AppendEnd(&code);
return code;
}();
const auto rect_vs = reinterpret_cast<uint64_t>(rect_vertex_code.data());
const auto rect_ps = reinterpret_cast<uint64_t>(rect_pixel_code.data());
ShaderMapUserData(rect_vs,
{.type = Prospero::ShaderBinaryType::kGs, .user_data = &native_user_data,
.code_size_bytes = static_cast<u32>(rect_vertex_code.size() * sizeof(u32))});
ShaderMapUserData(rect_ps,
{.type = Prospero::ShaderBinaryType::kPs,
.code_size_bytes = static_cast<u32>(rect_pixel_code.size() * sizeof(u32))});
shaders.SetEsShaderBase(rect_vs);
shaders.SetPsShaderBase(rect_ps);
shaders.SetGsShaderResource2({.user_sgpr = 4});
for (u32 i = 0; i < 4; i++) {
shaders.SetGsUserSgpr(i, rect_buffer.fields[i], HW::UserSgprType::Vsharp);
}
registers.SetPsInControl(0x8001);
registers.SetPsInputEna(2);
registers.SetPsInputAddr(2);
registers.SetPsInputSettings(0, 0);
for (const auto primitive : {Prospero::PrimitiveType::kRectList,
Prospero::PrimitiveType::kRectListLegacy}) {
const char *rect_name = primitive == Prospero::PrimitiveType::kRectList
? "RectListThreeRecords" : "LegacyRectListThreeRecords";
user_config.SetPrimitiveType(primitive);
registers.SetModeControl({});
TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), sparse_color.image_id,
{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, {});
RenderExecutorTestAccess::DrawAuto(
executor, scheduler.Current(), {.vertex_count = 3, .instance_count = 1});
const auto pixels = ReadCachedTexel(name, context, sparse_color.image_id,
{}, {extent, extent, 1});
for (u32 y = 0; y < extent; y++) {
for (u32 x = 0; x < extent; x++) {
const bool inside = x >= 4 && x < 28 && y >= 4 && y < 28;
const auto offset = 4 * (y * extent + x);
const std::array<float, 4> expected{
inside ? (x + 0.5f - 4) / 24 : 0,
inside ? (y + 0.5f - 4) / 24 : 0, 0, inside ? 1.0f : 0};
for (u32 component = 0; component < 4; component++) {
Require(rect_name, "three-vertex rectangle coverage and UV",
std::abs(std::bit_cast<float>(pixels[offset + component]) -
expected[component]) < 0.0001f,
"rectangle reconstruction lost its fourth corner or interpolated UVs");
}
}
}
}
DestroyBuffer(&stencil_readback);
vertex_shader = owned_vertex_shader;
@@ -15011,6 +15204,7 @@ private:
Require("VulkanHarness", "graphics", available_features12.shaderOutputLayer == true,
"vertex layer output is not supported");
Require("VulkanHarness", "graphics", available_features.fillModeNonSolid &&
available_features.tessellationShader &&
available_depth_clip.depthClipEnable && available_clip_control.depthClipControl &&
available_color_write.colorWriteEnable &&
available_feedback_layout.attachmentFeedbackLoopLayout &&
@@ -15076,6 +15270,7 @@ private:
device_features.sampleRateShading = true;
device_features.shaderInt64 = true;
device_features.fillModeNonSolid = true;
device_features.tessellationShader = true;
device_info.pEnabledFeatures = &device_features;
constexpr const char *device_extensions[] = {
VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME,
@@ -16969,6 +17164,117 @@ TestCase ScalarGetpcWritesNextInstructionPc() {
{O::S_GETPC_B64, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
}
TestCase ScalarBitcmpB64DynamicOperands() {
using O = ShaderOpcode;
struct Input {
u32 low;
u32 high;
u32 index;
};
const Input inputs[] = {
{1u, 0u, 0u}, {0x80000000u, 0u, 31u}, {0u, 1u, 32u},
{0u, 0x80000000u, 63u}, {1u, 0u, 64u},
{0x80000000u, 0u, 95u}, {0u, 1u, 96u},
{0u, 0x80000000u, 127u}, {1u, 0u, 128u},
{0u, 0x80000000u, 0xffffffffu}, {0u, 0u, 17u},
{0xffffffffu, 0xffffffffu, 42u},
{0x89abcdefu, 0x76543210u, 4u},
{0x89abcdefu, 0x76543210u, 36u},
};
TestCase test;
test.name = "ScalarBitcmpB64DynamicOperands";
for (const auto &input : inputs) {
test.initial.insert(test.initial.end(), {input.low, input.high, input.index});
}
test.expected = test.initial;
for (u32 i = 0; i < std::size(inputs); ++i) {
// Load the value and index on the GPU so the compares cannot constant-fold.
for (u32 component = 0; component < 3; ++component) {
AppendVMovU32(&test.code, 30, (i * 3 + component) * 4);
AppendBufferLoadDword(&test.code, 0, 30);
test.code.push_back(EncodeVop1(0x02, 20 + component, Vgpr(0)));
}
const uint64_t value = uint64_t{inputs[i].low} | (uint64_t{inputs[i].high} << 32);
const u32 bit = static_cast<u32>((value >> (inputs[i].index & 63u)) & 1u);
for (u32 expected_bit = 0; expected_bit < 2; ++expected_bit) {
const bool scc = bit == expected_bit;
test.code.push_back(EncodeSopc(0x0e + expected_bit, 20, 22));
test.code.push_back(EncodeSop2(0x0a, 24, InlineU32(1), InlineU32(0)));
AppendStoreSgpr(&test.code, 24, static_cast<u32>(test.expected.size()));
test.expected.push_back(scc ? 1u : 0u);
// Exercise SCC-driven control flow as well as S_CSELECT_B32.
test.code.push_back(EncodeSopp(0x05, 2)); // s_cbranch_scc1 to the true arm.
test.code.push_back(EncodeSMovB32(24, InlineU32(3)));
test.code.push_back(EncodeSopp(0x02, 1)); // s_branch past the true arm.
test.code.push_back(EncodeSMovB32(24, InlineU32(7)));
AppendStoreSgpr(&test.code, 24, static_cast<u32>(test.expected.size()));
test.expected.push_back(scc ? 7u : 3u);
}
}
AppendEnd(&test.code);
test.initial.resize(test.expected.size());
test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_READFIRSTLANE_B32,
O::S_BITCMP0_B64, O::S_BITCMP1_B64, O::S_CSELECT_B32,
O::S_CBRANCH_SCC1, O::S_BRANCH, O::S_MOV_B32,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.required_spirv = {"OpBitFieldUExtract", "OpULessThan", "OpSelect"};
return test;
}
TestCase ScalarBitcmpB64IntegerConstants() {
using O = ShaderOpcode;
struct Source {
u32 encoding;
uint64_t bits;
};
// Negative integer inlines sign-extend; B64 literals zero-extend.
const Source sources[] = {
{128u, 0u}, {129u, 1u}, {192u, 64u},
{193u, 0xffffffffffffffffull}, {208u, 0xfffffffffffffff0ull},
{255u, 0xffffffffu}, {255u, 0x80000000u},
};
const u32 indices[] = {0u, 4u, 6u, 31u, 32u, 63u, 64u, 127u};
TestCase test;
test.name = "ScalarBitcmpB64IntegerConstants";
test.initial.assign(std::begin(indices), std::end(indices));
test.expected = test.initial;
for (u32 i = 0; i < std::size(indices); ++i) {
AppendVMovU32(&test.code, 30, i * 4);
AppendBufferLoadDword(&test.code, 0, 30);
test.code.push_back(EncodeVop1(0x02, 22, Vgpr(0)));
for (const auto &source : sources) {
const u32 bit = static_cast<u32>((source.bits >> (indices[i] & 63u)) & 1u);
for (u32 expected_bit = 0; expected_bit < 2; ++expected_bit) {
test.code.push_back(EncodeSopc(0x0e + expected_bit, source.encoding, 22));
if (source.encoding == 255u) {
test.code.push_back(static_cast<u32>(source.bits));
}
test.code.push_back(EncodeSop2(0x0a, 24, InlineU32(1), InlineU32(0)));
AppendStoreSgpr(&test.code, 24, static_cast<u32>(test.expected.size()));
test.expected.push_back(bit == expected_bit ? 1u : 0u);
}
}
}
// An instruction has one literal word even when both sources select it.
for (u32 expected_bit = 0; expected_bit < 2; ++expected_bit) {
test.code.push_back(EncodeSopc(0x0e + expected_bit, 255u, 255u));
test.code.push_back(0x8000001fu); // Select bit 31 of the same zero-extended literal.
test.code.push_back(EncodeSop2(0x0a, 24, InlineU32(1), InlineU32(0)));
AppendStoreSgpr(&test.code, 24, static_cast<u32>(test.expected.size()));
test.expected.push_back(expected_bit);
}
AppendEnd(&test.code);
test.initial.resize(test.expected.size());
test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_READFIRSTLANE_B32,
O::S_BITCMP0_B64, O::S_BITCMP1_B64, O::S_CSELECT_B32,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.required_spirv = {"OpBitFieldUExtract"};
return test;
}
TestCase ScalarBitfieldPack() {
using O = ShaderOpcode;
@@ -17977,6 +18283,29 @@ TestCase Vop1SdwaNotCapturedByte0Source() {
return test;
}
TestCase Vop1SdwaNotPreservesHighWordDestination() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendBufferLoadDword(&code, 0, 30);
AppendVMovLiteral(&code, 3, 0xabcd5555u);
code.push_back(0x7e066ef9u);
code.push_back(0x00061400u);
AppendStoreVgpr(&code, 3, 0);
AppendEnd(&code);
TestCase test;
test.name = "Vop1SdwaNotPreservesHighWordDestination";
test.code = std::move(code);
test.initial = {0x12345678u};
test.expected = {0xabcda987u};
test.opcodes = {O::BUFFER_LOAD_DWORD, O::V_MOV_B32, O::V_NOT_B32,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.ir_counts = {{" = BitwiseNot32 ", 1}};
test.required_spirv = {"OpNot"};
return test;
}
TestCase Vop1SdwaMovByteDestinations() {
using O = ShaderOpcode;
@@ -19295,6 +19624,32 @@ TestCase VectorMinMaxF16Ops() {
O::S_ENDPGM}};
}
TestCase VectorCvtF16U16ByteSource() {
using O = ShaderOpcode;
constexpr std::array inputs = {0xff123456u, 0x80123456u, 0x00123456u};
std::vector<u32> code;
AppendVMovLiteral(&code, 16, 0xa5a55a5au);
for (u32 i = 0; i < inputs.size(); i++) {
AppendVMovU32(&code, 30, i * 4u);
AppendBufferLoadDword(&code, 18, 30);
// Convert unsigned byte3 to FP16 word0 while preserving word1 of v16.
code.insert(code.end(), {0x7e20a0f9u, 0x00031412u});
AppendStoreVgpr(&code, 16, i);
}
AppendEnd(&code);
TestCase test;
test.name = "VectorCvtF16U16ByteSource";
test.code = std::move(code);
test.initial.assign(inputs.begin(), inputs.end());
test.expected = {0xa5a55bf8u, 0xa5a55800u, 0xa5a50000u};
test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_CVT_F16_U16,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.required_spirv = {"OpBitFieldUExtract", "OpConvertUToF"};
return test;
}
TestCase VectorCvtU16F16Sdwa() {
using O = ShaderOpcode;
@@ -22173,6 +22528,75 @@ TestCase BufferLoadDwordx4ZeroesOnlyOutOfBoundsTail() {
return test;
}
TestCase BufferLoadsGpuSelectedDescriptors() {
using O = ShaderOpcode;
constexpr uint64_t GuestBase = 0x0000000110000000ull;
struct DescriptorCase {
u32 stride, records, mode, soffset;
bool swizzle, bound;
std::array<u32, 6> expected;
};
const DescriptorCase cases[] = {
{24, 2, 0, 0, false, true, {7, 8, 9, 10, 11, 12}},
{12, 2, 0, 0, false, true, {4, 5, 6, 0, 0, 0}},
{12, 2, 1, 0, false, true, {4, 5, 6, 7, 8, 9}},
{12, 1, 2, 0, false, true, {4, 5, 6, 7, 8, 9}},
{12, 0, 2, 0, false, true, {}},
// OOB3 checks offset + soffset, independently of index * stride.
{12, 20, 3, 4, false, true, {5, 6, 7, 8, 0, 0}},
{12, 2, 0, 0, false, false, {}},
// RDNA2 swizzling uses four-byte elements; soffset is added afterward.
{12, 2, 0, 4, true, true, {3, 11, 19, 0, 0, 0}},
{12, 8, 3, 4, true, true, {3, 11, 19, 0, 0, 0}},
{12, 4, 3, 4, true, true, {}},
};
TestCase test;
test.name = "BufferLoadsGpuSelectedDescriptors";
test.initial.resize(2048);
for (u32 i = 0; i < std::size(cases); ++i) {
const auto &input = cases[i];
const u32 data_offset = 4096 + i * 128;
// 120-byte table entries with a descriptor at byte 8, as in PPSA04677.
const std::array<u32, 4> descriptor{
static_cast<u32>(GuestBase + data_offset),
(input.stride << 16u) | (input.swizzle ? 1u << 31u : 0u) | 1u,
input.records, (input.bound ? 0x5204u : 0x204u) | (input.mode << 28u)};
std::copy(descriptor.begin(), descriptor.end(), test.initial.begin() + 130 + i * 30);
for (u32 word = 0; word < 32; ++word) {
test.initial[data_offset / 4 + word] = i * 100 + word + 1;
}
// Reverse the table order through a GPU load and readfirstlane, not host constants.
const u32 selected = static_cast<u32>(std::size(cases)) - 1 - i;
test.initial[64 + i] = selected;
AppendVMovU32(&test.code, 30, (64 + i) * 4);
AppendBufferLoadDword(&test.code, 0, 30);
test.code.push_back(EncodeVop1(0x02, 20, Vgpr(0)));
test.code.push_back(EncodeSop2(0x26, 20, 20, 255)); // s_mul_i32 s20, s20, 120
test.code.push_back(120);
test.code.push_back(EncodeSmem0(0x0a, 8, 0));
test.code.push_back(EncodeSmem1(520, 20));
AppendSMovLiteral(&test.code, 22, cases[selected].soffset);
AppendVMovU32(&test.code, 21, 1);
test.code.push_back(EncodeMubuf0(0x0e, 0, true, false));
test.code.push_back(EncodeMubuf1(0, 2, 21, 22));
test.code.push_back(EncodeMubuf0(0x0d, 16, true, false));
test.code.push_back(EncodeMubuf1(4, 2, 21, 22));
for (u32 component = 0; component < 6; ++component) {
AppendStoreVgpr(&test.code, component, i * 6 + component);
const u32 expected = cases[selected].expected[component];
test.expected.push_back(expected == 0 ? 0 : selected * 100 + expected);
}
}
AppendEnd(&test.code);
test.bda_mappings = {{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_LOAD_DWORDX4, O::BUFFER_LOAD_DWORDX2,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.required_spirv = {"OpConvertUToPtr", "PhysicalStorageBuffer"};
return test;
}
TestCase BufferStoreDwordx4DropsOnlyOutOfBoundsTail() {
using O = ShaderOpcode;
@@ -27061,6 +27485,8 @@ std::vector<TestCase> MakeCases() {
cases.push_back(ScalarSubvectorLoops(64));
AddCase(ScalarGetpcWritesNextInstructionPc);
AddCase(ScalarBitfieldPack);
AddCase(ScalarBitcmpB64DynamicOperands);
AddCase(ScalarBitcmpB64IntegerConstants);
AddCase(ScalarBrevB32PreservesScc);
AddCase(ScalarBfeI32CapturedRawSignExtends);
AddCase(BitfieldExtractWidthPastEndEdges);
@@ -27088,6 +27514,7 @@ std::vector<TestCase> MakeCases() {
AddCase(VectorFfbhI32NativeAndVop3OnGpu);
AddCase(Vop1SdwaFfblCapturedHighWordSource);
AddCase(Vop1SdwaNotCapturedByte0Source);
AddCase(Vop1SdwaNotPreservesHighWordDestination);
AddCase(Vop1SdwaMovByteDestinations);
AddCase(Vop2SdwaSubNcExactByte2Destination);
AddCase(Vop2SdwaAddNcCapturedHighWordDestination);
@@ -27126,6 +27553,7 @@ std::vector<TestCase> MakeCases() {
AddCase(CvtPkrtzF16F32SdwaAndOutputModifiers);
AddCase(PackedMinMaxF16NanAndSignedZeroEdges);
AddCase(VectorMinMaxF16Ops);
AddCase(VectorCvtF16U16ByteSource);
AddCase(VectorCvtU16F16Sdwa);
AddCase(NativeAndSdwa16BitDestinationWrites);
AddCase(VectorMinMaxMed3F16Ops);
@@ -27212,6 +27640,7 @@ std::vector<TestCase> MakeCases() {
AddCase(BufferLoadDwordx3SnapshotsOverlappingAddress);
AddCase(BufferLoadDwordx4SnapshotsOverlappingAddress);
AddCase(BufferLoadDwordx4ZeroesOnlyOutOfBoundsTail);
AddCase(BufferLoadsGpuSelectedDescriptors);
AddCase(BufferStoreDwordx4DropsOnlyOutOfBoundsTail);
AddCase(BufferLoadFormatXyzwRejectsPartialRecord);
AddCase(BufferStoreFormatXyzwDropsPartialRecord);
@@ -28770,6 +29199,17 @@ void CheckDepthTextureEncoding() {
IsSupportedDepthTextureEncoding(compressed_descriptor),
"valid compressed depth descriptor required a prior depth target");
const ShaderTextureResource disabled_metadata{{
0x20018100u, 0xc1600000u, 0x021bc1dfu, 0x91800204u,
0x00000000u, 0x00700000u, 0x00000000u, 0x0020037fu,
}};
auto retained_metadata_address = compressed_descriptor;
retained_metadata_address.fields[6] &= 0xff000000u;
Require("DepthTextureEncoding", "disabled metadata with retained address",
IsSupportedDepthTextureEncoding(disabled_metadata) &&
IsSupportedDepthTextureEncoding(retained_metadata_address),
"uncompressed depth rejected inactive metadata address bits");
const ShaderTextureResource first_use_depth{{
0x0225fc00u, 0x01600000u, 0x00000000u, 0x91800924u,
0x00000000u, 0x00700000u, 0x80280000u, 0x000225fdu,
@@ -31938,6 +32378,16 @@ int main(int argc, char **argv) {
return 0;
}
#endif
if (argc == 2 && std::strcmp(argv[1], "--indirect-buffer-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, BufferLoadsGpuSelectedDescriptors());
RunCase(&vulkan, BufferLoadDwordx4SnapshotsOverlappingAddress());
RunCase(&vulkan, BufferLoadDwordx4ZeroesOnlyOutOfBoundsTail());
RunCase(&vulkan, BufferLoadDwordIdxenUsesDescriptorStride());
RunCase(&vulkan, BufferStoreFormatXAddTidUsesLaneIndex());
RunCase(&vulkan, FlatVirtualAddressRebasesGuestAllocation());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--wave64-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, Wave32VccMasksPreserveOtherHalf());
@@ -32204,6 +32654,7 @@ int main(int argc, char **argv) {
if (argc == 2 && std::strcmp(argv[1], "--layered-image-only") == 0) {
VulkanHarness vulkan;
vulkan.CheckUnifiedImageViewCache();
vulkan.CheckCubeFaceStorageExpansion();
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--image-view-cache-only") == 0) {
@@ -32394,6 +32845,7 @@ int main(int argc, char **argv) {
#endif
vulkan.CheckUnifiedImageViewCache();
vulkan.CheckPackedTextureComponents();
vulkan.CheckCubeFaceStorageExpansion();
const auto tests = MakeCases();
const auto graphics_tests = MakeGraphicsCases();
CheckOpcodeCoverage(tests, graphics_tests);
+133 -34
View File
@@ -1679,45 +1679,14 @@ void TestNewShaderRecompilerSMovB32() {
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerSoppMarkers() {
void TestShaderStageBarriers() {
const uint32_t shader[] = {
EncodeSopp(0x00, 3), // s_nop 3
EncodeSopp(0x0c, 0), // s_waitcnt 0
EncodeSopp(0x10, 0x0f), // s_sendmsg 15
EncodeSopp(0x16, 0x2a), // s_ttracedata 42
EncodeSopp(0x20, 1), // s_inst_prefetch 1
EncodeSopp(0x0a, 0), // s_barrier
EncodeSopp(0x01, 0), // s_endpgm
};
auto options = MakeCompileOptions(ShaderType::Compute);
options.dump_ir = true;
auto result = RecompileForTest(shader, options);
Check((result.decoded_dump.find("s_nop 0x00000003") != std::string::npos),
"new decoder did not decode SOPP s_nop");
Check((result.decoded_dump.find("s_waitcnt 0x00000000") != std::string::npos),
"new decoder did not decode SOPP s_waitcnt");
Check((result.decoded_dump.find("s_sendmsg 0x0000000f") != std::string::npos),
"new decoder did not decode SOPP s_sendmsg");
Check((result.decoded_dump.find("s_ttracedata 0x0000002a") != std::string::npos),
"new decoder did not decode SOPP s_ttracedata");
Check((result.decoded_dump.find("s_inst_prefetch 0x00000001") != std::string::npos),
"new decoder did not decode SOPP s_inst_prefetch");
Check((result.decoded_dump.find("s_barrier") != std::string::npos),
"new decoder did not decode SOPP s_barrier");
Check((result.ir_dump.find("ControlNop null, 0x00000003") != std::string::npos),
"SOPP s_nop did not lower to an IR marker");
Check((result.ir_dump.find("Waitcnt null, 0x00000000") != std::string::npos),
"SOPP s_waitcnt did not lower to an IR marker");
Check((result.ir_dump.find("Sendmsg null, 0x0000000f") != std::string::npos),
"SOPP s_sendmsg did not lower to an IR marker");
Check((result.ir_dump.find("TtraceData null, 0x0000002a") != std::string::npos),
"SOPP s_ttracedata did not lower to an IR marker");
Check((result.ir_dump.find("InstPrefetch null, 0x00000001") != std::string::npos),
"SOPP s_inst_prefetch did not lower to an IR marker");
Check((result.ir_dump.find("Barrier null") != std::string::npos),
"SOPP s_barrier did not lower to an IR marker");
const auto result =
RecompileForTest(shader, MakeCompileOptions(ShaderType::Compute));
Check(SpirvContainsOpcode(result.spirv, 224),
"SPIR-V binary does not contain OpControlBarrier");
Check(
@@ -1725,6 +1694,70 @@ void TestNewShaderRecompilerSoppMarkers() {
result.spirv.end(),
"SPIR-V barrier does not use workgroup acquire-release memory semantics");
CheckSpirvBinaryValidates(result.spirv);
const auto vertex_result =
RecompileForTest(shader, MakeCompileOptions(ShaderType::Vertex));
CheckSpirvBinaryValidates(vertex_result.spirv);
Check(!SpirvContainsOpcode(vertex_result.spirv, 224),
"independent vertex invocations retained a workgroup barrier");
}
void TestNggVertexEntryState() {
using namespace ShaderRecompiler;
// PPSA03309 shader 5a39eb2021a5d2c1: retain its NGG prologue and replace
// the resource-dependent vertex body with a position export.
const uint32_t shader[] = {
0xbfa00003u, 0x93ebff03u, 0x00040018u, 0xbefe03c1u,
0x9380ff02u, 0x00090016u, 0x9381ff02u, 0x0009000cu,
0xbf8a0000u, 0xbf076b80u, 0xbf850003u, 0x8f6a8c00u,
0x887c6a01u, 0xbf900009u, 0xd7650001u, 0x000100c1u,
0xd7460001u, 0x04050a6bu, 0x7da80200u, 0xbf880002u,
0xf8000941u, 0x00000000u, 0xbf8cff0fu, 0xbefe03c1u,
0x7da80201u, EncodeSopp(0x08, 2),
EncodeExp0(0x0c, 0xf), EncodeExp1(5, 5, 5, 5), EncodeSopp(0x01),
};
HW::VertexShaderInfo regs{};
regs.es_regs.data_addr = reinterpret_cast<uint64_t>(shader);
ShaderUserData user_data{};
ShaderMappedData mapped{};
mapped.user_data = &user_data;
mapped.code_size_bytes = sizeof(shader);
ShaderMapUserData(regs.es_regs.data_addr, mapped);
std::vector<uint32_t> previous_key;
for (const uint32_t wave_size : {32u, 64u}) {
HW::Context context;
context.SetShaderStages(wave_size == 32u ? 0x00400000u : 0u);
ShaderVertexInputInfo input{};
const auto params = PrepareProgram(regs, context, HW::UserConfig{}, input);
Check(input.wave_size == wave_size,
"vertex preparation lost the native NGG wave size");
const auto key = MakeStageStaticKey(input);
Check(previous_key != key, "NGG wave sizes shared a shader cache key");
previous_key = key;
auto options = MakeCompileOptions(ShaderType::Vertex);
options.user_data_base = 8;
options.user_data = params.user_data;
options.input_info.vertex = &input;
options.wave_size = input.wave_size;
const auto result = RecompileForTest(params.code, options);
bool live_vertex_range = false;
for (const auto *block : result.program.blocks) {
for (const auto &inst : *block) {
if (inst.GetOpcode() == IR::ValueOpcode::UGreaterThan32) {
const auto count = inst.Arg(0).Resolve();
live_vertex_range |= count.IsImmediate() && count.U32() == wave_size;
}
}
}
Check(live_vertex_range && std::ranges::any_of(result.program.info.outputs,
[](const auto &output) {
return output.kind == IR::StageOutputKind::Position;
}),
"NGG launch counts suppressed the vertex export");
Check(result.program.wave_size == wave_size,
"NGG wave size was lost during translation");
CheckSpirvBinaryValidates(result.spirv);
}
}
void TestNewShaderRecompilerSopkWaitcntMarkers() {
@@ -3433,6 +3466,69 @@ void TestNewShaderRecompilerCapturedVop1SdwaByteConvert() {
Check(SpirvContainsOpcode(result.spirv, 112),
"captured SDWA byte conversion did not emit OpConvertUToF");
CheckSpirvBinaryValidates(result.spirv);
const uint32_t byte_sext[] = {0x7e20a0f9u, 0x000b1412u};
ShaderRecompiler::Decoder::Instruction decoded;
ShaderRecompiler::Decoder::DecodeInstruction(byte_sext, 0, decoded);
Check(decoded.word_count == 2u &&
decoded.opcode == ShaderRecompiler::Decoder::Opcode::UNSUPPORTED,
"V_CVT_F16_U16 accepted unimplemented SDWA byte sign extension");
}
void TestNewShaderRecompilerVop1SdwaNotDestination() {
auto options = MakeCompileOptions(ShaderType::Pixel);
const auto check_destination = [&](uint32_t modifier, uint32_t dst_sel,
uint32_t dst_unused) {
const uint32_t shader[] = {
0x7e066ef9u, modifier, EncodeExp0(0x00, 0x1),
EncodeExp1(3, 0, 0, 0), EncodeSopp(0x01),
};
ShaderRecompiler::Decoder::Instruction decoded;
ShaderRecompiler::Decoder::DecodeInstruction(shader, 0, decoded);
Check(decoded.opcode == ShaderRecompiler::Decoder::Opcode::V_NOT_B32,
"V_NOT_B32 SDWA opcode did not decode");
Check(decoded.word_count == 2u,
"V_NOT_B32 SDWA instruction has the wrong word count");
Check(decoded.dst.kind == ShaderRecompiler::Decoder::OperandKind::Vgpr &&
decoded.dst.reg == 3u && decoded.dst.sdwa_sel == dst_sel &&
decoded.dst.sdwa_dst_unused == dst_unused &&
decoded.dst.explicit_sdwa_dst,
"V_NOT_B32 SDWA destination metadata is incorrect");
Check(decoded.src0.kind == ShaderRecompiler::Decoder::OperandKind::Vgpr &&
decoded.src0.reg == 0u && decoded.src0.sdwa_sel == 6u &&
!decoded.src0.negate && !decoded.src0.absolute,
"V_NOT_B32 SDWA source metadata is incorrect");
auto result = RecompileForTest(shader, options);
CheckSpirvBinaryValidates(result.spirv);
};
check_destination(0x00061400u, 4u, 2u);
check_destination(0x00060500u, 5u, 0u);
check_destination(0x00060000u, 0u, 0u);
const auto check_rejected = [&](uint32_t modifier, const char *message) {
const uint32_t instruction[] = {0x7e066ef9u, modifier};
ShaderRecompiler::Decoder::Instruction decoded;
ShaderRecompiler::Decoder::DecodeInstruction(instruction, 0, decoded);
Check(decoded.word_count == 2u &&
decoded.opcode == ShaderRecompiler::Decoder::Opcode::UNSUPPORTED,
message);
};
check_rejected(0x00060700u, "V_NOT_B32 SDWA accepted destination selector 7");
check_rejected(0x00070600u, "V_NOT_B32 SDWA accepted source selector 7");
check_rejected(0x00061c00u,
"V_NOT_B32 SDWA accepted destination unused value 3");
check_rejected(0x00160400u, "V_NOT_B32 SDWA accepted source negation");
check_rejected(0x00260400u, "V_NOT_B32 SDWA accepted source absolute");
check_rejected(0x00062400u, "V_NOT_B32 SDWA accepted clamp");
check_rejected(0x00064400u, "V_NOT_B32 SDWA accepted output modifier");
check_rejected(0x00000400u,
"V_NOT_B32 SDWA partial destination accepted a byte source");
}
void TestNewShaderRecompilerBootB16PackedAndSdwaOpcodes() {
@@ -13387,6 +13483,8 @@ int main() {
TestNewShaderRecompilerSpirvSizeBaselines();
TestDemandDrivenSpirvDeclarations();
TestNewShaderRecompilerSMovB32();
TestShaderStageBarriers();
TestNggVertexEntryState();
TestNewShaderRecompilerClipDisabledPosition();
TestNewShaderRecompilerAuxPositionExports();
TestNewShaderRecompilerNativeWideScalarMemoryIr();
@@ -13397,6 +13495,7 @@ int main() {
TestNewShaderRecompilerDsReadWrite2Translation();
TestNewShaderRecompilerDsWideAndAtomicTranslation();
TestNewShaderRecompilerCapturedVop1SdwaByteConvert();
TestNewShaderRecompilerVop1SdwaNotDestination();
TestNewShaderRecompilerScalarMemoryBindingDomains();
// Opcode semantics and optimized SPIR-V are exercised by
// ShaderRecompilerComputeTests; keep the distinct decoder contract checks