Compare commits

...

10 Commits

Author SHA1 Message Date
nmzik 8cecaf4e30 Merge PR #716: Fix Unicode paths on Windows
Preserve native Windows paths for filesystem operations, command-line options,
and game icons. Complete Unicode handling in recursive file listings and file
logging, restore unmounting by host directory, and extend regression coverage.

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

Closes #714

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

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

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

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

* Fix save-memory compatibility and preserve existing saves

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 00:51:22 +04:00
Augusto Domingos fe45265fcd preserve native paths when creating directories 2026-09-19 17:53:15 +02:00
Augusto Domingos 8b294eb5b8 Add unicode path regression coverage 2026-09-19 15:48:30 +02:00
Augusto Domingos 07c9978ac6 keep game icon paths native in windows 2026-09-19 14:45:24 +02:00
Augusto Domingos b61b3f2a40 handle UTF-8 command-line paths 2026-09-19 14:12:35 +02:00
Augusto Domingos 001dfea501 Preserve native windows paths in filesystem operations 2026-09-19 13:11:55 +02:00
17 changed files with 1636 additions and 364 deletions
+3
View File
@@ -86,6 +86,9 @@ set(SPDLOG_BUILD_WARNINGS OFF CACHE BOOL "" FORCE)
set(SPDLOG_INSTALL OFF CACHE BOOL "" FORCE)
set(SPDLOG_FMT_EXTERNAL ON CACHE BOOL "" FORCE)
set(SPDLOG_NO_EXCEPTIONS ON CACHE BOOL "" FORCE)
if(WIN32)
set(SPDLOG_WCHAR_FILENAMES ON CACHE BOOL "" FORCE)
endif()
add_subdirectory(spdlog EXCLUDE_FROM_ALL)
if (NOT TARGET FFmpeg::ffmpeg)
+7
View File
@@ -450,6 +450,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 +540,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)
+49 -65
View File
@@ -245,88 +245,78 @@ void File::Printf(const char* format, ...) {
Write(s.data(), static_cast<uint32_t>(s.size()));
}
static std::filesystem::path WithoutTrailingSeparator(
const std::filesystem::path& path) {
if (!path.empty() && !path.has_filename() &&
path != path.root_path()) {
return path.parent_path();
}
return path;
}
bool File::IsDirectoryExisting(const std::filesystem::path& path) {
auto path_str = PathToGenericString(path);
return SysFileIsDirectoryExisting(path_str.ends_with("/") ? Common::RemoveLast(path_str, 1)
: path_str);
return SysFileIsDirectoryExisting(WithoutTrailingSeparator(path));
}
bool File::IsFileExisting(const std::filesystem::path& name) {
return SysFileIsFileExisting(name);
}
bool File::CreateDirectory(
const std::filesystem::path& path) // @suppress("Member declaration not found")
{
auto path_str = PathToGenericString(path);
return SysFileCreateDirectory(path_str.ends_with("/") ? Common::RemoveLast(path_str, 1)
: path_str);
bool File::CreateDirectory(const std::filesystem::path& path) {
return SysFileCreateDirectory(
WithoutTrailingSeparator(path));
}
bool File::DeleteDirectory(const std::filesystem::path& path) {
auto path_str = PathToGenericString(path);
return SysFileDeleteDirectory(path_str.ends_with("/") ? Common::RemoveLast(path_str, 1)
: path_str);
return SysFileDeleteDirectory(
WithoutTrailingSeparator(path));
}
bool File::CreateDirectories(const std::filesystem::path& path) {
std::string real_path = Common::ReplaceChar(PathToGenericString(path), '\\', '/');
const auto normalized_path =
WithoutTrailingSeparator(path);
std::vector<std::string> list = Common::Split(real_path, "/");
auto current_path = normalized_path.root_path();
std::string p;
for (const auto& component:
normalized_path.relative_path()) {
current_path /= component;
for (uint32_t si = 0; si < list.size(); si++) {
const std::string& s = list[si];
if (IsDirectoryExisting(current_path)) {
continue;
}
if (si != 0 || real_path.starts_with("/")) {
p += "/";
}
if (!CreateDirectory(current_path)) {
return false;
}
}
p += s;
if (IsDirectoryExisting(p)) {
continue;
}
if (!CreateDirectory(p)) // @suppress("Invalid arguments")
{
return false;
}
}
return true;
return true;
}
bool File::DeleteDirectories(const std::filesystem::path& path) {
std::string real_path = Common::ReplaceChar(PathToGenericString(path), '\\', '/');
const auto normalized_path =
WithoutTrailingSeparator(path);
std::vector<std::string> list = Common::Split(real_path, "/");
std::vector<std::filesystem::path> directories;
auto current_path = normalized_path.root_path();
std::string p;
std::vector<std::string> list2;
for (const auto& component:
normalized_path.relative_path()) {
current_path /= component;
directories.push_back(current_path);
}
for (uint32_t si = 0; si < list.size(); si++) {
const std::string& s = list[si];
for (auto it = directories.rbegin();
it != directories.rend(); ++it) {
if (!DeleteDirectory(*it)) {
return false;
}
}
if (si != 0 || real_path.starts_with("/")) {
p += "/";
}
p += s;
list2.push_back(p);
}
uint32_t num = list2.size();
for (uint32_t si = num - 1; si < num; si--) {
if (!DeleteDirectory(list2[si])) {
return false;
}
}
return true;
return true;
}
bool File::DeleteFile(
@@ -460,13 +450,7 @@ std::vector<File::FindInfo> File::FindFiles(const std::filesystem::path& path) {
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++;
}
const auto normalized_path = path.lexically_normal();
std::vector<File::FindInfo> ret;
ret.reserve(files.size());
@@ -474,7 +458,7 @@ std::vector<File::FindInfo> File::FindFiles(const std::filesystem::path& path) {
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.rel_path_with_name = f.path_with_name.lexically_normal().lexically_relative(normalized_path);
r.size = f.size;
SysTimeStruct at {};
+1 -2
View File
@@ -3,7 +3,6 @@
#include "common/assert.h"
#include "common/emulatorConfig.h"
#include "common/stringUtils.h"
#include <cstdio>
#include <filesystem>
@@ -47,7 +46,7 @@ std::shared_ptr<spdlog::logger> MakeFileLogger(std::string name
}
auto sink =
std::make_shared<spdlog::sinks::basic_file_sink_mt>(Common::PathToString(path), true);
std::make_shared<spdlog::sinks::basic_file_sink_mt>(path.native(), true);
return MakeLogger(std::move(name), std::move(sink));
}
+13 -28
View File
@@ -477,18 +477,10 @@ bool SysFileSetLastAccessAndWriteTimeUtc(const std::filesystem::path& name,
}
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 += "/";
}
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;
@@ -496,18 +488,19 @@ void SysFileFindFiles(const std::filesystem::path& path, std::vector<sys_file_fi
do {
std::filesystem::path file_name(data.cFileName);
auto file_name_str = Common::PathToString(file_name);
if (file_name_str == "." || file_name_str == "..") {
if (file_name == L"." || file_name == L"..") {
continue;
}
const auto full_path = path / file_name;
if ((data.dwFileAttributes & static_cast<DWORD>(FILE_ATTRIBUTE_DIRECTORY)) != 0u) {
SysFileFindFiles(std::filesystem::path(real_path) / file_name, out);
SysFileFindFiles(full_path, out);
} else {
sys_file_find_t r {};
r.path_with_name = std::filesystem::path(real_path) / file_name;
r.path_with_name = full_path;
r.size = (static_cast<uint64_t>(data.nFileSizeHigh) << 32u) +
static_cast<uint64_t>(data.nFileSizeLow);
r.last_access_time.is_invalid = false;
@@ -515,7 +508,7 @@ void SysFileFindFiles(const std::filesystem::path& path, std::vector<sys_file_fi
r.last_write_time.is_invalid = false;
r.last_write_time.time = data.ftLastWriteTime;
out.push_back(r);
out.push_back(std::move(r));
}
} while (FindNextFileW(h, &data) != 0);
@@ -524,18 +517,10 @@ void SysFileFindFiles(const std::filesystem::path& path, std::vector<sys_file_fi
}
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 +534,7 @@ void SysFileGetDents(const std::filesystem::path& path, std::vector<sys_dir_entr
r.is_file = ((data.dwFileAttributes & static_cast<DWORD>(FILE_ATTRIBUTE_DIRECTORY)) == 0u);
r.name = Common::PathToString(file_name);
out.push_back(r);
out.push_back(std::move(r));
} while (FindNextFileW(h, &data) != 0);
+8
View File
@@ -34,6 +34,14 @@ inline std::string PathToGenericString(const std::filesystem::path& path) {
#endif
}
inline std::filesystem::path PathFromUtf8(std::string_view text) {
#if defined(__cpp_char8_t)
return std::filesystem::path(std::u8string(text.begin(), text.end()));
#else
return std::filesystem::u8path(text.begin(), text.end());
#endif
}
inline bool EqualNoCase(std::string_view lhs, std::string_view rhs) {
if (lhs.size() != rhs.size()) {
return false;
+5 -3
View File
@@ -27,6 +27,7 @@
#include "common/systemInfo.h"
#include "common/threads.h"
#include "common/timer.h"
#include "common/stringUtils.h"
#include "graphics/host_gpu/graphicContext.h"
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
@@ -45,6 +46,7 @@
#include <string>
#include <vector>
#include <vulkan/vk_platform.h>
#include <filesystem>
#define STB_IMAGE_IMPLEMENTATION
#define STBI_NO_SIMD
@@ -945,10 +947,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 +976,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);
}
+8 -6
View File
@@ -251,16 +251,15 @@ void FileDescriptors::CloseAll() {
void MountPoints::Mount(const std::filesystem::path& folder, const std::string& point) {
Common::LockGuard lock(m_mutex);
auto folder_str = Common::FixDirectorySlash(Common::PathToGenericString(folder));
auto point_str = Common::FixDirectorySlash(point);
Umount(point_str);
MountPair p;
p.dir = folder_str;
p.dir = folder;
p.point = point_str;
m_mount_pairs.push_back(p);
m_mount_pairs.push_back(std::move(p));
}
void MountPoints::Umount(const std::string& folder_or_point) {
@@ -270,7 +269,7 @@ void MountPoints::Umount(const std::string& folder_or_point) {
const auto it = std::find_if(
m_mount_pairs.begin(), m_mount_pairs.end(), [&folder_or_point_str](const MountPair& p) {
return Common::PathToGenericString(p.dir) == folder_or_point_str ||
return Common::FixDirectorySlash(Common::PathToGenericString(p.dir)) == folder_or_point_str ||
p.point == folder_or_point_str;
});
if (it != m_mount_pairs.end()) {
@@ -343,14 +342,17 @@ std::filesystem::path MountPoints::ResolvePath(const std::string& mounted_name)
while (rel_path.starts_with('/')) {
rel_path = Common::RemoveFirst(rel_path, 1);
}
const auto native_rel_path = Common::PathFromUtf8(rel_path);
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
if (HasWindowsForbiddenFilenameCharacter(rel_path)) {
::printf("FileSystem: Windows-incompatible guest filename: %s\n",
mounted_name.c_str());
}
return p.dir / rel_path;
return p.dir / native_rel_path;
#else
return ResolvePathIgnoringCase(p.dir / rel_path);
return ResolvePathIgnoringCase(p.dir / native_rel_path);
#endif
}
+542 -130
View File
@@ -9,6 +9,7 @@
#include "common/threads.h"
#include "kernel/pthread.h"
#include "kernel/semaphore.h"
#include "libs/ajm/atrac9_decoder.h"
#include "libs/audio_internal.h"
#include "libs/errno.h"
#include "libs/libs.h"
@@ -18,8 +19,10 @@
#include <atomic>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <limits>
#include <magic_enum.hpp>
#include <memory>
#include <vector>
#include "libatrac9.h"
@@ -1824,26 +1827,146 @@ enum class Ngs2RackType {
CustomSampler,
};
struct Ngs2UserFxContext {
void* common;
void* param;
void* work;
uintptr_t user_data;
uint32_t max_voices;
uint32_t voice_index;
uint64_t reserved[4] {};
};
struct Ngs2UserFxProcessContext {
float** channels;
void* common;
const void* param;
void* work;
void* state;
uintptr_t user_data;
uint32_t flags;
uint32_t input_channels;
uint32_t output_channels;
uint32_t grain_samples;
uint32_t sample_rate;
uint32_t reserved {};
uint64_t reserved2[4] {};
};
struct Ngs2UserFxOption {
Ngs2CustomModuleOption header;
int KYTY_SYSV_ABI (*setup)(Ngs2UserFxContext*);
int KYTY_SYSV_ABI (*cleanup)(Ngs2UserFxContext*);
uintptr_t control;
int KYTY_SYSV_ABI (*process)(Ngs2UserFxProcessContext*);
size_t common_size;
size_t param_size;
size_t work_size;
uintptr_t user_data;
};
struct Ngs2RackInternal {
Ngs2Internal* ngs = nullptr;
Ngs2RackInternal* next = nullptr;
Ngs2RackType type = Ngs2RackType::Sampler;
Ngs2RackOptionUnion option;
Ngs2ContextBufferInfo buffer_info;
Ngs2BufferAllocator allocator;
Ngs2Internal* ngs = nullptr;
Ngs2RackInternal* next = nullptr;
Ngs2RackType type = Ngs2RackType::Sampler;
Ngs2RackOptionUnion option;
Ngs2ContextBufferInfo buffer_info;
Ngs2BufferAllocator allocator;
std::array<std::vector<uint8_t>, 24> common;
std::array<Ngs2UserFxOption, 24> fx {};
};
enum class Ngs2VoicePlayState { Empty, Playing, Paused, Stopped };
enum class Ngs2VoicePlayEvent { None, Play, Pause, Resume, Stop, StopImm, Kill };
struct Ngs2VoiceInternal {
Ngs2VoicePlayEvent event = Ngs2VoicePlayEvent::None;
Ngs2VoicePlayState state = Ngs2VoicePlayState::Empty;
Ngs2RackInternal* rack = nullptr;
uintptr_t callback = 0;
uintptr_t callback_data = 0;
uint32_t callback_flags = 0;
struct Port {
Ngs2VoiceInternal* dest = nullptr;
uint32_t input = 0;
float volume = 1.0f;
int32_t matrix = -1;
};
struct Block {
const uint8_t* data;
Ngs2WaveformBlock info;
uint32_t cursor = 0;
size_t data_cursor = 0;
uint32_t skip_remaining = info.num_skip_samples;
};
struct Module {
std::vector<uint8_t> param, work, state;
uint32_t flags = 1;
};
Ngs2VoicePlayEvent event = Ngs2VoicePlayEvent::None;
Ngs2VoicePlayState state = Ngs2VoicePlayState::Empty;
Ngs2RackInternal* rack = nullptr;
uintptr_t callback = 0;
uintptr_t callback_data = 0;
uint32_t callback_flags = 0;
std::deque<Block> blocks;
std::vector<Port> ports;
std::vector<std::vector<float>> matrices;
std::vector<Module> modules;
std::vector<float> samples;
std::vector<float> output_matrix;
uint32_t channels = 0;
uint32_t output_id = 0;
bool rendering = false;
bool rendered = false;
bool has_samples = false;
std::unique_ptr<Ajm::AjmAt9Decoder> decoder;
std::vector<float> decoded_frame;
bool accepts_blocks = true;
void SetupSampler(const Ngs2WaveformFormat& format) {
EXIT_NOT_IMPLEMENTED(format.sample_rate != rack->ngs->option.sample_rate ||
format.frame_margin != 0 || format.frame_offset != 0);
channels = format.num_channels;
blocks.clear();
decoder.reset();
decoded_frame.clear();
accepts_blocks = true;
if (format.waveform_type == NGS2_WAVEFORM_TYPE_ATRAC9) {
decoder = std::make_unique<Ajm::AjmAt9Decoder>(channels, format.sample_rate,
Ajm::AjmSampleEncoding::Float, 0);
const auto result =
decoder->Initialize(&format.config_data, sizeof(format.config_data));
EXIT_NOT_IMPLEMENTED(result.result != OK);
EXIT_NOT_IMPLEMENTED(result.format.channel_num != channels ||
result.format.sampling_frequency != format.sample_rate);
Ajm::AjmSidebandDecAt9CodecInfo info {};
decoder->WriteCodecInfo(&info, sizeof(info), result);
EXIT_NOT_IMPLEMENTED(info.frame_samples > std::numeric_limits<uint16_t>::max());
decoded_frame.resize(info.frame_samples * channels);
} else {
EXIT_NOT_IMPLEMENTED(format.waveform_type != 0x12);
}
}
void SetMatrix(uint32_t index, const float* levels, uint32_t count) {
EXIT_NOT_IMPLEMENTED(index >= matrices.size());
matrices[index].assign(levels, levels + count);
}
void SetVolume(uint32_t index, float volume) {
EXIT_NOT_IMPLEMENTED(index >= ports.size());
ports[index].volume = volume;
}
void SetPortMatrix(uint32_t index, int32_t matrix) {
EXIT_NOT_IMPLEMENTED(index >= ports.size());
ports[index].matrix = matrix;
}
void SetEvent(uint32_t id) {
switch (id) {
case 1: event = Ngs2VoicePlayEvent::Play; break;
case 2: event = Ngs2VoicePlayEvent::Stop; break;
case 4: event = Ngs2VoicePlayEvent::StopImm; break;
case 8: event = Ngs2VoicePlayEvent::Kill; break;
case 16: event = Ngs2VoicePlayEvent::Pause; break;
case 32: event = Ngs2VoicePlayEvent::Resume; break;
default: EXIT("unknown event_id: 0x%08" PRIx32 "\n", id);
}
}
};
struct Ngs2VoiceParamHeader {
@@ -1931,6 +2054,9 @@ static Ngs2RackInternal* g_racks_list = nullptr;
static std::atomic_uint32_t g_next_ngs_uid = 1;
static Common::Mutex g_racks_mutex;
static_assert(sizeof(Ngs2UserFxContext) == 72);
static_assert(sizeof(Ngs2UserFxProcessContext) == 104);
static_assert(sizeof(Ngs2UserFxOption) == 72);
static_assert(sizeof(Ngs2SystemOption) == 144);
static_assert(sizeof(Ngs2SystemInfo) == 184);
static_assert(sizeof(Ngs2RackOption) == 176);
@@ -2272,6 +2398,35 @@ int KYTY_SYSV_ABI Ngs2SystemDestroy(uintptr_t system_handle, Ngs2ContextBufferIn
return OK;
}
static void Ngs2InitModules(Ngs2RackInternal& rack) {
if (!Ngs2RackIsCustom(rack.type)) {
return;
}
const auto& custom = rack.option.custom_sampler.custom_rack_option;
EXIT_NOT_IMPLEMENTED(custom.num_buffers != 1);
auto* voices = reinterpret_cast<Ngs2VoiceInternal*>(&rack + 1);
for (uint32_t m = 0; m < custom.num_modules; ++m) {
if (custom.module[m].module_id != 0x1f) {
continue;
}
auto& fx = rack.fx[m];
fx = *reinterpret_cast<const Ngs2UserFxOption*>(custom.module[m].option);
rack.common[m].resize(fx.common_size);
for (uint32_t i = 0; i < rack.option.common.max_voices; ++i) {
auto& module = voices[i].modules[m];
module.param.resize(fx.param_size);
module.work.resize(fx.work_size);
module.state.resize(custom.module[m].state_size);
if (fx.setup != nullptr) {
Ngs2UserFxContext context {
rack.common[m].data(), module.param.data(), module.work.data(),
fx.user_data, rack.option.common.max_voices, i};
EXIT_NOT_IMPLEMENTED(fx.setup(&context) != OK);
}
}
}
}
int KYTY_SYSV_ABI Ngs2RackCreate(uintptr_t system_handle, uint32_t rack_id,
const Ngs2RackOption* option,
const Ngs2ContextBufferInfo* buffer_info, uintptr_t* handle) {
@@ -2313,7 +2468,7 @@ int KYTY_SYSV_ABI Ngs2RackCreate(uintptr_t system_handle, uint32_t rack_id,
static_cast<uint64_t>(buffer_info->host_buffer_size));
auto* ngs = reinterpret_cast<Ngs2Internal*>(system_handle);
auto* rack = static_cast<Ngs2RackInternal*>(buffer_info->host_buffer);
auto* rack = new (buffer_info->host_buffer) Ngs2RackInternal {};
auto* voices = reinterpret_cast<Ngs2VoiceInternal*>(rack + 1);
Common::LockGuard lock(ngs->mutex);
@@ -2373,10 +2528,15 @@ int KYTY_SYSV_ABI Ngs2RackCreate(uintptr_t system_handle, uint32_t rack_id,
}
for (uint32_t i = 0; i < option->max_voices; i++) {
voices[i].rack = rack;
voices[i].event = Ngs2VoicePlayEvent::None;
voices[i].state = Ngs2VoicePlayState::Empty;
auto& voice = *std::construct_at(voices + i);
voice.rack = rack;
voice.ports.resize(option->max_ports);
voice.matrices.resize(option->max_matrices);
if (Ngs2RackIsCustom(rack->type)) {
voice.modules.resize(rack->option.custom_sampler.custom_rack_option.num_modules);
}
}
Ngs2InitModules(*rack);
*handle = reinterpret_cast<uintptr_t>(rack);
@@ -2406,22 +2566,9 @@ int KYTY_SYSV_ABI Ngs2RackCreateWithAllocator(uintptr_t system_handle, uint32_t
EXIT_NOT_IMPLEMENTED(option->size < sizeof(Ngs2RackOption));
LOGF("\t rack_id = 0x%" PRIx32 "\n"
"\t name = %.64s\n"
"\t flags = %u\n"
"\t max_grain_samples = %u\n"
"\t max_voices = %u\n"
"\t max_input_delay_blocks = %u\n"
"\t max_matrices = %u\n"
"\t max_ports = %u\n"
"\t max_voice_channels = %u\n"
"\t max_output_channels = %u\n"
"\t alloc_handler = 0x%016" PRIx64 "\n"
LOGF("\t alloc_handler = 0x%016" PRIx64 "\n"
"\t free_handler = 0x%016" PRIx64 "\n"
"\t user_data = 0x%016" PRIx64 "\n",
rack_id, option->name, option->flags, option->max_grain_samples, option->max_voices,
option->max_input_delay_blocks, option->max_matrices, option->max_ports,
option->max_voice_channels, option->max_output_channels,
reinterpret_cast<uint64_t>(allocator->alloc_handler),
reinterpret_cast<uint64_t>(allocator->free_handler),
static_cast<uint64_t>(allocator->user_data));
@@ -2477,26 +2624,41 @@ int KYTY_SYSV_ABI Ngs2RackDestroy(uintptr_t rack_handle, Ngs2ContextBufferInfo*
if (ngs == nullptr) {
return ERROR_INVALID_RACK_HANDLE;
}
Ngs2ContextBufferInfo context_buffer;
Ngs2BufferAllocator allocator;
{
Common::LockGuard lock(ngs->mutex);
Common::LockGuard racks_lock(g_racks_mutex);
auto** link = &g_racks_list;
while (*link != nullptr && *link != rack) {
link = &(*link)->next;
{
Common::LockGuard racks_lock(g_racks_mutex);
auto** link = &g_racks_list;
while (*link != nullptr && *link != rack) {
link = &(*link)->next;
}
if (*link == nullptr) {
return ERROR_INVALID_RACK_HANDLE;
}
*link = rack->next;
}
if (*link == nullptr) {
return ERROR_INVALID_RACK_HANDLE;
auto* voices = reinterpret_cast<Ngs2VoiceInternal*>(rack + 1);
for (uint32_t i = 0; i < rack->option.common.max_voices; ++i) {
for (size_t m = 0; m < voices[i].modules.size(); ++m) {
const auto& fx = rack->fx[m];
if (fx.cleanup != nullptr) {
auto& module = voices[i].modules[m];
Ngs2UserFxContext context {rack->common[m].data(),
module.param.data(),
module.work.data(),
fx.user_data,
rack->option.common.max_voices,
i};
EXIT_NOT_IMPLEMENTED(fx.cleanup(&context) != OK);
}
}
std::destroy_at(voices + i);
}
*link = rack->next;
context_buffer = rack->buffer_info;
allocator = rack->allocator;
rack->ngs = nullptr;
rack->next = nullptr;
std::destroy_at(rack);
}
if (allocator.free_handler != nullptr) {
@@ -2539,75 +2701,231 @@ int KYTY_SYSV_ABI Ngs2RackUnlock(uintptr_t rack_handle) {
return OK;
}
static void Ngs2ApplyEvent(Ngs2VoiceInternal& voice) {
switch (voice.event) {
case Ngs2VoicePlayEvent::None: break;
case Ngs2VoicePlayEvent::Play:
if (voice.state == Ngs2VoicePlayState::Empty) {
voice.state = Ngs2VoicePlayState::Playing;
}
break;
case Ngs2VoicePlayEvent::Pause:
if (voice.state == Ngs2VoicePlayState::Playing) {
voice.state = Ngs2VoicePlayState::Paused;
}
break;
case Ngs2VoicePlayEvent::Resume:
if (voice.state == Ngs2VoicePlayState::Paused) {
voice.state = Ngs2VoicePlayState::Playing;
}
break;
case Ngs2VoicePlayEvent::Stop:
if (voice.state == Ngs2VoicePlayState::Playing) {
voice.state = Ngs2VoicePlayState::Stopped;
}
break;
case Ngs2VoicePlayEvent::StopImm:
case Ngs2VoicePlayEvent::Kill: voice.state = Ngs2VoicePlayState::Empty; break;
}
voice.event = Ngs2VoicePlayEvent::None;
}
static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
uint32_t output = 0;
while (output < grain && !voice.blocks.empty()) {
auto& block = voice.blocks.front();
auto count = std::min(grain - output, block.info.num_samples - block.cursor);
const int16_t* pcm = nullptr;
const float* decoded = nullptr;
if (voice.decoder != nullptr) {
const auto skip =
std::min(block.skip_remaining,
static_cast<uint32_t>(voice.decoded_frame.size() / voice.channels));
Ajm::AjmGaplessState gapless;
gapless.Set({block.info.num_samples - block.cursor, static_cast<uint16_t>(skip), 0},
true);
const auto result = voice.decoder->Decode(
block.data + block.data_cursor, block.info.data_size - block.data_cursor,
voice.decoded_frame.data(), voice.decoded_frame.size() * sizeof(float), false,
&gapless);
EXIT_NOT_IMPLEMENTED(result.result != OK || result.input_consumed == 0);
block.data_cursor += result.input_consumed;
block.skip_remaining -= skip - gapless.current.skip_samples;
const auto decoded_samples =
static_cast<uint32_t>(result.output_written / (sizeof(float) * voice.channels));
if (decoded_samples == 0) {
continue;
}
EXIT_NOT_IMPLEMENTED(decoded_samples > count);
count = decoded_samples;
decoded = voice.decoded_frame.data();
} else {
pcm = reinterpret_cast<const int16_t*>(block.data) +
(block.info.num_skip_samples + block.cursor) * voice.channels;
}
for (uint32_t c = 0; c < voice.channels; ++c) {
for (uint32_t i = 0; i < count; ++i) {
const auto index = i * voice.channels + c;
voice.samples[c * grain + output + i] =
decoded != nullptr ? decoded[index] : pcm[index] / 32768.0f;
}
}
output += count;
block.cursor += count;
if (block.cursor == block.info.num_samples) {
struct CallbackInfo {
uintptr_t data, voice;
uint32_t flag, reserved;
uintptr_t user;
const void* block_data;
size_t size;
uint32_t repeats, attributes;
} info {voice.callback_data,
reinterpret_cast<uintptr_t>(&voice),
1,
0,
block.info.user_data,
block.data,
block.info.data_size,
0,
0};
static_assert(sizeof(CallbackInfo) == 56);
voice.blocks.pop_front();
if (voice.blocks.empty() && !voice.accepts_blocks) {
voice.state = Ngs2VoicePlayState::Empty;
}
if (voice.callback != 0 && (voice.callback_flags & 1u) != 0) {
reinterpret_cast<void KYTY_SYSV_ABI (*)(const CallbackInfo*)>(voice.callback)(
&info);
}
}
}
voice.has_samples = output != 0;
}
static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2VoiceInternal*>& voices,
uint32_t grain) {
if (voice.rendered) {
return;
}
EXIT_NOT_IMPLEMENTED(voice.rendering);
voice.rendering = true;
voice.samples.assign(grain * voice.channels, 0.0f);
voice.has_samples = false;
if (voice.state == Ngs2VoicePlayState::Playing) {
if (voice.rack->type == Ngs2RackType::CustomSampler) {
Ngs2ConsumeSamples(voice, grain);
}
for (auto* source: voices) {
for (const auto& port: source->ports) {
if (port.dest != &voice || port.volume == 0.0f) {
continue;
}
Ngs2RenderVoice(*source, voices, grain);
if (!source->has_samples) {
continue;
}
EXIT_NOT_IMPLEMENTED(port.input != 0);
const auto* matrix = port.matrix < 0 ? nullptr : &source->matrices.at(port.matrix);
for (uint32_t dst = 0; dst < voice.channels; ++dst) {
for (uint32_t src = 0; src < source->channels; ++src) {
const float level =
port.volume * (matrix == nullptr
? (src == dst ? 1.0f : 0.0f)
: matrix->at(dst * source->channels + src));
for (uint32_t i = 0; i < grain; ++i) {
voice.samples[dst * grain + i] +=
source->samples[src * grain + i] * level;
}
}
}
voice.has_samples = true;
}
}
if (voice.has_samples) {
const auto& custom = voice.rack->option.custom_sampler.custom_rack_option;
for (size_t m = 0; m < voice.modules.size(); ++m) {
EXIT_NOT_IMPLEMENTED(custom.module[m].module_id != 0x1f);
EXIT_NOT_IMPLEMENTED(custom.module[m].source_buffer_id != 0 ||
custom.module[m].dest_buffer_id != 0);
auto& module = voice.modules[m];
const auto& fx = voice.rack->fx[m];
std::vector<float*> channels(voice.channels);
for (uint32_t c = 0; c < voice.channels; ++c) {
channels[c] = voice.samples.data() + c * grain;
}
Ngs2UserFxProcessContext context {channels.data(),
voice.rack->common[m].data(),
module.param.data(),
module.work.data(),
module.state.data(),
fx.user_data,
module.flags,
voice.channels,
voice.channels,
grain,
voice.rack->ngs->option.sample_rate};
EXIT_NOT_IMPLEMENTED(fx.process(&context) != OK);
module.flags = 0;
}
}
}
voice.rendering = false;
voice.rendered = true;
}
int KYTY_SYSV_ABI Ngs2SystemRender(uintptr_t system_handle, const Ngs2RenderBufferInfo* buffer_info,
uint32_t num_buffer_info) {
static std::atomic_uint32_t render_log_count = 0;
const auto log_index = render_log_count.fetch_add(1, std::memory_order_relaxed);
const bool log_this_call = (log_index < 16 || (log_index % 600) == 0);
if (log_this_call) {
PRINT_NAME();
LOGF("\t call_count = %" PRIu32 "\n", log_index + 1);
}
EXIT_NOT_IMPLEMENTED(buffer_info == nullptr);
EXIT_NOT_IMPLEMENTED(system_handle == 0);
EXIT_NOT_IMPLEMENTED(num_buffer_info == 0);
EXIT_NOT_IMPLEMENTED(buffer_info == nullptr || system_handle == 0 || num_buffer_info == 0);
auto* ngs = reinterpret_cast<Ngs2Internal*>(system_handle);
Common::LockGuard lock(ngs->mutex);
for (uint32_t i = 0; i < num_buffer_info; i++) {
std::vector<Ngs2VoiceInternal*> voices;
{
Common::LockGuard racks_lock(g_racks_mutex);
for (auto* rack = g_racks_list; rack != nullptr; rack = rack->next) {
if (rack->ngs != ngs) {
continue;
}
auto* items = reinterpret_cast<Ngs2VoiceInternal*>(rack + 1);
for (uint32_t i = 0; i < rack->option.common.max_voices; ++i) {
Ngs2ApplyEvent(items[i]);
items[i].rendered = false;
if (items[i].channels != 0) {
voices.push_back(items + i);
}
}
}
}
for (uint32_t i = 0; i < num_buffer_info; ++i) {
if (buffer_info[i].buffer != nullptr && buffer_info[i].buffer_size != 0) {
std::memset(buffer_info[i].buffer, 0, buffer_info[i].buffer_size);
}
}
Common::LockGuard racks_lock(g_racks_mutex);
for (auto* rack = g_racks_list; rack != nullptr; rack = rack->next) {
if (rack->ngs == ngs) {
auto* voices = reinterpret_cast<Ngs2VoiceInternal*>(rack + 1);
for (uint32_t i = 0; i < rack->option.common.max_voices; i++) {
auto& voice = voices[i];
switch (voice.event) {
case Ngs2VoicePlayEvent::None:
if (voice.state == Ngs2VoicePlayState::Playing ||
voice.state == Ngs2VoicePlayState::Stopped) {
voice.state = Ngs2VoicePlayState::Empty;
}
break;
case Ngs2VoicePlayEvent::Play:
if (voice.state == Ngs2VoicePlayState::Empty) {
voice.state = Ngs2VoicePlayState::Playing;
}
break;
case Ngs2VoicePlayEvent::Pause:
if (voice.state == Ngs2VoicePlayState::Playing) {
voice.state = Ngs2VoicePlayState::Paused;
}
break;
case Ngs2VoicePlayEvent::Resume:
if (voice.state == Ngs2VoicePlayState::Paused) {
voice.state = Ngs2VoicePlayState::Playing;
}
break;
case Ngs2VoicePlayEvent::Stop:
if (voice.state == Ngs2VoicePlayState::Playing) {
voice.state = Ngs2VoicePlayState::Stopped;
}
break;
case Ngs2VoicePlayEvent::StopImm:
case Ngs2VoicePlayEvent::Kill: voice.state = Ngs2VoicePlayState::Empty; break;
const auto grain = ngs->option.num_grain_samples;
for (auto* voice: voices) {
Ngs2RenderVoice(*voice, voices, grain);
if (voice->rack->type != Ngs2RackType::Mastering || !voice->has_samples) {
continue;
}
EXIT_NOT_IMPLEMENTED(voice->output_id >= num_buffer_info);
const auto& output = buffer_info[voice->output_id];
EXIT_NOT_IMPLEMENTED(output.buffer == nullptr);
EXIT_NOT_IMPLEMENTED(output.waveform_type != 0x18 ||
output.num_channels != voice->channels);
EXIT_NOT_IMPLEMENTED(output.buffer_size < grain * output.num_channels * sizeof(float));
auto* pcm = static_cast<float*>(output.buffer);
for (uint32_t dst = 0; dst < output.num_channels; ++dst) {
for (uint32_t src = 0; src < voice->channels; ++src) {
const float level = voice->output_matrix.empty()
? (src == dst ? 1.0f : 0.0f)
: voice->output_matrix.at(src * output.num_channels + dst);
for (uint32_t i = 0; i < grain; ++i) {
pcm[i * output.num_channels + dst] += voice->samples[src * grain + i] * level;
}
voice.event = Ngs2VoicePlayEvent::None;
}
}
}
ngs->render_count++;
++ngs->render_count;
return OK;
}
@@ -2944,6 +3262,7 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
case 0x0001: {
EXIT_NOT_IMPLEMENTED(param->size != sizeof(Ngs2VoiceMatrixLevelsParam));
const auto* ml = reinterpret_cast<const Ngs2VoiceMatrixLevelsParam*>(param);
voice->SetMatrix(ml->matrix_id, ml->levels, ml->num_levels);
LOGF("\t matrix_id = %u\n"
"\t num_levels = %u\n"
"\t levels = 0x%016" PRIx64 "\n",
@@ -2954,6 +3273,7 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
EXIT_NOT_IMPLEMENTED(param->size != sizeof(Ngs2VoicePortVolumeParam));
const auto* volume =
reinterpret_cast<const Ngs2VoicePortVolumeParam*>(param);
voice->SetVolume(volume->port, volume->level);
LOGF("\t port = %u\n"
"\t level = %f\n",
volume->port, volume->level);
@@ -2962,6 +3282,7 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
case 0x0003: {
EXIT_NOT_IMPLEMENTED(param->size != sizeof(Ngs2VoicePortMatrixParam));
const auto* pm = reinterpret_cast<const Ngs2VoicePortMatrixParam*>(param);
voice->SetPortMatrix(pm->port, pm->matrix_id);
LOGF("\t port = %u\n"
"\t matrix_id = %d\n",
pm->port, pm->matrix_id);
@@ -2978,6 +3299,10 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
case 0x0005: {
EXIT_NOT_IMPLEMENTED(param->size != sizeof(Ngs2VoicePatchParam));
const auto* patch = reinterpret_cast<const Ngs2VoicePatchParam*>(param);
EXIT_NOT_IMPLEMENTED(patch->port >= voice->ports.size());
voice->ports[patch->port].dest =
reinterpret_cast<Ngs2VoiceInternal*>(patch->dest_handle);
voice->ports[patch->port].input = patch->dest_input_id;
LOGF("\t connect->port = %u\n"
"\t connect->dest_input_id = %u\n"
"\t connect->dest_handle = 0x%016" PRIx64 "\n",
@@ -2987,15 +3312,7 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
case 0x0006: {
EXIT_NOT_IMPLEMENTED(param->size != sizeof(Ngs2VoiceEventParam));
const auto* event = reinterpret_cast<const Ngs2VoiceEventParam*>(param);
switch (event->event_id) {
case 0x0001: voice->event = Ngs2VoicePlayEvent::Play; break;
case 0x0002: voice->event = Ngs2VoicePlayEvent::Stop; break;
case 0x0004: voice->event = Ngs2VoicePlayEvent::StopImm; break;
case 0x0008: voice->event = Ngs2VoicePlayEvent::Kill; break;
case 0x0010: voice->event = Ngs2VoicePlayEvent::Pause; break;
case 0x0020: voice->event = Ngs2VoicePlayEvent::Resume; break;
default: EXIT("unknown event_id: 0x%08" PRIx32 "\n", event->event_id);
}
voice->SetEvent(event->event_id);
LOGF("\t event = %u\n", event->event_id);
break;
}
@@ -3020,24 +3337,89 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
case 0x1000: EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Sampler); break;
case 0x2000: EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Submixer); break;
case 0x2001: EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Reverb); break;
case 0x3000: EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Mastering); break;
case 0x4000: {
EXIT_NOT_IMPLEMENTED(!Ngs2RackIsCustom(voice->rack->type));
auto cid = param->id & 0xffffu;
auto module_id = (cid >> 8u) & 0xffu;
auto ctl_id = (cid >> 5u) & 0x7u;
auto module_no = cid & 0x1fu;
LOGF("\t custom module_id = 0x%02" PRIx32 ", ctl_id = 0x%" PRIx32
", module_no = %" PRIu32 "\n",
module_id, ctl_id, module_no);
case 0x3000: {
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Mastering);
const auto* values = reinterpret_cast<const uint32_t*>(param + 1);
switch (param->id & 0xffffu) {
case 0: voice->channels = values[0]; break;
case 1: {
const auto* matrix =
reinterpret_cast<const Ngs2VoiceMatrixLevelsParam*>(param);
EXIT_NOT_IMPLEMENTED(matrix->matrix_id != 5);
voice->output_matrix.assign(matrix->levels,
matrix->levels + matrix->num_levels);
break;
}
case 5: voice->output_id = values[0]; break;
default: EXIT("unsupported mastering control: 0x%08" PRIx32 "\n", param->id);
}
break;
}
case 0x4001:
case 0x4000: {
EXIT_NOT_IMPLEMENTED(!Ngs2RackIsCustom(voice->rack->type));
const auto index = param->id & 0x1fu;
EXIT_NOT_IMPLEMENTED((param->id & 0xffffffe0u) != 0x40001f00u ||
index >= voice->modules.size());
struct FxParam {
Ngs2VoiceParamHeader header;
const void* data;
size_t size;
};
const auto& fx = *reinterpret_cast<const FxParam*>(param);
auto& module = voice->modules[index];
EXIT_NOT_IMPLEMENTED(voice->rack->fx[index].control != 0 ||
fx.size != module.param.size());
std::memcpy(module.param.data(), fx.data, fx.size);
module.flags |= 2;
break;
}
case 0x4001: {
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::CustomSampler);
switch (param->id & 0xffffu) {
case 0: {
const auto& format =
*reinterpret_cast<const Ngs2WaveformFormat*>(param + 1);
voice->SetupSampler(format);
break;
}
case 1: {
struct BlocksParam {
Ngs2VoiceParamHeader header;
const uint8_t* data;
uint32_t flags, count;
const Ngs2WaveformBlock* blocks;
};
const auto& blocks = *reinterpret_cast<const BlocksParam*>(param);
EXIT_NOT_IMPLEMENTED(!voice->accepts_blocks ||
(blocks.flags != 0 && blocks.flags != 0x11));
voice->accepts_blocks = (blocks.flags & 1u) != 0;
for (uint32_t i = 0; i < blocks.count; ++i) {
const auto& block = blocks.blocks[i];
EXIT_NOT_IMPLEMENTED(block.num_repeats != 0 || block.num_samples == 0);
EXIT_NOT_IMPLEMENTED(
voice->decoder == nullptr &&
(uint64_t(block.num_skip_samples) + block.num_samples) *
voice->channels * 2 >
block.data_size);
voice->blocks.push_back({blocks.data + block.data_offset, block});
}
break;
}
case 5:
EXIT_NOT_IMPLEMENTED(*reinterpret_cast<const float*>(param + 1) != 1.0f);
break;
default: EXIT("unsupported sampler control: 0x%08" PRIx32 "\n", param->id);
}
break;
case 0x4002:
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::CustomSubmixer);
}
case 0x4002: {
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::CustomSubmixer ||
(param->id & 0xffffu) != 0);
const auto* channels = reinterpret_cast<const uint32_t*>(param + 1);
EXIT_NOT_IMPLEMENTED(channels[0] != channels[1]);
voice->channels = channels[0];
break;
}
case 0x4003:
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::CustomMastering);
break;
@@ -3055,14 +3437,44 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
}
int KYTY_SYSV_ABI Ngs2VoiceRunCommands(uintptr_t voice_handle, const void* commands,
uint32_t num_commands, uint32_t flags) {
PRINT_NAME();
(void)voice_handle;
(void)commands;
(void)num_commands;
(void)flags;
size_t num_commands) {
struct Command {
uint32_t id;
uint8_t flags, type;
uint16_t count;
union {
uint32_t u;
int32_t i;
float f;
const float* levels;
} value;
};
auto* voice = reinterpret_cast<Ngs2VoiceInternal*>(voice_handle);
Common::LockGuard lock(voice->rack->ngs->mutex);
const auto* params = static_cast<const Command*>(commands);
for (size_t i = 0; i < num_commands; ++i) {
const auto& p = params[i];
const auto index = p.id >> 24;
switch (p.id & 0xffffffu) {
case 2:
EXIT_NOT_IMPLEMENTED(p.type != 4);
voice->SetEvent(p.value.u);
break;
case 5:
EXIT_NOT_IMPLEMENTED(p.type != 0x11);
voice->SetMatrix(index, p.value.levels, p.count);
break;
case 6:
EXIT_NOT_IMPLEMENTED(p.type != 1);
voice->SetVolume(index, p.value.f);
break;
case 7:
EXIT_NOT_IMPLEMENTED(p.type != 3);
voice->SetPortMatrix(index, p.value.i);
break;
default: EXIT("unknown command: 0x%08" PRIx32 "\n", p.id);
}
}
return OK;
}
+1 -1
View File
@@ -336,7 +336,7 @@ int KYTY_SYSV_ABI Ngs2RackGetVoiceHandle(uintptr_t rack_handle, uint32_t voice_i
uintptr_t* handle);
int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamHeader* param_list);
int KYTY_SYSV_ABI Ngs2VoiceRunCommands(uintptr_t voice_handle, const void* commands,
uint32_t num_commands, uint32_t flags);
size_t num_commands);
int KYTY_SYSV_ABI Ngs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* state,
size_t state_size);
int KYTY_SYSV_ABI Ngs2VoiceGetStateFlags(uintptr_t voice_handle, uint32_t* state_flags);
+80 -54
View File
@@ -117,6 +117,13 @@ struct AvPlayerEventReplacement {
void* object_pointer = nullptr;
AvPlayerEventCallback event_callback = nullptr;
};
static void emit_event(AvPlayerEventReplacement event, int32_t id, void* data = nullptr) {
if (event.event_callback != nullptr) {
LOGF("\t event_id = %d\n", id);
event.event_callback(event.object_pointer, id, 0, data);
}
}
struct AvPlayerUri {
const char* name = nullptr;
uint32_t length = 0;
@@ -639,9 +646,9 @@ private:
class Source {
public:
Source(AvPlayerMemAllocator m, AvPlayerFileReplacement f, int32_t video_buffers, bool vdec2,
uint32_t demux_buffer_size)
: mem(m), file(f),
Source(AvPlayerMemAllocator m, AvPlayerFileReplacement f, AvPlayerEventReplacement e,
int32_t video_buffers, bool vdec2, uint32_t demux_buffer_size)
: mem(m), file(f), event(e),
max_video_buffers(std::clamp(video_buffers <= 0 ? 2 : video_buffers, 2, 16)),
use_vdec2(vdec2),
video_packets(
@@ -817,21 +824,26 @@ 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);
was_playing_video = !stopped && video_id.has_value();
stopped = true;
}
StopWorkers();
{
std::lock_guard lock(mutex);
ResetNoLock();
}
stop_event = TakeStopEvent();
}
if (was_playing_video) {
::printf("AvPlayer video stopped\n");
}
emit_event(stop_event, AVPLAYER_EVENT_STATE_STOP);
return 0;
}
void Pause() {
@@ -860,9 +872,7 @@ 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 !stopped && !pipeline_failed && !DrainedNoLock();
}
uint64_t CurrentTime() const {
std::lock_guard lock(mutex);
@@ -945,6 +955,7 @@ public:
current_video = std::move(*frame);
*out = current_video->info;
RecordLoopBoundary(*current_video);
completion_changed.notify_all();
if (deliver_seek_frame) {
seek_video_frame_pending = false;
}
@@ -976,6 +987,7 @@ public:
current_audio->info.details.audio.language_code, 4);
last_audio_ts = out->time_stamp;
RecordLoopBoundary(*current_audio);
completion_changed.notify_all();
return true;
}
std::optional<int32_t> TakeWarning() {
@@ -988,6 +1000,13 @@ public:
}
private:
bool DrainedNoLock() const {
return demux_eof && video_done && audio_done && video_frames.Empty() &&
audio_frames.Empty();
}
AvPlayerEventReplacement TakeStopEvent() {
return stop_event_sent.exchange(true) ? AvPlayerEventReplacement {} : event;
}
void RecordLoopBoundary(const ReadyFrame& frame) {
if (frame.timestamp_offset > last_output_loop_offset) {
last_output_loop_offset = frame.timestamp_offset;
@@ -1002,23 +1021,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 +1055,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 +1184,7 @@ private:
return true;
}
bool StartWorkers() {
std::unique_lock lock(mutex);
worker_stop = false;
interrupt_io = false;
pipeline_failed = false;
@@ -1183,20 +1194,26 @@ 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;
}
stop_event_sent = false;
return true;
}
void NotifyWorkers() {
completion_changed.notify_all();
video_packets.Notify();
audio_packets.Notify();
video_buffers.Notify();
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 +1229,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() {
@@ -1269,6 +1289,13 @@ private:
demux_eof = true;
video_packets.Notify();
audio_packets.Notify();
std::unique_lock lock(mutex);
completion_changed.wait(lock, [&] { return worker_stop || DrainedNoLock(); });
if (!worker_stop) {
const auto stop_event = TakeStopEvent();
lock.unlock();
emit_event(stop_event, AVPLAYER_EVENT_STATE_STOP);
}
}
void VideoDecoder() {
Decoder(video_packets, video_ctx, video_buffers, video_frames, video_done,
@@ -1316,7 +1343,11 @@ private:
break;
}
}
done = true;
{
std::lock_guard lock(mutex);
done = true;
}
completion_changed.notify_all();
}
bool DecodePacket(AVCodecContext* codec, AVPacket* packet, uint64_t timestamp_offset,
WorkQueue<std::unique_ptr<GuestBuffer>>& buffers,
@@ -1607,6 +1638,7 @@ private:
}
AvPlayerMemAllocator mem;
AvPlayerFileReplacement file;
AvPlayerEventReplacement event;
int max_video_buffers = 2;
bool use_vdec2 = false;
AvPlayerSourceType source_type = AvPlayerSourceUnknown;
@@ -1640,6 +1672,8 @@ private:
std::atomic_bool demux_eof {true};
std::atomic_bool video_done {true};
std::atomic_bool audio_done {true};
std::atomic_bool stop_event_sent {true};
std::condition_variable completion_changed;
std::mutex lifecycle_mutex;
mutable std::mutex mutex;
bool stopped = true;
@@ -1673,19 +1707,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 +1741,17 @@ 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;
}
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 +1900,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 +1915,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,9 +1924,7 @@ 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();
@@ -1906,7 +1932,7 @@ int KYTY_SYSV_ABI AvPlayerPause(AvPlayerInternal* h) {
return AVPLAYER_ERROR_INVALID_PARAMS;
}
h->source->Pause();
emit_event(h, AVPLAYER_EVENT_STATE_PAUSE);
emit_event(h->event, AVPLAYER_EVENT_STATE_PAUSE);
return 0;
}
int KYTY_SYSV_ABI AvPlayerResume(AvPlayerInternal* h) {
@@ -1915,7 +1941,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) {
@@ -2017,7 +2043,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;
}
+306 -53
View File
@@ -14,7 +14,9 @@
#include <algorithm>
#include <cstring>
#include <ctime>
#include <deque>
#include <map>
#include <vector>
namespace Libs {
@@ -176,6 +178,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 +239,155 @@ 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);
}
// Keep memory.dat as raw guest bytes. Replacing a flushed temporary file protects the
// previous save if writing fails or the emulator exits partway through an update.
static int write_memory_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 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_memory_file(directory / "param.bin", &memory.param, sizeof(memory.param));
if (result == OK) {
result =
write_memory_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 +463,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 +489,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 +518,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())) {
@@ -423,9 +573,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 +593,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));
@@ -481,9 +634,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 +650,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 +693,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 +709,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 +737,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 +755,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 +798,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 +815,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 +886,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 +896,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 +910,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 +986,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 +1030,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 +1064,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);
+10 -10
View File
@@ -647,20 +647,20 @@ uint64_t SystemContentGetFlexibleMemorySize() {
return sc->psf.GetFlexibleMemorySize();
}
bool SystemContentGetIconPath(std::string* path) {
if (path == nullptr) {
return false;
}
bool SystemContentGetIconPath(std::filesystem::path* path) {
if (path == nullptr) {
return false;
}
auto* sc = Common::Singleton<SystemContent>::Instance();
auto* sc = Common::Singleton<SystemContent>::Instance();
if (sc->icon_path.empty()) {
return false;
}
if (sc->icon_path.empty()) {
return false;
}
*path = Common::PathToString(sc->icon_path);
*path = sc->icon_path;
return true;
return true;
}
bool SystemContentGetChunksNum(uint32_t* num) {
+1 -1
View File
@@ -15,7 +15,7 @@ bool SystemContentParamSfoGetInt(const char* name, int32_t* value);
bool SystemContentParamSfoGetString(const char* name, std::string* value);
bool SystemContentParamSfoGetString(const char* name, char* value, size_t value_size);
uint64_t SystemContentGetFlexibleMemorySize();
bool SystemContentGetIconPath(std::string* path);
bool SystemContentGetIconPath(std::filesystem::path* path);
bool SystemContentGetChunksNum(uint32_t* num);
} // namespace Loader
+50 -11
View File
@@ -10,6 +10,8 @@
#include <charconv>
#include <cstdio>
#include <string_view>
#include <vector>
#include <fmt/format.h>
#include <magic_enum.hpp>
@@ -207,15 +209,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 +232,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 +299,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 +318,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 +349,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 +375,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
+120
View File
@@ -4,12 +4,14 @@
#include "common/logging/log.h"
#include "common/subsystems.h"
#include "common/threads.h"
#include "common/stringUtils.h"
#include "graphics/presentation/window/windowInternal.h"
#include "kernel/fileSystem.h"
#include "libs/errno.h"
#include "libs/network.h"
#include "loader/symbolDatabase.h"
#include <algorithm>
#include <array>
#include <chrono>
@@ -89,6 +91,33 @@ void CheckSaveRename(const std::filesystem::path &root,
"renamed save contents");
}
void TestSaveOpenVisibility() {
constexpr char Path[] = "/savedata0/visible-save.dat";
constexpr char Payload[] = "saved progress";
const int fd = FileSystem::KernelOpen(Path, 0xa01, 0777);
Check(fd >= 3, "create save file exclusively");
FileSystem::FileStat stat {};
Check(FileSystem::KernelStat(Path, &stat) == OK && stat.st_size == 0,
"created save file is visible before close");
Check(FileSystem::KernelOpen(Path, 0xa01, 0777) ==
Libs::LibKernel::KERNEL_ERROR_EEXIST,
"exclusive creation detects an open save file");
Check(FileSystem::KernelWrite(fd, Payload, sizeof(Payload) - 1) ==
sizeof(Payload) - 1,
"populate save file before truncation");
Check(FileSystem::KernelClose(fd) == OK, "close populated save file");
Check(FileSystem::KernelStat(Path, &stat) == OK &&
stat.st_size == sizeof(Payload) - 1,
"save file contains the truncation fixture");
const int truncated = FileSystem::KernelOpen(Path, 0x401, 0777);
Check(truncated >= 3, "truncate existing save file");
Check(FileSystem::KernelStat(Path, &stat) == OK && stat.st_size == 0,
"save truncation is visible before close");
Check(FileSystem::KernelClose(truncated) == OK, "close truncated save file");
}
void CheckMountRoot(const std::filesystem::path &root) {
Common::File cache;
Check(cache.Create(root / "rpf.cache"), "create directory listing fixture");
@@ -126,6 +155,94 @@ 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();
for (const auto &directory : {unicode_root, std::filesystem::relative(unicode_root)}) {
// A Windows temporary directory may be on a different drive from the working directory.
if (directory.empty()) {
continue;
}
const auto files = Common::File::FindFiles(directory);
const auto relative_file =
std::filesystem::path(u8"nested/asset-\u00e9-\u65e5\u672c\u8a9e.bin");
Check(std::any_of(files.begin(), files.end(), [&](const auto &file) {
return file.path_with_name.lexically_normal() ==
(directory / relative_file).lexically_normal() &&
file.rel_path_with_name == relative_file;
}), "recursive enumeration preserves native and relative Unicode paths");
}
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 +504,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();
+432
View File
@@ -0,0 +1,432 @@
// 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;
} // 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&) {
g_mounted_directory = directory;
}
void Umount(const std::string&) {}
} // namespace Libs::LibKernel::FileSystem
namespace {
using namespace Libs::SaveData;
namespace fs = std::filesystem;
#define CHECK(condition) \
do { \
if (!(condition)) { \
std::fprintf(stderr, "SaveDataMemoryTests:%d: %s\n", __LINE__, #condition); \
std::abort(); \
} \
} while (false)
SaveDataMemorySetup2 SetupParam(int32_t user = 1, uint32_t slot = 0, size_t size = 16) {
SaveDataMemorySetup2 setup {};
setup.user_id = user;
setup.slot_id = slot;
setup.memory_size = size;
return setup;
}
size_t Setup(int32_t user = 1, uint32_t slot = 0, size_t size = 16, uint32_t option = 0) {
auto setup = SetupParam(user, slot, size);
setup.option = option;
SaveDataMemorySetupResult result {};
CHECK(SaveDataSetupSaveDataMemory2(&setup, &result) == OK);
return result.existed_memory_size;
}
int Set(void* bytes, size_t size, int64_t offset = 0, int32_t user = 1, uint32_t slot = 0) {
SaveDataMemoryData data {bytes, size, offset, {}};
SaveDataMemorySet2 set {};
set.user_id = user;
set.slot_id = slot;
set.data = &data;
set.data_num = 1;
return SaveDataSetSaveDataMemory2(&set);
}
std::vector<uint8_t> Get(size_t size = 16, int32_t user = 1, uint32_t slot = 0) {
std::vector<uint8_t> bytes(size);
SaveDataMemoryData data {bytes.data(), size, 0, {}};
SaveDataMemoryGet2 get {};
get.user_id = user;
get.slot_id = slot;
get.data = &data;
CHECK(SaveDataGetSaveDataMemory2(&get) == OK);
return bytes;
}
void Reset(const char* title) {
CHECK(SaveDataTerminate() == OK);
g_test_title = title;
}
void TestValidationAndScatter() {
Reset("VALIDATE");
uint8_t byte = 0x55;
SaveDataMemoryGet2 get {};
get.user_id = 1;
SaveDataMemorySync sync {};
sync.user_id = 1;
CHECK(SaveDataGetSaveDataMemory2(&get) == SAVE_DATA_ERROR_MEMORY_NOT_READY);
CHECK(Set(&byte, 1) == SAVE_DATA_ERROR_MEMORY_NOT_READY);
CHECK(SaveDataSyncSaveDataMemory(&sync) == SAVE_DATA_ERROR_MEMORY_NOT_READY);
CHECK(SaveDataSetupSaveDataMemory2(nullptr, nullptr) == SAVE_DATA_ERROR_PARAMETER);
auto invalid_user = SetupParam(-1);
CHECK(SaveDataSetupSaveDataMemory2(&invalid_user, nullptr) ==
SAVE_DATA_ERROR_INVALID_LOGIN_USER);
for (auto invalid:
{SetupParam(1, 4), SetupParam(1, 0, 0), SetupParam(1, 0, 32 * 1024 * 1024 + 1)}) {
CHECK(SaveDataSetupSaveDataMemory2(&invalid, nullptr) == SAVE_DATA_ERROR_PARAMETER);
}
CHECK(Setup() == 0);
auto setup = SetupParam();
CHECK(SaveDataSetupSaveDataMemory2(&setup, nullptr) == SAVE_DATA_ERROR_BUSY);
std::array<uint8_t, 16> original {};
for (size_t i = 0; i < original.size(); ++i) {
original[i] = static_cast<uint8_t>(i);
}
CHECK(Set(original.data(), original.size()) == OK);
SaveDataMemoryData data[] = {{&byte, 1, 2, {}}, {&byte, 1, 16, {}}};
SaveDataMemorySet2 set {};
set.user_id = 1;
set.data = data;
set.data_num = 2;
CHECK(SaveDataSetSaveDataMemory2(&set) == SAVE_DATA_ERROR_PARAMETER);
CHECK(Get() == std::vector<uint8_t>(original.begin(), original.end()));
data[1].offset = 12;
CHECK(SaveDataSetSaveDataMemory2(&set) == OK);
original[2] = original[12] = byte;
CHECK(Get() == std::vector<uint8_t>(original.begin(), original.end()));
byte = 0xaa;
set.data_num = 0;
CHECK(SaveDataSetSaveDataMemory2(&set) == OK);
original[2] = byte;
CHECK(Get() == std::vector<uint8_t>(original.begin(), original.end()));
set.data = nullptr;
CHECK(SaveDataSetSaveDataMemory2(&set) == OK);
CHECK(Get() == std::vector<uint8_t>(original.begin(), original.end()));
set.data_num = 6;
CHECK(SaveDataSetSaveDataMemory2(&set) == SAVE_DATA_ERROR_PARAMETER);
set.data_num = 1;
set.data = nullptr;
CHECK(SaveDataSetSaveDataMemory2(&set) == SAVE_DATA_ERROR_PARAMETER);
CHECK(Set(&byte, 1, -1) == SAVE_DATA_ERROR_PARAMETER);
CHECK(Set(&byte, 1, std::numeric_limits<int64_t>::max()) == SAVE_DATA_ERROR_PARAMETER);
CHECK(Set(&byte, std::numeric_limits<size_t>::max(), 1) == SAVE_DATA_ERROR_PARAMETER);
SaveDataMemoryData invalid {&byte, std::numeric_limits<size_t>::max(), 1, {}};
get.data = &invalid;
CHECK(SaveDataGetSaveDataMemory2(&get) == SAVE_DATA_ERROR_PARAMETER);
CHECK(byte == 0xaa);
CHECK(Get() == std::vector<uint8_t>(original.begin(), original.end()));
CHECK(SaveDataTerminate() == OK);
CHECK(Setup() == original.size());
CHECK(Get() == std::vector<uint8_t>(original.begin(), original.end()));
}
void TestIsolationAndSync() {
Reset("ISOLATE");
uint8_t byte = 11;
CHECK(Setup() == 0);
CHECK(Set(&byte, 1) == OK);
CHECK(Setup(2) == 0);
CHECK(Setup(1, 1) == 0);
CHECK(Get(16, 2) == std::vector<uint8_t>(16));
CHECK(Get(16, 1, 1) == std::vector<uint8_t>(16));
byte = 22;
CHECK(Set(&byte, 1, 0, 2) == OK);
byte = 33;
CHECK(Set(&byte, 1, 0, 1, 1) == OK);
CHECK(Get()[0] == 11);
CHECK(Get(16, 2)[0] == 22);
CHECK(Get(16, 1, 1)[0] == 33);
SaveDataMemorySync sync {};
sync.user_id = 1;
sync.slot_id = 1;
CHECK(SaveDataSyncSaveDataMemory(&sync) == OK);
SaveDataEvent event {};
CHECK(SaveDataGetEventResult(nullptr, &event) == OK);
CHECK(event.type == 3 && event.error_code == OK && event.user_id == 1);
CHECK(std::string(event.dir_name.data) == "sce_sdmemory1");
CHECK(SaveDataGetEventResult(nullptr, &event) == SAVE_DATA_ERROR_NOT_FOUND);
sync.option = 1;
CHECK(SaveDataSyncSaveDataMemory(&sync) == OK);
CHECK(SaveDataGetEventResult(nullptr, &event) == SAVE_DATA_ERROR_NOT_FOUND);
sync.option = 2;
CHECK(SaveDataSyncSaveDataMemory(&sync) == SAVE_DATA_ERROR_PARAMETER);
Reset("OTHER");
CHECK(Setup() == 0);
CHECK(Get() == std::vector<uint8_t>(16));
Reset("ISOLATE");
CHECK(Setup() == 16);
CHECK(Setup(2) == 16);
CHECK(Setup(1, 1) == 16);
CHECK(Get()[0] == 11 && Get(16, 2)[0] == 22 && Get(16, 1, 1)[0] == 33);
Reset("OTHER");
SceSaveDataTitleId title {};
std::strcpy(title.data, "ISOLATE");
SceSaveDataDirName name {};
std::strcpy(name.data, "sce_sdmemory");
struct SaveDataTransferringMount mount {};
mount.user_id = 2;
mount.title_id = &title;
mount.dir_name = &name;
SaveDataMountResult result {};
CHECK(SaveDataTransferringMount(&mount, &result) == OK);
CHECK(g_mounted_directory == fs::path("_SaveData/ISOLATE/sce_sdmemory/2"));
CHECK(std::ifstream(g_mounted_directory / "memory.dat", std::ios::binary).get() == 22);
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
struct SaveDataDelete del {};
del.user_id = 1;
del.title_id = &title;
del.dir_name = &name;
CHECK(SaveDataDelete(&del) == OK);
Reset("ISOLATE");
CHECK(Setup() == 0);
CHECK(Setup(2) == 16);
CHECK(Get(16, 2)[0] == 22);
}
void TestFailedWritePreservesSave() {
Reset("IOFAIL");
CHECK(Setup() == 0);
uint8_t byte = 42;
CHECK(Set(&byte, 1) == OK);
fs::rename("_SaveData", "save-backup");
{
std::ofstream blocker("_SaveData");
blocker << "not a directory";
}
byte = 99;
CHECK(Set(&byte, 1) < 0);
CHECK(Get()[0] == 42);
CHECK(fs::remove("_SaveData"));
fs::rename("save-backup", "_SaveData");
CHECK(SaveDataTerminate() == OK);
CHECK(Setup() == 16);
CHECK(Get()[0] == 42);
}
void WriteRestartFixture() {
auto setup = SetupParam(7, 3);
setup.option = 1;
std::array<uint8_t, 4> icon_bytes {1, 2, 3, 4};
SaveDataIcon icon {icon_bytes.data(), icon_bytes.size(), icon_bytes.size(), {}};
setup.init_icon = &icon;
setup.icon_memory_size = icon_bytes.size();
SaveDataParam param {};
std::strcpy(param.title, "Astro progress");
param.user_param = 17;
setup.init_param = &param;
CHECK(SaveDataSetupSaveDataMemory2(&setup, nullptr) == OK);
std::array<uint8_t, 4> progress {11, 22, 33, 44};
SaveDataMemoryData data {progress.data(), progress.size(), 4, {}};
SaveDataMemorySet2 set {};
set.user_id = 7;
set.slot_id = 3;
set.data = &data;
set.icon = &icon;
CHECK(SaveDataSetSaveDataMemory2(&set) == OK);
set.data = nullptr;
param.user_param = 18;
std::strcpy(param.detail, "Coins, level state, rescued bots");
set.param = &param;
CHECK(SaveDataSetSaveDataMemory2(&set) == OK);
// No Sync or Terminate: Set must survive process exit on its own.
}
void ReadRestartFixture() {
CHECK(Setup(7, 3, 24, 1) == 16);
auto expected = std::vector<uint8_t>(24);
expected[4] = 11;
expected[5] = 22;
expected[6] = 33;
expected[7] = 44;
CHECK(Get(24, 7, 3) == expected);
SaveDataParam param {};
std::array<uint8_t, 4> icon_bytes {9, 9, 9, 9};
SaveDataIcon icon {icon_bytes.data(), icon_bytes.size(), icon_bytes.size(), {}};
SaveDataMemoryGet2 get {};
get.user_id = 7;
get.slot_id = 3;
get.param = &param;
get.icon = &icon;
CHECK(SaveDataGetSaveDataMemory2(&get) == OK);
CHECK(icon.data_size == 0);
CHECK(
std::all_of(icon_bytes.begin(), icon_bytes.end(), [](uint8_t byte) { return byte == 9; }));
CHECK(std::string(param.title) == "Astro progress");
CHECK(std::string(param.detail) == "Coins, level state, rescued bots");
CHECK(param.user_param == 18);
CHECK(SaveDataTerminate() == OK);
CHECK(Setup(7, 3, 6) == 16);
expected.resize(6);
CHECK(Get(6, 7, 3) == expected);
}
SceSaveDataDirName DirName(const char* text) {
SceSaveDataDirName name {};
std::snprintf(name.data, sizeof(name.data), "%s", text);
return name;
}
std::vector<std::string> Search(int32_t user, const SceSaveDataTitleId* title = nullptr) {
SaveDataDirNameSearchCond cond {};
cond.user_id = user;
cond.title_id = title;
std::array<SceSaveDataDirName, 8> names {};
SaveDataDirNameSearchResult result {};
result.dir_names = names.data();
result.dir_names_num = names.size();
CHECK(SaveDataDirNameSearch(&cond, &result) == OK);
CHECK(result.hit_num == result.set_num && result.set_num <= names.size());
std::vector<std::string> found;
for (uint32_t i = 0; i < result.set_num; ++i) {
found.emplace_back(names[i].data);
}
return found;
}
void TestClassicSavePaths() {
Reset("CLASSIC");
const std::vector<std::string> names {"save.1", "save1", "slot@A", "slotA"};
fs::create_directories("_SaveData/CLASSIC/save.1");
{
std::ofstream saved("_SaveData/CLASSIC/save.1/progress");
saved << "save.1";
}
CHECK(Search(1) == std::vector<std::string> {"save.1"});
for (const auto& text: names) {
auto name = DirName(text.c_str());
struct SaveDataMount3 mount {};
mount.user_id = 1;
mount.dir_name = &name;
mount.mount_mode = text == "save.1" ? 1 : 4;
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 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();
CHECK(SaveDataTerminate() == OK);
fs::current_path(previous);
fs::remove_all(temp);
return 0;
}