Files
LibretroAdmin 93ae3b9520 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.
2026-10-28 20:21:15 +01:00

382 lines
9.1 KiB
C

#include "opera_clio.h"
#include "opera_clock.h"
#include "opera_core.h"
#include "opera_state.h"
#include "opera_timing.h"
#include "opera_vdlp.h"
#include <string.h>
#define DEFAULT_TIMER_SLACK 64UL
#define DEFAULT_CPU_FREQ 12500000UL
#define MIN_CPU_FREQ 1000000UL
#define SND_FREQ 44100UL
/* CLIO timers scan 16 slots at 4 M25M ticks per slot, then wait SLACK ticks. */
#define M25M_FREQ 25000000ULL
#define TIMER_SLOT_TICKS 4ULL
#define TIMER_SLOT_COUNT 16UL
#define CYCLES_PER_SND(FQ,FS) ((((uint64_t)FQ) << 16) / ((uint64_t)FS))
#define CYCLES_PER_SCANLINE(FQ,FR,FS) ((((uint64_t)FQ)<<32)/(((uint64_t)FR)*((uint64_t)FS)))
#define CYCLES_PER_M25M(FQ,TICKS) \
(((uint64_t)FQ * ((uint64_t)TICKS) << 16) / M25M_FREQ)
#define CYCLES_PER_TIMER_SLOT(FQ) CYCLES_PER_M25M(FQ,TIMER_SLOT_TICKS)
#define CYCLES_PER_TIMER_GAP(FQ,SLACK) \
CYCLES_PER_M25M(FQ,(TIMER_SLOT_TICKS + ((uint64_t)SLACK)))
#define DEFAULT_CPSL CYCLES_PER_SCANLINE(DEFAULT_CPU_FREQ, \
OPERA_NTSC_FIELD_RATE_1616, \
OPERA_NTSC_FIELD_SIZE)
typedef struct opera_clock_s opera_clock_t;
struct opera_clock_s
{
uint32_t cpu_freq;
uint64_t cpu_cycles;
int32_t dsp_acc;
int32_t vdl_acc;
int32_t timer_acc;
uint32_t timer_slack;
uint32_t timer_slot;
uint32_t timer_in_gap;
uint32_t field_size;
uint32_t field_rate;
int32_t cycles_per_snd;
int32_t cycles_per_scanline;
int32_t cycles_per_timer_slot;
int32_t cycles_per_timer_gap;
int32_t cycles_until_timer;
};
static opera_clock_t g_CLOCK =
{
/*.cpu_freq =*/ DEFAULT_CPU_FREQ,
/*.cpu_cycles =*/ 0,
/*.dsp_acc =*/ 0,
/*.vdl_acc =*/ 0,
/*.timer_acc =*/ 0,
/*.timer_slack =*/ DEFAULT_TIMER_SLACK,
/*.timer_slot =*/ 0,
/*.timer_in_gap =*/ 0,
/*.field_size =*/ OPERA_NTSC_FIELD_SIZE,
/*.field_rate =*/ OPERA_NTSC_FIELD_RATE_1616,
/*.cycles_per_snd =*/ CYCLES_PER_SND(DEFAULT_CPU_FREQ,SND_FREQ),
/*.cycles_per_scanline =*/ DEFAULT_CPSL,
/*.cycles_per_timer_slot=*/ CYCLES_PER_TIMER_SLOT(DEFAULT_CPU_FREQ),
/*.cycles_per_timer_gap =*/ CYCLES_PER_TIMER_GAP(DEFAULT_CPU_FREQ,DEFAULT_TIMER_SLACK),
/*.cycles_until_timer =*/ CYCLES_PER_TIMER_SLOT(DEFAULT_CPU_FREQ)
};
static
bool
opera_clock_state_write_payload(opera_state_writer_t *writer_,
opera_clock_t const *state_)
{
return (opera_state_write_u32(writer_,state_->cpu_freq) &&
opera_state_write_u64(writer_,state_->cpu_cycles) &&
opera_state_write_i32(writer_,state_->dsp_acc) &&
opera_state_write_i32(writer_,state_->vdl_acc) &&
opera_state_write_i32(writer_,state_->timer_acc) &&
opera_state_write_u32(writer_,state_->timer_slack) &&
opera_state_write_u32(writer_,state_->timer_slot) &&
opera_state_write_u32(writer_,state_->timer_in_gap) &&
opera_state_write_u32(writer_,state_->field_size) &&
opera_state_write_u32(writer_,state_->field_rate));
}
static
uint32_t
opera_clock_state_payload_size(void)
{
opera_state_writer_t writer;
opera_state_writer_init(&writer,NULL,UINT32_MAX);
opera_clock_state_write_payload(&writer,&g_CLOCK);
return opera_state_writer_ok(&writer) ? opera_state_writer_used(&writer) : 0;
}
static
uint32_t
calc_cycles_per_snd(void)
{
uint64_t rv;
rv = CYCLES_PER_SND(g_CLOCK.cpu_freq,SND_FREQ);
return rv;
}
/*
For greater percision the field rate is stored as 16.16 so cpu_freq
requires the 32 rather than 16 LSL
*/
static
uint32_t
calc_cycles_per_scanline(void)
{
uint64_t rv;
rv = CYCLES_PER_SCANLINE(g_CLOCK.cpu_freq,
g_CLOCK.field_rate,
g_CLOCK.field_size);
return rv;
}
static
uint32_t
calc_cycles_per_timer_slot(void)
{
uint64_t rv;
rv = CYCLES_PER_TIMER_SLOT(g_CLOCK.cpu_freq);
return rv;
}
static
uint32_t
calc_cycles_per_timer_gap(void)
{
uint64_t rv;
rv = CYCLES_PER_TIMER_GAP(g_CLOCK.cpu_freq,
g_CLOCK.timer_slack);
return rv;
}
static
void
recalculate_cycles_per(void)
{
g_CLOCK.cycles_per_snd = calc_cycles_per_snd();
g_CLOCK.cycles_per_scanline = calc_cycles_per_scanline();
g_CLOCK.cycles_per_timer_slot = calc_cycles_per_timer_slot();
g_CLOCK.cycles_per_timer_gap = calc_cycles_per_timer_gap();
g_CLOCK.cycles_until_timer = (g_CLOCK.timer_in_gap ?
g_CLOCK.cycles_per_timer_gap :
g_CLOCK.cycles_per_timer_slot);
}
void
opera_clock_reset(void)
{
g_CLOCK.cpu_cycles = 0;
g_CLOCK.dsp_acc = 0;
g_CLOCK.vdl_acc = 0;
g_CLOCK.timer_acc = 0;
g_CLOCK.timer_slack = DEFAULT_TIMER_SLACK;
g_CLOCK.timer_slot = 0;
g_CLOCK.timer_in_gap = 0;
recalculate_cycles_per();
}
uint32_t
opera_clock_state_size(void)
{
uint32_t payload_size;
payload_size = opera_clock_state_payload_size();
if(payload_size == 0)
return 0;
return opera_state_chunk_size(payload_size);
}
uint32_t
opera_clock_state_save(void *buf_)
{
uint32_t payload_size;
opera_state_writer_t writer;
payload_size = opera_clock_state_payload_size();
if(payload_size == 0)
return 0;
opera_state_writer_init(&writer,buf_,opera_state_chunk_size(payload_size));
opera_state_write_chunk_header(&writer,"CLCK",payload_size);
opera_clock_state_write_payload(&writer,&g_CLOCK);
return opera_state_writer_ok(&writer) ? opera_state_writer_used(&writer) : 0;
}
uint32_t
opera_clock_state_load(void const *buf_,
uint32_t const size_)
{
opera_clock_t state;
opera_state_reader_t reader;
opera_state_reader_t payload;
memset(&state,0,sizeof(state));
opera_state_reader_init(&reader,buf_,size_);
if(!opera_state_read_chunk(&reader,"CLCK",&payload) ||
!opera_state_read_u32(&payload,&state.cpu_freq) ||
!opera_state_read_u64(&payload,&state.cpu_cycles) ||
!opera_state_read_i32(&payload,&state.dsp_acc) ||
!opera_state_read_i32(&payload,&state.vdl_acc) ||
!opera_state_read_i32(&payload,&state.timer_acc) ||
!opera_state_read_u32(&payload,&state.timer_slack) ||
!opera_state_read_u32(&payload,&state.timer_slot) ||
!opera_state_read_u32(&payload,&state.timer_in_gap) ||
!opera_state_read_u32(&payload,&state.field_size) ||
!opera_state_read_u32(&payload,&state.field_rate) ||
!opera_state_reader_finished(&payload))
return 0;
if((state.cpu_freq < MIN_CPU_FREQ) ||
(state.timer_slot >= TIMER_SLOT_COUNT) ||
(state.timer_in_gap > 1) ||
(state.field_size == 0) ||
(state.field_rate == 0))
return 0;
if(opera_state_validating())
return opera_state_reader_used(&reader);
g_CLOCK = state;
recalculate_cycles_per();
return opera_state_reader_used(&reader);
}
void
opera_clock_cpu_set_freq(const uint32_t freq_)
{
uint32_t freq;
freq = ((freq_ < MIN_CPU_FREQ) ? MIN_CPU_FREQ : freq_);
g_CLOCK.cpu_freq = freq;
recalculate_cycles_per();
}
void
opera_clock_cpu_set_freq_mul(const float mul_)
{
float freq;
freq = (DEFAULT_CPU_FREQ * mul_);
opera_clock_cpu_set_freq((uint32_t)freq);
}
uint32_t
opera_clock_cpu_get_freq(void)
{
return g_CLOCK.cpu_freq;
}
uint32_t
opera_clock_cpu_get_default_freq(void)
{
return DEFAULT_CPU_FREQ;
}
double
opera_clock_field_rate(void)
{
return ((double)g_CLOCK.field_rate / OPERA_FIELD_RATE_ONE_1616);
}
uint64_t
opera_clock_cpu_get_cycles(void)
{
return g_CLOCK.cpu_cycles;
}
int
opera_clock_vdl_queued(void)
{
if(g_CLOCK.vdl_acc >= g_CLOCK.cycles_per_scanline)
{
g_CLOCK.vdl_acc -= g_CLOCK.cycles_per_scanline;
return 1;
}
return 0;
}
int
opera_clock_dsp_queued(void)
{
if(g_CLOCK.dsp_acc >= g_CLOCK.cycles_per_snd)
{
g_CLOCK.dsp_acc -= g_CLOCK.cycles_per_snd;
return 1;
}
return 0;
}
int
opera_clock_timer_queued(uint32_t *timer_)
{
if(g_CLOCK.timer_acc >= g_CLOCK.cycles_until_timer)
{
g_CLOCK.timer_acc -= g_CLOCK.cycles_until_timer;
*timer_ = g_CLOCK.timer_slot;
g_CLOCK.timer_slot++;
if(g_CLOCK.timer_slot >= TIMER_SLOT_COUNT)
{
g_CLOCK.timer_slot = 0;
g_CLOCK.timer_in_gap = 1;
g_CLOCK.cycles_until_timer = g_CLOCK.cycles_per_timer_gap;
}
else
{
g_CLOCK.timer_in_gap = 0;
g_CLOCK.cycles_until_timer = g_CLOCK.cycles_per_timer_slot;
}
return 1;
}
return 0;
}
void
opera_clock_push_cycles(const uint32_t clks_)
{
uint32_t clks1616;
g_CLOCK.cpu_cycles += clks_;
clks1616 = (clks_ << 16);
g_CLOCK.dsp_acc += clks1616;
g_CLOCK.vdl_acc += clks1616;
g_CLOCK.timer_acc += clks1616;
}
void
opera_clock_region_set_ntsc(void)
{
g_CLOCK.field_rate = OPERA_NTSC_FIELD_RATE_1616;
g_CLOCK.field_size = OPERA_NTSC_FIELD_SIZE;
recalculate_cycles_per();
}
void
opera_clock_region_set_pal(void)
{
g_CLOCK.field_rate = OPERA_PAL_FIELD_RATE_1616;
g_CLOCK.field_size = OPERA_PAL_FIELD_SIZE;
recalculate_cycles_per();
}
void
opera_clock_timer_set_delay(const uint32_t td_)
{
g_CLOCK.timer_slack = td_;
recalculate_cycles_per();
}