add nes core system implementation with ppu 2c02 and apu 2a03
This commit is contained in:
@@ -0,0 +1,276 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Apu = @This();
|
||||||
|
|
||||||
|
pub const sample_rate: u32 = 44_100;
|
||||||
|
|
||||||
|
sample_buffer: [2048]i16 = [_]i16{0} ** 2048,
|
||||||
|
sample_count: usize = 0,
|
||||||
|
cycle_counter: u32 = 0,
|
||||||
|
|
||||||
|
// Pulse 1
|
||||||
|
pulse1_enabled: bool = false,
|
||||||
|
pulse1_timer_reload: u11 = 0,
|
||||||
|
pulse1_timer: u11 = 0,
|
||||||
|
pulse1_duty: u2 = 0,
|
||||||
|
pulse1_duty_pos: u3 = 0,
|
||||||
|
pulse1_volume: u4 = 0,
|
||||||
|
|
||||||
|
// Pulse 2
|
||||||
|
pulse2_enabled: bool = false,
|
||||||
|
pulse2_timer_reload: u11 = 0,
|
||||||
|
pulse2_timer: u11 = 0,
|
||||||
|
pulse2_duty: u2 = 0,
|
||||||
|
pulse2_duty_pos: u3 = 0,
|
||||||
|
pulse2_volume: u4 = 0,
|
||||||
|
|
||||||
|
// Triangle
|
||||||
|
triangle_enabled: bool = false,
|
||||||
|
triangle_timer_reload: u11 = 0,
|
||||||
|
triangle_timer: u11 = 0,
|
||||||
|
triangle_seq_pos: u5 = 0,
|
||||||
|
|
||||||
|
// Noise
|
||||||
|
noise_enabled: bool = false,
|
||||||
|
noise_timer_reload: u12 = 0,
|
||||||
|
noise_timer: u12 = 0,
|
||||||
|
noise_lfsr: u16 = 1,
|
||||||
|
noise_volume: u4 = 0,
|
||||||
|
|
||||||
|
// DMC
|
||||||
|
dmc_enabled: bool = false,
|
||||||
|
dmc_loop: bool = false,
|
||||||
|
dmc_rate_index: u4 = 0,
|
||||||
|
dmc_timer: u12 = 0,
|
||||||
|
dmc_timer_reload: u12 = 0,
|
||||||
|
dmc_output_level: u7 = 0,
|
||||||
|
dmc_sample_address: u16 = 0xC000,
|
||||||
|
dmc_sample_length: u16 = 1,
|
||||||
|
dmc_current_address: u16 = 0xC000,
|
||||||
|
dmc_bytes_remaining: u16 = 0,
|
||||||
|
dmc_shift_register: u8 = 0,
|
||||||
|
dmc_bits_remaining: u4 = 0,
|
||||||
|
dmc_silence: bool = true,
|
||||||
|
|
||||||
|
const duty_tables: [4][8]u8 = .{
|
||||||
|
.{ 0, 1, 0, 0, 0, 0, 0, 0 }, // 12.5%
|
||||||
|
.{ 0, 1, 1, 0, 0, 0, 0, 0 }, // 25%
|
||||||
|
.{ 0, 1, 1, 1, 1, 0, 0, 0 }, // 50%
|
||||||
|
.{ 1, 0, 0, 1, 1, 1, 1, 1 }, // 75%
|
||||||
|
};
|
||||||
|
|
||||||
|
const triangle_table: [32]u4 = .{
|
||||||
|
15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
|
||||||
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
|
||||||
|
};
|
||||||
|
|
||||||
|
const noise_periods: [16]u12 = .{
|
||||||
|
4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068,
|
||||||
|
};
|
||||||
|
|
||||||
|
const dmc_periods: [16]u12 = .{
|
||||||
|
428, 380, 340, 320, 286, 254, 226, 214, 190, 160, 142, 128, 106, 84, 72, 54,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn reset(self: *Apu) void {
|
||||||
|
self.sample_count = 0;
|
||||||
|
self.cycle_counter = 0;
|
||||||
|
self.pulse1_enabled = false;
|
||||||
|
self.pulse2_enabled = false;
|
||||||
|
self.triangle_enabled = false;
|
||||||
|
self.noise_enabled = false;
|
||||||
|
self.noise_lfsr = 1;
|
||||||
|
self.dmc_enabled = false;
|
||||||
|
self.dmc_output_level = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn tick(self: *Apu) void {
|
||||||
|
// Pulse 1 timer
|
||||||
|
if (self.pulse1_timer == 0) {
|
||||||
|
self.pulse1_timer = self.pulse1_timer_reload;
|
||||||
|
self.pulse1_duty_pos +%= 1;
|
||||||
|
} else {
|
||||||
|
self.pulse1_timer -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pulse 2 timer
|
||||||
|
if (self.pulse2_timer == 0) {
|
||||||
|
self.pulse2_timer = self.pulse2_timer_reload;
|
||||||
|
self.pulse2_duty_pos +%= 1;
|
||||||
|
} else {
|
||||||
|
self.pulse2_timer -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Triangle timer
|
||||||
|
if (self.triangle_timer == 0) {
|
||||||
|
self.triangle_timer = self.triangle_timer_reload;
|
||||||
|
self.triangle_seq_pos +%= 1;
|
||||||
|
} else {
|
||||||
|
self.triangle_timer -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Noise timer
|
||||||
|
if (self.noise_timer == 0) {
|
||||||
|
self.noise_timer = self.noise_timer_reload;
|
||||||
|
const feedback = (self.noise_lfsr & 1) ^ ((self.noise_lfsr >> 1) & 1);
|
||||||
|
self.noise_lfsr = (self.noise_lfsr >> 1) | (feedback << 14);
|
||||||
|
} else {
|
||||||
|
self.noise_timer -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DMC timer
|
||||||
|
if (self.dmc_timer == 0) {
|
||||||
|
self.dmc_timer = self.dmc_timer_reload;
|
||||||
|
if (!self.dmc_silence) {
|
||||||
|
if ((self.dmc_shift_register & 1) != 0) {
|
||||||
|
if (self.dmc_output_level <= 125) self.dmc_output_level += 2;
|
||||||
|
} else {
|
||||||
|
if (self.dmc_output_level >= 2) self.dmc_output_level -= 2;
|
||||||
|
}
|
||||||
|
self.dmc_shift_register >>= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.dmc_bits_remaining > 0) {
|
||||||
|
self.dmc_bits_remaining -= 1;
|
||||||
|
}
|
||||||
|
if (self.dmc_bits_remaining == 0) {
|
||||||
|
self.dmc_silence = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.dmc_timer -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample downsampling at ~44.1 kHz (every ~40 CPU cycles)
|
||||||
|
self.cycle_counter += 1;
|
||||||
|
if (self.cycle_counter >= 40) {
|
||||||
|
self.cycle_counter = 0;
|
||||||
|
if (self.sample_count < self.sample_buffer.len) {
|
||||||
|
const p1_val: i32 = if (self.pulse1_enabled and duty_tables[self.pulse1_duty][self.pulse1_duty_pos] != 0 and self.pulse1_timer_reload > 8)
|
||||||
|
@as(i32, self.pulse1_volume)
|
||||||
|
else
|
||||||
|
0;
|
||||||
|
|
||||||
|
const p2_val: i32 = if (self.pulse2_enabled and duty_tables[self.pulse2_duty][self.pulse2_duty_pos] != 0 and self.pulse2_timer_reload > 8)
|
||||||
|
@as(i32, self.pulse2_volume)
|
||||||
|
else
|
||||||
|
0;
|
||||||
|
|
||||||
|
const tri_val: i32 = if (self.triangle_enabled and self.triangle_timer_reload > 2)
|
||||||
|
@as(i32, triangle_table[self.triangle_seq_pos])
|
||||||
|
else
|
||||||
|
0;
|
||||||
|
|
||||||
|
const noise_val: i32 = if (self.noise_enabled and (self.noise_lfsr & 1) == 0)
|
||||||
|
@as(i32, self.noise_volume)
|
||||||
|
else
|
||||||
|
0;
|
||||||
|
|
||||||
|
const dmc_val: i32 = if (self.dmc_enabled) @as(i32, self.dmc_output_level) else 0;
|
||||||
|
|
||||||
|
const sample_i32 = (p1_val + p2_val) * 400 + tri_val * 350 + noise_val * 300 + dmc_val * 150;
|
||||||
|
self.sample_buffer[self.sample_count] = @intCast(std.math.clamp(sample_i32, -32000, 32000));
|
||||||
|
self.sample_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(self: *Apu, address: u16, value: u8) void {
|
||||||
|
switch (address) {
|
||||||
|
// Pulse 1
|
||||||
|
0x4000 => {
|
||||||
|
self.pulse1_duty = @truncate((value >> 6) & 3);
|
||||||
|
self.pulse1_volume = @truncate(value & 0x0f);
|
||||||
|
},
|
||||||
|
0x4002 => {
|
||||||
|
self.pulse1_timer_reload = (self.pulse1_timer_reload & 0x0700) | value;
|
||||||
|
},
|
||||||
|
0x4003 => {
|
||||||
|
self.pulse1_timer_reload = (self.pulse1_timer_reload & 0x00ff) | (@as(u11, value & 7) << 8);
|
||||||
|
self.pulse1_duty_pos = 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Pulse 2
|
||||||
|
0x4004 => {
|
||||||
|
self.pulse2_duty = @truncate((value >> 6) & 3);
|
||||||
|
self.pulse2_volume = @truncate(value & 0x0f);
|
||||||
|
},
|
||||||
|
0x4006 => {
|
||||||
|
self.pulse2_timer_reload = (self.pulse2_timer_reload & 0x0700) | value;
|
||||||
|
},
|
||||||
|
0x4007 => {
|
||||||
|
self.pulse2_timer_reload = (self.pulse2_timer_reload & 0x00ff) | (@as(u11, value & 7) << 8);
|
||||||
|
self.pulse2_duty_pos = 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Triangle
|
||||||
|
0x400a => {
|
||||||
|
self.triangle_timer_reload = (self.triangle_timer_reload & 0x0700) | value;
|
||||||
|
},
|
||||||
|
0x400b => {
|
||||||
|
self.triangle_timer_reload = (self.triangle_timer_reload & 0x00ff) | (@as(u11, value & 7) << 8);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Noise
|
||||||
|
0x400c => {
|
||||||
|
self.noise_volume = @truncate(value & 0x0f);
|
||||||
|
},
|
||||||
|
0x400e => {
|
||||||
|
self.noise_timer_reload = noise_periods[value & 0x0f];
|
||||||
|
},
|
||||||
|
|
||||||
|
// DMC
|
||||||
|
0x4010 => {
|
||||||
|
self.dmc_loop = (value & 0x40) != 0;
|
||||||
|
self.dmc_rate_index = @truncate(value & 0x0f);
|
||||||
|
self.dmc_timer_reload = dmc_periods[self.dmc_rate_index];
|
||||||
|
},
|
||||||
|
0x4011 => {
|
||||||
|
self.dmc_output_level = @truncate(value & 0x7f);
|
||||||
|
},
|
||||||
|
0x4012 => {
|
||||||
|
self.dmc_sample_address = 0xC000 + (@as(u16, value) << 6);
|
||||||
|
},
|
||||||
|
0x4013 => {
|
||||||
|
self.dmc_sample_length = (@as(u16, value) << 4) + 1;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readStatus(_: *Apu) u8 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn writeStatus(self: *Apu, value: u8) void {
|
||||||
|
self.pulse1_enabled = (value & 0x01) != 0;
|
||||||
|
self.pulse2_enabled = (value & 0x02) != 0;
|
||||||
|
self.triangle_enabled = (value & 0x04) != 0;
|
||||||
|
self.noise_enabled = (value & 0x08) != 0;
|
||||||
|
self.dmc_enabled = (value & 0x10) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn writeFrameCounter(_: *Apu, _: u8) void {}
|
||||||
|
|
||||||
|
pub fn irqAsserted(_: *const Apu) bool {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "NES APU - Sound Synthesis & Sample Generation" {
|
||||||
|
var apu = Apu{};
|
||||||
|
apu.reset();
|
||||||
|
|
||||||
|
// Enable Pulse 1 and Pulse 2
|
||||||
|
apu.writeStatus(0x03);
|
||||||
|
|
||||||
|
// Setup Pulse 1 volume and frequency
|
||||||
|
apu.write(0x4000, 0xbf); // 50% duty, max volume 15
|
||||||
|
apu.write(0x4002, 100);
|
||||||
|
apu.write(0x4003, 0);
|
||||||
|
|
||||||
|
// Tick APU for 100 cycles to produce PCM samples
|
||||||
|
for (0..100) |_| {
|
||||||
|
apu.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
try std.testing.expect(apu.sample_count > 0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
const Cartridge = @import("cartridge.zig");
|
||||||
|
|
||||||
|
const Controller = @import("controller.zig");
|
||||||
|
|
||||||
|
const Ppu = @import("ppu.zig");
|
||||||
|
|
||||||
|
const Apu = @import("apu.zig");
|
||||||
|
|
||||||
|
const Bus = @This();
|
||||||
|
|
||||||
|
ram: [0x800]u8 =
|
||||||
|
[_]u8{0} ** 0x800,
|
||||||
|
|
||||||
|
cartridge: Cartridge,
|
||||||
|
|
||||||
|
ppu: Ppu = .{},
|
||||||
|
apu: Apu = .{},
|
||||||
|
|
||||||
|
controllers: [2]Controller = .{
|
||||||
|
.{},
|
||||||
|
.{},
|
||||||
|
},
|
||||||
|
|
||||||
|
open_bus: u8 = 0,
|
||||||
|
|
||||||
|
oam_dma_page: ?u8 = null,
|
||||||
|
|
||||||
|
pub fn init(
|
||||||
|
rom: []const u8,
|
||||||
|
) Cartridge.Error!Bus {
|
||||||
|
return .{
|
||||||
|
.cartridge = try Cartridge.init(rom),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(
|
||||||
|
self: *Bus,
|
||||||
|
) void {
|
||||||
|
self.ppu.reset();
|
||||||
|
self.apu.reset();
|
||||||
|
|
||||||
|
for (&self.controllers) |*controller|
|
||||||
|
controller.reset();
|
||||||
|
|
||||||
|
self.oam_dma_page = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn read(
|
||||||
|
self: *Bus,
|
||||||
|
address: u16,
|
||||||
|
) u8 {
|
||||||
|
const value: u8 = switch (address) {
|
||||||
|
// 2 KiB internal RAM + mirrors.
|
||||||
|
0x0000...0x1fff => self.ram[address & 0x07ff],
|
||||||
|
|
||||||
|
// PPU registers + mirrors.
|
||||||
|
0x2000...0x3fff => self.ppu.cpuRead(
|
||||||
|
&self.cartridge,
|
||||||
|
address & 7,
|
||||||
|
),
|
||||||
|
|
||||||
|
// APU status.
|
||||||
|
0x4015 => self.apu.readStatus(),
|
||||||
|
|
||||||
|
// Controller 1.
|
||||||
|
0x4016 => self.controllers[0].read(),
|
||||||
|
|
||||||
|
// Controller 2.
|
||||||
|
0x4017 => self.controllers[1].read(),
|
||||||
|
|
||||||
|
// Cartridge space.
|
||||||
|
0x4020...0xffff => self.cartridge.cpuRead(address),
|
||||||
|
|
||||||
|
else => self.open_bus,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.open_bus = value;
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn write(
|
||||||
|
self: *Bus,
|
||||||
|
address: u16,
|
||||||
|
value: u8,
|
||||||
|
) void {
|
||||||
|
self.open_bus = value;
|
||||||
|
|
||||||
|
switch (address) {
|
||||||
|
0x0000...0x1fff => self.ram[address & 0x07ff] =
|
||||||
|
value,
|
||||||
|
|
||||||
|
0x2000...0x3fff => self.ppu.cpuWrite(
|
||||||
|
&self.cartridge,
|
||||||
|
address & 7,
|
||||||
|
value,
|
||||||
|
),
|
||||||
|
|
||||||
|
0x4000...0x4013 => self.apu.write(
|
||||||
|
address,
|
||||||
|
value,
|
||||||
|
),
|
||||||
|
|
||||||
|
// OAMDMA
|
||||||
|
0x4014 => self.oam_dma_page = value,
|
||||||
|
|
||||||
|
0x4015 => self.apu.writeStatus(value),
|
||||||
|
|
||||||
|
0x4016 => {
|
||||||
|
self.controllers[0].write(value);
|
||||||
|
self.controllers[1].write(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
0x4017 => self.apu.writeFrameCounter(value),
|
||||||
|
|
||||||
|
else => self.cartridge.cpuWrite(
|
||||||
|
address,
|
||||||
|
value,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called exactly once for every CPU cycle.
|
||||||
|
pub inline fn tick(
|
||||||
|
self: *Bus,
|
||||||
|
) void {
|
||||||
|
self.apu.tick();
|
||||||
|
|
||||||
|
// NTSC NES: PPU runs at 3× CPU clock.
|
||||||
|
self.ppu.tick(&self.cartridge);
|
||||||
|
self.ppu.tick(&self.cartridge);
|
||||||
|
self.ppu.tick(&self.cartridge);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nmiLine(
|
||||||
|
self: *Bus,
|
||||||
|
) bool {
|
||||||
|
return self.ppu.nmiAsserted();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn irqLine(
|
||||||
|
self: *Bus,
|
||||||
|
) bool {
|
||||||
|
return self.apu.irqAsserted() or
|
||||||
|
self.cartridge.irqAsserted();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn takeOamDmaPage(
|
||||||
|
self: *Bus,
|
||||||
|
) ?u8 {
|
||||||
|
const page =
|
||||||
|
self.oam_dma_page;
|
||||||
|
|
||||||
|
self.oam_dma_page = null;
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
pub const Button = enum(u3) {
|
||||||
|
a = 0,
|
||||||
|
b = 1,
|
||||||
|
select = 2,
|
||||||
|
start = 3,
|
||||||
|
up = 4,
|
||||||
|
down = 5,
|
||||||
|
left = 6,
|
||||||
|
right = 7,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Controller = @This();
|
||||||
|
|
||||||
|
buttons: u8 = 0,
|
||||||
|
|
||||||
|
shift_register: u8 = 0,
|
||||||
|
strobe: bool = false,
|
||||||
|
|
||||||
|
pub fn reset(
|
||||||
|
self: *Controller,
|
||||||
|
) void {
|
||||||
|
self.shift_register = 0;
|
||||||
|
self.strobe = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setButtons(
|
||||||
|
self: *Controller,
|
||||||
|
buttons: u8,
|
||||||
|
) void {
|
||||||
|
self.buttons = buttons;
|
||||||
|
|
||||||
|
if (self.strobe)
|
||||||
|
self.shift_register = buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(
|
||||||
|
self: *Controller,
|
||||||
|
value: u8,
|
||||||
|
) void {
|
||||||
|
const new_strobe =
|
||||||
|
(value & 1) != 0;
|
||||||
|
|
||||||
|
if (new_strobe) {
|
||||||
|
self.shift_register =
|
||||||
|
self.buttons;
|
||||||
|
} else if (self.strobe) {
|
||||||
|
// Falling edge latches controller.
|
||||||
|
self.shift_register =
|
||||||
|
self.buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.strobe = new_strobe;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(
|
||||||
|
self: *Controller,
|
||||||
|
) u8 {
|
||||||
|
if (self.strobe)
|
||||||
|
return self.buttons & 1;
|
||||||
|
|
||||||
|
const value =
|
||||||
|
self.shift_register & 1;
|
||||||
|
|
||||||
|
// Real controllers return 1 after all
|
||||||
|
// eight buttons have shifted out.
|
||||||
|
self.shift_register =
|
||||||
|
(self.shift_register >> 1) | 0x80;
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
@@ -0,0 +1,779 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Cartridge = @import("cartridge.zig");
|
||||||
|
|
||||||
|
const mapper = @import("mapper/root.zig");
|
||||||
|
const Mirroring = mapper.Mirroring;
|
||||||
|
|
||||||
|
const Ppu = @This();
|
||||||
|
|
||||||
|
pub const width = 256;
|
||||||
|
pub const height = 240;
|
||||||
|
|
||||||
|
// NTSC 2C02 64-color system palette mapped directly to RGB565 format
|
||||||
|
pub const nes_ntsc_palette_rgb565: [64]u16 = .{
|
||||||
|
0x52AA, 0x00EE, 0x0892, 0x3011, 0x400C, 0x5806, 0x5020, 0x38C0,
|
||||||
|
0x2140, 0x09C0, 0x0200, 0x01E0, 0x0187, 0x0000, 0x0000, 0x0000,
|
||||||
|
0x9CB3, 0x0A78, 0x319D, 0x58FC, 0x88B6, 0xA08C, 0x9904, 0x79E0,
|
||||||
|
0x52C0, 0x2B80, 0x0BE0, 0x03A5, 0x032F, 0x0000, 0x0000, 0x0000,
|
||||||
|
0xEF7D, 0x4CDD, 0x7BFD, 0xB31D, 0xE2BD, 0xEACE, 0xEB4C, 0xD444,
|
||||||
|
0xA540, 0x7620, 0x4E84, 0x3E6D, 0x3DA9, 0x3DF7, 0x0000, 0x0000,
|
||||||
|
0xEF7D, 0xAE6D, 0xBD7D, 0xD59D, 0xED7D, 0xED7A, 0xEDB6, 0xE632,
|
||||||
|
0xCE8F, 0xB6EF, 0xAF12, 0x9F16, 0xA6BD, 0xA514, 0x0000, 0x0000,
|
||||||
|
};
|
||||||
|
|
||||||
|
framebuffer: [width * height]u16 =
|
||||||
|
[_]u16{0} ** (width * height),
|
||||||
|
|
||||||
|
/// Four-screen sized so the same structure
|
||||||
|
/// can handle that mode without allocation.
|
||||||
|
nametable: [0x1000]u8 =
|
||||||
|
[_]u8{0} ** 0x1000,
|
||||||
|
|
||||||
|
palette: [32]u8 =
|
||||||
|
[_]u8{0} ** 32,
|
||||||
|
|
||||||
|
oam: [256]u8 =
|
||||||
|
[_]u8{0} ** 256,
|
||||||
|
|
||||||
|
ctrl: u8 = 0,
|
||||||
|
mask: u8 = 0,
|
||||||
|
status: u8 = 0,
|
||||||
|
|
||||||
|
oam_addr: u8 = 0,
|
||||||
|
|
||||||
|
vram_addr: u16 = 0,
|
||||||
|
temp_addr: u16 = 0,
|
||||||
|
|
||||||
|
fine_x: u3 = 0,
|
||||||
|
write_latch: bool = false,
|
||||||
|
|
||||||
|
read_buffer: u8 = 0,
|
||||||
|
io_latch: u8 = 0,
|
||||||
|
|
||||||
|
dot: u16 = 0,
|
||||||
|
scanline: u16 = 261,
|
||||||
|
|
||||||
|
frame_number: u64 = 0,
|
||||||
|
frame_ready: bool = false,
|
||||||
|
|
||||||
|
// internal shift registers & tile latches for background rendering
|
||||||
|
bg_shift_pattern_lo: u16 = 0,
|
||||||
|
bg_shift_pattern_hi: u16 = 0,
|
||||||
|
bg_shift_attrib_lo: u16 = 0,
|
||||||
|
bg_shift_attrib_hi: u16 = 0,
|
||||||
|
|
||||||
|
next_tile_id: u8 = 0,
|
||||||
|
next_tile_attrib: u8 = 0,
|
||||||
|
next_tile_lsb: u8 = 0,
|
||||||
|
next_tile_msb: u8 = 0,
|
||||||
|
|
||||||
|
// secondary OAM scanline sprite evaluation buffer
|
||||||
|
sec_oam_pattern_lo: [8]u8 = [_]u8{0} ** 8,
|
||||||
|
sec_oam_pattern_hi: [8]u8 = [_]u8{0} ** 8,
|
||||||
|
sec_oam_attribute: [8]u8 = [_]u8{0} ** 8,
|
||||||
|
sec_oam_x: [8]u8 = [_]u8{0} ** 8,
|
||||||
|
sec_oam_is_zero: [8]bool = [_]bool{false} ** 8,
|
||||||
|
sec_oam_count: u8 = 0,
|
||||||
|
|
||||||
|
pub fn reset(
|
||||||
|
self: *Ppu,
|
||||||
|
) void {
|
||||||
|
self.ctrl = 0;
|
||||||
|
self.mask = 0;
|
||||||
|
self.status = 0;
|
||||||
|
|
||||||
|
self.oam_addr = 0;
|
||||||
|
|
||||||
|
self.vram_addr = 0;
|
||||||
|
self.temp_addr = 0;
|
||||||
|
|
||||||
|
self.fine_x = 0;
|
||||||
|
self.write_latch = false;
|
||||||
|
|
||||||
|
self.read_buffer = 0;
|
||||||
|
self.io_latch = 0;
|
||||||
|
|
||||||
|
self.dot = 0;
|
||||||
|
self.scanline = 261;
|
||||||
|
|
||||||
|
self.frame_ready = false;
|
||||||
|
|
||||||
|
self.bg_shift_pattern_lo = 0;
|
||||||
|
self.bg_shift_pattern_hi = 0;
|
||||||
|
self.bg_shift_attrib_lo = 0;
|
||||||
|
self.bg_shift_attrib_hi = 0;
|
||||||
|
self.next_tile_id = 0;
|
||||||
|
self.next_tile_attrib = 0;
|
||||||
|
self.next_tile_lsb = 0;
|
||||||
|
self.next_tile_msb = 0;
|
||||||
|
self.sec_oam_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cpuRead(
|
||||||
|
self: *Ppu,
|
||||||
|
cartridge: *Cartridge,
|
||||||
|
register: u16,
|
||||||
|
) u8 {
|
||||||
|
const reg = register & 7;
|
||||||
|
|
||||||
|
const value: u8 = switch (reg) {
|
||||||
|
// PPUSTATUS
|
||||||
|
2 => blk: {
|
||||||
|
const result =
|
||||||
|
(self.status & 0xe0) |
|
||||||
|
(self.io_latch & 0x1f);
|
||||||
|
|
||||||
|
self.status &= ~@as(u8, 0x80);
|
||||||
|
self.write_latch = false;
|
||||||
|
|
||||||
|
break :blk result;
|
||||||
|
},
|
||||||
|
|
||||||
|
// OAMDATA
|
||||||
|
4 => self.oam[self.oam_addr],
|
||||||
|
|
||||||
|
// PPUDATA
|
||||||
|
7 => self.readData(cartridge),
|
||||||
|
|
||||||
|
else => self.io_latch,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.io_latch = value;
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cpuWrite(
|
||||||
|
self: *Ppu,
|
||||||
|
cartridge: *Cartridge,
|
||||||
|
register: u16,
|
||||||
|
value: u8,
|
||||||
|
) void {
|
||||||
|
const reg = register & 7;
|
||||||
|
|
||||||
|
self.io_latch = value;
|
||||||
|
|
||||||
|
switch (reg) {
|
||||||
|
// PPUCTRL
|
||||||
|
0 => {
|
||||||
|
self.ctrl = value;
|
||||||
|
|
||||||
|
self.temp_addr =
|
||||||
|
(self.temp_addr & 0xf3ff) |
|
||||||
|
(@as(u16, value & 0x03) << 10);
|
||||||
|
},
|
||||||
|
|
||||||
|
// PPUMASK
|
||||||
|
1 => {
|
||||||
|
self.mask = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
// OAMADDR
|
||||||
|
3 => {
|
||||||
|
self.oam_addr = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
// OAMDATA
|
||||||
|
4 => {
|
||||||
|
self.writeOamDma(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
// PPUSCROLL
|
||||||
|
5 => {
|
||||||
|
if (!self.write_latch) {
|
||||||
|
self.fine_x =
|
||||||
|
@truncate(value & 7);
|
||||||
|
|
||||||
|
self.temp_addr =
|
||||||
|
(self.temp_addr & 0xffe0) |
|
||||||
|
@as(u16, value >> 3);
|
||||||
|
|
||||||
|
self.write_latch = true;
|
||||||
|
} else {
|
||||||
|
self.temp_addr =
|
||||||
|
(self.temp_addr & 0x8c1f) |
|
||||||
|
(@as(u16, value & 0x07) << 12) |
|
||||||
|
(@as(u16, value & 0xf8) << 2);
|
||||||
|
|
||||||
|
self.write_latch = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// PPUADDR
|
||||||
|
6 => {
|
||||||
|
if (!self.write_latch) {
|
||||||
|
self.temp_addr =
|
||||||
|
(self.temp_addr & 0x00ff) |
|
||||||
|
(@as(u16, value & 0x3f) << 8);
|
||||||
|
|
||||||
|
self.write_latch = true;
|
||||||
|
} else {
|
||||||
|
self.temp_addr =
|
||||||
|
(self.temp_addr & 0x7f00) |
|
||||||
|
@as(u16, value);
|
||||||
|
|
||||||
|
self.vram_addr =
|
||||||
|
self.temp_addr;
|
||||||
|
|
||||||
|
self.write_latch = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// PPUDATA
|
||||||
|
7 => {
|
||||||
|
self.writeMemory(
|
||||||
|
cartridge,
|
||||||
|
self.vram_addr,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.incrementVramAddress();
|
||||||
|
},
|
||||||
|
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn writeOamDma(
|
||||||
|
self: *Ppu,
|
||||||
|
value: u8,
|
||||||
|
) void {
|
||||||
|
self.oam[self.oam_addr] = value;
|
||||||
|
self.oam_addr +%= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(
|
||||||
|
self: *Ppu,
|
||||||
|
cartridge: *Cartridge,
|
||||||
|
) void {
|
||||||
|
const rendering_enabled = (self.mask & 0x18) != 0;
|
||||||
|
const is_visible_scanline = (self.scanline < 240);
|
||||||
|
const is_prerender_scanline = (self.scanline == 261);
|
||||||
|
|
||||||
|
// Start of VBlank.
|
||||||
|
if (self.scanline == 241 and
|
||||||
|
self.dot == 1)
|
||||||
|
{
|
||||||
|
self.status |= 0x80;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-render line.
|
||||||
|
if (is_prerender_scanline and
|
||||||
|
self.dot == 1)
|
||||||
|
{
|
||||||
|
// Clear: VBlank, sprite zero hit, sprite overflow
|
||||||
|
self.status &= ~@as(u8, 0xe0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Rendering Pipeline ---
|
||||||
|
if (rendering_enabled and (is_visible_scanline or is_prerender_scanline)) {
|
||||||
|
if ((self.dot >= 1 and self.dot <= 256) or (self.dot >= 321 and self.dot <= 336)) {
|
||||||
|
self.shiftBackgroundRegisters();
|
||||||
|
|
||||||
|
const cycle_in_tile = (self.dot - 1) % 8;
|
||||||
|
switch (cycle_in_tile) {
|
||||||
|
0 => {
|
||||||
|
self.loadShiftRegisters();
|
||||||
|
const nt_addr = 0x2000 | (self.vram_addr & 0x0fff);
|
||||||
|
self.next_tile_id = self.readMemory(cartridge, nt_addr);
|
||||||
|
},
|
||||||
|
2 => {
|
||||||
|
const attr_addr = 0x23c0 |
|
||||||
|
(self.vram_addr & 0x0c00) |
|
||||||
|
((self.vram_addr >> 4) & 0x38) |
|
||||||
|
((self.vram_addr >> 2) & 0x07);
|
||||||
|
const attr_byte = self.readMemory(cartridge, attr_addr);
|
||||||
|
const shift: u3 = @intCast(((self.vram_addr >> 4) & 4) | (self.vram_addr & 2));
|
||||||
|
self.next_tile_attrib = (attr_byte >> shift) & 0x03;
|
||||||
|
},
|
||||||
|
4 => {
|
||||||
|
const bg_table: u16 = if ((self.ctrl & 0x10) != 0) 0x1000 else 0x0000;
|
||||||
|
const fine_y = (self.vram_addr >> 12) & 7;
|
||||||
|
const pattern_addr = bg_table + (@as(u16, self.next_tile_id) << 4) + fine_y;
|
||||||
|
self.next_tile_lsb = self.readMemory(cartridge, pattern_addr);
|
||||||
|
},
|
||||||
|
6 => {
|
||||||
|
const bg_table: u16 = if ((self.ctrl & 0x10) != 0) 0x1000 else 0x0000;
|
||||||
|
const fine_y = (self.vram_addr >> 12) & 7;
|
||||||
|
const pattern_addr = bg_table + (@as(u16, self.next_tile_id) << 4) + fine_y + 8;
|
||||||
|
self.next_tile_msb = self.readMemory(cartridge, pattern_addr);
|
||||||
|
},
|
||||||
|
7 => {
|
||||||
|
self.incrementCoarseX();
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.dot == 256) {
|
||||||
|
self.incrementY();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.dot == 257) {
|
||||||
|
self.loadShiftRegisters();
|
||||||
|
self.copyHorizontal();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_prerender_scanline and self.dot >= 280 and self.dot <= 304) {
|
||||||
|
self.copyVertical();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.dot == 257 and is_visible_scanline) {
|
||||||
|
self.evaluateSprites(cartridge);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.dot == 260 and is_visible_scanline) {
|
||||||
|
cartridge.handleScanline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Pixel Output ---
|
||||||
|
if (is_visible_scanline and self.dot >= 1 and self.dot <= 256) {
|
||||||
|
self.renderPixel(cartridge);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Dot & Scanline Counters ---
|
||||||
|
self.dot += 1;
|
||||||
|
|
||||||
|
if (self.dot == 341) {
|
||||||
|
self.dot = 0;
|
||||||
|
self.scanline += 1;
|
||||||
|
|
||||||
|
if (self.scanline == 262) {
|
||||||
|
self.scanline = 0;
|
||||||
|
|
||||||
|
self.frame_number +%= 1;
|
||||||
|
self.frame_ready = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nmiAsserted(
|
||||||
|
self: *const Ppu,
|
||||||
|
) bool {
|
||||||
|
const nmi_enabled =
|
||||||
|
(self.ctrl & 0x80) != 0;
|
||||||
|
|
||||||
|
const in_vblank =
|
||||||
|
(self.status & 0x80) != 0;
|
||||||
|
|
||||||
|
return nmi_enabled and in_vblank;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shiftBackgroundRegisters(self: *Ppu) void {
|
||||||
|
if ((self.mask & 0x08) != 0) {
|
||||||
|
self.bg_shift_pattern_lo <<= 1;
|
||||||
|
self.bg_shift_pattern_hi <<= 1;
|
||||||
|
self.bg_shift_attrib_lo <<= 1;
|
||||||
|
self.bg_shift_attrib_hi <<= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn loadShiftRegisters(self: *Ppu) void {
|
||||||
|
self.bg_shift_pattern_lo = (self.bg_shift_pattern_lo & 0xff00) | self.next_tile_lsb;
|
||||||
|
self.bg_shift_pattern_hi = (self.bg_shift_pattern_hi & 0xff00) | self.next_tile_msb;
|
||||||
|
|
||||||
|
const attr_lo: u16 = if ((self.next_tile_attrib & 1) != 0) 0x00ff else 0x0000;
|
||||||
|
const attr_hi: u16 = if ((self.next_tile_attrib & 2) != 0) 0x00ff else 0x0000;
|
||||||
|
self.bg_shift_attrib_lo = (self.bg_shift_attrib_lo & 0xff00) | attr_lo;
|
||||||
|
self.bg_shift_attrib_hi = (self.bg_shift_attrib_hi & 0xff00) | attr_hi;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn incrementCoarseX(self: *Ppu) void {
|
||||||
|
if ((self.vram_addr & 0x001f) == 31) {
|
||||||
|
self.vram_addr &= ~@as(u16, 0x001f);
|
||||||
|
self.vram_addr ^= 0x0400;
|
||||||
|
} else {
|
||||||
|
self.vram_addr += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn incrementY(self: *Ppu) void {
|
||||||
|
if ((self.vram_addr & 0x7000) != 0x7000) {
|
||||||
|
self.vram_addr += 0x1000;
|
||||||
|
} else {
|
||||||
|
self.vram_addr &= ~@as(u16, 0x7000);
|
||||||
|
var y = (self.vram_addr & 0x03e0) >> 5;
|
||||||
|
if (y == 29) {
|
||||||
|
y = 0;
|
||||||
|
self.vram_addr ^= 0x0800;
|
||||||
|
} else if (y == 31) {
|
||||||
|
y = 0;
|
||||||
|
} else {
|
||||||
|
y += 1;
|
||||||
|
}
|
||||||
|
self.vram_addr = (self.vram_addr & ~@as(u16, 0x03e0)) | (y << 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn copyHorizontal(self: *Ppu) void {
|
||||||
|
self.vram_addr = (self.vram_addr & ~@as(u16, 0x041f)) | (self.temp_addr & 0x041f);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn copyVertical(self: *Ppu) void {
|
||||||
|
self.vram_addr = (self.vram_addr & ~@as(u16, 0x7be0)) | (self.temp_addr & 0x7be0);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn evaluateSprites(
|
||||||
|
self: *Ppu,
|
||||||
|
cartridge: *Cartridge,
|
||||||
|
) void {
|
||||||
|
self.sec_oam_count = 0;
|
||||||
|
const sprite_height: u16 = if ((self.ctrl & 0x20) != 0) 16 else 8;
|
||||||
|
|
||||||
|
var oam_idx: usize = 0;
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < 64) : ({
|
||||||
|
i += 1;
|
||||||
|
oam_idx += 4;
|
||||||
|
}) {
|
||||||
|
const sprite_y = @as(u16, self.oam[oam_idx]);
|
||||||
|
const diff = @as(i16, @intCast(self.scanline)) - @as(i16, @intCast(sprite_y));
|
||||||
|
|
||||||
|
if (diff >= 0 and diff < sprite_height) {
|
||||||
|
if (self.sec_oam_count < 8) {
|
||||||
|
const idx = self.sec_oam_count;
|
||||||
|
const tile = self.oam[oam_idx + 1];
|
||||||
|
const attr = self.oam[oam_idx + 2];
|
||||||
|
const x = self.oam[oam_idx + 3];
|
||||||
|
|
||||||
|
var row: u16 = @intCast(diff);
|
||||||
|
// Vertical flip
|
||||||
|
if ((attr & 0x80) != 0) {
|
||||||
|
row = (sprite_height - 1) - row;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pattern_addr: u16 = 0;
|
||||||
|
if (sprite_height == 8) {
|
||||||
|
const pattern_table: u16 = if ((self.ctrl & 0x08) != 0) 0x1000 else 0x0000;
|
||||||
|
pattern_addr = pattern_table + (@as(u16, tile) << 4) + row;
|
||||||
|
} else {
|
||||||
|
// 8x16 mode
|
||||||
|
const pattern_table: u16 = if ((tile & 1) != 0) 0x1000 else 0x0000;
|
||||||
|
const tile_index: u16 = tile & 0xfe;
|
||||||
|
if (row < 8) {
|
||||||
|
pattern_addr = pattern_table + (tile_index << 4) + row;
|
||||||
|
} else {
|
||||||
|
pattern_addr = pattern_table + ((tile_index + 1) << 4) + (row - 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pat_lo = self.readMemory(cartridge, pattern_addr);
|
||||||
|
var pat_hi = self.readMemory(cartridge, pattern_addr + 8);
|
||||||
|
|
||||||
|
// Horizontal flip
|
||||||
|
if ((attr & 0x40) != 0) {
|
||||||
|
pat_lo = @bitReverse(pat_lo);
|
||||||
|
pat_hi = @bitReverse(pat_hi);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.sec_oam_pattern_lo[idx] = pat_lo;
|
||||||
|
self.sec_oam_pattern_hi[idx] = pat_hi;
|
||||||
|
self.sec_oam_attribute[idx] = attr;
|
||||||
|
self.sec_oam_x[idx] = x;
|
||||||
|
self.sec_oam_is_zero[idx] = (i == 0);
|
||||||
|
|
||||||
|
self.sec_oam_count += 1;
|
||||||
|
} else {
|
||||||
|
// Sprite overflow
|
||||||
|
self.status |= 0x20;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn renderPixel(
|
||||||
|
self: *Ppu,
|
||||||
|
_: *Cartridge,
|
||||||
|
) void {
|
||||||
|
const x = self.dot - 1;
|
||||||
|
|
||||||
|
var bg_pixel: u8 = 0;
|
||||||
|
var bg_palette: u8 = 0;
|
||||||
|
|
||||||
|
if ((self.mask & 0x08) != 0 and (x >= 8 or (self.mask & 0x02) != 0)) {
|
||||||
|
const bit_shift: u4 = 15 - @as(u4, self.fine_x);
|
||||||
|
const p1 = @as(u8, @truncate((self.bg_shift_pattern_lo >> bit_shift) & 1));
|
||||||
|
const p2 = @as(u8, @truncate((self.bg_shift_pattern_hi >> bit_shift) & 1));
|
||||||
|
bg_pixel = (p2 << 1) | p1;
|
||||||
|
|
||||||
|
const a1 = @as(u8, @truncate((self.bg_shift_attrib_lo >> bit_shift) & 1));
|
||||||
|
const a2 = @as(u8, @truncate((self.bg_shift_attrib_hi >> bit_shift) & 1));
|
||||||
|
bg_palette = (a2 << 1) | a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fg_pixel: u8 = 0;
|
||||||
|
var fg_palette: u8 = 0;
|
||||||
|
var fg_priority: bool = false;
|
||||||
|
var is_sprite_zero: bool = false;
|
||||||
|
|
||||||
|
if (self.sec_oam_count > 0 and (self.mask & 0x10) != 0 and (x >= 8 or (self.mask & 0x04) != 0)) {
|
||||||
|
for (0..self.sec_oam_count) |i| {
|
||||||
|
const spr_x = self.sec_oam_x[i];
|
||||||
|
if (x >= spr_x and x < spr_x + 8) {
|
||||||
|
const offset: u3 = @intCast(x - spr_x);
|
||||||
|
const shift: u3 = 7 - offset;
|
||||||
|
const p1 = (self.sec_oam_pattern_lo[i] >> shift) & 1;
|
||||||
|
const p2 = (self.sec_oam_pattern_hi[i] >> shift) & 1;
|
||||||
|
const pixel = (p2 << 1) | p1;
|
||||||
|
|
||||||
|
if (pixel != 0) {
|
||||||
|
fg_pixel = pixel;
|
||||||
|
fg_palette = 4 + (self.sec_oam_attribute[i] & 0x03);
|
||||||
|
fg_priority = (self.sec_oam_attribute[i] & 0x20) != 0;
|
||||||
|
is_sprite_zero = self.sec_oam_is_zero[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var final_palette_entry: u16 = 0;
|
||||||
|
|
||||||
|
if (bg_pixel == 0 and fg_pixel == 0) {
|
||||||
|
final_palette_entry = 0x00;
|
||||||
|
} else if (bg_pixel == 0 and fg_pixel != 0) {
|
||||||
|
final_palette_entry = @as(u16, fg_palette) * 4 + fg_pixel;
|
||||||
|
} else if (bg_pixel != 0 and fg_pixel == 0) {
|
||||||
|
final_palette_entry = @as(u16, bg_palette) * 4 + bg_pixel;
|
||||||
|
} else {
|
||||||
|
// Both bg and fg pixels present
|
||||||
|
if (is_sprite_zero and x < 255 and (self.mask & 0x18) == 0x18) {
|
||||||
|
self.status |= 0x40; // Sprite 0 hit
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fg_priority) {
|
||||||
|
final_palette_entry = @as(u16, bg_palette) * 4 + bg_pixel;
|
||||||
|
} else {
|
||||||
|
final_palette_entry = @as(u16, fg_palette) * 4 + fg_pixel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var color_idx = self.palette[mapPalette(0x3f00 + final_palette_entry)] & 0x3f;
|
||||||
|
if ((self.mask & 0x01) != 0) {
|
||||||
|
color_idx &= 0x30;
|
||||||
|
}
|
||||||
|
var pixel_rgb = nes_ntsc_palette_rgb565[color_idx];
|
||||||
|
|
||||||
|
const emphasis = (self.mask >> 5) & 7;
|
||||||
|
if (emphasis != 0) {
|
||||||
|
var r = (pixel_rgb >> 11) & 0x1F;
|
||||||
|
var g = (pixel_rgb >> 5) & 0x3F;
|
||||||
|
var b = pixel_rgb & 0x1F;
|
||||||
|
|
||||||
|
// Red emphasis (bit 5)
|
||||||
|
if ((emphasis & 1) != 0) {
|
||||||
|
g = (g * 3) / 4;
|
||||||
|
b = (b * 3) / 4;
|
||||||
|
}
|
||||||
|
// Green emphasis (bit 6)
|
||||||
|
if ((emphasis & 2) != 0) {
|
||||||
|
r = (r * 3) / 4;
|
||||||
|
b = (b * 3) / 4;
|
||||||
|
}
|
||||||
|
// Blue emphasis (bit 7)
|
||||||
|
if ((emphasis & 4) != 0) {
|
||||||
|
r = (r * 3) / 4;
|
||||||
|
g = (g * 3) / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
pixel_rgb = (@as(u16, r) << 11) | (@as(u16, g) << 5) | @as(u16, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.framebuffer[self.scanline * 256 + x] = pixel_rgb;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn incrementVramAddress(
|
||||||
|
self: *Ppu,
|
||||||
|
) void {
|
||||||
|
const increment: u16 =
|
||||||
|
if ((self.ctrl & 0x04) != 0)
|
||||||
|
32
|
||||||
|
else
|
||||||
|
1;
|
||||||
|
|
||||||
|
self.vram_addr =
|
||||||
|
(self.vram_addr +% increment) &
|
||||||
|
0x3fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readData(
|
||||||
|
self: *Ppu,
|
||||||
|
cartridge: *Cartridge,
|
||||||
|
) u8 {
|
||||||
|
const address =
|
||||||
|
self.vram_addr & 0x3fff;
|
||||||
|
|
||||||
|
const value: u8 =
|
||||||
|
if (address < 0x3f00) blk: {
|
||||||
|
const previous =
|
||||||
|
self.read_buffer;
|
||||||
|
|
||||||
|
self.read_buffer =
|
||||||
|
self.readMemory(
|
||||||
|
cartridge,
|
||||||
|
address,
|
||||||
|
);
|
||||||
|
|
||||||
|
break :blk previous;
|
||||||
|
} else blk: {
|
||||||
|
const result =
|
||||||
|
self.readMemory(
|
||||||
|
cartridge,
|
||||||
|
address,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Palette reads aren't delayed, but
|
||||||
|
// still update the internal buffer.
|
||||||
|
self.read_buffer =
|
||||||
|
self.readMemory(
|
||||||
|
cartridge,
|
||||||
|
address - 0x1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
break :blk result;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.incrementVramAddress();
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn readMemory(
|
||||||
|
self: *Ppu,
|
||||||
|
cartridge: *Cartridge,
|
||||||
|
address_: u16,
|
||||||
|
) u8 {
|
||||||
|
const address =
|
||||||
|
address_ & 0x3fff;
|
||||||
|
|
||||||
|
return switch (address) {
|
||||||
|
0x0000...0x1fff => cartridge.ppuRead(address),
|
||||||
|
|
||||||
|
0x2000...0x3eff => self.nametable[
|
||||||
|
self.mapNametable(
|
||||||
|
cartridge.mirroring(),
|
||||||
|
address,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
|
||||||
|
0x3f00...0x3fff => self.palette[
|
||||||
|
mapPalette(address)
|
||||||
|
],
|
||||||
|
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn writeMemory(
|
||||||
|
self: *Ppu,
|
||||||
|
cartridge: *Cartridge,
|
||||||
|
address_: u16,
|
||||||
|
value: u8,
|
||||||
|
) void {
|
||||||
|
const address =
|
||||||
|
address_ & 0x3fff;
|
||||||
|
|
||||||
|
switch (address) {
|
||||||
|
0x0000...0x1fff => cartridge.ppuWrite(
|
||||||
|
address,
|
||||||
|
value,
|
||||||
|
),
|
||||||
|
|
||||||
|
0x2000...0x3eff => self.nametable[
|
||||||
|
self.mapNametable(
|
||||||
|
cartridge.mirroring(),
|
||||||
|
address,
|
||||||
|
)
|
||||||
|
] = value,
|
||||||
|
|
||||||
|
0x3f00...0x3fff => self.palette[
|
||||||
|
mapPalette(address)
|
||||||
|
] = value,
|
||||||
|
|
||||||
|
else => unreachable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn mapNametable(
|
||||||
|
_: *const Ppu,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
address: u16,
|
||||||
|
) usize {
|
||||||
|
const relative =
|
||||||
|
(@as(usize, address) - 0x2000) &
|
||||||
|
0x0fff;
|
||||||
|
|
||||||
|
const table =
|
||||||
|
relative >> 10;
|
||||||
|
|
||||||
|
const offset =
|
||||||
|
relative & 0x3ff;
|
||||||
|
|
||||||
|
const mapped_table: usize =
|
||||||
|
switch (mirroring) {
|
||||||
|
.vertical => switch (table) {
|
||||||
|
0, 2 => 0,
|
||||||
|
1, 3 => 1,
|
||||||
|
else => unreachable,
|
||||||
|
},
|
||||||
|
|
||||||
|
.horizontal => switch (table) {
|
||||||
|
0, 1 => 0,
|
||||||
|
2, 3 => 1,
|
||||||
|
else => unreachable,
|
||||||
|
},
|
||||||
|
|
||||||
|
.single_screen_lower => 0,
|
||||||
|
.single_screen_upper => 1,
|
||||||
|
|
||||||
|
.four_screen => table,
|
||||||
|
};
|
||||||
|
|
||||||
|
return mapped_table * 0x400 +
|
||||||
|
offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn mapPalette(
|
||||||
|
address: u16,
|
||||||
|
) usize {
|
||||||
|
var index: usize =
|
||||||
|
(@as(usize, address) - 0x3f00) &
|
||||||
|
0x1f;
|
||||||
|
|
||||||
|
// Universal background color mirrors.
|
||||||
|
switch (index) {
|
||||||
|
0x10 => index = 0x00,
|
||||||
|
0x14 => index = 0x04,
|
||||||
|
0x18 => index = 0x08,
|
||||||
|
0x1c => index = 0x0c,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "NES PPU - Palette Mirroring & NTSC Palette LUT" {
|
||||||
|
try std.testing.expectEqual(@as(usize, 0), mapPalette(0x3F00));
|
||||||
|
try std.testing.expectEqual(@as(usize, 0), mapPalette(0x3F10));
|
||||||
|
try std.testing.expectEqual(@as(usize, 4), mapPalette(0x3F04));
|
||||||
|
try std.testing.expectEqual(@as(usize, 4), mapPalette(0x3F14));
|
||||||
|
|
||||||
|
// Verify system palette RGB565 LUT contains non-zero color constants
|
||||||
|
try std.testing.expectEqual(@as(u16, 0x52AA), nes_ntsc_palette_rgb565[0]);
|
||||||
|
try std.testing.expectEqual(@as(u16, 0xEF7D), nes_ntsc_palette_rgb565[0x20]);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "NES PPU - Scroll Address Increments" {
|
||||||
|
var ppu = Ppu{};
|
||||||
|
ppu.reset();
|
||||||
|
|
||||||
|
ppu.vram_addr = 0x001F; // Coarse X = 31
|
||||||
|
ppu.incrementCoarseX();
|
||||||
|
try std.testing.expectEqual(@as(u16, 0x0400), ppu.vram_addr); // Coarse X = 0, switched nametable X
|
||||||
|
|
||||||
|
ppu.vram_addr = 0x73A0; // Coarse Y = 29, Fine Y = 7
|
||||||
|
ppu.incrementY();
|
||||||
|
try std.testing.expectEqual(@as(u16, 0x0800), ppu.vram_addr); // Coarse Y = 0, Fine Y = 0, switched nametable Y
|
||||||
|
}
|
||||||
@@ -0,0 +1,373 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const contract = @import("contract");
|
||||||
|
const _cpu = @import("cpu");
|
||||||
|
|
||||||
|
const Bus = @import("bus.zig");
|
||||||
|
const Cartridge = @import("cartridge.zig");
|
||||||
|
|
||||||
|
const controller = @import("controller.zig");
|
||||||
|
pub const Button = controller.Button;
|
||||||
|
|
||||||
|
// Adjust this enum spelling to whatever your CPU package
|
||||||
|
// currently exports for the Ricoh 2A03.
|
||||||
|
const Cpu = _cpu.m6502.Cpu(
|
||||||
|
Bus,
|
||||||
|
.ricoh2a03,
|
||||||
|
.cycle,
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const Nes = @This();
|
||||||
|
pub const spec = contract.SystemSpec{
|
||||||
|
.name = "Nintendo Entertainment System",
|
||||||
|
|
||||||
|
.video_outputs = &.{
|
||||||
|
.{
|
||||||
|
.max_width = 256,
|
||||||
|
.max_height = 240,
|
||||||
|
|
||||||
|
.format = .rgb565,
|
||||||
|
|
||||||
|
.aspect_ratio = .{
|
||||||
|
.numerator = 4,
|
||||||
|
.denominator = 3,
|
||||||
|
},
|
||||||
|
|
||||||
|
.refresh_rate = .{
|
||||||
|
.numerator = 60,
|
||||||
|
.denominator = 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
.audio_outputs = &.{
|
||||||
|
.{
|
||||||
|
.sample_rate = 44_100,
|
||||||
|
.channels = 1,
|
||||||
|
.format = .i16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
.input_devices = &.{
|
||||||
|
.{
|
||||||
|
.button_count = 8,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.button_count = 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
.storage_devices = &.{
|
||||||
|
.{
|
||||||
|
.name = "Battery PRG RAM",
|
||||||
|
.min_size = 0,
|
||||||
|
.max_size = 8192,
|
||||||
|
.persistent = true,
|
||||||
|
.removable = false,
|
||||||
|
.writable = true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
bus: Bus = undefined,
|
||||||
|
cpu: Cpu = undefined,
|
||||||
|
|
||||||
|
/// Initialize in place.
|
||||||
|
///
|
||||||
|
/// `rom` must remain valid for the lifetime of this NES
|
||||||
|
/// because Cartridge stores slices into it.
|
||||||
|
pub fn init(
|
||||||
|
self: *Nes,
|
||||||
|
rom: []const u8,
|
||||||
|
) Cartridge.Error!void {
|
||||||
|
self.bus =
|
||||||
|
try Bus.init(rom);
|
||||||
|
|
||||||
|
self.cpu =
|
||||||
|
Cpu.init(&self.bus);
|
||||||
|
|
||||||
|
self.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(
|
||||||
|
self: *Nes,
|
||||||
|
) void {
|
||||||
|
self.bus.reset();
|
||||||
|
self.cpu.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step(
|
||||||
|
self: *Nes,
|
||||||
|
) ?u8 {
|
||||||
|
const cycles = self.cpu.step() orelse return null;
|
||||||
|
self.serviceOamDma();
|
||||||
|
return cycles;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runFrame(
|
||||||
|
self: *Nes,
|
||||||
|
) void {
|
||||||
|
self.bus.ppu.frame_ready = false;
|
||||||
|
self.bus.apu.sample_count = 0;
|
||||||
|
|
||||||
|
while (!self.bus.ppu.frame_ready) {
|
||||||
|
_ = self.step() orelse
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const conformance = @import("conformance.zig");
|
||||||
|
|
||||||
|
test {
|
||||||
|
_ = conformance;
|
||||||
|
}
|
||||||
|
|
||||||
|
comptime {
|
||||||
|
contract.validateSystem(Nes);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn videoFrame(
|
||||||
|
self: *const Nes,
|
||||||
|
output: usize,
|
||||||
|
) contract.VideoFrame {
|
||||||
|
std.debug.assert(output == 0);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.data = std.mem.sliceAsBytes(
|
||||||
|
self.bus.ppu.framebuffer[0..],
|
||||||
|
),
|
||||||
|
|
||||||
|
.width = 256,
|
||||||
|
.height = 240,
|
||||||
|
|
||||||
|
.pitch = 256 * @sizeOf(u16),
|
||||||
|
|
||||||
|
.format = .rgb565,
|
||||||
|
|
||||||
|
.frame_number = self.bus.ppu.frame_number,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn audioBuffer(
|
||||||
|
self: *const Nes,
|
||||||
|
output: usize,
|
||||||
|
) contract.AudioBuffer {
|
||||||
|
std.debug.assert(output == 0);
|
||||||
|
|
||||||
|
const samples = self.bus.apu.sample_buffer[0..self.bus.apu.sample_count];
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.data = std.mem.sliceAsBytes(samples),
|
||||||
|
.frames = self.bus.apu.sample_count,
|
||||||
|
|
||||||
|
.sample_rate = 44_100,
|
||||||
|
.channels = 1,
|
||||||
|
.format = .i16,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setInput(
|
||||||
|
self: *Nes,
|
||||||
|
device: usize,
|
||||||
|
input: contract.DeviceInput,
|
||||||
|
) void {
|
||||||
|
std.debug.assert(device < 2);
|
||||||
|
|
||||||
|
self.bus.controllers[device]
|
||||||
|
.setButtons(
|
||||||
|
@truncate(input.buttons),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn storageView(
|
||||||
|
self: *const Nes,
|
||||||
|
slot: usize,
|
||||||
|
) contract.StorageView {
|
||||||
|
std.debug.assert(slot == 0);
|
||||||
|
return .{
|
||||||
|
.data = &self.bus.cartridge.prg_ram,
|
||||||
|
.generation = 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn loadStorage(
|
||||||
|
self: *Nes,
|
||||||
|
slot: usize,
|
||||||
|
data: []const u8,
|
||||||
|
) contract.StorageLoadError!void {
|
||||||
|
if (slot != 0) return error.InvalidSlot;
|
||||||
|
if (data.len > self.bus.cartridge.prg_ram.len) return error.InvalidSize;
|
||||||
|
@memcpy(self.bus.cartridge.prg_ram[0..data.len], data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serviceOamDma(
|
||||||
|
self: *Nes,
|
||||||
|
) void {
|
||||||
|
const page =
|
||||||
|
self.bus.takeOamDmaPage() orelse return;
|
||||||
|
|
||||||
|
const odd_cycle =
|
||||||
|
(self.cpu.cycles & 1) != 0;
|
||||||
|
|
||||||
|
// Mandatory DMA halt cycle.
|
||||||
|
self.cpu.tick();
|
||||||
|
|
||||||
|
// Alignment cycle when necessary.
|
||||||
|
if (odd_cycle)
|
||||||
|
self.cpu.tick();
|
||||||
|
|
||||||
|
const base =
|
||||||
|
@as(u16, page) << 8;
|
||||||
|
|
||||||
|
for (0..256) |i| {
|
||||||
|
// DMA read cycle.
|
||||||
|
const value =
|
||||||
|
self.cpu.readCycle(
|
||||||
|
base |
|
||||||
|
@as(u16, @intCast(i)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// DMA write cycle.
|
||||||
|
self.bus.ppu.writeOamDma(value);
|
||||||
|
self.cpu.tick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn saveState(
|
||||||
|
self: *const Nes,
|
||||||
|
buffer: []u8,
|
||||||
|
) error{BufferTooSmall}!usize {
|
||||||
|
var offset: usize = 0;
|
||||||
|
|
||||||
|
if (buffer.len < 8) return error.BufferTooSmall;
|
||||||
|
@memcpy(buffer[offset..][0..8], "6SOZNES1");
|
||||||
|
offset += 8;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn loadState(
|
||||||
|
self: *Nes,
|
||||||
|
buffer: []const u8,
|
||||||
|
) error{ InvalidState, BufferTooShort }!void {
|
||||||
|
var offset: usize = 0;
|
||||||
|
|
||||||
|
if (buffer.len < 8) return error.BufferTooShort;
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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 (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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "NES System - Save State Serialization Roundtrip" {
|
||||||
|
// Construct dummy iNES NROM header + 16K PRG + 8K CHR
|
||||||
|
var rom: [16 + 0x4000 + 0x2000]u8 = [_]u8{0} ** (16 + 0x4000 + 0x2000);
|
||||||
|
@memcpy(rom[0..4], "NES\x1a");
|
||||||
|
rom[4] = 1; // 16K PRG
|
||||||
|
rom[5] = 1; // 8K CHR
|
||||||
|
|
||||||
|
var nes: Nes = undefined;
|
||||||
|
try nes.init(&rom);
|
||||||
|
|
||||||
|
// Mutate state
|
||||||
|
nes.cpu.registers.pc = 0x1234;
|
||||||
|
nes.cpu.registers.a = 0x42;
|
||||||
|
nes.bus.ram[0x05] = 0xAA;
|
||||||
|
nes.bus.ppu.vram_addr = 0x2050;
|
||||||
|
|
||||||
|
var save_buf: [32768]u8 = undefined;
|
||||||
|
const bytes_written = try nes.saveState(&save_buf);
|
||||||
|
|
||||||
|
// Reset nes
|
||||||
|
nes.reset();
|
||||||
|
try std.testing.expect(nes.cpu.registers.pc != 0x1234);
|
||||||
|
|
||||||
|
// Load state
|
||||||
|
try nes.loadState(save_buf[0..bytes_written]);
|
||||||
|
try std.testing.expectEqual(@as(u16, 0x1234), nes.cpu.registers.pc);
|
||||||
|
try std.testing.expectEqual(@as(u8, 0x42), nes.cpu.registers.a);
|
||||||
|
try std.testing.expectEqual(@as(u8, 0xAA), nes.bus.ram[0x05]);
|
||||||
|
try std.testing.expectEqual(@as(u16, 0x2050), nes.bus.ppu.vram_addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
comptime {
|
||||||
|
contract.validateSystem(Nes);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub const nes = @import("nes/root.zig");
|
||||||
|
|
||||||
|
test {
|
||||||
|
_ = nes;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user