From 6206c230c425c19f78dcb3f39729ddf8f82ad9dc Mon Sep 17 00:00:00 2001 From: Lindamea1 <322051987+Lindamea1@users.noreply.github.com> Date: Mon, 19 Oct 2026 14:34:40 +0100 Subject: [PATCH] fix(libSceSaveData.native): count PS5 save data in 64 KiB blocks, at most 16384 The PS5 references count save data differently from the PS4: SharpEmu uses a 64 KiB block, and SharpEmu and KytyPS5 allow 48 to 16384 blocks per save, which at 64 KiB a block is 3 MiB to 1 GiB, the PS4's byte range. The module used the PS4 values, 32 KiB blocks and a 32768-block maximum. At 64 KiB a block, sceSaveDataDirNameSearch and sceSaveDataGetMountInfo therefore counted up to twice the blocks a save's files use, and a save without a recorded size reported 32768 blocks, which is 2 GiB. The search test now expects 64 KiB blocks and the 16384 maximum, and the block accounting gets its own technical debt entry. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libSceSaveData.native/Export.cpp | 2 +- .../prx/libSceSaveData.native/SaveData.hpp | 2 +- core/libs/tests/GuestSaveDataSearch.cpp | 43 +++++++++++++------ docs/dev/debt/libSceSaveData.native.md | 3 +- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 925cabeb1378..2143f6e129bd 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -360,7 +360,7 @@ int APS5_VABI sceSaveDataDeleteTransactionResource(int32_t resource) { return SAVE_DATA_OK; } -static constexpr std::uint64_t SAVE_DATA_BLOCK_BYTES = 32768; +static constexpr std::uint64_t SAVE_DATA_BLOCK_BYTES = 65536; static std::uint64_t save_blocks(const std::string& real_path) { std::ifstream file(blocks_path(real_path)); diff --git a/core/libs/prx/libSceSaveData.native/SaveData.hpp b/core/libs/prx/libSceSaveData.native/SaveData.hpp index ceffb09c0164..1a8681cb38cc 100644 --- a/core/libs/prx/libSceSaveData.native/SaveData.hpp +++ b/core/libs/prx/libSceSaveData.native/SaveData.hpp @@ -25,7 +25,7 @@ constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_RDWR = 2; constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_CREATE = 4; constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_CREATE2 = 32; -constexpr std::uint64_t SAVE_DATA_BLOCKS_MAX = 32768; +constexpr std::uint64_t SAVE_DATA_BLOCKS_MAX = 16384; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_ALL = 0; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_TITLE = 1; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_SUB_TITLE = 2; diff --git a/core/libs/tests/GuestSaveDataSearch.cpp b/core/libs/tests/GuestSaveDataSearch.cpp index 6cea92d92100..ed1e11b46383 100644 --- a/core/libs/tests/GuestSaveDataSearch.cpp +++ b/core/libs/tests/GuestSaveDataSearch.cpp @@ -187,7 +187,7 @@ static void SearchAndMount() { for (std::size_t i = 0; i < 2; ++i) { SaveDataSearchInfo info{}; std::memcpy(&info, infos.data() + i * 48, sizeof(info)); - Require(info.blocks == 32768 && info.free_blocks == 32768, "search info must report the save's blocks and free blocks"); + Require(info.blocks == 16384 && info.free_blocks == 16384, "search info must report the save's blocks and free blocks"); for (const auto byte : info.reserved) Require(byte == 0, "search info reserved bytes must be zero"); } std::array found{}; @@ -246,6 +246,21 @@ static std::vector SizeSearch(std::uint32_t key, std::vector return {infos.begin(), infos.end()}; } +static SaveDataMountInfo MountInfo(const char* name) { + SceSaveDataDirName dirName{}; + std::snprintf(dirName.data, sizeof(dirName.data), "%s", name); + SaveDataMount3 mount{}; + mount.user_id = 1; + mount.dir_name = &dirName; + mount.mount_mode = 1; + SaveDataMountResult mounted{}; + Require(sceSaveDataMount3(&mount, &mounted) == 0, "mounting the sized save failed"); + SaveDataMountInfo info{}; + Require(sceSaveDataGetMountInfo(&mounted.mount_point, &info) == 0, "mount info failed"); + Require(sceSaveDataUmount2(0, &mounted.mount_point) == 0, "unmount failed"); + return info; +} + static void SaveSize() { CreateSized("SIZE_A", 200, 0); CreateSized("SIZE_B", 96, 40000); @@ -253,23 +268,22 @@ static void SaveSize() { auto infos = SizeSearch(0, found); Require(found == std::vector{"SIZE_A", "SIZE_B"}, "the sized saves must sort by name"); Require(infos[0].blocks == 200 && infos[0].free_blocks == 200, "an empty save must report all its blocks free"); - Require(infos[1].blocks == 96 && infos[1].free_blocks == 94, "a save must report the blocks it was created with, less those its files take"); + Require(infos[1].blocks == 96 && infos[1].free_blocks == 95, "a save must report the blocks it was created with, less those its files take"); SizeSearch(2, found); Require(found == std::vector{"SIZE_B", "SIZE_A"}, "saves must sort by blocks"); infos = SizeSearch(5, found); Require(found == std::vector{"SIZE_B", "SIZE_A"} && infos[0].free_blocks < infos[1].free_blocks, "saves must sort by free blocks"); - SceSaveDataDirName dirName{}; - std::snprintf(dirName.data, sizeof(dirName.data), "SIZE_B"); - SaveDataMount3 mount{}; - mount.user_id = 1; - mount.dir_name = &dirName; - mount.mount_mode = 1; - SaveDataMountResult mounted{}; - Require(sceSaveDataMount3(&mount, &mounted) == 0, "mounting the sized save failed"); - SaveDataMountInfo info{}; - Require(sceSaveDataGetMountInfo(&mounted.mount_point, &info) == 0, "mount info failed"); - Require(info.blocks == 96 && info.free_blocks == 94, "mount info must report the save's blocks and free blocks"); - Require(sceSaveDataUmount2(0, &mounted.mount_point) == 0, "unmount failed"); + const auto info = MountInfo("SIZE_B"); + Require(info.blocks == 96 && info.free_blocks == 95, "mount info must report the save's blocks and free blocks"); +} + +static void BlockSize() { + CreateSized("BLOCK_A", 48, 70000); + auto info = MountInfo("BLOCK_A"); + Require(info.blocks == 48 && info.free_blocks == 46, "a file must take whole 64 KiB blocks"); + CreateSized("BLOCK_B", 32768, 0); + info = MountInfo("BLOCK_B"); + Require(info.blocks == 16384 && info.free_blocks == 16384, "a save must hold at most 16384 blocks"); } int main() { @@ -285,6 +299,7 @@ int main() { OtherTitle(); TrailingWildcards(); SaveSize(); + BlockSize(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); status = 1; diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index ad4a4bf0b482..100927036203 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -8,7 +8,8 @@ - [sceSaveDataConvert](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the parameter block (user id at `+0x00`, `SceSaveDataDirName` pointer at `+0x08`, 48 opaque bytes after it) is read from PPSA28416's single call, not confirmed by KytyPS5, shadPS4, fpPS4 or SharpProspero, which have no body for it. Host saves have no size limit, so an existing save returns `SCE_OK` without touching its files and no event is queued; what the console returns for a save that is mounted, already converted or needs no conversion is unknown. The directory name is validated as `sceSaveDataDelete` does - [sceSaveDataGetConvertProgress](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA16064; signature and error codes follow the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-get-convert-progress.md) (`float* progress`). Save data conversion on host filesystems completes synchronously, so the progress query returns `SAVE_DATA_OK` with the last conversion's progress (`0.0f` before any conversion or when conversion fails, `1.0f` after successful conversion), `SAVE_DATA_ERROR_PARAMETER` if `progress` is null, or `SAVE_DATA_ERROR_NOT_INITIALIZED` when the library is uninitialized; intermediate asynchronous conversion progress on console is unmeasured - [sceSaveDataCancel](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA23126, which fails to load without it; added without an implementation because its signature is unknown. Every save data call here finishes before it returns, so nothing is ever in flight to cancel, but what the console returns then is unknown -- [sceSaveDataDirNameSearch](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. A save's size is the `blocks` of the `sceSaveDataMount3` call that created it, kept beside the save in `.blocks`; a save without a kept size takes that of its next mount that names one, and otherwise reports `SAVE_DATA_BLOCKS_MAX`. The search infos (blocks, free blocks and 32 reserved bytes per returned name, following shadPS4's `OrbisSaveDataSearchInfo`), `sceSaveDataGetMountInfo` and the two block keys use that size and, as free, what the save's files leave of it, each file rounded up to 32 KiB blocks; the console's file system overhead is not known and not counted. Keys other than 0, 1, 2, 3 and 5 (including 4) and orders above 1 return `SCE_SAVE_DATA_ERROR_PARAMETER`; shadPS4 and KytyPS5 only reject keys above 5, so key 4 is rejected here because no source names it. In the directory name pattern `%` matches any run of characters, none included, and `_` exactly one, as the comments of shadPS4's matcher describe; also not confirmed on hardware +- [sceSaveDataDirNameSearch](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. The search infos (blocks, free blocks and 32 reserved bytes per returned name) follow shadPS4's `OrbisSaveDataSearchInfo`. Keys other than 0, 1, 2, 3 and 5 (including 4) and orders above 1 return `SCE_SAVE_DATA_ERROR_PARAMETER`; shadPS4 and KytyPS5 only reject keys above 5, so key 4 is rejected here because no source names it. In the directory name pattern `%` matches any run of characters, none included, and `_` exactly one, as the comments of shadPS4's matcher describe; also not confirmed on hardware +- [sceSaveDataDirNameSearch, sceSaveDataGetMountInfo, sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the 64 KiB block of [SharpEmu](https://github.com/sharpemu/sharpemu/blob/c6d304cc3328bad62cb671327ace181110cf2ddb/src/SharpEmu.Libs/SaveData/SaveDataExports.cs#L82-L84) and the 48 to 16384-block range of SharpEmu and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L30-L31) are not measured on a console, and file system overhead is not counted. What the console does with a `blocks` outside that range is unknown: KytyPS5 rejects the creating mount and SharpEmu clamps it; here a larger one is kept as 16384 and a smaller non-zero one as given. A save's size is the `blocks` of the mount that created it, kept in `.blocks`, else that of its next mount that names one, else 16384; its free blocks are that size less its files, each rounded up to 64 KiB blocks - [sceSaveDataInitialize3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a repeated initialize succeeds: PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin fails on any error. Initializations are counted and `sceSaveDataTerminate` ends the session at the last one; how the console pairs them is unknown ### Functional