From 6c9fe8d1eafd96e9a3f7ad9234e71160778967b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20=C5=9Een?= Date: Fri, 14 Aug 2026 16:26:21 +0200 Subject: [PATCH] add save-state contract specification and system spec integration --- src/contract/root.zig | 3 + src/contract/state.zig | 9 +++ src/contract/system.zig | 17 +++++ src/system/nes/root.zig | 158 ++++++++++++++++++++++++++-------------- 4 files changed, 132 insertions(+), 55 deletions(-) create mode 100644 src/contract/state.zig diff --git a/src/contract/root.zig b/src/contract/root.zig index ea52102..ca4ef75 100644 --- a/src/contract/root.zig +++ b/src/contract/root.zig @@ -4,6 +4,7 @@ pub const audio = @import("audio.zig"); pub const input = @import("input.zig"); pub const storage = @import("storage.zig"); pub const system = @import("system.zig"); +pub const state = @import("state.zig"); pub const Rational = common.Rational; @@ -31,3 +32,5 @@ pub const StorageLoadError = storage.LoadError; pub const SystemSpec = system.Spec; pub const validateSystem = system.validate; + +pub const StateSpec = state.Spec; diff --git a/src/contract/state.zig b/src/contract/state.zig new file mode 100644 index 0000000..0fdb882 --- /dev/null +++ b/src/contract/state.zig @@ -0,0 +1,9 @@ +pub const Spec = struct { + max_size: usize, +}; + +pub const Error = error{ + BufferTooSmall, + InvalidState, + UnsupportedVersion, +}; diff --git a/src/contract/system.zig b/src/contract/system.zig index 1d18877..c5791af 100644 --- a/src/contract/system.zig +++ b/src/contract/system.zig @@ -2,6 +2,7 @@ const video = @import("video.zig"); const audio = @import("audio.zig"); const input = @import("input.zig"); const storage = @import("storage.zig"); +const state = @import("state.zig"); pub const Spec = struct { name: []const u8, @@ -14,6 +15,8 @@ pub const Spec = struct { input_devices: []const input.DeviceSpec = &.{}, storage_devices: []const storage.Spec = &.{}, + + save_state: ?state.Spec = null, }; pub fn validate(comptime System: type) void { @@ -108,6 +111,20 @@ pub fn validate(comptime System: type) void { ) storage.LoadError!void, ); } + + if (System.spec.save_state != null) { + requireFunction( + System, + "saveState", + fn (*const System, []u8) state.Error!usize, + ); + + requireFunction( + System, + "loadState", + fn (*System, []const u8) state.Error!void, + ); + } } } diff --git a/src/system/nes/root.zig b/src/system/nes/root.zig index e591b89..840d27a 100644 --- a/src/system/nes/root.zig +++ b/src/system/nes/root.zig @@ -17,7 +17,6 @@ const Cpu = _cpu.m6502.Cpu( .cycle, ); - const Nes = @This(); pub const spec = contract.SystemSpec{ .name = "Nintendo Entertainment System", @@ -68,6 +67,10 @@ pub const spec = contract.SystemSpec{ .writable = true, }, }, + + .save_state = .{ + .max_size = 32 * 1024, + }, }; bus: Bus = undefined, @@ -127,7 +130,6 @@ comptime { contract.validateSystem(Nes); } - pub fn videoFrame( self: *const Nes, output: usize, @@ -238,7 +240,7 @@ fn serviceOamDma( pub fn saveState( self: *const Nes, buffer: []u8, -) error{BufferTooSmall}!usize { +) contract.state.Error!usize { var offset: usize = 0; if (buffer.len < 8) return error.BufferTooSmall; @@ -247,42 +249,65 @@ pub fn saveState( // CPU if (offset + 15 > buffer.len) return error.BufferTooSmall; - std.mem.writeInt(u16, buffer[offset..][0..2], self.cpu.registers.pc, .little); offset += 2; - buffer[offset] = self.cpu.registers.a; offset += 1; - buffer[offset] = self.cpu.registers.x; offset += 1; - buffer[offset] = self.cpu.registers.y; offset += 1; - buffer[offset] = self.cpu.registers.sp; offset += 1; - buffer[offset] = @bitCast(self.cpu.registers.status); offset += 1; - std.mem.writeInt(u64, buffer[offset..][0..8], self.cpu.cycles, .little); offset += 8; + std.mem.writeInt(u16, buffer[offset..][0..2], self.cpu.registers.pc, .little); + offset += 2; + buffer[offset] = self.cpu.registers.a; + offset += 1; + buffer[offset] = self.cpu.registers.x; + offset += 1; + buffer[offset] = self.cpu.registers.y; + offset += 1; + buffer[offset] = self.cpu.registers.sp; + offset += 1; + buffer[offset] = @bitCast(self.cpu.registers.status); + offset += 1; + std.mem.writeInt(u64, buffer[offset..][0..8], self.cpu.cycles, .little); + offset += 8; // Bus RAM & PRG RAM if (offset + self.bus.ram.len + self.bus.cartridge.prg_ram.len > buffer.len) return error.BufferTooSmall; - @memcpy(buffer[offset..][0..self.bus.ram.len], &self.bus.ram); offset += self.bus.ram.len; - @memcpy(buffer[offset..][0..self.bus.cartridge.prg_ram.len], &self.bus.cartridge.prg_ram); offset += self.bus.cartridge.prg_ram.len; + @memcpy(buffer[offset..][0..self.bus.ram.len], &self.bus.ram); + offset += self.bus.ram.len; + @memcpy(buffer[offset..][0..self.bus.cartridge.prg_ram.len], &self.bus.cartridge.prg_ram); + offset += self.bus.cartridge.prg_ram.len; if (self.bus.cartridge.chr_is_ram) { if (offset + self.bus.cartridge.chr_ram.len > buffer.len) return error.BufferTooSmall; - @memcpy(buffer[offset..][0..self.bus.cartridge.chr_ram.len], &self.bus.cartridge.chr_ram); offset += self.bus.cartridge.chr_ram.len; + @memcpy(buffer[offset..][0..self.bus.cartridge.chr_ram.len], &self.bus.cartridge.chr_ram); + offset += self.bus.cartridge.chr_ram.len; } // PPU State const ppu_size = 1 + 1 + 1 + 1 + 2 + 2 + 1 + 1 + 2 + 2 + 4096 + 32 + 256; if (offset + ppu_size > buffer.len) return error.BufferTooSmall; - buffer[offset] = self.bus.ppu.ctrl; offset += 1; - buffer[offset] = self.bus.ppu.mask; offset += 1; - buffer[offset] = self.bus.ppu.status; offset += 1; - buffer[offset] = self.bus.ppu.oam_addr; offset += 1; - std.mem.writeInt(u16, buffer[offset..][0..2], self.bus.ppu.vram_addr, .little); offset += 2; - std.mem.writeInt(u16, buffer[offset..][0..2], self.bus.ppu.temp_addr, .little); offset += 2; - buffer[offset] = self.bus.ppu.fine_x; offset += 1; - buffer[offset] = if (self.bus.ppu.write_latch) 1 else 0; offset += 1; - std.mem.writeInt(u16, buffer[offset..][0..2], self.bus.ppu.dot, .little); offset += 2; - std.mem.writeInt(u16, buffer[offset..][0..2], self.bus.ppu.scanline, .little); offset += 2; + buffer[offset] = self.bus.ppu.ctrl; + offset += 1; + buffer[offset] = self.bus.ppu.mask; + offset += 1; + buffer[offset] = self.bus.ppu.status; + offset += 1; + buffer[offset] = self.bus.ppu.oam_addr; + offset += 1; + std.mem.writeInt(u16, buffer[offset..][0..2], self.bus.ppu.vram_addr, .little); + offset += 2; + std.mem.writeInt(u16, buffer[offset..][0..2], self.bus.ppu.temp_addr, .little); + offset += 2; + buffer[offset] = self.bus.ppu.fine_x; + offset += 1; + buffer[offset] = if (self.bus.ppu.write_latch) 1 else 0; + offset += 1; + std.mem.writeInt(u16, buffer[offset..][0..2], self.bus.ppu.dot, .little); + offset += 2; + std.mem.writeInt(u16, buffer[offset..][0..2], self.bus.ppu.scanline, .little); + offset += 2; - @memcpy(buffer[offset..][0..4096], &self.bus.ppu.nametable); offset += 4096; - @memcpy(buffer[offset..][0..32], &self.bus.ppu.palette); offset += 32; - @memcpy(buffer[offset..][0..256], &self.bus.ppu.oam); offset += 256; + @memcpy(buffer[offset..][0..4096], &self.bus.ppu.nametable); + offset += 4096; + @memcpy(buffer[offset..][0..32], &self.bus.ppu.palette); + offset += 32; + @memcpy(buffer[offset..][0..256], &self.bus.ppu.oam); + offset += 256; return offset; } @@ -290,51 +315,74 @@ pub fn saveState( pub fn loadState( self: *Nes, buffer: []const u8, -) error{ InvalidState, BufferTooShort }!void { +) contract.state.Error!void { var offset: usize = 0; - if (buffer.len < 8) return error.BufferTooShort; + if (buffer.len < 8) return error.InvalidState; if (!std.mem.eql(u8, buffer[0..8], "6SOZNES1")) return error.InvalidState; offset += 8; // CPU - if (offset + 15 > buffer.len) return error.BufferTooShort; - self.cpu.registers.pc = std.mem.readInt(u16, buffer[offset..][0..2], .little); offset += 2; - self.cpu.registers.a = buffer[offset]; offset += 1; - self.cpu.registers.x = buffer[offset]; offset += 1; - self.cpu.registers.y = buffer[offset]; offset += 1; - self.cpu.registers.sp = buffer[offset]; offset += 1; - self.cpu.registers.status = @bitCast(buffer[offset]); offset += 1; - self.cpu.cycles = std.mem.readInt(u64, buffer[offset..][0..8], .little); offset += 8; + if (offset + 15 > buffer.len) return error.InvalidState; + self.cpu.registers.pc = std.mem.readInt(u16, buffer[offset..][0..2], .little); + offset += 2; + self.cpu.registers.a = buffer[offset]; + offset += 1; + self.cpu.registers.x = buffer[offset]; + offset += 1; + self.cpu.registers.y = buffer[offset]; + offset += 1; + self.cpu.registers.sp = buffer[offset]; + offset += 1; + self.cpu.registers.status = @bitCast(buffer[offset]); + offset += 1; + self.cpu.cycles = std.mem.readInt(u64, buffer[offset..][0..8], .little); + offset += 8; // Bus RAM & PRG RAM - if (offset + self.bus.ram.len + self.bus.cartridge.prg_ram.len > buffer.len) return error.BufferTooShort; - @memcpy(&self.bus.ram, buffer[offset..][0..self.bus.ram.len]); offset += self.bus.ram.len; - @memcpy(&self.bus.cartridge.prg_ram, buffer[offset..][0..self.bus.cartridge.prg_ram.len]); offset += self.bus.cartridge.prg_ram.len; + if (offset + self.bus.ram.len + self.bus.cartridge.prg_ram.len > buffer.len) return error.InvalidState; + @memcpy(&self.bus.ram, buffer[offset..][0..self.bus.ram.len]); + offset += self.bus.ram.len; + @memcpy(&self.bus.cartridge.prg_ram, buffer[offset..][0..self.bus.cartridge.prg_ram.len]); + offset += self.bus.cartridge.prg_ram.len; if (self.bus.cartridge.chr_is_ram) { - if (offset + self.bus.cartridge.chr_ram.len > buffer.len) return error.BufferTooShort; - @memcpy(&self.bus.cartridge.chr_ram, buffer[offset..][0..self.bus.cartridge.chr_ram.len]); offset += self.bus.cartridge.chr_ram.len; + if (offset + self.bus.cartridge.chr_ram.len > buffer.len) return error.InvalidState; + @memcpy(&self.bus.cartridge.chr_ram, buffer[offset..][0..self.bus.cartridge.chr_ram.len]); + offset += self.bus.cartridge.chr_ram.len; } // PPU State const ppu_size = 1 + 1 + 1 + 1 + 2 + 2 + 1 + 1 + 2 + 2 + 4096 + 32 + 256; - if (offset + ppu_size > buffer.len) return error.BufferTooShort; + if (offset + ppu_size > buffer.len) return error.InvalidState; - self.bus.ppu.ctrl = buffer[offset]; offset += 1; - self.bus.ppu.mask = buffer[offset]; offset += 1; - self.bus.ppu.status = buffer[offset]; offset += 1; - self.bus.ppu.oam_addr = buffer[offset]; offset += 1; - self.bus.ppu.vram_addr = std.mem.readInt(u16, buffer[offset..][0..2], .little); offset += 2; - self.bus.ppu.temp_addr = std.mem.readInt(u16, buffer[offset..][0..2], .little); offset += 2; - self.bus.ppu.fine_x = @truncate(buffer[offset]); offset += 1; - self.bus.ppu.write_latch = buffer[offset] != 0; offset += 1; - self.bus.ppu.dot = std.mem.readInt(u16, buffer[offset..][0..2], .little); offset += 2; - self.bus.ppu.scanline = std.mem.readInt(u16, buffer[offset..][0..2], .little); offset += 2; + self.bus.ppu.ctrl = buffer[offset]; + offset += 1; + self.bus.ppu.mask = buffer[offset]; + offset += 1; + self.bus.ppu.status = buffer[offset]; + offset += 1; + self.bus.ppu.oam_addr = buffer[offset]; + offset += 1; + self.bus.ppu.vram_addr = std.mem.readInt(u16, buffer[offset..][0..2], .little); + offset += 2; + self.bus.ppu.temp_addr = std.mem.readInt(u16, buffer[offset..][0..2], .little); + offset += 2; + self.bus.ppu.fine_x = @truncate(buffer[offset]); + offset += 1; + self.bus.ppu.write_latch = buffer[offset] != 0; + offset += 1; + self.bus.ppu.dot = std.mem.readInt(u16, buffer[offset..][0..2], .little); + offset += 2; + self.bus.ppu.scanline = std.mem.readInt(u16, buffer[offset..][0..2], .little); + offset += 2; - @memcpy(&self.bus.ppu.nametable, buffer[offset..][0..4096]); offset += 4096; - @memcpy(&self.bus.ppu.palette, buffer[offset..][0..32]); offset += 32; - @memcpy(&self.bus.ppu.oam, buffer[offset..][0..256]); offset += 256; + @memcpy(&self.bus.ppu.nametable, buffer[offset..][0..4096]); + offset += 4096; + @memcpy(&self.bus.ppu.palette, buffer[offset..][0..32]); + offset += 32; + @memcpy(&self.bus.ppu.oam, buffer[offset..][0..256]); + offset += 256; } test "NES System - Save State Serialization Roundtrip" {