mirror of
https://github.com/libretro/opera-libretro.git
synced 2026-11-07 04:25:58 -10:00
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:
1 parent
a8b62eba35
commit
93ae3b9520
15 files changed
+143
-3
No files matched your search
+29
-1
@@ -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,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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) ||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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_,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user