mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 18:25:35 -04:00
1204 lines
46 KiB
C
1204 lines
46 KiB
C
#include "boot_state.h"
|
|
#include "execution_profile.h"
|
|
#include "fntrace.h"
|
|
#include "mod_memory.h"
|
|
#include "overlay_api.h" /* PSX_OVERLAY_CODEGEN_HASH / _ABI_TAG / _CODEGEN_VER */
|
|
|
|
/* Build-selected HLE may use private execution boundaries. Zero preserves
|
|
* the existing snapshot key for titles that do not select an implementation. */
|
|
#ifndef PSX_SAVESTATE_IMPL_TAG
|
|
#define PSX_SAVESTATE_IMPL_TAG 0u
|
|
#endif
|
|
#define BOOT_STATE_IMPLEMENTATION_KEY ((uint32_t)PSX_OVERLAY_CODEGEN_VER ^ (uint32_t)PSX_SAVESTATE_IMPL_TAG)
|
|
#include "dirty_ram_interp.h"
|
|
#include "gpu.h" /* gpu_get_vram — CPU-auth mirror under dual-raster */
|
|
#include "gpu_render.h" /* gr_vram_transfer_in / gr_vram_transfer_out */
|
|
#include "gpu_vram_dirty.h"
|
|
#include "cpu_state.h" /* gte_canonicalize_cpu_state after CPU wire restore */
|
|
#include "interrupts.h"
|
|
#include "psx_cycles.h"
|
|
#include "psx_icache.h" /* g_psx_icache_tv — fetch-cost tags in BS_SEC_ICACHE */
|
|
#include "psx_memory.h"
|
|
#include "pst_wire.h"
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <zlib.h>
|
|
#if defined(_WIN32)
|
|
# include <windows.h>
|
|
#else
|
|
# include <time.h>
|
|
#endif
|
|
|
|
static double boot_state_mono_ms(void) {
|
|
#if defined(_WIN32)
|
|
static LARGE_INTEGER freq;
|
|
LARGE_INTEGER c;
|
|
if (!freq.QuadPart) QueryPerformanceFrequency(&freq);
|
|
QueryPerformanceCounter(&c);
|
|
return (double)c.QuadPart * 1000.0 / (double)freq.QuadPart;
|
|
#else
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1.0e6;
|
|
#endif
|
|
}
|
|
|
|
/* Compress payloads at/above this size (RAM/VRAM/SPU/dirty dominate I/O). */
|
|
#define BOOT_STATE_ZLIB_MIN 256u
|
|
|
|
#define SPAD_SIZE (1024u)
|
|
#define VRAM_W 1024
|
|
#define VRAM_H 512
|
|
#define VRAM_SIZE ((uint32_t)(VRAM_W * VRAM_H * 2)) /* 1 MB, 16bpp */
|
|
|
|
/* ---- core accessors (existing runtime modules) ---- */
|
|
extern uint8_t* memory_get_ram_ptr(void);
|
|
extern uint32_t memory_get_ram_bytes(void);
|
|
extern uint8_t* memory_get_scratchpad_ptr(void);
|
|
extern uint32_t i_stat;
|
|
extern uint32_t i_mask;
|
|
extern uint64_t psx_cycle_count;
|
|
extern void timers_get_snapshot(uint16_t counter[3], uint32_t mode[3],
|
|
uint16_t target[3], int32_t irq_line[3],
|
|
uint32_t frac[3]);
|
|
extern void timers_set_snapshot(const uint16_t counter[3], const uint32_t mode[3],
|
|
const uint16_t target[3], const int32_t irq_line[3],
|
|
const uint32_t frac[3]);
|
|
|
|
/* ---- per-subsystem complete-state accessors (defined in each module) ---- */
|
|
extern uint32_t gpu_snapshot_bytes(void);
|
|
extern void gpu_snapshot_write(uint8_t* p);
|
|
extern int gpu_snapshot_read(const uint8_t* p, uint32_t len);
|
|
extern uint32_t spu_snapshot_bytes(void);
|
|
extern void spu_snapshot_write(uint8_t* p);
|
|
extern int spu_snapshot_read(const uint8_t* p, uint32_t len);
|
|
extern int spu_snapshot_len_ok(uint32_t len);
|
|
extern uint8_t* spu_get_ram_ptr(void);
|
|
extern uint32_t spu_get_ram_bytes(void);
|
|
extern uint32_t cdrom_snapshot_bytes(void);
|
|
extern int cdrom_snapshot_validate(const uint8_t* p, uint32_t len);
|
|
extern void cdrom_snapshot_write(uint8_t* p);
|
|
extern int cdrom_snapshot_read(const uint8_t* p, uint32_t len);
|
|
extern uint32_t dma_snapshot_bytes(void);
|
|
extern void dma_snapshot_write(uint8_t* p);
|
|
extern int dma_snapshot_read(const uint8_t* p, uint32_t len);
|
|
extern uint32_t sio_snapshot_bytes(void);
|
|
extern void sio_snapshot_write(uint8_t* p);
|
|
extern int sio_snapshot_read(const uint8_t* p, uint32_t len);
|
|
extern uint32_t mdec_snapshot_bytes(void);
|
|
extern void mdec_snapshot_write(uint8_t* p);
|
|
extern int mdec_snapshot_read(const uint8_t* p, uint32_t len);
|
|
/* Non-mutating pre-checks for the readers whose acceptance is not just an
|
|
* exact length (the two-pass load below). */
|
|
extern int sio_snapshot_validate(const uint8_t* p, uint32_t len);
|
|
extern int mdec_snapshot_validate(const uint8_t* p, uint32_t len);
|
|
|
|
/* Header compatibility cookie (h.reserved): enhancement-memory layout, live
|
|
* RAM geometry and the fixed execution implementation. Reject incompatible
|
|
* snapshots in the header, before any section changes the live machine. */
|
|
#define BOOT_STATE_RAM_8MB_COOKIE 0x384D4252u /* "RBM8" */
|
|
static uint32_t boot_state_ram_cookie(void) {
|
|
return psx_ram_8mb_active() ? BOOT_STATE_RAM_8MB_COOKIE : 0u;
|
|
}
|
|
static uint32_t boot_state_layout_cookie(void) {
|
|
return psx_mod_memory_layout_cookie() ^ boot_state_ram_cookie() ^ PSX_EXECUTION_COOKIE;
|
|
}
|
|
|
|
/* CPU regs wire: 32+3+32+32+32 LE u32 = 131 * 4 = 524 bytes (no padding). */
|
|
#define CPU_REGS_WIRE_BYTES (524u)
|
|
/* Timer wire: 3*u16 + 3*u32 + 3*u16 + 3*i32 + 3*u32 = 48 bytes (no pad holes). */
|
|
#define TIMER_REGS_WIRE_BYTES (48u)
|
|
/* CPU stall wire: 2*u32 + 33 + 3*u8 + u32 = 48 bytes. */
|
|
#define BS_CPUTIM_BYTES (48u)
|
|
/* Set by a BS_SEC_CPUTIM load; psx_cycles_resync_after_restore consumes it. */
|
|
int g_boot_state_cpu_timing_restored;
|
|
|
|
/* ---- deferred capture state (armed before first boot, fired at handoff) ---- */
|
|
static char s_capture_path[512];
|
|
static uint32_t s_capture_checksum;
|
|
static uint32_t s_capture_entry_pc;
|
|
|
|
/* §96: persistent VRAM mirror for raw ring snaps — patch dirty scanlines only. */
|
|
static uint16_t s_vram_mirror[VRAM_W * VRAM_H];
|
|
static int s_vram_mirror_valid;
|
|
static uint32_t s_last_vram_dirty_rows;
|
|
static int s_last_vram_incremental;
|
|
|
|
uint32_t boot_state_last_vram_dirty_rows(void)
|
|
{
|
|
return s_last_vram_dirty_rows;
|
|
}
|
|
|
|
int boot_state_last_vram_incremental(void)
|
|
{
|
|
return s_last_vram_incremental;
|
|
}
|
|
|
|
void boot_state_vram_mirror_reset(void)
|
|
{
|
|
s_vram_mirror_valid = 0;
|
|
s_last_vram_dirty_rows = VRAM_H;
|
|
s_last_vram_incremental = 0;
|
|
}
|
|
|
|
/* Build s_vram_mirror from live CPU VRAM using dirty rows when possible.
|
|
* Always emits a full 1 MiB BS_SEC_VRAM (loads stay independent).
|
|
* Only used while gpu_vram_dirty_tracking() (rollback netplay). */
|
|
static int sync_vram_mirror_for_save(void)
|
|
{
|
|
const uint16_t *live = gpu_get_vram();
|
|
uint32_t dirty_n;
|
|
uint32_t y;
|
|
|
|
if (!gpu_vram_dirty_tracking()) {
|
|
/* Should not be called offline — full refresh fallback. */
|
|
if (live)
|
|
memcpy(s_vram_mirror, live, VRAM_SIZE);
|
|
else
|
|
gr_vram_transfer_out(0, 0, VRAM_W, VRAM_H, s_vram_mirror);
|
|
s_vram_mirror_valid = 1;
|
|
s_last_vram_dirty_rows = VRAM_H;
|
|
s_last_vram_incremental = 0;
|
|
return 1;
|
|
}
|
|
|
|
dirty_n = gpu_vram_dirty_row_count();
|
|
s_last_vram_dirty_rows = dirty_n;
|
|
|
|
if (!live) {
|
|
gr_vram_transfer_out(0, 0, VRAM_W, VRAM_H, s_vram_mirror);
|
|
s_vram_mirror_valid = 1;
|
|
s_last_vram_dirty_rows = VRAM_H;
|
|
s_last_vram_incremental = 0;
|
|
gpu_vram_dirty_clear();
|
|
return 1;
|
|
}
|
|
|
|
if (!s_vram_mirror_valid || dirty_n >= VRAM_H) {
|
|
memcpy(s_vram_mirror, live, VRAM_SIZE);
|
|
s_vram_mirror_valid = 1;
|
|
s_last_vram_incremental = 0;
|
|
} else if (dirty_n == 0u) {
|
|
/* Mirror already matches live. */
|
|
s_last_vram_incremental = 1;
|
|
} else {
|
|
const uint64_t *mask = gpu_vram_dirty_mask();
|
|
for (y = 0; y < VRAM_H; y++) {
|
|
if (mask[y >> 6] & ((uint64_t)1u << (y & 63u))) {
|
|
memcpy(s_vram_mirror + (size_t)y * VRAM_W,
|
|
live + (size_t)y * VRAM_W,
|
|
(size_t)VRAM_W * sizeof(uint16_t));
|
|
}
|
|
}
|
|
s_last_vram_incremental = 1;
|
|
}
|
|
|
|
if (gpu_vram_dirty_verify_enabled()) {
|
|
uint16_t *full = (uint16_t *)malloc(VRAM_SIZE);
|
|
if (full) {
|
|
gr_vram_transfer_out(0, 0, VRAM_W, VRAM_H, full);
|
|
if (memcmp(full, s_vram_mirror, VRAM_SIZE) != 0) {
|
|
fprintf(stderr,
|
|
"psxrecomp: VRAM dirty VERIFY FAIL dirty_rows=%u "
|
|
"incr=%d — forcing full mirror\n",
|
|
(unsigned)dirty_n, s_last_vram_incremental);
|
|
fflush(stderr);
|
|
memcpy(s_vram_mirror, full, VRAM_SIZE);
|
|
s_last_vram_incremental = 0;
|
|
s_last_vram_dirty_rows = VRAM_H;
|
|
}
|
|
free(full);
|
|
}
|
|
}
|
|
|
|
gpu_vram_dirty_clear();
|
|
return 1;
|
|
}
|
|
|
|
/* File or growable memory sink — both save paths share one serializer. */
|
|
typedef struct BsOut {
|
|
FILE* f; /* non-NULL => write to file */
|
|
uint8_t* data; /* memory sink (owned by caller / save_buffer) */
|
|
size_t len;
|
|
size_t cap;
|
|
int no_zlib; /* 1 => always raw sections (netplay snap ring) */
|
|
} BsOut;
|
|
|
|
static int bs_write(BsOut* o, const void* p, size_t n) {
|
|
if (!n) return 1;
|
|
if (o->f)
|
|
return fwrite(p, 1, n, o->f) == n;
|
|
if (o->len + n > o->cap) {
|
|
size_t nc = o->cap ? o->cap * 2u : (256u * 1024u);
|
|
uint8_t* nd;
|
|
while (nc < o->len + n) {
|
|
if (nc > (SIZE_MAX / 2u)) return 0;
|
|
nc *= 2u;
|
|
}
|
|
nd = (uint8_t*)realloc(o->data, nc);
|
|
if (!nd) return 0;
|
|
o->data = nd;
|
|
o->cap = nc;
|
|
}
|
|
memcpy(o->data + o->len, p, n);
|
|
o->len += n;
|
|
return 1;
|
|
}
|
|
|
|
static int write_header_le(BsOut* o, const BootStateHeader* h) {
|
|
uint8_t buf[BOOT_STATE_HEADER_WIRE_BYTES];
|
|
PstW w;
|
|
pst_w_init(&w, buf, sizeof buf);
|
|
if (!pst_w_u32(&w, h->magic) ||
|
|
!pst_w_u32(&w, h->version) ||
|
|
!pst_w_u32(&w, h->bios_checksum) ||
|
|
!pst_w_u32(&w, h->entry_pc) ||
|
|
!pst_w_u32(&w, h->codegen_hash) ||
|
|
!pst_w_i32(&w, h->abi_tag) ||
|
|
!pst_w_u32(&w, h->codegen_ver) ||
|
|
!pst_w_u32(&w, h->section_count) ||
|
|
!pst_w_u32(&w, h->reserved) ||
|
|
w.written != BOOT_STATE_HEADER_WIRE_BYTES)
|
|
return 0;
|
|
return bs_write(o, buf, sizeof buf);
|
|
}
|
|
|
|
static int write_section_raw(BsOut* o, uint32_t tag, uint32_t flags,
|
|
const void* data, uint64_t len) {
|
|
uint8_t hdr[16];
|
|
PstW w;
|
|
pst_w_init(&w, hdr, sizeof hdr);
|
|
if (!pst_w_u32(&w, tag) || !pst_w_u32(&w, flags) || !pst_w_u64(&w, len))
|
|
return 0;
|
|
if (!bs_write(o, hdr, sizeof hdr)) return 0;
|
|
if (len && !bs_write(o, data, (size_t)len)) return 0;
|
|
return 1;
|
|
}
|
|
|
|
/* Prefer zlib for large blobs (smaller disk + faster load on slow storage).
|
|
* Falls back to raw if compressBound/compress fails.
|
|
* o->no_zlib skips compress entirely (in-memory netplay ring). */
|
|
static int write_section(BsOut* o, uint32_t tag, const void* data, uint64_t len) {
|
|
if (!data && len) return 0;
|
|
if (!o->no_zlib && len >= BOOT_STATE_ZLIB_MIN && len <= 0xffffffffu) {
|
|
uLong bound = compressBound((uLong)len);
|
|
uint8_t* packed = (uint8_t*)malloc(4u + (size_t)bound);
|
|
if (packed) {
|
|
PstW lw;
|
|
uLong dest_len = bound;
|
|
pst_w_init(&lw, packed, 4);
|
|
if (pst_w_u32(&lw, (uint32_t)len) &&
|
|
compress2(packed + 4, &dest_len, (const Bytef*)data, (uLong)len,
|
|
Z_BEST_SPEED) == Z_OK) {
|
|
uint64_t payload = 4u + (uint64_t)dest_len;
|
|
int ok = write_section_raw(o, tag, BOOT_STATE_SEC_ZLIB,
|
|
packed, payload);
|
|
free(packed);
|
|
return ok;
|
|
}
|
|
free(packed);
|
|
}
|
|
}
|
|
return write_section_raw(o, tag, 0u, data, len);
|
|
}
|
|
|
|
static int write_module_section(BsOut* o, uint32_t tag,
|
|
uint32_t (*bytes)(void),
|
|
void (*write)(uint8_t*)) {
|
|
uint32_t n = bytes();
|
|
uint8_t* buf = (uint8_t*)malloc(n ? n : 1);
|
|
if (!buf) return 0;
|
|
write(buf);
|
|
int ok = write_section(o, tag, buf, n);
|
|
free(buf);
|
|
return ok;
|
|
}
|
|
|
|
static int write_cpu_section(BsOut* o, const CPUState* cpu) {
|
|
uint8_t buf[CPU_REGS_WIRE_BYTES];
|
|
PstW w;
|
|
pst_w_init(&w, buf, sizeof buf);
|
|
for (int i = 0; i < 32; i++)
|
|
if (!pst_w_u32(&w, cpu->gpr[i])) return 0;
|
|
if (!pst_w_u32(&w, cpu->pc) || !pst_w_u32(&w, cpu->hi) || !pst_w_u32(&w, cpu->lo))
|
|
return 0;
|
|
for (int i = 0; i < 32; i++)
|
|
if (!pst_w_u32(&w, cpu->cop0[i])) return 0;
|
|
for (int i = 0; i < 32; i++) {
|
|
uint32_t v = cpu->gte_data[i];
|
|
/* LZCR is derived from LZCS (what MFC2 31 returns, and what a load
|
|
* recomputes). Serialize the derived value: the raw backing stays 0
|
|
* until the first LZCS write, so a live save and a replayed save of
|
|
* the same tick differed here although no guest could tell. */
|
|
if (i == 31) {
|
|
uint32_t bits = (cpu->gte_data[30] & 0x80000000u) ? ~cpu->gte_data[30]
|
|
: cpu->gte_data[30];
|
|
v = 0;
|
|
if (bits == 0) v = 32;
|
|
else while (!(bits & 0x80000000u)) { bits <<= 1; ++v; }
|
|
}
|
|
if (!pst_w_u32(&w, v)) return 0;
|
|
}
|
|
for (int i = 0; i < 32; i++)
|
|
if (!pst_w_u32(&w, cpu->gte_ctrl[i])) return 0;
|
|
if (w.written != CPU_REGS_WIRE_BYTES) return 0;
|
|
return write_section(o, BS_SEC_CPU, buf, CPU_REGS_WIRE_BYTES);
|
|
}
|
|
|
|
static int write_timer_section(BsOut* o) {
|
|
uint16_t counter[3], target[3];
|
|
uint32_t mode[3], frac[3];
|
|
int32_t irq_line[3];
|
|
uint8_t buf[TIMER_REGS_WIRE_BYTES];
|
|
PstW w;
|
|
timers_get_snapshot(counter, mode, target, irq_line, frac);
|
|
pst_w_init(&w, buf, sizeof buf);
|
|
for (int i = 0; i < 3; i++)
|
|
if (!pst_w_u16(&w, counter[i])) return 0;
|
|
for (int i = 0; i < 3; i++)
|
|
if (!pst_w_u32(&w, mode[i])) return 0;
|
|
for (int i = 0; i < 3; i++)
|
|
if (!pst_w_u16(&w, target[i])) return 0;
|
|
for (int i = 0; i < 3; i++)
|
|
if (!pst_w_i32(&w, irq_line[i])) return 0;
|
|
for (int i = 0; i < 3; i++)
|
|
if (!pst_w_u32(&w, frac[i])) return 0;
|
|
if (w.written != TIMER_REGS_WIRE_BYTES) return 0;
|
|
return write_section(o, BS_SEC_TIMER, buf, TIMER_REGS_WIRE_BYTES);
|
|
}
|
|
|
|
/* ============================ SAVE ============================ */
|
|
|
|
/* Classic full VRAM section (offline / zlib / tracking off). */
|
|
static int write_vram_section_full(BsOut *o)
|
|
{
|
|
uint16_t *vbuf = (uint16_t *)malloc(VRAM_SIZE);
|
|
int ok;
|
|
if (!vbuf)
|
|
return 0;
|
|
gr_vram_transfer_out(0, 0, VRAM_W, VRAM_H, vbuf);
|
|
s_last_vram_dirty_rows = VRAM_H;
|
|
s_last_vram_incremental = 0;
|
|
#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
|
|
ok = write_section(o, BS_SEC_VRAM, vbuf, VRAM_SIZE);
|
|
#else
|
|
{
|
|
uint8_t *wire = (uint8_t *)malloc(VRAM_SIZE);
|
|
if (!wire) {
|
|
free(vbuf);
|
|
return 0;
|
|
}
|
|
{
|
|
PstW w;
|
|
pst_w_init(&w, wire, VRAM_SIZE);
|
|
ok = pst_w_pod(&w, vbuf, VRAM_SIZE, 2) &&
|
|
write_section(o, BS_SEC_VRAM, wire, VRAM_SIZE);
|
|
}
|
|
free(wire);
|
|
}
|
|
#endif
|
|
free(vbuf);
|
|
return ok;
|
|
}
|
|
|
|
static int boot_state_save_to(BsOut* o, const CPUState* cpu,
|
|
uint32_t bios_checksum, uint32_t entry_pc) {
|
|
BootStateHeader h;
|
|
int ok;
|
|
memset(&h, 0, sizeof h);
|
|
h.magic = BOOT_STATE_MAGIC;
|
|
h.version = BOOT_STATE_VERSION;
|
|
h.reserved = boot_state_layout_cookie();
|
|
h.bios_checksum = bios_checksum;
|
|
h.entry_pc = entry_pc;
|
|
h.codegen_hash = (uint32_t)PSX_OVERLAY_CODEGEN_HASH;
|
|
h.abi_tag = (int32_t)PSX_OVERLAY_ABI_TAG;
|
|
h.codegen_ver = BOOT_STATE_IMPLEMENTATION_KEY;
|
|
h.section_count = 18 + (cpu ? 1u : 0u) + (psx_mod_memory_snapshot_bytes() ? 1u : 0u) +
|
|
(psx_guest_cycle_scale_config() != 1u ? 1u : 0u);
|
|
|
|
/* Publish deferred cycle charges before ANY section: IRQ (cycles since
|
|
* VBlank) and the root counters were written before the old flush point
|
|
* (just ahead of CLOCK), so they lagged the saved clock by whatever batch
|
|
* was pending -- a host-path quantity (live vs rollback replay). */
|
|
psx_cyc_batch_flush();
|
|
/* Devices (root counters, VBlank phase, CD/SPU/DMA) are serviced lazily
|
|
* up to the next event deadline, so between deadlines they lag the clock
|
|
* by a host-path amount. A load re-anchors them at the restored clock,
|
|
* dropping that lag: live and replay saves of one tick then differed in
|
|
* cycles_since_vblank and the counters at the same clock. Bring every
|
|
* device to the clock first so the snap is the canonical machine at it
|
|
* (service_to_now replays the exact event-bounded timeline). */
|
|
psx_devices_service_to_now();
|
|
/* The SPU renders in whole samples when the host audio pump or a guest
|
|
* SPU access asks; between those it lags the clock by a path-dependent
|
|
* amount (live pumps at VBlank, rollback replay suppresses output). */
|
|
{ extern void spu_catch_up(void); spu_catch_up(); }
|
|
|
|
ok = write_header_le(o, &h);
|
|
|
|
if (ok) ok = write_cpu_section(o, cpu);
|
|
if (ok) ok = write_section(o, BS_SEC_RAM, memory_get_ram_ptr(), memory_get_ram_bytes());
|
|
if (ok) ok = write_section(o, BS_SEC_SPAD, memory_get_scratchpad_ptr(), SPAD_SIZE);
|
|
if (ok && psx_mod_memory_snapshot_bytes())
|
|
ok = write_module_section(o, BS_SEC_MODMEM, psx_mod_memory_snapshot_bytes,
|
|
psx_mod_memory_snapshot_write);
|
|
if (ok) {
|
|
/* 12B: i_stat, i_mask, cycles_since_vblank. Zeroing csv on warm load
|
|
* rebased every tip to phase 0 and forked MotK wait-loop resim
|
|
* (IRQ at CD54 vs CDA0). Selfcheck already restored csv out-of-band. */
|
|
uint8_t irq[12];
|
|
PstW w;
|
|
pst_w_init(&w, irq, sizeof irq);
|
|
ok = pst_w_u32(&w, i_stat) && pst_w_u32(&w, i_mask) &&
|
|
pst_w_u32(&w, interrupts_get_cycles_since_vblank()) &&
|
|
write_section(o, BS_SEC_IRQ, irq, 12);
|
|
}
|
|
if (ok) ok = write_timer_section(o);
|
|
if (ok) {
|
|
uint8_t cyc[8];
|
|
PstW w;
|
|
pst_w_init(&w, cyc, sizeof cyc);
|
|
/* Publish deferred load-charge batch before snapshotting the clock. */
|
|
psx_cyc_batch_flush();
|
|
ok = pst_w_u64(&w, psx_cycle_count) &&
|
|
write_section(o, BS_SEC_CLOCK, cyc, 8);
|
|
}
|
|
if (ok) ok = write_module_section(o, BS_SEC_GPU, gpu_snapshot_bytes, gpu_snapshot_write);
|
|
if (ok) {
|
|
/* §96 incremental mirror only while RB dirty-tracking is on.
|
|
* Offline / delay-sync / zlib disk: classic full transfer_out. */
|
|
if (o->no_zlib && gpu_vram_dirty_tracking()) {
|
|
ok = sync_vram_mirror_for_save();
|
|
if (ok) {
|
|
#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
|
|
ok = write_section(o, BS_SEC_VRAM, s_vram_mirror, VRAM_SIZE);
|
|
#else
|
|
{
|
|
uint8_t* wire = (uint8_t*)malloc(VRAM_SIZE);
|
|
if (!wire) ok = 0;
|
|
else {
|
|
PstW w;
|
|
pst_w_init(&w, wire, VRAM_SIZE);
|
|
ok = pst_w_pod(&w, s_vram_mirror, VRAM_SIZE, 2) &&
|
|
write_section(o, BS_SEC_VRAM, wire, VRAM_SIZE);
|
|
free(wire);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
} else {
|
|
ok = write_vram_section_full(o);
|
|
}
|
|
}
|
|
if (ok) ok = write_module_section(o, BS_SEC_SPU, spu_snapshot_bytes, spu_snapshot_write);
|
|
if (ok) ok = write_section(o, BS_SEC_SPURAM, spu_get_ram_ptr(), spu_get_ram_bytes());
|
|
if (ok) ok = write_module_section(o, BS_SEC_CDROM, cdrom_snapshot_bytes, cdrom_snapshot_write);
|
|
if (ok) ok = write_module_section(o, BS_SEC_DMA, dma_snapshot_bytes, dma_snapshot_write);
|
|
if (ok) ok = write_module_section(o, BS_SEC_SIO, sio_snapshot_bytes, sio_snapshot_write);
|
|
if (ok) ok = write_module_section(o, BS_SEC_MDEC, mdec_snapshot_bytes, mdec_snapshot_write);
|
|
if (ok) {
|
|
/* I-cache tags: warm loads must replay with the fetch-cost state the
|
|
* live timeline had, or miss cycles differ per peer/retry and IRQ
|
|
* delivery forks a few wait-loop iterations (MotK abort@940). */
|
|
uint8_t ib[1024u * 4u];
|
|
PstW w;
|
|
pst_w_init(&w, ib, sizeof ib);
|
|
ok = 1;
|
|
for (uint32_t i = 0; ok && i < 1024u; i++)
|
|
ok = pst_w_u32(&w, g_psx_icache_tv[i]);
|
|
if (ok) ok = write_section(o, BS_SEC_ICACHE, ib, sizeof ib);
|
|
}
|
|
if (ok) {
|
|
uint32_t wc = dirty_ram_get_bitmap_word_count();
|
|
uint64_t nbytes = (uint64_t)wc * 4u;
|
|
uint8_t* db = (uint8_t*)malloc(nbytes ? (size_t)nbytes : 1);
|
|
if (!db) ok = 0;
|
|
else {
|
|
PstW w;
|
|
pst_w_init(&w, db, (size_t)nbytes);
|
|
ok = 1;
|
|
for (uint32_t i = 0; ok && i < wc; i++)
|
|
ok = pst_w_u32(&w, dirty_ram_get_bitmap_word(i));
|
|
if (ok) ok = write_section(o, BS_SEC_DIRTY, db, nbytes);
|
|
free(db);
|
|
}
|
|
}
|
|
if (ok) {
|
|
uint8_t handoff[4];
|
|
PstW w;
|
|
pst_w_init(&w, handoff, sizeof handoff);
|
|
ok = pst_w_u32(&w, fntrace_is_game_started() ? 1u : 0u) &&
|
|
write_section(o, BS_SEC_GAME_START, handoff, sizeof handoff);
|
|
}
|
|
if (ok) {
|
|
extern uint32_t spu_cd_snapshot_bytes(void);
|
|
extern void spu_cd_snapshot_write(uint8_t *p);
|
|
ok = write_module_section(o, BS_SEC_SPUCD, spu_cd_snapshot_bytes, spu_cd_snapshot_write);
|
|
}
|
|
if (ok && cpu) {
|
|
/* In-flight CPU stalls, relative to the clock. They used to be
|
|
* re-anchored at the restored clock on every load, so a GTE / mul-div
|
|
* op or load give-back in flight at the snap point cost the live run
|
|
* its remaining stall but the replay nothing: replaying a snap with
|
|
* the same rows ran a few cycles short of the live tick
|
|
* (PSX_RB_DET_SELFTEST: IRQ/TIMER drift at equal clock). */
|
|
uint8_t tb[BS_CPUTIM_BYTES];
|
|
PstW w;
|
|
uint64_t now = psx_cycle_count;
|
|
uint32_t gte_rem = cpu->gte_ts_done > now ? (uint32_t)(cpu->gte_ts_done - now) : 0u;
|
|
uint32_t md_rem = cpu->muldiv_ts_done > now ? (uint32_t)(cpu->muldiv_ts_done - now) : 0u;
|
|
pst_w_init(&w, tb, sizeof tb);
|
|
ok = pst_w_u32(&w, gte_rem) && pst_w_u32(&w, md_rem) &&
|
|
pst_w_bytes(&w, cpu->read_absorb, sizeof cpu->read_absorb) &&
|
|
pst_w_u8(&w, cpu->read_absorb_which) && pst_w_u8(&w, cpu->read_fudge) &&
|
|
pst_w_u8(&w, cpu->ld_which_t) && pst_w_u32(&w, cpu->ld_absorb) &&
|
|
w.written == BS_CPUTIM_BYTES &&
|
|
write_section(o, BS_SEC_CPUTIM, tb, sizeof tb);
|
|
}
|
|
if (ok && psx_guest_cycle_scale_config() != 1u) {
|
|
/* Rollback re-simulation must resume with the same carried fraction
|
|
* and gate verdicts, or scaled charges land a cycle apart. */
|
|
uint32_t g[3];
|
|
uint8_t gb[PSX_GCS_SNAPSHOT_BYTES];
|
|
PstW w;
|
|
psx_guest_cycle_scale_snapshot(g);
|
|
pst_w_init(&w, gb, sizeof gb);
|
|
ok = pst_w_u32(&w, g[0]) && pst_w_u32(&w, g[1]) && pst_w_u32(&w, g[2]) &&
|
|
write_section(o, BS_SEC_GCS, gb, sizeof gb);
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
int boot_state_save(const CPUState* cpu, uint32_t bios_checksum,
|
|
uint32_t entry_pc, const char* path) {
|
|
BsOut o;
|
|
FILE* f = fopen(path, "wb");
|
|
int ok;
|
|
if (!f) return 0;
|
|
memset(&o, 0, sizeof o);
|
|
o.f = f;
|
|
ok = boot_state_save_to(&o, cpu, bios_checksum, entry_pc);
|
|
fclose(f);
|
|
if (!ok)
|
|
remove(path);
|
|
return ok;
|
|
}
|
|
|
|
static int boot_state_save_buffer_ex(const CPUState* cpu, uint32_t bios_checksum,
|
|
uint32_t entry_pc, uint8_t** out_data,
|
|
size_t* out_len, int no_zlib) {
|
|
BsOut o;
|
|
if (!out_data || !out_len) return 0;
|
|
*out_data = NULL;
|
|
*out_len = 0;
|
|
memset(&o, 0, sizeof o);
|
|
o.no_zlib = no_zlib ? 1 : 0;
|
|
/* Compressed MotK ~1.3-1.5 MiB; raw ~3.5-4 MiB (2 MB RAM) or ~9.5 MiB (8 MB). */
|
|
o.cap = no_zlib ? (12u * 1024u * 1024u) : (2u * 1024u * 1024u);
|
|
o.data = (uint8_t*)malloc(o.cap);
|
|
if (!o.data) return 0;
|
|
if (!boot_state_save_to(&o, cpu, bios_checksum, entry_pc)) {
|
|
free(o.data);
|
|
return 0;
|
|
}
|
|
*out_data = o.data;
|
|
*out_len = o.len;
|
|
return 1;
|
|
}
|
|
|
|
int boot_state_save_buffer(const CPUState* cpu, uint32_t bios_checksum,
|
|
uint32_t entry_pc, uint8_t** out_data,
|
|
size_t* out_len) {
|
|
return boot_state_save_buffer_ex(cpu, bios_checksum, entry_pc, out_data,
|
|
out_len, 0);
|
|
}
|
|
|
|
int boot_state_save_buffer_raw(const CPUState* cpu, uint32_t bios_checksum,
|
|
uint32_t entry_pc, uint8_t** out_data,
|
|
size_t* out_len) {
|
|
return boot_state_save_buffer_ex(cpu, bios_checksum, entry_pc, out_data,
|
|
out_len, 1);
|
|
}
|
|
|
|
/* ============================ LOAD ============================ */
|
|
|
|
static int apply_section(uint32_t tag, const uint8_t* p, uint32_t len,
|
|
CPUState* cpu, uint32_t entry_pc) {
|
|
switch (tag) {
|
|
case BS_SEC_CPU: {
|
|
PstR r;
|
|
if (len != CPU_REGS_WIRE_BYTES) return 0;
|
|
pst_r_init(&r, p, len);
|
|
for (int i = 0; i < 32; i++)
|
|
if (!pst_r_u32(&r, &cpu->gpr[i])) return 0;
|
|
if (!pst_r_u32(&r, &cpu->pc) || !pst_r_u32(&r, &cpu->hi) ||
|
|
!pst_r_u32(&r, &cpu->lo))
|
|
return 0;
|
|
(void)entry_pc;
|
|
for (int i = 0; i < 32; i++)
|
|
if (!pst_r_u32(&r, &cpu->cop0[i])) return 0;
|
|
for (int i = 0; i < 32; i++)
|
|
if (!pst_r_u32(&r, &cpu->gte_data[i])) return 0;
|
|
for (int i = 0; i < 32; i++)
|
|
if (!pst_r_u32(&r, &cpu->gte_ctrl[i])) return 0;
|
|
/* Architectural normalize + drop host-only projection provenance that
|
|
* belonged to the pre-load timeline (not part of the wire format). */
|
|
gte_canonicalize_cpu_state(cpu);
|
|
return 1;
|
|
}
|
|
case BS_SEC_RAM:
|
|
if (len != memory_get_ram_bytes()) return 0;
|
|
memcpy(memory_get_ram_ptr(), p, memory_get_ram_bytes());
|
|
{
|
|
extern void psx_kernel_bless_note_range(uint32_t phys, uint32_t l);
|
|
psx_kernel_bless_note_range(0, memory_get_ram_bytes());
|
|
}
|
|
return 1;
|
|
case BS_SEC_SPAD:
|
|
if (len != SPAD_SIZE) return 0;
|
|
memcpy(memory_get_scratchpad_ptr(), p, SPAD_SIZE);
|
|
return 1;
|
|
case BS_SEC_IRQ: {
|
|
PstR r;
|
|
uint32_t st, mk, csv;
|
|
if (len != 8 && len != 12) return 0;
|
|
pst_r_init(&r, p, len);
|
|
if (!pst_r_u32(&r, &st) || !pst_r_u32(&r, &mk)) return 0;
|
|
i_stat = st;
|
|
i_mask = mk;
|
|
if (len == 12) {
|
|
if (!pst_r_u32(&r, &csv)) return 0;
|
|
interrupts_set_cycles_since_vblank(csv);
|
|
} else {
|
|
/* Legacy UI/disk snaps: no phase — rebase like pre-csv saves. */
|
|
interrupts_set_cycles_since_vblank(0);
|
|
}
|
|
return 1;
|
|
}
|
|
case BS_SEC_TIMER: {
|
|
uint16_t counter[3], target[3];
|
|
uint32_t mode[3], frac[3];
|
|
int32_t irq_line[3];
|
|
PstR r;
|
|
if (len != TIMER_REGS_WIRE_BYTES) return 0;
|
|
pst_r_init(&r, p, len);
|
|
for (int i = 0; i < 3; i++)
|
|
if (!pst_r_u16(&r, &counter[i])) return 0;
|
|
for (int i = 0; i < 3; i++)
|
|
if (!pst_r_u32(&r, &mode[i])) return 0;
|
|
for (int i = 0; i < 3; i++)
|
|
if (!pst_r_u16(&r, &target[i])) return 0;
|
|
for (int i = 0; i < 3; i++)
|
|
if (!pst_r_i32(&r, &irq_line[i])) return 0;
|
|
for (int i = 0; i < 3; i++)
|
|
if (!pst_r_u32(&r, &frac[i])) return 0;
|
|
timers_set_snapshot(counter, mode, target, irq_line, frac);
|
|
return 1;
|
|
}
|
|
case BS_SEC_CLOCK: {
|
|
PstR r;
|
|
uint64_t cyc;
|
|
if (len != 8) return 0;
|
|
pst_r_init(&r, p, len);
|
|
if (!pst_r_u64(&r, &cyc)) return 0;
|
|
psx_cycle_count = cyc;
|
|
g_psx_gcs_frac = 0u; /* BS_SEC_GCS (if present) restores it */
|
|
return 1;
|
|
}
|
|
case BS_SEC_GPU:
|
|
return gpu_snapshot_read(p, len);
|
|
case BS_SEC_VRAM: {
|
|
if (len != VRAM_SIZE) return 0;
|
|
#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
|
|
/* Wire == host layout: upload straight from the section buffer. */
|
|
gr_vram_transfer_in(0, 0, VRAM_W, VRAM_H, (const uint16_t*)p);
|
|
if (gpu_vram_dirty_tracking()) {
|
|
memcpy(s_vram_mirror, p, VRAM_SIZE);
|
|
s_vram_mirror_valid = 1;
|
|
gpu_vram_dirty_clear();
|
|
} else {
|
|
s_vram_mirror_valid = 0;
|
|
}
|
|
return 1;
|
|
#else
|
|
{
|
|
uint16_t* vbuf;
|
|
PstR r;
|
|
vbuf = (uint16_t*)malloc(VRAM_SIZE);
|
|
if (!vbuf) return 0;
|
|
pst_r_init(&r, p, len);
|
|
if (!pst_r_pod(&r, vbuf, VRAM_SIZE, 2)) {
|
|
free(vbuf);
|
|
return 0;
|
|
}
|
|
gr_vram_transfer_in(0, 0, VRAM_W, VRAM_H, vbuf);
|
|
if (gpu_vram_dirty_tracking()) {
|
|
memcpy(s_vram_mirror, vbuf, VRAM_SIZE);
|
|
s_vram_mirror_valid = 1;
|
|
gpu_vram_dirty_clear();
|
|
} else {
|
|
s_vram_mirror_valid = 0;
|
|
}
|
|
free(vbuf);
|
|
return 1;
|
|
}
|
|
#endif
|
|
}
|
|
case BS_SEC_SPU:
|
|
return spu_snapshot_read(p, len);
|
|
case BS_SEC_SPURAM:
|
|
if (len != spu_get_ram_bytes()) return 0;
|
|
memcpy(spu_get_ram_ptr(), p, len);
|
|
return 1;
|
|
case BS_SEC_CDROM:
|
|
return cdrom_snapshot_read(p, len);
|
|
case BS_SEC_DMA:
|
|
return dma_snapshot_read(p, len);
|
|
case BS_SEC_SIO:
|
|
return sio_snapshot_read(p, len);
|
|
case BS_SEC_MDEC:
|
|
return mdec_snapshot_read(p, len);
|
|
case BS_SEC_DIRTY: {
|
|
uint32_t wc;
|
|
uint32_t* words;
|
|
PstR r;
|
|
if (len % 4u) return 0;
|
|
wc = len / 4u;
|
|
words = (uint32_t*)malloc(len ? len : 1);
|
|
if (!words) return 0;
|
|
pst_r_init(&r, p, len);
|
|
for (uint32_t i = 0; i < wc; i++) {
|
|
if (!pst_r_u32(&r, &words[i])) {
|
|
free(words);
|
|
return 0;
|
|
}
|
|
}
|
|
dirty_ram_set_bitmap_words(words, wc);
|
|
free(words);
|
|
return 1;
|
|
}
|
|
case BS_SEC_MODMEM:
|
|
return psx_mod_memory_snapshot_read(p, len);
|
|
case BS_SEC_GAME_START:
|
|
fntrace_restore_game_started(p[0]);
|
|
return 1;
|
|
case BS_SEC_SPUCD: {
|
|
extern int spu_cd_snapshot_read(const uint8_t *p, uint32_t len);
|
|
return spu_cd_snapshot_read(p, len);
|
|
}
|
|
case BS_SEC_CPUTIM: {
|
|
PstR r;
|
|
uint32_t gte_rem, md_rem;
|
|
if (len != BS_CPUTIM_BYTES || !cpu) return 0;
|
|
pst_r_init(&r, p, len);
|
|
if (!pst_r_u32(&r, >e_rem) || !pst_r_u32(&r, &md_rem) ||
|
|
!pst_r_bytes(&r, cpu->read_absorb, sizeof cpu->read_absorb) ||
|
|
!pst_r_u8(&r, &cpu->read_absorb_which) || !pst_r_u8(&r, &cpu->read_fudge) ||
|
|
!pst_r_u8(&r, &cpu->ld_which_t) || !pst_r_u32(&r, &cpu->ld_absorb))
|
|
return 0;
|
|
/* CLOCK precedes this section, so psx_cycle_count is restored. */
|
|
cpu->gte_ts_done = psx_cycle_count + gte_rem;
|
|
cpu->muldiv_ts_done = psx_cycle_count + md_rem;
|
|
g_boot_state_cpu_timing_restored = 1;
|
|
return 1;
|
|
}
|
|
case BS_SEC_GCS: {
|
|
PstR r;
|
|
uint32_t g[3];
|
|
if (len != PSX_GCS_SNAPSHOT_BYTES) return 0;
|
|
pst_r_init(&r, p, len);
|
|
if (!pst_r_u32(&r, &g[0]) || !pst_r_u32(&r, &g[1]) || !pst_r_u32(&r, &g[2]))
|
|
return 0;
|
|
psx_guest_cycle_scale_restore(g);
|
|
return 1;
|
|
}
|
|
case BS_SEC_ICACHE: {
|
|
PstR r;
|
|
if (len != 1024u * 4u) return 0;
|
|
pst_r_init(&r, p, len);
|
|
for (uint32_t i = 0; i < 1024u; i++)
|
|
if (!pst_r_u32(&r, &g_psx_icache_tv[i])) return 0;
|
|
return 1;
|
|
}
|
|
default:
|
|
/* Unknown section: SKIP, never fail. This was `return 0`, which made
|
|
* every state written by a build with one extra section a poison pill
|
|
* for every build without it -- and worse than unloadable: the apply
|
|
* loop had already restored RAM/VRAM/CPU by the time it hit the
|
|
* unknown tag, so the refusal left a HALF-RESTORED machine that
|
|
* wedged or died at PC=0. A sectioned state format exists precisely
|
|
* so readers can step over what they do not know. */
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/* Pass-1 acceptance of one section: 1 iff apply_section(tag, p, len) cannot
|
|
* fail. It mutates nothing, so the loader can refuse a state before the first
|
|
* byte of machine state changes. Every check here is the exact precondition
|
|
* the corresponding reader applies; readers that only ever read fixed-width
|
|
* fields fail only on length. Unknown tags are skipped, as in apply_section. */
|
|
/* One framed section, inflated if needed, held until pass 2 has applied it. */
|
|
typedef struct {
|
|
uint32_t tag;
|
|
const uint8_t* p;
|
|
uint32_t len;
|
|
uint8_t* owned; /* inflated payload (NULL when p points into file) */
|
|
} BsSection;
|
|
/* Current writers emit 16-17 sections; the cap only bounds a hostile count. */
|
|
#define BOOT_STATE_MAX_SECTIONS 256u
|
|
|
|
static int validate_section(uint32_t tag, const uint8_t* p, uint32_t len) {
|
|
switch (tag) {
|
|
case BS_SEC_CPU: return len == CPU_REGS_WIRE_BYTES;
|
|
case BS_SEC_RAM: return len == memory_get_ram_bytes();
|
|
case BS_SEC_SPAD: return len == SPAD_SIZE;
|
|
case BS_SEC_IRQ: return len == 8u || len == 12u;
|
|
case BS_SEC_TIMER: return len == TIMER_REGS_WIRE_BYTES;
|
|
case BS_SEC_CLOCK: return len == 8u;
|
|
case BS_SEC_GPU: return len == gpu_snapshot_bytes();
|
|
case BS_SEC_VRAM: return len == VRAM_SIZE;
|
|
case BS_SEC_SPU: return spu_snapshot_len_ok(len);
|
|
case BS_SEC_SPURAM: return len == spu_get_ram_bytes();
|
|
case BS_SEC_CDROM: return cdrom_snapshot_validate(p, len);
|
|
case BS_SEC_DMA: return len == dma_snapshot_bytes();
|
|
case BS_SEC_SIO: return sio_snapshot_validate(p, len);
|
|
case BS_SEC_MDEC: return mdec_snapshot_validate(p, len);
|
|
/* A longer dirty bitmap than the live RAM has pages is a state from a
|
|
* larger geometry; shorter (older) bitmaps zero-fill as before. */
|
|
case BS_SEC_DIRTY: return len % 4u == 0u &&
|
|
len / 4u <= dirty_ram_get_bitmap_word_count();
|
|
case BS_SEC_MODMEM: return psx_mod_memory_snapshot_validate(p, len);
|
|
case BS_SEC_GAME_START:
|
|
return len == 4u && p[0] <= 1u && !p[1] && !p[2] && !p[3];
|
|
case BS_SEC_ICACHE: return len == 1024u * 4u;
|
|
case BS_SEC_GCS: return len == PSX_GCS_SNAPSHOT_BYTES;
|
|
case BS_SEC_CPUTIM: return len == BS_CPUTIM_BYTES;
|
|
case BS_SEC_SPUCD: return len >= 4u && (len - 4u) % 4u == 0u;
|
|
default: return 1;
|
|
}
|
|
}
|
|
|
|
static int boot_state_parse_header(const uint8_t* file, size_t file_len,
|
|
BootStateHeader* h_out) {
|
|
PstR hr;
|
|
if (!file || !h_out || file_len < BOOT_STATE_HEADER_WIRE_BYTES ||
|
|
file_len > 64u * 1024u * 1024u) {
|
|
return 0;
|
|
}
|
|
pst_r_init(&hr, file, BOOT_STATE_HEADER_WIRE_BYTES);
|
|
memset(h_out, 0, sizeof(*h_out));
|
|
if (!pst_r_u32(&hr, &h_out->magic) ||
|
|
!pst_r_u32(&hr, &h_out->version) ||
|
|
!pst_r_u32(&hr, &h_out->bios_checksum) ||
|
|
!pst_r_u32(&hr, &h_out->entry_pc) ||
|
|
!pst_r_u32(&hr, &h_out->codegen_hash) ||
|
|
!pst_r_i32(&hr, &h_out->abi_tag) ||
|
|
!pst_r_u32(&hr, &h_out->codegen_ver) ||
|
|
!pst_r_u32(&hr, &h_out->section_count) ||
|
|
!pst_r_u32(&hr, &h_out->reserved)) {
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static void boot_state_append_reason(char* reason, size_t reason_cap,
|
|
const char* part) {
|
|
size_t n;
|
|
if (!reason || reason_cap == 0 || !part || !part[0]) return;
|
|
n = strlen(reason);
|
|
if (n + 1 >= reason_cap) return;
|
|
if (n > 0) {
|
|
reason[n++] = ',';
|
|
reason[n] = '\0';
|
|
if (n + 1 >= reason_cap) return;
|
|
}
|
|
snprintf(reason + n, reason_cap - n, "%s", part);
|
|
}
|
|
|
|
int boot_state_check_buffer(const uint8_t* file, size_t file_len,
|
|
uint32_t bios_checksum, uint32_t entry_pc,
|
|
char* reason, size_t reason_cap) {
|
|
BootStateHeader h;
|
|
char part[96];
|
|
|
|
if (reason && reason_cap)
|
|
reason[0] = '\0';
|
|
|
|
if (!file || file_len < BOOT_STATE_HEADER_WIRE_BYTES) {
|
|
boot_state_append_reason(reason, reason_cap, "missing_or_truncated");
|
|
return 0;
|
|
}
|
|
if (file_len > 64u * 1024u * 1024u) {
|
|
boot_state_append_reason(reason, reason_cap, "too_large");
|
|
return 0;
|
|
}
|
|
if (!boot_state_parse_header(file, file_len, &h)) {
|
|
boot_state_append_reason(reason, reason_cap, "header_parse");
|
|
return 0;
|
|
}
|
|
|
|
if (h.magic != BOOT_STATE_MAGIC) {
|
|
snprintf(part, sizeof(part), "magic=%08X(want %08X)",
|
|
(unsigned)h.magic, (unsigned)BOOT_STATE_MAGIC);
|
|
boot_state_append_reason(reason, reason_cap, part);
|
|
}
|
|
if (h.reserved != boot_state_layout_cookie()) {
|
|
if ((h.reserved ^ BOOT_STATE_RAM_8MB_COOKIE) == boot_state_layout_cookie()) {
|
|
snprintf(part, sizeof(part), "main_ram=%uMiB(want %uMiB)",
|
|
psx_ram_8mb_active() ? 2u : 8u,
|
|
(unsigned)(memory_get_ram_bytes() >> 20));
|
|
boot_state_append_reason(reason, reason_cap, part);
|
|
} else {
|
|
boot_state_append_reason(reason, reason_cap, "enhancement_memory_layout_or_execution_profile");
|
|
}
|
|
return 0;
|
|
}
|
|
if (h.version < BOOT_STATE_VERSION_MIN_READ ||
|
|
h.version > BOOT_STATE_VERSION) {
|
|
snprintf(part, sizeof(part), "version=%u(want %u..%u)",
|
|
(unsigned)h.version, (unsigned)BOOT_STATE_VERSION_MIN_READ,
|
|
(unsigned)BOOT_STATE_VERSION);
|
|
boot_state_append_reason(reason, reason_cap, part);
|
|
}
|
|
if (h.bios_checksum != bios_checksum) {
|
|
snprintf(part, sizeof(part), "bios=%08X(want %08X)",
|
|
(unsigned)h.bios_checksum, (unsigned)bios_checksum);
|
|
boot_state_append_reason(reason, reason_cap, part);
|
|
}
|
|
if (h.entry_pc != entry_pc) {
|
|
snprintf(part, sizeof(part), "entry=%08X(want %08X)",
|
|
(unsigned)h.entry_pc, (unsigned)entry_pc);
|
|
boot_state_append_reason(reason, reason_cap, part);
|
|
}
|
|
if (h.codegen_hash != (uint32_t)PSX_OVERLAY_CODEGEN_HASH) {
|
|
snprintf(part, sizeof(part), "codegen_hash=%08X(want %08X)",
|
|
(unsigned)h.codegen_hash,
|
|
(unsigned)PSX_OVERLAY_CODEGEN_HASH);
|
|
boot_state_append_reason(reason, reason_cap, part);
|
|
}
|
|
if (h.abi_tag != (int32_t)PSX_OVERLAY_ABI_TAG) {
|
|
snprintf(part, sizeof(part), "abi_tag=%d(want %d)",
|
|
(int)h.abi_tag, (int)PSX_OVERLAY_ABI_TAG);
|
|
boot_state_append_reason(reason, reason_cap, part);
|
|
}
|
|
if (h.codegen_ver != BOOT_STATE_IMPLEMENTATION_KEY) {
|
|
snprintf(part, sizeof(part), "codegen_ver=%u(want %u)",
|
|
(unsigned)h.codegen_ver, (unsigned)BOOT_STATE_IMPLEMENTATION_KEY);
|
|
boot_state_append_reason(reason, reason_cap, part);
|
|
}
|
|
|
|
if (reason && reason_cap && reason[0])
|
|
return 0;
|
|
return 1;
|
|
}
|
|
|
|
int boot_state_load_buffer(const uint8_t* file, size_t file_len,
|
|
uint32_t bios_checksum, uint32_t entry_pc,
|
|
CPUState* cpu) {
|
|
const uint8_t* cur;
|
|
const uint8_t* end;
|
|
BootStateHeader h;
|
|
char reject[256];
|
|
const uint32_t required =
|
|
(1u<<BS_SEC_CPU)|(1u<<BS_SEC_RAM)|(1u<<BS_SEC_SPAD)|(1u<<BS_SEC_IRQ)|
|
|
(1u<<BS_SEC_TIMER)|(1u<<BS_SEC_CLOCK)|(1u<<BS_SEC_GPU)|(1u<<BS_SEC_VRAM)|
|
|
(1u<<BS_SEC_SPU)|(1u<<BS_SEC_SPURAM)|(1u<<BS_SEC_CDROM)|(1u<<BS_SEC_DMA)|
|
|
(1u<<BS_SEC_SIO)|(1u<<BS_SEC_MDEC)|(1u<<BS_SEC_DIRTY)|
|
|
(psx_mod_memory_snapshot_bytes() ? (1u<<BS_SEC_MODMEM) : 0u);
|
|
uint32_t seen = 0;
|
|
int ok = 1;
|
|
const double t0 = boot_state_mono_ms();
|
|
double inflate_ms = 0.0;
|
|
double apply_ram_ms = 0.0;
|
|
double apply_vram_ms = 0.0;
|
|
double apply_spuram_ms = 0.0;
|
|
double apply_other_ms = 0.0;
|
|
BsSection* sections = NULL;
|
|
uint32_t n_sections = 0;
|
|
|
|
if (!boot_state_check_buffer(file, file_len, bios_checksum, entry_pc,
|
|
reject, sizeof(reject))) {
|
|
fprintf(stderr, "boot_state: reject — %s\n",
|
|
reject[0] ? reject : "unknown");
|
|
return 0;
|
|
}
|
|
if (!boot_state_parse_header(file, file_len, &h))
|
|
return 0;
|
|
if (h.section_count == 0u || h.section_count > BOOT_STATE_MAX_SECTIONS)
|
|
return 0;
|
|
sections = (BsSection*)calloc(h.section_count, sizeof(*sections));
|
|
if (!sections) return 0;
|
|
|
|
cur = file + BOOT_STATE_HEADER_WIRE_BYTES;
|
|
end = file + file_len;
|
|
|
|
/* PASS 1 -- frame, inflate and validate EVERY section; mutate nothing.
|
|
* The old single pass applied each section as it was parsed, so a state
|
|
* rejected at section N (a RAM section of the other geometry, a corrupt
|
|
* tail, a missing required section) left sections 0..N-1 -- the CPU
|
|
* first of all -- already restored: savestate/rewind/netplay then
|
|
* reported a failed load over a half-overwritten machine. */
|
|
for (uint32_t i = 0; ok && i < h.section_count; i++) {
|
|
PstR sh;
|
|
uint32_t tag = 0, pad = 0;
|
|
uint64_t len = 0;
|
|
const uint8_t* payload;
|
|
BsSection* s = §ions[n_sections];
|
|
|
|
if ((size_t)(end - cur) < 16u) { ok = 0; break; }
|
|
pst_r_init(&sh, cur, 16);
|
|
if (!pst_r_u32(&sh, &tag) || !pst_r_u32(&sh, &pad) || !pst_r_u64(&sh, &len)) {
|
|
ok = 0; break;
|
|
}
|
|
cur += 16;
|
|
if (len > 64u * 1024u * 1024u || (uint64_t)(end - cur) < len) {
|
|
ok = 0; break;
|
|
}
|
|
payload = cur;
|
|
cur += (size_t)len;
|
|
|
|
if (h.version >= 4u && pad == BOOT_STATE_SEC_ZLIB) {
|
|
PstR lr;
|
|
uint32_t raw_len = 0;
|
|
uLong dest_len;
|
|
double t_inf;
|
|
if (len < 4u) { ok = 0; break; }
|
|
pst_r_init(&lr, payload, 4);
|
|
if (!pst_r_u32(&lr, &raw_len) || raw_len == 0 ||
|
|
raw_len > 64u * 1024u * 1024u) {
|
|
ok = 0; break;
|
|
}
|
|
s->owned = (uint8_t*)malloc(raw_len);
|
|
if (!s->owned) { ok = 0; break; }
|
|
n_sections++; /* owned buffer is freed below on every path */
|
|
dest_len = (uLong)raw_len;
|
|
t_inf = boot_state_mono_ms();
|
|
if (uncompress(s->owned, &dest_len, payload + 4,
|
|
(uLong)(len - 4u)) != Z_OK ||
|
|
dest_len != (uLong)raw_len) {
|
|
ok = 0;
|
|
break;
|
|
}
|
|
inflate_ms += boot_state_mono_ms() - t_inf;
|
|
s->p = s->owned;
|
|
s->len = raw_len;
|
|
} else if (pad != 0u) {
|
|
/* v3 requires pad==0; v4 unknown/extra flags are a hard reject. */
|
|
ok = 0;
|
|
break;
|
|
} else {
|
|
if (len > 0xffffffffu) { ok = 0; break; }
|
|
n_sections++;
|
|
s->p = payload;
|
|
s->len = (uint32_t)len;
|
|
}
|
|
s->tag = tag;
|
|
if (!validate_section(tag, s->p, s->len)) { ok = 0; break; }
|
|
if (tag == BS_SEC_GAME_START && (seen & (1u << tag))) {
|
|
ok = 0; break;
|
|
}
|
|
if (tag < 32) seen |= (1u << tag);
|
|
}
|
|
if (ok && (seen & required) != required)
|
|
ok = 0;
|
|
if (ok && h.version >= 9u && !(seen & (1u << BS_SEC_GAME_START)))
|
|
ok = 0;
|
|
|
|
/* PASS 2 -- apply. Every section passed validate_section, whose checks
|
|
* are exactly the preconditions apply_section can fail on, so this pass
|
|
* does not fail on well-framed input; the check below stays as the
|
|
* backstop. test_boot_state_load_atomic pins both passes. */
|
|
for (uint32_t i = 0; ok && i < n_sections; i++) {
|
|
const double t_sec = boot_state_mono_ms();
|
|
if (!apply_section(sections[i].tag, sections[i].p, sections[i].len,
|
|
cpu, entry_pc))
|
|
ok = 0;
|
|
{
|
|
const double dt = boot_state_mono_ms() - t_sec;
|
|
if (sections[i].tag == BS_SEC_RAM) apply_ram_ms += dt;
|
|
else if (sections[i].tag == BS_SEC_VRAM) apply_vram_ms += dt;
|
|
else if (sections[i].tag == BS_SEC_SPURAM) apply_spuram_ms += dt;
|
|
else apply_other_ms += dt;
|
|
}
|
|
}
|
|
for (uint32_t i = 0; i < n_sections; i++)
|
|
free(sections[i].owned);
|
|
free(sections);
|
|
if (!ok)
|
|
return 0;
|
|
|
|
/* RAM was memcpy'd; force overlay revalidation before resume. */
|
|
overlay_watch_invalidate_after_ram_restore();
|
|
|
|
{
|
|
const double total_ms = boot_state_mono_ms() - t0;
|
|
fprintf(stderr,
|
|
"savestate: load_timing read=0.0 inflate=%.1f "
|
|
"apply_ram=%.1f apply_vram=%.1f apply_spuram=%.1f "
|
|
"apply_other=%.1f total=%.1f ms (file=%zu)\n",
|
|
inflate_ms,
|
|
apply_ram_ms, apply_vram_ms, apply_spuram_ms,
|
|
apply_other_ms, total_ms, file_len);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
int boot_state_load(const char* path, uint32_t bios_checksum,
|
|
uint32_t entry_pc, CPUState* cpu) {
|
|
FILE* f = fopen(path, "rb");
|
|
long sz;
|
|
uint8_t* file = NULL;
|
|
size_t file_len = 0;
|
|
int ok;
|
|
const double t0 = boot_state_mono_ms();
|
|
double t_after_read;
|
|
|
|
if (!f) {
|
|
fprintf(stderr, "boot_state: reject — missing %s\n",
|
|
path ? path : "(null)");
|
|
return 0;
|
|
}
|
|
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return 0; }
|
|
sz = ftell(f);
|
|
if (sz < (long)BOOT_STATE_HEADER_WIRE_BYTES || sz > 64L * 1024L * 1024L) {
|
|
fprintf(stderr, "boot_state: reject — bad size %ld for %s\n",
|
|
sz, path ? path : "(null)");
|
|
fclose(f);
|
|
return 0;
|
|
}
|
|
if (fseek(f, 0, SEEK_SET) != 0) { fclose(f); return 0; }
|
|
file_len = (size_t)sz;
|
|
file = (uint8_t*)malloc(file_len);
|
|
if (!file) { fclose(f); return 0; }
|
|
if (fread(file, 1, file_len, f) != file_len) {
|
|
free(file);
|
|
fclose(f);
|
|
return 0;
|
|
}
|
|
fclose(f);
|
|
t_after_read = boot_state_mono_ms();
|
|
(void)t0;
|
|
(void)t_after_read;
|
|
|
|
ok = boot_state_load_buffer(file, file_len, bios_checksum, entry_pc, cpu);
|
|
free(file);
|
|
return ok;
|
|
}
|
|
|
|
void boot_state_set_capture(const char* path, uint32_t bios_checksum,
|
|
uint32_t entry_pc) {
|
|
strncpy(s_capture_path, path, sizeof(s_capture_path) - 1);
|
|
s_capture_path[sizeof(s_capture_path) - 1] = '\0';
|
|
s_capture_checksum = bios_checksum;
|
|
s_capture_entry_pc = entry_pc;
|
|
}
|
|
|
|
void boot_state_trigger_capture(const CPUState* cpu) {
|
|
if (!s_capture_path[0]) return;
|
|
boot_state_save(cpu, s_capture_checksum, s_capture_entry_pc, s_capture_path);
|
|
s_capture_path[0] = '\0';
|
|
}
|