mirror of
https://github.com/ARMSX2/ARMSX1.git
synced 2026-08-24 16:53:35 -07:00
377 lines
18 KiB
C
377 lines
18 KiB
C
/*
|
|
ARMSX PS1 core — save states.
|
|
|
|
============================================================================
|
|
CONTAINER FORMAT ("ARMSXPSS", format version 1)
|
|
============================================================================
|
|
|
|
Everything below is written as fixed-width LITTLE-ENDIAN integers through the
|
|
psx_sw_* / psx_sr_* helpers. No struct is ever memcpy'd into the stream, so
|
|
the layout does not depend on the compiler's padding or on the host's byte
|
|
order. Floats are the one exception to "integers only": they are written as
|
|
their raw IEEE-754 binary32 bit pattern (a u32), which is portable across
|
|
every target this core builds for but IS a hard requirement — a build on a
|
|
non-IEEE-754 float host would read back garbage.
|
|
|
|
File layout:
|
|
|
|
+--------------------------------------------------------------+
|
|
| header (32 bytes) |
|
|
+--------------------------------------------------------------+
|
|
| section 0 (16 byte TLV header + payload) |
|
|
| section 1 |
|
|
| ... |
|
|
+--------------------------------------------------------------+
|
|
|
|
Header:
|
|
|
|
off size field
|
|
0x00 8 magic "ARMSXPSS"
|
|
0x08 4 format_version PSX_STATE_FORMAT_VERSION. A reader refuses
|
|
anything it does not equal exactly.
|
|
0x0c 4 core_abi PSX_STATE_CORE_ABI. Bumped whenever ANY
|
|
section's payload layout changes, so old
|
|
states are rejected with a clear error
|
|
instead of being half-decoded.
|
|
0x10 4 section_count
|
|
0x14 4 header_size (32; lets a future header grow)
|
|
0x18 8 reserved (0)
|
|
|
|
Section TLV:
|
|
|
|
off size field
|
|
0x00 4 id FourCC, e.g. 'CPU '. See PSX_SS_*.
|
|
0x04 4 version per-section payload version
|
|
0x08 8 length payload length in bytes
|
|
0x10 len payload
|
|
|
|
Because every section carries its own length, a section the reader does not
|
|
know about is skipped cleanly, and a device that a future core stops
|
|
emitting simply goes missing rather than shifting every following byte. The
|
|
reader tracks which MANDATORY sections it saw and fails the load (without
|
|
having touched the machine) if any is absent.
|
|
|
|
That "skipped cleanly" is what let PSX_SS_THUMB (the preview image a
|
|
front-end's save/load picker draws) be added WITHOUT touching
|
|
format_version or core_abi. It is not in the mandatory list, so:
|
|
|
|
- a state written before it existed still loads, it just has no preview;
|
|
- a state written with one still loads on a build that predates it,
|
|
because an unknown id at section version 1 is skipped.
|
|
|
|
Nothing already on a user's device is invalidated in either direction. The
|
|
price of that is the obvious one: a state saved before this landed can never
|
|
grow a thumbnail, because the pixels were never recorded. Any new section
|
|
that changes an EXISTING payload still has to bump core_abi.
|
|
|
|
============================================================================
|
|
WHAT IS *NOT* IN THE STREAM
|
|
============================================================================
|
|
|
|
- Pointers, of any kind. Devices are restored in place, into the objects the
|
|
front-end already holds references to (psx_t and every psx_*_t hanging off
|
|
it keep their identity across a load).
|
|
- Host resources: the open disc image, the SDL audio device, the renderer,
|
|
the memory-card FILE paths, the psx_gpu_t::udata[] / event_cb_table[]
|
|
front-end hooks, psx_bus_t::write_observer, psx_cpu_t::bus, the kcall
|
|
hooks, and the cached-interpreter block cache (a derived value — it is
|
|
invalidated on load and rebuilt lazily).
|
|
- The BIOS and EXP1 ROM images (read-only). The BIOS is instead FINGERPRINTED
|
|
into the identity section and a mismatch refuses the load, because a state
|
|
taken under a different BIOS resumes into different kernel code.
|
|
- The disc image. Only the CD-ROM controller's LOGICAL state is stored (LBA,
|
|
state machine, sector/response/parameter FIFOs, XA + CDDA decode state,
|
|
shell/motor bits). The disc is identified by a fingerprint taken from its
|
|
primary volume descriptor; loading a state against a different disc, or
|
|
against no disc at all, is refused.
|
|
|
|
============================================================================
|
|
THREADING
|
|
============================================================================
|
|
|
|
psx_save_state()/psx_load_state() must run on the emulation thread. Hosts on
|
|
another thread (the Android JNI layer) call psx_state_request_slot(), which
|
|
parks the request and blocks until the emulation thread services it from
|
|
psx_state_service_requests() — see state.c for the handshake.
|
|
*/
|
|
|
|
#ifndef PSX_STATE_H
|
|
#define PSX_STATE_H
|
|
|
|
#include <stdint.h>
|
|
#include <stddef.h>
|
|
|
|
#define PSX_STATE_MAGIC0 0x4d525041u /* "ARMS" */
|
|
#define PSX_STATE_MAGIC1 0x53535058u /* "XPSS" */
|
|
|
|
#define PSX_STATE_FORMAT_VERSION 1u
|
|
|
|
/* Bump on ANY change to a section payload. */
|
|
#define PSX_STATE_CORE_ABI 1u
|
|
|
|
#define PSX_STATE_HEADER_SIZE 32u
|
|
#define PSX_STATE_SECTION_HEADER_SIZE 16u
|
|
|
|
#define PSX_FOURCC(a, b, c, d) \
|
|
(((uint32_t)(unsigned char)(a)) | \
|
|
(((uint32_t)(unsigned char)(b)) << 8) | \
|
|
(((uint32_t)(unsigned char)(c)) << 16) | \
|
|
(((uint32_t)(unsigned char)(d)) << 24))
|
|
|
|
#define PSX_SS_IDENTITY PSX_FOURCC('I', 'D', 'N', 'T')
|
|
#define PSX_SS_CPU PSX_FOURCC('C', 'P', 'U', ' ')
|
|
#define PSX_SS_BUS PSX_FOURCC('B', 'U', 'S', ' ')
|
|
#define PSX_SS_RAM PSX_FOURCC('R', 'A', 'M', ' ')
|
|
#define PSX_SS_SCRATCHPAD PSX_FOURCC('S', 'P', 'A', 'D')
|
|
#define PSX_SS_MC1 PSX_FOURCC('M', 'C', '1', ' ')
|
|
#define PSX_SS_MC2 PSX_FOURCC('M', 'C', '2', ' ')
|
|
#define PSX_SS_MC3 PSX_FOURCC('M', 'C', '3', ' ')
|
|
#define PSX_SS_IC PSX_FOURCC('I', 'C', ' ', ' ')
|
|
#define PSX_SS_DMA PSX_FOURCC('D', 'M', 'A', ' ')
|
|
#define PSX_SS_GPU PSX_FOURCC('G', 'P', 'U', ' ')
|
|
#define PSX_SS_SPU PSX_FOURCC('S', 'P', 'U', ' ')
|
|
#define PSX_SS_TIMER PSX_FOURCC('T', 'M', 'R', ' ')
|
|
#define PSX_SS_CDROM PSX_FOURCC('C', 'D', 'R', 'M')
|
|
#define PSX_SS_PAD PSX_FOURCC('P', 'A', 'D', ' ')
|
|
#define PSX_SS_MDEC PSX_FOURCC('M', 'D', 'E', 'C')
|
|
#define PSX_SS_EXP2 PSX_FOURCC('E', 'X', 'P', '2')
|
|
/* Optional, never mandatory — see the compatibility note above. Payload:
|
|
|
|
off size field
|
|
0x00 4 image_format PSX_STATE_THUMB_PNG
|
|
0x04 4 width pixels
|
|
0x08 4 height pixels
|
|
0x0c 4 image_size bytes that follow
|
|
0x10 n image the encoded image
|
|
|
|
The width/height are metadata: the image is self-describing, but a reader
|
|
that only wants to lay out a tile should not have to parse it to find out
|
|
how big it is. */
|
|
#define PSX_SS_THUMB PSX_FOURCC('T', 'H', 'M', 'B')
|
|
|
|
/* Optional, never mandatory — same compatibility contract as PSX_SS_THUMB.
|
|
Memory-card fingerprints, so a load can tell whether the cards still hold
|
|
what the state was taken against. Payload:
|
|
|
|
off size field
|
|
0x00 4 slot_count always PSX_STATE_MCARD_SLOTS today; a reader
|
|
stops after the slots it knows about
|
|
then, per slot:
|
|
+0x00 1 present 0 = no card was attached in that slot
|
|
+0x01 8 content_hash FNV-1a over the whole 128 KiB card image
|
|
+0x09 8 session_id nonce stamped on the card when it was attached
|
|
+0x11 4 write_generation 128-byte sectors the game had written
|
|
+0x15 8 file_mtime st_mtime of the card file, seconds
|
|
|
|
content_hash answers "is it different". session_id + write_generation answer
|
|
"which way", and only within one attach of one card: generations from two
|
|
different attaches are not comparable, which is exactly why the nonce is
|
|
there — without it a fresh card would read as "older" than last week's state.
|
|
|
|
file_mtime is recorded for diagnosis and is NOT part of the verdict. It
|
|
cannot separate "the same card moved forward" from "a different card was
|
|
imported or erased", both of which leave the file newer; and since the image
|
|
is only flushed when the card is destroyed, it does not move at all while the
|
|
game is saving. A cross-session difference is therefore always the soft
|
|
verdict, which is the honest answer rather than a guess.
|
|
|
|
A state written before this existed simply has no section, which the reader
|
|
treats as "unknown" and passes silently. See PSX_STATE_ERR_CARD_* below. */
|
|
#define PSX_SS_MCARD PSX_FOURCC('M', 'C', 'F', 'P')
|
|
|
|
#define PSX_STATE_MCARD_SLOTS 2u
|
|
|
|
#define PSX_STATE_THUMB_PNG 1u
|
|
|
|
/* Refuses anything larger, so a corrupt or hostile length cannot turn a picker
|
|
refresh into a huge allocation. A 384x288 preview is a few tens of KB. */
|
|
#define PSX_STATE_THUMB_MAX_BYTES (4u * 1024u * 1024u)
|
|
|
|
/* Flags for psx_save_state_to_memory_ex().
|
|
|
|
NO_THUMBNAIL skips the PSX_SS_THUMB capture. The section is optional in every
|
|
sense (see the compatibility note above), so a state written without it is an
|
|
ordinary state that simply has no preview — it loads through the normal
|
|
reader with no special case. Rewind and runahead pass this because the PNG
|
|
encode is the single most expensive part of a capture and nothing is ever
|
|
going to draw a preview of a snapshot that lives for half a second. */
|
|
#define PSX_STATE_SAVE_NO_THUMBNAIL 0x00000001u
|
|
|
|
/* psx_save_state / psx_load_state result codes. 0 == success. */
|
|
enum {
|
|
PSX_STATE_OK = 0,
|
|
PSX_STATE_ERR_ARG = -1, /* NULL machine / path */
|
|
PSX_STATE_ERR_IO = -2, /* fopen / fread / fwrite failed */
|
|
PSX_STATE_ERR_MAGIC = -3, /* not an ARMSX state */
|
|
PSX_STATE_ERR_VERSION = -4, /* format_version / core_abi mismatch */
|
|
PSX_STATE_ERR_TRUNCATED = -5, /* stream ran out mid-section */
|
|
PSX_STATE_ERR_MISSING = -6, /* a mandatory section is absent */
|
|
PSX_STATE_ERR_GEOMETRY = -7, /* RAM / SPU RAM / FIFO size mismatch */
|
|
PSX_STATE_ERR_NO_DISC = -8, /* state has a disc, machine does not */
|
|
PSX_STATE_ERR_WRONG_DISC = -9, /* different disc in the drive */
|
|
PSX_STATE_ERR_WRONG_BIOS = -10, /* different BIOS image */
|
|
PSX_STATE_ERR_UNSUPPORTED = -11, /* the machine cannot be captured faithfully */
|
|
PSX_STATE_ERR_NO_MACHINE = -12, /* no VM registered / running */
|
|
PSX_STATE_ERR_BUSY = -13, /* another request is already parked */
|
|
PSX_STATE_ERR_TIMEOUT = -14, /* the emulation thread never serviced it */
|
|
|
|
/* Memory-card divergence. Raised in the load's identity phase, BEFORE any
|
|
byte of the machine has been touched, exactly like WRONG_DISC — so a
|
|
front-end can put the question to the user and re-issue the load with
|
|
PSX_STATE_LOAD_IGNORE_CARD_DIVERGENCE without anything having half
|
|
happened in between. Both are advisory: neither means the state is
|
|
damaged, only that the cards moved on since it was taken. */
|
|
|
|
/* The card carries writes the state does not: the game saved AFTER this
|
|
state was captured. Loading rewinds the console behind its own card. */
|
|
PSX_STATE_ERR_CARD_NEWER = -15,
|
|
|
|
/* The card differs, but nothing establishes which way (a different session,
|
|
an imported or erased card, a rewound one). */
|
|
PSX_STATE_ERR_CARD_DIVERGED = -16
|
|
};
|
|
|
|
/* Flags for psx_load_state_ex() / psx_load_state_from_memory_ex() /
|
|
psx_state_request_slot_ex(). */
|
|
#define PSX_STATE_LOAD_IGNORE_CARD_DIVERGENCE 0x00000001u
|
|
|
|
const char* psx_state_strerror(int code);
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Byte-stream helpers. Little-endian, fixed width, never a raw struct. */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
typedef struct {
|
|
uint8_t* buf;
|
|
size_t size; /* bytes written */
|
|
size_t capacity;
|
|
int error; /* sticky: set on an allocation failure */
|
|
} psx_state_writer_t;
|
|
|
|
typedef struct {
|
|
const uint8_t* buf;
|
|
size_t size;
|
|
size_t offset;
|
|
int error; /* sticky: set on an over-read */
|
|
} psx_state_reader_t;
|
|
|
|
void psx_sw_init(psx_state_writer_t*);
|
|
/* Start a writer on a buffer the CALLER already owns, so a repeated capture
|
|
(rewind, runahead) reuses one allocation instead of malloc/free per frame.
|
|
The writer grows it in place if it has to; ownership goes back to the caller
|
|
through the writer's buf/capacity fields. Passing NULL/0 is identical to
|
|
psx_sw_init(). */
|
|
void psx_sw_adopt(psx_state_writer_t*, void* buf, size_t capacity);
|
|
void psx_sw_free(psx_state_writer_t*);
|
|
void psx_sw_bytes(psx_state_writer_t*, const void*, size_t);
|
|
void psx_sw_u8(psx_state_writer_t*, uint8_t);
|
|
void psx_sw_u16(psx_state_writer_t*, uint16_t);
|
|
void psx_sw_u32(psx_state_writer_t*, uint32_t);
|
|
void psx_sw_u64(psx_state_writer_t*, uint64_t);
|
|
void psx_sw_i32(psx_state_writer_t*, int32_t);
|
|
void psx_sw_i64(psx_state_writer_t*, int64_t);
|
|
void psx_sw_f32(psx_state_writer_t*, float);
|
|
/* u16/i16 arrays, written element-wise so the on-disk order is fixed. */
|
|
void psx_sw_u16_array(psx_state_writer_t*, const uint16_t*, size_t count);
|
|
void psx_sw_i16_array(psx_state_writer_t*, const int16_t*, size_t count);
|
|
void psx_sw_u32_array(psx_state_writer_t*, const uint32_t*, size_t count);
|
|
void psx_sw_i32_array(psx_state_writer_t*, const int32_t*, size_t count);
|
|
|
|
void psx_sr_init(psx_state_reader_t*, const void*, size_t);
|
|
void psx_sr_bytes(psx_state_reader_t*, void*, size_t);
|
|
void psx_sr_skip(psx_state_reader_t*, size_t);
|
|
uint8_t psx_sr_u8(psx_state_reader_t*);
|
|
uint16_t psx_sr_u16(psx_state_reader_t*);
|
|
uint32_t psx_sr_u32(psx_state_reader_t*);
|
|
uint64_t psx_sr_u64(psx_state_reader_t*);
|
|
int32_t psx_sr_i32(psx_state_reader_t*);
|
|
int64_t psx_sr_i64(psx_state_reader_t*);
|
|
float psx_sr_f32(psx_state_reader_t*);
|
|
void psx_sr_u16_array(psx_state_reader_t*, uint16_t*, size_t count);
|
|
void psx_sr_i16_array(psx_state_reader_t*, int16_t*, size_t count);
|
|
void psx_sr_u32_array(psx_state_reader_t*, uint32_t*, size_t count);
|
|
void psx_sr_i32_array(psx_state_reader_t*, int32_t*, size_t count);
|
|
|
|
/* FNV-1a, used for the BIOS and disc fingerprints. */
|
|
uint64_t psx_state_fnv1a(const void* data, size_t size, uint64_t seed);
|
|
#define PSX_STATE_FNV_SEED 0xcbf29ce484222325ull
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Deferred, cross-thread request queue (see the THREADING note above). */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
enum {
|
|
PSX_STATE_OP_NONE = 0,
|
|
PSX_STATE_OP_SAVE = 1,
|
|
PSX_STATE_OP_LOAD = 2
|
|
};
|
|
|
|
struct psx_t;
|
|
|
|
/* Registered by psx_init() / cleared by psx_destroy(). Lets a host that only
|
|
has a JNI entry point reach the running machine without the front-end
|
|
handing it a pointer. */
|
|
void psx_state_set_machine(struct psx_t*);
|
|
|
|
/* Emulation thread. Cheap (one atomic load) when nothing is parked. Called
|
|
from psx_update(). */
|
|
void psx_state_service_requests(void);
|
|
|
|
/* Any thread. Parks a slot save/load and blocks up to timeout_ms for the
|
|
emulation thread to run it. Returns PSX_STATE_OK or one of the negative
|
|
codes above; PSX_STATE_ERR_TIMEOUT means the emulation thread never got
|
|
there (typically: the front-end has the VM paused). base_dir is the host's
|
|
data directory; slot files land in <base_dir>/savestates/. */
|
|
int psx_state_request_slot(int op, int slot, const char* base_dir, int timeout_ms);
|
|
|
|
/* The same, with PSX_STATE_LOAD_* flags. psx_state_request_slot() is exactly
|
|
this with flags == 0, so an existing caller keeps the checked behaviour and a
|
|
front-end that has already asked the user passes
|
|
PSX_STATE_LOAD_IGNORE_CARD_DIVERGENCE to say "yes, load it anyway". */
|
|
int psx_state_request_slot_ex(int op, int slot, const char* base_dir, int timeout_ms,
|
|
unsigned flags);
|
|
|
|
/* Builds the on-disk path for a slot: <base_dir>/savestates/<game>.slot<N>.pss
|
|
where <game> is derived from the mounted disc (sanitised file stem + the
|
|
low 32 bits of the disc fingerprint), or "nodisc" when running the BIOS
|
|
shell. Returns 0 on success. */
|
|
int psx_state_slot_path(struct psx_t*, int slot, const char* base_dir, char* out, size_t out_size);
|
|
|
|
/* Slot occupancy, for a front-end's save/load picker. Returns 1 when a state
|
|
for [slot] exists under <base_dir>/savestates/ for the disc currently in the
|
|
drive, filling out_disc_path with that disc's path; 0 otherwise (including
|
|
"no machine registered").
|
|
|
|
Uses the machine registered by psx_state_set_machine() so a host with only a
|
|
JNI entry point can ask without holding a psx_t. Any thread: this only stats
|
|
a file and reads the disc path, which does not change while a disc is
|
|
mounted — it never touches the machine's mutable state, so it does NOT have
|
|
to be deferred to the emulation thread the way a save or load does. */
|
|
int psx_state_slot_info(int slot, const char* base_dir, char* out_disc_path, size_t out_size);
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Preview images */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
/* Pulls the PSX_SS_THUMB image out of a state file and hands back a malloc()'d
|
|
copy the caller frees. Returns PSX_STATE_OK, or a negative code — most often
|
|
PSX_STATE_ERR_MISSING, which just means "that state predates thumbnails" and
|
|
is not an error a front-end should report.
|
|
|
|
Only the container framing is parsed: no device payload is decoded, nothing
|
|
is written to any machine, and the core_abi is deliberately NOT checked, so a
|
|
future ABI bump does not blind the picker. Any thread — it opens a file, and
|
|
nothing else.
|
|
|
|
The image is a PNG (image_format == PSX_STATE_THUMB_PNG). A caller that gets
|
|
any other image_format should treat the state as having no preview rather
|
|
than guessing: the format tag exists precisely so the encoding can change
|
|
without every reader having to be updated in lockstep. */
|
|
int psx_state_read_thumbnail(const char* path, void** out_data, size_t* out_size);
|
|
|
|
/* The same, addressed by slot for the disc currently in the drive. Returns
|
|
PSX_STATE_ERR_NO_MACHINE when nothing is running. */
|
|
int psx_state_slot_thumbnail(int slot, const char* base_dir, void** out_data, size_t* out_size);
|
|
|
|
#endif
|