retro_unserialize: validate instead of backing up the running state

Every state load first serialized the running state into a fresh
allocation the size of a state -- 25.5 MB -- so a failed load could be
rolled back. That doubled the cost of every load, and run-ahead and
rewind load every frame.

A current-format state is now checked first and then loaded. Each
current-format loader already parses its chunk into locals and checks
it before touching emulator state; with opera_state_set_validating()
set they stop there, so a state that validates is one the real load
accepts in full and there is nothing to roll back. In that pass:

- the MEM chunk's 25 MB payload is stepped over (sizes and config are
  still checked), not copied;
- the xbus devices are not reset, and the CD-ROM device fields are
  checked against a scratch copy;
- the memory-map sync, pbus reset and other side effects after loading
  are skipped.

An older format, or a state carrying an older CD-ROM chunk, is not
validated and takes the old backup path, as does anything that fails
validation, so a refused state still leaves the running state as it
was.

Measured with a libretro driver over 200 loads: retro_unserialize went
from 9.08 ms to 4.20 ms on average (best 7.80 ms to 3.58 ms). Checked:
save, load and save again is byte-identical; a state loaded and run 60
frames matches the original run byte for byte; and a sweep flipping
each of 9721 bytes (every 64th byte of the first 360 KB, every byte of
the last 4 KB) is accepted or refused exactly as before, with no
refused load ever changing the running state.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-10-28 20:21:15 +01:00
1 parent a8b62eba35
commit 93ae3b9520
15 files changed
+143 -3

No files matched your search

+29 -1
View File
@@ -324,6 +324,9 @@ opera_3do_runtime_state_load(void const *data_,
if((field_state != 0) && (field_state != 1))
return 0;
if(opera_state_validating())
return opera_state_reader_used(&reader);
field = field_state;
g_FRAME_CYCLE_REMAINDER = remainder_state;
@@ -375,6 +378,9 @@ opera_3do_prng_state_load(void const *data_,
!opera_state_reader_finished(&payload))
return 0;
if(opera_state_validating())
return opera_state_reader_used(&reader);
prng16_state_set(prng16_state);
prng32_state_set(prng32_state);
@@ -637,7 +643,8 @@ opera_3do_state_load_chunked(const void *data_,
&data,
end))
return 0;
opera_arm_sync_memory_map();
if(!opera_state_validating())
opera_arm_sync_memory_map();
if(!opera_3do_state_load_step(opera_sport_state_load,
0,
&data,
@@ -664,6 +671,9 @@ opera_3do_state_load_chunked(const void *data_,
if(data != end)
return 0;
if(opera_state_validating())
return (uint32_t)(data - start);
opera_pbus_reset();
opera_xbus_set_legacy_no_device_abort(0);
@@ -692,6 +702,24 @@ opera_3do_state_load_v3(const void *data_,
true);
}
bool
opera_3do_state_validate(void const *data_,
size_t size_)
{
uint32_t rv;
/* Only the current format: the older loaders reset parts of the
* emulator before they read, so they cannot be dry-run. */
if(opera_state_get_version(data_,size_) != OPERA_STATE_VERSION_V3)
return false;
opera_state_set_validating(true);
rv = opera_3do_state_load_v3(data_,size_);
opera_state_set_validating(false);
return ((rv != 0) && (rv == size_));
}
uint32_t
opera_3do_state_load(void const *data_,
size_t size_)
+4
View File
@@ -4,12 +4,16 @@
#include "opera_core.h"
#include "opera_vdlp.h"
#include <boolean.h>
#include <stddef.h>
#include <stdint.h>
uint32_t opera_3do_state_size(void);
uint32_t opera_3do_state_save(void *buf, size_t size);
uint32_t opera_3do_state_load(void const *buf, size_t size);
/* True when @buf is a current-format state opera_3do_state_load() will
* accept in full. Checks without changing any emulator state. */
bool opera_3do_state_validate(void const *buf, size_t size);
int opera_3do_init(opera_ext_interface_t callback);
void opera_3do_destroy(void);
+3
View File
@@ -347,6 +347,9 @@ opera_arm_state_load(void const *data_,
!opera_state_reader_finished(&payload))
return 0;
if(opera_state_validating())
return opera_state_reader_used(&reader);
CPU = state;
g_SOFT_RESET_PENDING = soft_reset_pending;
set_rom_overlay(rom_overlay);
+13
View File
@@ -2579,6 +2579,14 @@ opera_cdrom_state_load_v2(cdrom_device_t *cd_,
return 0;
rv = opera_state_reader_used(&reader);
if(opera_state_validating())
{
/* The device checks run against a scratch copy. */
static cdrom_device_t scratch;
return cdrom_state_v2_device_load(&scratch,&device_state) ? rv : 0;
}
launch = g_CDROM_STATE.ode.launch;
ode_state_str_restore(root,
g_CDROM_STATE.ode.root,
@@ -2628,6 +2636,11 @@ opera_cdrom_state_load(cdrom_device_t *cd_,
return rv;
}
/* The older chunk layouts are not validated; a state carrying one
* is loaded the slow way, with a backup. */
if(opera_state_validating())
return 0;
if(data_size == sizeof(*cd_))
return opera_cdrom_state_load_v1(cd_,data_,chunk_size);
+3
View File
@@ -296,6 +296,9 @@ opera_clio_state_load(const void *buf_,
!opera_state_reader_finished(&payload))
return 0;
if(opera_state_validating())
return opera_state_reader_used(&reader);
CLIO = state;
flagtime = flagtime_state;
TIMER_VAL = timer_val_state;
+3
View File
@@ -235,6 +235,9 @@ opera_clock_state_load(void const *buf_,
(state.field_rate == 0))
return 0;
if(opera_state_validating())
return opera_state_reader_used(&reader);
g_CLOCK = state;
recalculate_cycles_per();
+3
View File
@@ -653,6 +653,9 @@ opera_dsp_state_load(const void *buf_,
!opera_state_reader_finished(&payload))
return 0;
if(opera_state_validating())
return opera_state_reader_used(&reader);
DSP = state;
return opera_state_reader_used(&reader);
+3
View File
@@ -701,6 +701,9 @@ opera_madam_state_load(const void *buf_,
if(!opera_state_reader_finished(&payload))
return 0;
if(opera_state_validating())
return opera_state_reader_used(&reader);
MADAM = state;
g_CEL_DRAW_PENDING = (pending != 0);
// The clock chunk restores the same absolute CPU-cycle timebase.
+10
View File
@@ -371,6 +371,16 @@ opera_mem_state_load(void const *data_,
NVRAM_SIZE)))
return 0;
/* Validating: the sizes were checked above, so the bulk payload is
* only stepped over, not copied. */
if(opera_state_validating())
{
if(!opera_state_skip_bytes(&payload,opera_state_reader_remaining(&payload)) ||
!opera_state_reader_finished(&payload))
return 0;
return opera_state_reader_used(&reader);
}
if(!opera_state_read_bytes(&payload,DRAM,MAX_HIRES_RAM_SIZE) ||
!opera_state_read_bytes(&payload,ROM1,ROM1_SIZE) ||
!opera_state_read_bytes(&payload,ROM2,ROM2_SIZE) ||
+3
View File
@@ -261,6 +261,9 @@ opera_sport_state_load(const void *buf_,
!opera_state_reader_finished(&payload))
return 0;
if(opera_state_validating())
return opera_state_reader_used(&reader);
SPORT = state;
return opera_state_reader_used(&reader);
+31
View File
@@ -347,6 +347,37 @@ opera_state_reader_ok(opera_state_reader_t const *reader_)
return !reader_->failed;
}
static bool g_STATE_VALIDATING = false;
void
opera_state_set_validating(bool validating_)
{
g_STATE_VALIDATING = validating_;
}
bool
opera_state_validating(void)
{
return g_STATE_VALIDATING;
}
bool
opera_state_skip_bytes(opera_state_reader_t *reader_,
uint32_t size_)
{
if(reader_->failed)
return false;
if(size_ > opera_state_reader_remaining(reader_))
{
opera_state_reader_fail(reader_);
return false;
}
reader_->offset += size_;
return true;
}
bool
opera_state_read_bytes(opera_state_reader_t *reader_,
void *dst_,
+12
View File
@@ -113,6 +113,18 @@ bool opera_state_read_chunk(opera_state_reader_t *reader,
bool opera_state_read_bytes(opera_state_reader_t *reader,
void *dst,
uint32_t size);
bool opera_state_skip_bytes(opera_state_reader_t *reader,
uint32_t size);
/*
* Validation pass. While set, every current-format state loader parses
* and checks its chunk exactly as it would for a real load but stops
* short of changing any emulator state, so a state that validates is
* one the real load accepts. Lets a load skip taking a full backup of
* the running state to roll back to.
*/
void opera_state_set_validating(bool validating);
bool opera_state_validating(void);
bool opera_state_read_u8(opera_state_reader_t *reader, uint8_t *value);
bool opera_state_read_i8(opera_state_reader_t *reader, int8_t *value);
bool opera_state_read_u16(opera_state_reader_t *reader, uint16_t *value);
+3
View File
@@ -1044,6 +1044,9 @@ opera_vdlp_state_load(const void *buf_,
!opera_state_reader_finished(&payload))
return 0;
if(opera_state_validating())
return opera_state_reader_used(&reader);
_migrate_vdlp_state(&state);
g_VDLP = state;
+4 -2
View File
@@ -611,7 +611,8 @@ opera_xbus_state_load(void const *data_,
(device_count > 15))
return 0;
XBUS = state;
if(!opera_state_validating())
XBUS = state;
for(i = 0; i < device_count; i++)
{
@@ -624,7 +625,8 @@ opera_xbus_state_load(void const *data_,
(xdev[slot] == NULL))
return 0;
xdev[slot](XBP_RESET,NULL);
if(!opera_state_validating())
xdev[slot](XBP_RESET,NULL);
remaining = opera_state_reader_remaining(&payload);
savedata.data = &payload.data[payload.offset];
savedata.size = remaining;
+19
View File
@@ -261,6 +261,25 @@ retro_unserialize(void const *data_,
uint32_t restore_size;
void *backup_state;
/*
* A current-format state is checked first and then loaded: a state
* that validates is one the load accepts in full, so there is
* nothing to roll back and no need to save the running state first.
* That backup was a full serialize into a fresh allocation the size
* of a state on every load -- twice the work for run-ahead and
* rewind, which load every frame. Anything else (an older format,
* a state that does not validate) takes the backup path below, which
* leaves the running state as it was if the load fails.
*/
if(opera_3do_state_validate(data_,size_))
{
size = opera_3do_state_load(data_,size_);
if(size != size_)
opera_log_printf(OPERA_LOG_ERROR,
"[Opera]: state validated but failed to load\n");
return (size == size_);
}
backup_state = malloc(retro_serialize_size());
if(backup_state == NULL)
return false;