Persist save allocations for search and mount capacity queries

This commit is contained in:
nmzik 2026-09-21 22:35:12 +04:00
parent e40438f95e
commit 8dc9b9d532
4 changed files with 137 additions and 24 deletions

View File

@ -27,6 +27,7 @@ namespace SaveData {
// TODO(): specify dir at launcher
static constexpr char SAVE_DATA_DIR[] = "_SaveData";
static constexpr uint64_t SAVE_DATA_BLOCKS_MIN = 48;
static constexpr uint64_t SAVE_DATA_BLOCKS_MAX = 16384;
struct SceSaveDataDirName {
@ -305,9 +306,9 @@ static bool valid_memory_range(const SaveDataMemoryData& data, size_t 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
// 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) {
static int write_save_file(const std::filesystem::path& path, const void* data, uint32_t size) {
const auto temporary = std::filesystem::path(path.string() + ".tmp");
Common::File file;
if (!file.Create(temporary)) {
@ -329,6 +330,21 @@ static int write_memory_file(const std::filesystem::path& path, const void* data
return SAVE_DATA_ERROR_INTERNAL;
}
static int read_save_blocks(const std::filesystem::path& directory, uint64_t* blocks) {
Common::File file(directory / "sce_sys" / "blocks.bin", Common::File::Mode::Read);
if (file.IsInvalid() || file.Size() != sizeof(*blocks)) {
return SAVE_DATA_ERROR_BROKEN;
}
uint64_t value = 0;
uint32_t read = 0;
file.Read(&value, sizeof(value), &read);
if (read != sizeof(value) || value < SAVE_DATA_BLOCKS_MIN || value > SAVE_DATA_BLOCKS_MAX) {
return SAVE_DATA_ERROR_BROKEN;
}
*blocks = value;
return OK;
}
static int save_memory(const std::filesystem::path& directory, const SaveDataMemory& memory) {
std::error_code error;
std::filesystem::create_directories(directory, error);
@ -336,10 +352,10 @@ static int save_memory(const std::filesystem::path& directory, const SaveDataMem
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));
int result = write_save_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());
write_save_file(directory / "memory.dat", memory.data.data(), memory.data.size());
}
return result;
}
@ -561,9 +577,14 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
result->params[i] = {};
}
if (result->infos != nullptr) {
result->infos[i] = {};
result->infos[i].blocks = SAVE_DATA_BLOCKS_MAX;
result->infos[i].free_blocks = SAVE_DATA_BLOCKS_MAX;
auto& info = result->infos[i];
info = {};
const int status = read_save_blocks(root / dir_list[i], &info.blocks);
if (status != OK) {
return status;
}
// Host-directory saves report their allocation as free space.
info.free_blocks = info.blocks;
}
}
@ -621,10 +642,22 @@ int KYTY_SYSV_ABI SaveDataMount3(const SaveDataMount3* mount, SaveDataMountResul
bool created = false;
if ((create || create2) && !Common::File::IsDirectoryExisting(mount_dir)) {
Common::File::CreateDirectories(mount_dir);
if (mount->blocks < SAVE_DATA_BLOCKS_MIN || mount->blocks > SAVE_DATA_BLOCKS_MAX) {
return SAVE_DATA_ERROR_PARAMETER;
}
const auto metadata = std::filesystem::path(mount_dir) / "sce_sys";
std::error_code error;
std::filesystem::create_directories(metadata, error);
if (error) {
return SAVE_DATA_ERROR_INTERNAL;
}
const int status = write_save_file(metadata / "blocks.bin", &mount->blocks,
sizeof(mount->blocks));
if (status != OK) {
std::filesystem::remove_all(mount_dir, error);
return status;
}
created = true;
EXIT_NOT_IMPLEMENTED((!Common::File::IsDirectoryExisting(mount_dir)));
}
return mount_save_data(slot, dir_name, mount_dir, created ? 1u : 0u, mount_result);
@ -1101,13 +1134,23 @@ int KYTY_SYSV_ABI SaveDataGetMountInfo(const SaveDataMountPoint* mount_point,
SaveDataMountInfo* info) {
PRINT_NAME();
EXIT_NOT_IMPLEMENTED(mount_point == nullptr);
EXIT_NOT_IMPLEMENTED(info == nullptr);
if (mount_point == nullptr || info == nullptr ||
std::memchr(mount_point->data, 0, sizeof(mount_point->data)) == nullptr) {
return SAVE_DATA_ERROR_PARAMETER;
}
Common::LockGuard lock(g_mount_mutex);
if (g_mount_slots.Find(mount_point->data) == SaveDataMountSlots::FULL) {
return SAVE_DATA_ERROR_NOT_MOUNTED;
}
uint64_t blocks = 0;
const int status = read_save_blocks(
LibKernel::FileSystem::GetRealFilename(mount_point->data), &blocks);
if (status != OK) {
return status;
}
*info = {};
info->blocks = SAVE_DATA_BLOCKS_MAX;
info->free_blocks = SAVE_DATA_BLOCKS_MAX;
info->blocks = blocks;
info->free_blocks = blocks;
return OK;
}

View File

@ -17,6 +17,7 @@
namespace {
std::string g_test_title = "PPSA21564";
std::filesystem::path g_mounted_directory;
std::map<std::string, std::filesystem::path> g_test_mounts;
} // namespace
namespace Loader {
@ -33,10 +34,14 @@ Common::Time GetTime() {
} // namespace Loader
namespace Libs::LibKernel::FileSystem {
void Mount(const std::filesystem::path& directory, const std::string&) {
void Mount(const std::filesystem::path& directory, const std::string& point) {
g_mounted_directory = directory;
g_test_mounts[point] = directory;
}
void Umount(const std::string& point) { g_test_mounts.erase(point); }
std::filesystem::path GetRealFilename(const std::string& point) {
return g_test_mounts.at(point);
}
void Umount(const std::string&) {}
} // namespace Libs::LibKernel::FileSystem
namespace {
@ -337,6 +342,7 @@ void TestClassicSavePaths() {
mount.user_id = 1;
mount.dir_name = &name;
mount.mount_mode = text == "save.1" ? 1 : 4;
mount.blocks = 48;
SaveDataMountResult result {};
CHECK(SaveDataMount3(&mount, &result) == OK);
CHECK(g_mounted_directory == fs::path("_SaveData") / "CLASSIC" / text);
@ -385,6 +391,70 @@ void TestClassicSavePaths() {
CHECK(Search(1) == names);
}
void TestSaveAllocations() {
Reset("CAPACITY");
std::array<SceSaveDataDirName, 2> names {DirName("Options"), DirName("Player")};
const std::array<uint64_t, 2> allocations {48, 96};
for (const auto blocks : {0u, 47u, 16385u}) {
struct SaveDataMount3 invalid {};
invalid.user_id = 1;
invalid.dir_name = &names[0];
invalid.mount_mode = 4;
invalid.blocks = blocks;
SaveDataMountResult result {};
CHECK(SaveDataMount3(&invalid, &result) == SAVE_DATA_ERROR_PARAMETER);
CHECK(!fs::exists("_SaveData/CAPACITY/Options"));
}
for (size_t i = 0; i < names.size(); i++) {
struct SaveDataMount3 mount {};
mount.user_id = 1;
mount.dir_name = &names[i];
mount.mount_mode = 4;
mount.blocks = allocations[i];
SaveDataMountResult result {};
CHECK(SaveDataMount3(&mount, &result) == OK);
CHECK(result.mount_status == 1);
std::ofstream(g_mounted_directory / "USR-DATA") << "saved payload";
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
SaveDataMountInfo info {};
CHECK(SaveDataGetMountInfo(&result.mount_point, &info) == SAVE_DATA_ERROR_NOT_MOUNTED);
}
Reset("CAPACITY");
std::array<SceSaveDataDirName, 2> found {};
std::array<SaveDataSearchInfo, 2> infos {};
SaveDataDirNameSearchCond cond {};
cond.user_id = 1;
SaveDataDirNameSearchResult search {};
search.dir_names = found.data();
search.dir_names_num = found.size();
search.infos = infos.data();
CHECK(SaveDataDirNameSearch(&cond, &search) == OK && search.set_num == 2);
for (size_t i = 0; i < names.size(); i++) {
CHECK(std::string(found[i].data) == names[i].data);
CHECK(infos[i].blocks == allocations[i] && infos[i].free_blocks == allocations[i]);
struct SaveDataMount3 mount {};
mount.user_id = 1;
mount.dir_name = &names[i];
mount.mount_mode = 34;
mount.blocks = 16384;
SaveDataMountResult result {};
CHECK(SaveDataMount3(&mount, &result) == OK && result.mount_status == 0);
std::ofstream(g_mounted_directory / "USR-DATA", std::ios::app) << "more data";
SaveDataMountInfo info {};
CHECK(SaveDataGetMountInfo(&result.mount_point, &info) == OK);
CHECK(info.blocks == allocations[i] && info.free_blocks == allocations[i]);
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
}
CHECK(infos[0].blocks + infos[1].blocks == 144);
const fs::path metadata = "_SaveData/CAPACITY/Options/sce_sys/blocks.bin";
fs::resize_file(metadata, 7);
CHECK(SaveDataDirNameSearch(&cond, &search) == SAVE_DATA_ERROR_BROKEN);
CHECK(fs::file_size(metadata) == 7);
CHECK(fs::remove(metadata));
CHECK(SaveDataDirNameSearch(&cond, &search) == SAVE_DATA_ERROR_BROKEN);
CHECK(!fs::exists(metadata));
}
void RunChild(const fs::path& executable, const char* mode) {
#ifdef _WIN32
CHECK(_spawnl(_P_WAIT, executable.string().c_str(), executable.string().c_str(), mode,
@ -425,6 +495,7 @@ int main(int argc, char** argv) {
TestIsolationAndSync();
TestFailedWritePreservesSave();
TestClassicSavePaths();
TestSaveAllocations();
CHECK(SaveDataTerminate() == OK);
fs::current_path(previous);
fs::remove_all(temp);

View File

@ -19624,7 +19624,7 @@ TestCase VectorMinMaxF16Ops() {
O::S_ENDPGM}};
}
TestCase VectorCvtF16U16CapturedByteSource() {
TestCase VectorCvtF16U16ByteSource() {
using O = ShaderOpcode;
constexpr std::array inputs = {0xff123456u, 0x80123456u, 0x00123456u};
@ -19633,15 +19633,14 @@ TestCase VectorCvtF16U16CapturedByteSource() {
for (u32 i = 0; i < inputs.size(); i++) {
AppendVMovU32(&code, 30, i * 4u);
AppendBufferLoadDword(&code, 18, 30);
// Captured shader 0x15082e211725205f: unsigned byte3 -> FP16 word0,
// preserving word1 of v16.
// Convert unsigned byte3 to FP16 word0 while preserving word1 of v16.
code.insert(code.end(), {0x7e20a0f9u, 0x00031412u});
AppendStoreVgpr(&code, 16, i);
}
AppendEnd(&code);
TestCase test;
test.name = "VectorCvtF16U16CapturedByteSource";
test.name = "VectorCvtF16U16ByteSource";
test.code = std::move(code);
test.initial.assign(inputs.begin(), inputs.end());
test.expected = {0xa5a55bf8u, 0xa5a55800u, 0xa5a50000u};
@ -27554,7 +27553,7 @@ std::vector<TestCase> MakeCases() {
AddCase(CvtPkrtzF16F32SdwaAndOutputModifiers);
AddCase(PackedMinMaxF16NanAndSignedZeroEdges);
AddCase(VectorMinMaxF16Ops);
AddCase(VectorCvtF16U16CapturedByteSource);
AddCase(VectorCvtF16U16ByteSource);
AddCase(VectorCvtU16F16Sdwa);
AddCase(NativeAndSdwa16BitDestinationWrites);
AddCase(VectorMinMaxMed3F16Ops);

View File

@ -1753,7 +1753,7 @@ void TestNggVertexEntryState() {
[](const auto &output) {
return output.kind == IR::StageOutputKind::Position;
}),
"NGG launch counts suppressed the captured vertex export");
"NGG launch counts suppressed the vertex export");
Check(result.program.wave_size == wave_size,
"NGG wave size was lost during translation");
CheckSpirvBinaryValidates(result.spirv);