Serialize PRNG state in libretro save states

Save states in libretro builds now also contain the state of the
pseudorandom number generator used to implement the WASI `random_get`
function for better save state determinism.

I've also changed the PRNG from MT19937 to PCG.
This commit is contained in:
刘皓 2025-06-11 15:54:21 -04:00
parent c3a6b1b69d
commit ee31cccb81
No known key found for this signature in database
GPG key ID: 7901753DB465B711
5 changed files with 58 additions and 21 deletions

View file

@ -125,11 +125,11 @@ sandbox::~sandbox() {
wasm2c_ruby_free(RB);
}
bool sandbox::sandbox_serialize_fdtable(void *&data, wasm_size_t &max_size) const {
bool sandbox::sandbox_serialize_wasi(void *&data, wasm_size_t &max_size) const {
return wasi->sandbox_serialize(data, max_size);
}
bool sandbox::sandbox_deserialize_fdtable(const void *&data, wasm_size_t &max_size) {
bool sandbox::sandbox_deserialize_wasi(const void *&data, wasm_size_t &max_size) {
return wasi->sandbox_deserialize(data, max_size);
}

View file

@ -67,8 +67,8 @@ namespace mkxp_sandbox {
inline struct mkxp_sandbox::bindings *operator->() noexcept { return &*bindings; }
sandbox();
~sandbox();
bool sandbox_serialize_fdtable(void *&data, wasm_size_t &max_size) const;
bool sandbox_deserialize_fdtable(const void *&data, wasm_size_t &max_size);
bool sandbox_serialize_wasi(void *&data, wasm_size_t &max_size) const;
bool sandbox_deserialize_wasi(const void *&data, wasm_size_t &max_size);
Movie *get_movie_from_main_thread();
Movie *get_movie_from_audio_thread();
void set_movie(Movie *new_movie);

View file

@ -52,7 +52,15 @@ struct fs_file *wasi_file_entry::file_handle() const noexcept {
return (struct fs_file *)handle;
}
wasi_t::w2c_wasi__snapshot__preview1(std::shared_ptr<struct w2c_ruby> ruby) : ruby(ruby) {
wasi_t::w2c_wasi__snapshot__preview1(std::shared_ptr<struct w2c_ruby> ruby) : ruby(ruby), prng_buffer_size(0) {
// Initialize PRNG
static_assert(sizeof(unsigned int) == sizeof(uint32_t), "unsigned int should be 32 bits");
static std::random_device dev;
prng_state = dev();
prng_state <<= 32U;
prng_state |= dev();
std::memset(prng_buffer, 0, 4);
// Initialize WASI file descriptor table
fdtable.push_back({nullptr, wasi_fd_type::STDIN});
fdtable.push_back({nullptr, wasi_fd_type::STDOUT});
@ -151,6 +159,13 @@ struct mkxp_sandbox::sandbox_str_guard wasi_t::str(wasm_ptr_t address) const noe
}
bool wasi_t::sandbox_serialize(void *&data, mkxp_sandbox::wasm_size_t &max_size) const {
if (!::sandbox_serialize(prng_state, data, max_size)) return false;
if (max_size < 4) return false;
std::memcpy(data, prng_buffer, 4);
data = (uint8_t *)data + 4;
max_size -= 4;
if (!::sandbox_serialize((uint8_t)prng_buffer_size, data, max_size)) return false;
if (!::sandbox_serialize((uint32_t)fdtable.size(), data, max_size)) return false;
uint32_t num_free_handles = 0;
@ -188,6 +203,17 @@ bool wasi_t::sandbox_serialize(void *&data, mkxp_sandbox::wasm_size_t &max_size)
}
bool wasi_t::sandbox_deserialize(const void *&data, mkxp_sandbox::wasm_size_t &max_size) {
if (!::sandbox_deserialize(prng_state, data, max_size)) return false;
if (max_size < 4) return false;
std::memcpy(prng_buffer, data, 4);
data = (uint8_t *)data + 4;
max_size -= 4;
{
uint8_t size;
if (!::sandbox_deserialize(size, data, max_size)) return false;
prng_buffer_size = size % 4;
}
uint32_t size;
if (!::sandbox_deserialize(size, data, max_size)) return false;
if (size < fdtable.size() && (fdtable[size].type == wasi_fd_type::FS || fdtable[size].type == wasi_fd_type::STDIN || fdtable[size].type == wasi_fd_type::STDOUT || fdtable[size].type == wasi_fd_type::STDERR)) return false;
@ -1193,23 +1219,30 @@ extern "C" void w2c_wasi__snapshot__preview1_proc_exit(wasi_t *wasi, uint32_t rv
extern "C" uint32_t w2c_wasi__snapshot__preview1_random_get(wasi_t *wasi, wasm_ptr_t buf, uint32_t buf_len) {
WASI_DEBUG("random_get(0x%08x (%u))\n", buf, buf_len);
static std::random_device dev;
static std::mt19937 rng(dev());
static uint32_t rng_buffer;
static uint32_t rng_buffer_size = 0;
while (buf_len > 0) {
if (rng_buffer_size == 0) {
rng_buffer = rng();
rng_buffer_size = 4;
if (wasi->prng_buffer_size == 0) {
wasi->prng_buffer_size = 4;
// PCG32 XSH RR (based on https://github.com/imneme/pcg-cpp, licensed MIT)
uint64_t state = wasi->prng_state;
wasi->prng_state = wasi->prng_state * (uint64_t)6364136223846793005U + (uint64_t)1442695040888963407U; // Advance state before computing output to improve instruction-level parallelism
uint32_t xsh = (state ^ (state >> 18U)) >> 27U;
uint32_t rot = state >> 59U;
uint32_t out = xsh >> rot | xsh << ((uint32_t)32U - rot);
#ifdef MKXPZ_BIG_ENDIAN
// Byte swap the output on big-endian machines to preserve state state compatibility across machines with different endiannesses
std::reverse_copy((uint8_t *)&out, (uint8_t *)&out + 4, wasi->prng_buffer);
#else
std::memcpy(wasi->prng_buffer, &out, 4);
#endif // MKXPZ_BIG_ENDIAN
} else {
uint32_t n = std::min(rng_buffer_size, buf_len);
wasi->arycpy(buf, (uint8_t *)&rng_buffer + (4 - n), n);
uint32_t n = std::min(buf_len, wasi->prng_buffer_size);
wasi->arycpy(buf, wasi->prng_buffer + ((uint32_t)4 - n), n);
buf += n;
buf_len -= n;
rng_buffer_size -= n;
wasi->prng_buffer_size -= n;
}
}
return WASI_ESUCCESS;
}

View file

@ -200,6 +200,10 @@ typedef struct w2c_wasi__snapshot__preview1 {
// List of vacant WASI file descriptors so that we can reallocate vacant WASI file descriptors quickly.
boost::container::priority_deque<uint32_t> vacant_fds;
uint64_t prng_state;
uint8_t prng_buffer[4];
uint32_t prng_buffer_size;
w2c_wasi__snapshot__preview1(std::shared_ptr<struct w2c_ruby> ruby);
~w2c_wasi__snapshot__preview1();
uint32_t allocate_file_descriptor(enum wasi_fd_type type, void *handle = nullptr);

View file

@ -1701,8 +1701,8 @@ extern "C" RETRO_API bool retro_serialize(void *data, size_t len) {
}
}
// Write the open WASI file descriptors
if (!sb().sandbox_serialize_fdtable(data, max_size)) return false;
// Write the pseudorandom number generator state and open WASI file descriptors
if (!sb().sandbox_serialize_wasi(data, max_size)) return false;
SER_OBJECTS_BEGIN;
@ -1898,8 +1898,8 @@ extern "C" RETRO_API bool retro_unserialize(const void *data, size_t len) {
}
}
// Read the open WASI file descriptors
if (!sb().sandbox_deserialize_fdtable(data, max_size)) DESER_FAIL;
// Read the pseudorandom number generator state and open WASI file descriptors
if (!sb().sandbox_deserialize_wasi(data, max_size)) DESER_FAIL;
DESER_OBJECTS_BEGIN;
for (const auto &object : sb()->objects) {