Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 162 additions & 3 deletions docs/savestate-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ PHPBoy savestates capture the complete state of the emulator at a specific point
"ppu": { ... },
"memory": { ... },
"cartridge": { ... },
"timer": { ... },
"interrupts": { ... },
"cgb": { ... },
"apu": { ... },
"clock": { ... }
}
```
Expand Down Expand Up @@ -66,7 +70,13 @@ PHPBoy savestates capture the complete state of the emulator at a specific point
"stat": 0x00,
"bgp": 0xFC,
"obp0": 0xFF,
"obp1": 0xFF
"obp1": 0xFF,
"cgbPalette": {
"bgPalette": "base64-encoded data (64 bytes)",
"objPalette": "base64-encoded data (64 bytes)",
"bgIndex": 0x00,
"objIndex": 0x00
}
}
```

Expand All @@ -79,20 +89,36 @@ PHPBoy savestates capture the complete state of the emulator at a specific point
- **lcdc** (integer): LCD Control register
- **stat** (integer): LCD Status register
- **bgp, obp0, obp1** (integer): DMG palette registers
- **cgbPalette** (object): CGB color palette state (optional, CGB only)
- **bgPalette** (string): Base64-encoded background palette memory (8 palettes × 4 colors × 2 bytes = 64 bytes)
- **objPalette** (string): Base64-encoded object palette memory (8 palettes × 4 colors × 2 bytes = 64 bytes)
- **bgIndex** (integer): Background palette index register (BCPS/BGPI) with auto-increment flag
- **objIndex** (integer): Object palette index register (OCPS/OBPI) with auto-increment flag

### Memory State

```json
"memory": {
"vram": "base64-encoded data (8KB)",
"vramBank0": "base64-encoded data (8KB)",
"vramBank1": "base64-encoded data (8KB)",
"vramCurrentBank": 0,
"wram": "base64-encoded data (8KB)",
"hram": "base64-encoded data (127 bytes)",
"oam": "base64-encoded data (160 bytes)"
}
```

- **vramBank0** (string): Base64-encoded VRAM bank 0 (8KB)
- **vramBank1** (string): Base64-encoded VRAM bank 1 (8KB, CGB only)
- **vramCurrentBank** (integer): Currently selected VRAM bank (0 or 1, CGB only)
- **wram** (string): Base64-encoded work RAM (8KB)
- **hram** (string): Base64-encoded high RAM (127 bytes, 0xFF80-0xFFFE)
- **oam** (string): Base64-encoded OAM sprite attribute table (160 bytes)

All memory regions are base64-encoded for compact storage.

**Note:** For backward compatibility, old savestates with single `"vram"` field are still supported and will only restore bank 0.

### Cartridge State

```json
Expand All @@ -109,6 +135,99 @@ All memory regions are base64-encoded for compact storage.
- **ramEnabled** (boolean): RAM enable state
- **ram** (string): Base64-encoded cartridge RAM (size varies by cartridge)

### Timer State

```json
"timer": {
"div": 0xAB,
"divCounter": 1234,
"tima": 0x00,
"tma": 0x00,
"tac": 0x00,
"timaCounter": 0
}
```

- **div** (integer): DIV register (0xFF04) - Divider register value (0x00-0xFF)
- **divCounter** (integer): Internal 16-bit divider counter
- **tima** (integer): TIMA register (0xFF05) - Timer counter (0x00-0xFF)
- **tma** (integer): TMA register (0xFF06) - Timer modulo (0x00-0xFF)
- **tac** (integer): TAC register (0xFF07) - Timer control (0x00-0x07)
- **timaCounter** (integer): Internal TIMA counter accumulator

**Optional:** This field is optional for backward compatibility. If missing, timer state will be initialized to default values.

### Interrupt State

```json
"interrupts": {
"if": 0xE0,
"ie": 0x00
}
```

- **if** (integer): IF register (0xFF0F) - Interrupt flags (bits 0-4: VBlank, LCD, Timer, Serial, Joypad)
- **ie** (integer): IE register (0xFFFF) - Interrupt enable mask (bits 0-4)

**Optional:** This field is optional for backward compatibility. If missing, interrupt state will be initialized to default values.

### CGB Controller State

```json
"cgb": {
"key0": 0x80,
"key1": 0x00,
"opri": 0x00,
"doubleSpeed": false,
"key0Writable": false
}
```

- **key0** (integer): KEY0 register (0xFF4C) - CGB mode indicator (0x04=DMG compat, 0x80=CGB mode)
- **key1** (integer): KEY1 register (0xFF4D) - Speed switch control (bit 0: prepare switch)
- **opri** (integer): OPRI register (0xFF6C) - Object priority mode (bit 0)
- **doubleSpeed** (boolean): Current speed mode (false=normal 4MHz, true=double 8MHz)
- **key0Writable** (boolean): Whether KEY0 register is still writable (locked after boot ROM disable)

**Optional:** This field is optional for backward compatibility. If missing, CGB state will be initialized based on cartridge type.

### APU State

```json
"apu": {
"registers": {
"nr10": 0x80, "nr11": 0xBF, "nr12": 0xF3, "nr13": 0xFF, "nr14": 0xBF,
"nr21": 0x3F, "nr22": 0x00, "nr23": 0xFF, "nr24": 0xBF,
"nr30": 0x7F, "nr31": 0xFF, "nr32": 0x9F, "nr33": 0xFF, "nr34": 0xBF,
"nr41": 0xFF, "nr42": 0x00, "nr43": 0x00, "nr44": 0xBF,
"nr50": 0x77, "nr51": 0xF3, "nr52": 0xF1
},
"waveRam": "base64-encoded data (16 bytes)",
"frameSequencerCycles": 0,
"frameSequencerStep": 0,
"sampleCycles": 0.0,
"enabled": true
}
```

- **registers** (object): All APU control registers
- **nr10-nr14** (integers): Channel 1 (square with sweep) registers
- **nr21-nr24** (integers): Channel 2 (square) registers
- **nr30-nr34** (integers): Channel 3 (wave) registers
- **nr41-nr44** (integers): Channel 4 (noise) registers
- **nr50** (integer): Master volume and VIN panning
- **nr51** (integer): Sound panning for all channels
- **nr52** (integer): Sound on/off and channel status
- **waveRam** (string): Base64-encoded Wave RAM (16 bytes, 0xFF30-0xFF3F) for Channel 3
- **frameSequencerCycles** (integer): Frame sequencer cycle accumulator
- **frameSequencerStep** (integer): Current frame sequencer step (0-7)
- **sampleCycles** (float): Sample generation cycle accumulator
- **enabled** (boolean): Master APU enable state

**Optional:** This field is optional for backward compatibility. If missing, APU will be initialized to default state.

**Note:** This saves APU register state and basic timing, but NOT full channel internal state (frequency timers, length counters, envelope timers, duty positions). Audio restoration is partial - basic configuration is preserved but channel timing may drift slightly.

### Clock State

```json
Expand All @@ -125,6 +244,17 @@ All memory regions are base64-encoded for compact storage.

Savestates include a version number. Loading a savestate with a different version will fail with an error message indicating the version mismatch.

### Backward Compatibility

The savestate format maintains backward compatibility with older versions:

- **Required fields:** `magic`, `version`, `cpu`, `ppu`, `memory`, `cartridge`, `clock` (always present)
- **Optional fields:** `timer`, `interrupts`, `cgb`, `apu` (gracefully handle missing data)
- **Old VRAM format:** Single `"vram"` field is still supported for compatibility with pre-1.0 savestates
- **Missing fields:** If optional fields are missing, they are initialized to sensible defaults

This allows newer emulator versions to load older savestates, though some state (timer, interrupts, etc.) will be reset to defaults.

### Future Compatibility

Future versions may add new fields but must maintain backward compatibility for core fields. Optional fields should have sensible defaults.
Expand Down Expand Up @@ -157,4 +287,33 @@ $manager->deserialize($stateArray);
- Savestates are **not portable** across different ROM versions
- Always use the same ROM file when loading a savestate
- Savestates capture exact emulator state but not the ROM itself
- File size: ~10-20 KB for typical games (mostly cartridge RAM)
- **File size:** ~15-30 KB for typical games
- Base savestate: ~5 KB (registers, state, timers, etc.)
- VRAM: ~22 KB (16 KB for CGB dual banks, base64-encoded)
- Cartridge RAM: Varies by game (0-128 KB)
- CGB color palettes: ~175 bytes (base64-encoded)
- APU state: ~500 bytes

## State Completeness

### Fully Saved ✅
- CPU registers and flags
- PPU state and timing
- All memory (VRAM, WRAM, HRAM, OAM)
- Cartridge state and RAM
- Timer registers and internal counters
- Interrupt flags and enables
- CGB color palettes and hardware state
- APU registers and Wave RAM
- System clock cycles

### Partially Saved ⚠️
- **APU channels:** Register state saved, but NOT internal timers/counters
- Wave channel (CH3) fully preserved via Wave RAM
- Other channels may have minor timing drift after load

### Not Saved ❌
- OAM DMA transfer progress (mid-frame only)
- Serial port transfer state
- WRAM banking (CGB has 32KB, only 8KB currently saved)
- Full APU channel internal state (frequency timers, length counters, etc.)
106 changes: 106 additions & 0 deletions src/Apu/Apu.php
Original file line number Diff line number Diff line change
Expand Up @@ -365,4 +365,110 @@ private function writeNR52(int $value): void
$this->frameSequencerStep = 0;
}
}

/**
* Get frame sequencer cycles (for savestate serialization).
*
* @return int Frame sequencer cycles
*/
public function getFrameSequencerCycles(): int
{
return $this->frameSequencerCycles;
}

/**
* Get frame sequencer step (for savestate serialization).
*
* @return int Frame sequencer step (0-7)
*/
public function getFrameSequencerStep(): int
{
return $this->frameSequencerStep;
}

/**
* Get sample cycles accumulator (for savestate serialization).
*
* @return float Sample cycles
*/
public function getSampleCycles(): float
{
return $this->sampleCycles;
}

/**
* Get enabled state (for savestate serialization).
*
* @return bool True if APU is enabled
*/
public function isEnabled(): bool
{
return $this->enabled;
}

/**
* Set frame sequencer cycles (for savestate deserialization).
*
* @param int $cycles Frame sequencer cycles
*/
public function setFrameSequencerCycles(int $cycles): void
{
$this->frameSequencerCycles = $cycles;
}

/**
* Set frame sequencer step (for savestate deserialization).
*
* @param int $step Frame sequencer step (0-7)
*/
public function setFrameSequencerStep(int $step): void
{
$this->frameSequencerStep = $step & 0x07;
}

/**
* Set sample cycles accumulator (for savestate deserialization).
*
* @param float $cycles Sample cycles
*/
public function setSampleCycles(float $cycles): void
{
$this->sampleCycles = $cycles;
}

/**
* Set enabled state (for savestate deserialization).
*
* @param bool $enabled True to enable APU
*/
public function setEnabled(bool $enabled): void
{
$this->enabled = $enabled;
}

/**
* Get Wave RAM data (for savestate serialization).
*
* @return array<int, int> Wave RAM (16 bytes)
*/
public function getWaveRam(): array
{
$waveRam = [];
for ($i = 0; $i < 16; $i++) {
$waveRam[] = $this->channel3->readWaveRam($i);
}
return $waveRam;
}

/**
* Set Wave RAM data (for savestate deserialization).
*
* @param array<int, int> $waveRam Wave RAM (16 bytes)
*/
public function setWaveRam(array $waveRam): void
{
for ($i = 0; $i < min(16, count($waveRam)); $i++) {
$this->channel3->writeWaveRam($i, $waveRam[$i]);
}
}
}
32 changes: 32 additions & 0 deletions src/Emulator.php
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,38 @@ public function getSerial(): ?Serial
return $this->serial;
}

/**
* Get the timer.
*/
public function getTimer(): ?Timer
{
return $this->timer;
}

/**
* Get the interrupt controller.
*/
public function getInterruptController(): ?InterruptController
{
return $this->interruptController;
}

/**
* Get the CGB controller.
*/
public function getCgbController(): ?CgbController
{
return $this->cgb;
}

/**
* Get the APU.
*/
public function getApu(): ?Apu
{
return $this->apu;
}

/**
* Save the current emulator state to a file.
*
Expand Down
Loading