mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-29 16:51:20 +00:00
Compare commits
44
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b095caefe | ||
|
|
8dc9b9d532 | ||
|
|
e40438f95e | ||
|
|
a6a56c8a44 | ||
|
|
4b074cf9e9 | ||
|
|
7c9e18e73b | ||
|
|
d2df2cad74 | ||
|
|
b2a80a3a96 | ||
|
|
cfb8f7b246 | ||
|
|
d792dfa885 | ||
|
|
12ff571609 | ||
|
|
d3c182f385 | ||
|
|
bf4dcdce52 | ||
|
|
476204cd4b | ||
|
|
f9e56ef8a0 | ||
|
|
61761b65f2 | ||
|
|
ba55ba5dc5 | ||
|
|
c997cdb837 | ||
|
|
fe4f9423e3 | ||
|
|
81af130147 | ||
|
|
ada0514084 | ||
|
|
d7da244475 | ||
|
|
549495575c | ||
|
|
c3cc8454b9 | ||
|
|
b8ad1d8c4b | ||
|
|
cfb110e0aa | ||
|
|
e930657bf6 | ||
|
|
0ad9149c90 | ||
|
|
25f2699523 | ||
|
|
a849f25376 | ||
|
|
fa51da4d76 | ||
|
|
6b79b7a003 | ||
|
|
102a10f0fe | ||
|
|
8d66c4ef6d | ||
|
|
8cecaf4e30 | ||
|
|
82b2711cdf | ||
|
|
35a275e97d | ||
|
|
303a0203b6 | ||
|
|
cfbaee07f0 | ||
|
|
fe45265fcd | ||
|
|
8b294eb5b8 | ||
|
|
07c9978ac6 | ||
|
|
b61b3f2a40 | ||
|
|
001dfea501 |
No files matched your search
@@ -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
|
||||
|
||||
|
||||
Vendored
+3
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 |
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = ¶m;
|
||||
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 = ¶m;
|
||||
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 = ¶m;
|
||||
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;
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user