Savestates: format V4, 25.5 MB down to 6.6 MB

A state stored the whole 22 MB DRAM allocation -- sized for the
largest memory configuration and the high-resolution VRAM copies --
plus both ROMs, whatever the game used, and the DSP's 70 KB of decode
tables. V4 keeps everything else as it was and changes two chunks:

- MEM2 replaces MEM. It holds the memory configuration, then only the
  RAM that configuration uses (DRAM, VRAM and VRAM's three high-
  resolution copies), then NVRAM. The copies are kept whether or not
  high resolution is on, because it can be switched mid-game and the
  state size must not change under a frontend that sized its buffers
  once; the configuration itself is fixed for a session. The ROMs are
  left out: they are the BIOS and font loaded for the session and
  never change. For the stock 2 MB/1 MB configuration that is 6 MB of
  RAM instead of 22 MB, and 2 MB of ROM gone.
- DSP2 replaces DSPP. INSTTRAS, REGCONV and BRCONDTAB are decode tables
  opera_dsp_init() builds and nothing changes, so they are not stored;
  a load keeps the running core's.

V3 states still load: MEM and DSPP are still read, and the tables a
DSPP chunk carries are stepped over. A core without this change
refuses a V4 state as an unknown version.

Measured with a libretro driver, stock configuration, together with
the array I/O change before this:

             size        serialize   unserialize
  before     25550222    3.8 ms      4.2 ms
  V4          6605970    0.5 ms      0.6 ms

Checked: a V3 state saved by the previous core loads and plays the
next 240 frames with audio and video identical to the original run; a
V4 state does the same; save-load-save is byte-identical; a replay of
60 frames from a loaded state matches; a sweep flipping 9721 bytes
never lets a refused load change the running state; and the changed
files build big-endian (PowerPC).
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-10-28 20:58:23 +01:00
1 parent 02f675e3c3
commit 54fe4f3754
4 files changed
+228 -103

No files matched your search

+22 -4
View File
@@ -702,19 +702,35 @@ opera_3do_state_load_v3(const void *data_,
true);
}
/* V4 differs from V3 only in its memory chunk, which
* opera_mem_state_load() tells apart by name. */
static
uint32_t
opera_3do_state_load_v4(const void *data_,
size_t const size_)
{
return opera_3do_state_load_chunked(data_,
size_,
OPERA_STATE_VERSION_V4,
true);
}
bool
opera_3do_state_validate(void const *data_,
size_t size_)
{
uint32_t rv;
uint32_t version;
/* 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)
/* Only the chunked V3/V4 formats: the older loaders reset parts of
* the emulator before they read, so they cannot be dry-run. */
version = opera_state_get_version(data_,size_);
if((version != OPERA_STATE_VERSION_V3) &&
(version != OPERA_STATE_VERSION_V4))
return false;
opera_state_set_validating(true);
rv = opera_3do_state_load_v3(data_,size_);
rv = opera_3do_state_load_chunked(data_,size_,version,true);
opera_state_set_validating(false);
return ((rv != 0) && (rv == size_));
@@ -738,6 +754,8 @@ opera_3do_state_load(void const *data_,
return opera_3do_state_load_v2(data_,size_);
case OPERA_STATE_VERSION_V3:
return opera_3do_state_load_v3(data_,size_);
case OPERA_STATE_VERSION_V4:
return opera_3do_state_load_v4(data_,size_);
default:
opera_log_printf(OPERA_LOG_ERROR,
"[Opera]: unable to load state - unknown state version %x\n",
+106 -79
View File
@@ -472,29 +472,19 @@ opera_dsp_state_size_v1(void)
return opera_state_save_size(sizeof(DSP));
}
/*
* INSTTRAS, REGCONV and BRCONDTAB are decode tables opera_dsp_init()
* builds and nothing changes afterwards, so the DSP2 chunk (state V4)
* leaves them out and a load keeps the running core's. The older DSPP
* chunk carried them; it is still read.
*/
/* Everything after the tables. */
static
bool
dsp_state_write_payload(opera_state_writer_t *writer_,
dsp_t const *state_)
dsp_state_write_live(opera_state_writer_t *writer_,
dsp_t const *state_)
{
uint32_t i;
uint32_t j;
if(!opera_state_write_u32(writer_,state_->RBASEx4))
return false;
for(i = 0; i < 0x8000; i++)
if(!opera_state_write_u8(writer_,state_->INSTTRAS[i].req.raw) ||
!opera_state_write_i8(writer_,(int8_t)state_->INSTTRAS[i].BS))
return false;
if(!opera_state_write_u16_array(writer_,&state_->REGCONV[0][0],8 * 16))
return false;
for(i = 0; i < 32; i++)
for(j = 0; j < 32; j++)
if(!opera_state_write_i32(writer_,state_->BRCONDTAB[i][j]))
return false;
if(!opera_state_write_u16_array(writer_,state_->NMem,2048) ||
!opera_state_write_u16_array(writer_,state_->IMem,1024) ||
@@ -527,6 +517,15 @@ dsp_state_write_payload(opera_state_writer_t *writer_,
return true;
}
static
bool
dsp_state_write_payload(opera_state_writer_t *writer_,
dsp_t const *state_)
{
return (opera_state_write_u32(writer_,state_->RBASEx4) &&
dsp_state_write_live(writer_,state_));
}
static
uint32_t
dsp_state_payload_size(void)
@@ -553,7 +552,7 @@ opera_dsp_state_save(void *buf_)
payload_size = dsp_state_payload_size();
opera_state_writer_init(&writer,buf_,opera_state_chunk_size(payload_size));
opera_state_write_chunk_header(&writer,"DSPP",payload_size);
opera_state_write_chunk_header(&writer,"DSP2",payload_size);
dsp_state_write_payload(&writer,&DSP);
return opera_state_writer_ok(&writer) ? opera_state_writer_used(&writer) : 0;
@@ -566,101 +565,129 @@ opera_dsp_state_load_v1(const void *buf_,
return opera_state_load_sized(&DSP,"DSPP",buf_,size_,sizeof(DSP));
}
/* The state a DSP2 chunk holds: the DSP minus its decode tables. */
typedef struct dsp_live_s dsp_live_t;
struct dsp_live_s
{
uint32_t RBASEx4;
uint16_t NMem[2048];
uint16_t IMem[1024];
int REGi;
REGSTAG_t dregs;
INTAG_t flags;
int CPUSupply[16];
};
static
bool
dsp_state_read_payload(opera_state_reader_t *reader_,
dsp_t *state_)
dsp_state_read_live(opera_state_reader_t *reader_,
dsp_live_t *live_)
{
uint32_t i;
uint32_t j;
int8_t bs;
uint8_t b;
memset(state_,0,sizeof(*state_));
if(!opera_state_read_u32(reader_,&state_->RBASEx4))
return false;
for(i = 0; i < 0x8000; i++)
{
if(!opera_state_read_u8(reader_,&state_->INSTTRAS[i].req.raw) ||
!opera_state_read_i8(reader_,&bs))
return false;
state_->INSTTRAS[i].BS = (char)bs;
}
if(!opera_state_read_u16_array(reader_,&state_->REGCONV[0][0],8 * 16))
return false;
for(i = 0; i < 32; i++)
for(j = 0; j < 32; j++)
{
int32_t v;
if(!opera_state_read_i32(reader_,&v))
return false;
state_->BRCONDTAB[i][j] = v;
}
if(!opera_state_read_u16_array(reader_,state_->NMem,2048) ||
!opera_state_read_u16_array(reader_,state_->IMem,1024) ||
!opera_state_read_i32(reader_,&state_->REGi) ||
!opera_state_read_u32(reader_,&state_->dregs.PC) ||
!opera_state_read_u16(reader_,&state_->dregs.AudioOutStatus) ||
!opera_state_read_u16(reader_,&state_->dregs.Sema4Status) ||
!opera_state_read_u16(reader_,&state_->dregs.Sema4Data) ||
!opera_state_read_i16(reader_,&state_->dregs.DSPPCNT) ||
!opera_state_read_i16(reader_,&state_->dregs.DSPPRLD) ||
!opera_state_read_i16(reader_,&state_->dregs.AUDCNT) ||
!opera_state_read_u16(reader_,&state_->dregs.INT) ||
!opera_state_read_i16(reader_,&state_->flags.MULT1) ||
!opera_state_read_i16(reader_,&state_->flags.MULT2) ||
!opera_state_read_i16(reader_,&state_->flags.ALU1) ||
!opera_state_read_i16(reader_,&state_->flags.ALU2) ||
!opera_state_read_i32(reader_,&state_->flags.BS) ||
!opera_state_read_u16(reader_,&state_->flags.RMAP) ||
!opera_state_read_u16(reader_,&state_->flags.nOP_MASK) ||
!opera_state_read_u16(reader_,&state_->flags.WRITEBACK) ||
!opera_state_read_u8(reader_,&state_->flags.req.raw) ||
if(!opera_state_read_u16_array(reader_,live_->NMem,2048) ||
!opera_state_read_u16_array(reader_,live_->IMem,1024) ||
!opera_state_read_i32(reader_,&live_->REGi) ||
!opera_state_read_u32(reader_,&live_->dregs.PC) ||
!opera_state_read_u16(reader_,&live_->dregs.AudioOutStatus) ||
!opera_state_read_u16(reader_,&live_->dregs.Sema4Status) ||
!opera_state_read_u16(reader_,&live_->dregs.Sema4Data) ||
!opera_state_read_i16(reader_,&live_->dregs.DSPPCNT) ||
!opera_state_read_i16(reader_,&live_->dregs.DSPPRLD) ||
!opera_state_read_i16(reader_,&live_->dregs.AUDCNT) ||
!opera_state_read_u16(reader_,&live_->dregs.INT) ||
!opera_state_read_i16(reader_,&live_->flags.MULT1) ||
!opera_state_read_i16(reader_,&live_->flags.MULT2) ||
!opera_state_read_i16(reader_,&live_->flags.ALU1) ||
!opera_state_read_i16(reader_,&live_->flags.ALU2) ||
!opera_state_read_i32(reader_,&live_->flags.BS) ||
!opera_state_read_u16(reader_,&live_->flags.RMAP) ||
!opera_state_read_u16(reader_,&live_->flags.nOP_MASK) ||
!opera_state_read_u16(reader_,&live_->flags.WRITEBACK) ||
!opera_state_read_u8(reader_,&live_->flags.req.raw) ||
!opera_state_read_u8(reader_,&b))
return false;
state_->flags.Running = (b != 0);
live_->flags.Running = (b != 0);
if(!opera_state_read_u8(reader_,&b))
return false;
state_->flags.GenFIQ = (b != 0);
live_->flags.GenFIQ = (b != 0);
for(i = 0; i < 16; i++)
{
int32_t v;
if(!opera_state_read_i32(reader_,&v))
return false;
state_->CPUSupply[i] = v;
live_->CPUSupply[i] = v;
}
return true;
}
uint32_t
opera_dsp_state_load(const void *buf_,
uint32_t const size_)
static
void
dsp_state_apply_live(dsp_t *dsp_,
dsp_live_t const *live_)
{
dsp_t state;
dsp_->RBASEx4 = live_->RBASEx4;
memcpy(dsp_->NMem,live_->NMem,sizeof(dsp_->NMem));
memcpy(dsp_->IMem,live_->IMem,sizeof(dsp_->IMem));
dsp_->REGi = live_->REGi;
dsp_->dregs = live_->dregs;
dsp_->flags = live_->flags;
memcpy(dsp_->CPUSupply,live_->CPUSupply,sizeof(dsp_->CPUSupply));
}
/* The DSPP chunk (state V3): the tables are in the stream. They are
* the ones opera_dsp_init() builds, so they are stepped over and the
* running core's are kept. */
static
bool
dsp_state_skip_tables(opera_state_reader_t *reader_)
{
return opera_state_skip_bytes(reader_,
(0x8000 * 2) +
(8 * 16 * sizeof(uint16_t)) +
(32 * 32 * sizeof(int32_t)));
}
static
uint32_t
opera_dsp_state_load_chunk(const void *buf_,
uint32_t const size_,
char const *name_,
bool tables_)
{
dsp_live_t live;
opera_state_reader_t reader;
opera_state_reader_t payload;
opera_state_reader_init(&reader,buf_,size_);
if(!opera_state_read_chunk(&reader,"DSPP",&payload) ||
!dsp_state_read_payload(&payload,&state) ||
if(!opera_state_read_chunk(&reader,name_,&payload) ||
!opera_state_read_u32(&payload,&live.RBASEx4) ||
(tables_ && !dsp_state_skip_tables(&payload)) ||
!dsp_state_read_live(&payload,&live) ||
!opera_state_reader_finished(&payload))
return 0;
if(opera_state_validating())
return opera_state_reader_used(&reader);
DSP = state;
dsp_state_apply_live(&DSP,&live);
return opera_state_reader_used(&reader);
}
uint32_t
opera_dsp_state_load(const void *buf_,
uint32_t const size_)
{
if((size_ >= 4) && !memcmp(buf_,"DSP2",4))
return opera_dsp_state_load_chunk(buf_,size_,"DSP2",false);
return opera_dsp_state_load_chunk(buf_,size_,"DSPP",true);
}
static
uint16_t
dsp_operand_load1(void)
+96 -19
View File
@@ -280,36 +280,59 @@ opera_mem_state_size_v1()
return size;
}
/*
* The MEM2 chunk: the memory configuration, how many RAM bytes follow,
* that much of DRAM, then NVRAM.
*
* The RAM stored is what the configuration uses: DRAM plus VRAM and its
* three high-resolution copies, which sit directly above it. That is
* fixed for a session -- the configuration is chosen once, and the
* copies are kept whether or not high resolution is on, since it can
* be switched at any time -- so the state size does not change under a
* frontend that sized its buffers once. The ROMs are not stored: they
* are the BIOS and font loaded for the session and never change.
*
* The older MEM chunk (V3) stored the whole 22 MB DRAM allocation and
* both ROMs, 24.1 MB, whatever the configuration.
*/
static
uint32_t
opera_mem_state_ram_bytes(opera_mem_cfg_t const cfg_)
{
return (opera_mem_dram_size(cfg_) + (opera_mem_vram_size(cfg_) * 4));
}
static
uint32_t
opera_mem_state_payload_size(void)
{
return (sizeof(uint8_t) +
sizeof(uint32_t) +
opera_mem_state_ram_bytes(g_MEM_CFG) +
NVRAM_SIZE);
}
uint32_t
opera_mem_state_size()
{
return opera_state_chunk_size(sizeof(uint8_t) +
MAX_HIRES_RAM_SIZE +
ROM1_SIZE +
ROM2_SIZE +
NVRAM_SIZE);
return opera_state_chunk_size(opera_mem_state_payload_size());
}
uint32_t
opera_mem_state_save(void *data_)
{
opera_mem_state_t memstate;
uint32_t ram_bytes;
opera_state_writer_t writer;
memstate.mem_cfg = opera_mem_cfg();
ram_bytes = opera_mem_state_ram_bytes(g_MEM_CFG);
opera_state_writer_init(&writer,data_,opera_mem_state_size());
opera_state_write_chunk_header(&writer,
"MEM",
sizeof(memstate.mem_cfg) +
MAX_HIRES_RAM_SIZE +
ROM1_SIZE +
ROM2_SIZE +
NVRAM_SIZE);
opera_state_write_u8(&writer,memstate.mem_cfg);
opera_state_write_bytes(&writer,DRAM,MAX_HIRES_RAM_SIZE);
opera_state_write_bytes(&writer,ROM1,ROM1_SIZE);
opera_state_write_bytes(&writer,ROM2,ROM2_SIZE);
"MEM2",
opera_mem_state_payload_size());
opera_state_write_u8(&writer,(uint8_t)g_MEM_CFG);
opera_state_write_u32(&writer,ram_bytes);
opera_state_write_bytes(&writer,DRAM,ram_bytes);
opera_state_write_bytes(&writer,NVRAM,NVRAM_SIZE);
return opera_state_writer_ok(&writer) ? opera_state_writer_used(&writer) : 0;
@@ -352,9 +375,10 @@ opera_mem_state_load_v1(void const *data_,
return (data - start);
}
static
uint32_t
opera_mem_state_load(void const *data_,
uint32_t const size_)
opera_mem_state_load_mem(void const *data_,
uint32_t const size_)
{
uint8_t mem_cfg;
opera_state_reader_t reader;
@@ -393,3 +417,56 @@ opera_mem_state_load(void const *data_,
return opera_state_reader_used(&reader);
}
static
uint32_t
opera_mem_state_load_mem2(void const *data_,
uint32_t const size_)
{
uint8_t mem_cfg;
uint32_t ram_bytes;
opera_state_reader_t reader;
opera_state_reader_t payload;
opera_state_reader_init(&reader,data_,size_);
if(!opera_state_read_chunk(&reader,"MEM2",&payload) ||
!opera_state_read_u8(&payload,&mem_cfg) ||
!opera_mem_is_valid_cfg((opera_mem_cfg_t)mem_cfg) ||
!opera_state_read_u32(&payload,&ram_bytes) ||
(ram_bytes != opera_mem_state_ram_bytes((opera_mem_cfg_t)mem_cfg)) ||
(ram_bytes > MAX_HIRES_RAM_SIZE) ||
(opera_state_reader_remaining(&payload) != (ram_bytes + NVRAM_SIZE)))
return 0;
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,ram_bytes) ||
!opera_state_read_bytes(&payload,NVRAM,NVRAM_SIZE) ||
!opera_state_reader_finished(&payload))
return 0;
_setup_dram_vram((opera_mem_cfg_t)mem_cfg);
g_MEM_CFG = (opera_mem_cfg_t)mem_cfg;
return opera_state_reader_used(&reader);
}
/* MEM2 from V4 states, MEM from V3; the chunk name says which. */
uint32_t
opera_mem_state_load(void const *data_,
uint32_t const size_)
{
uint8_t const *name = (uint8_t const*)data_;
if((size_ >= 4) && !memcmp(name,"MEM2",4))
return opera_mem_state_load_mem2(data_,size_);
return opera_mem_state_load_mem(data_,size_);
}
+4 -1
View File
@@ -8,7 +8,10 @@
#define OPERA_STATE_VERSION_V1 0x01
#define OPERA_STATE_VERSION_V2 0x02
#define OPERA_STATE_VERSION_V3 0x03
#define OPERA_STATE_VERSION_CURRENT OPERA_STATE_VERSION_V3
/* V4: the MEM chunk became MEM2 -- only the RAM the memory
* configuration uses, and no ROMs. Otherwise laid out as V3. */
#define OPERA_STATE_VERSION_V4 0x04
#define OPERA_STATE_VERSION_CURRENT OPERA_STATE_VERSION_V4
typedef struct opera_state_data_t opera_state_data_t;
struct opera_state_data_t