Skip to content

Commit 4482264

Browse files
committed
WIP
1 parent a429dc1 commit 4482264

6 files changed

Lines changed: 330 additions & 99 deletions

File tree

src/cpu/rocket68_cpu.zig

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ fn cpuWrite32(_: ?*c.M68kCpu, address: c.u32, value: c.u32) callconv(.c) void {
3838
bus.write32(address, value);
3939
}
4040

41+
fn cpuIntAck(core: ?*c.M68kCpu, _: c_int) callconv(.c) c_int {
42+
const cpu = core orelse return -1;
43+
cpu.irq_level = 0;
44+
return -1;
45+
}
46+
4147
pub const Cpu = struct {
4248
const default_stack_pointer: u32 = 0x00FF_FE00;
4349
const default_program_counter: u32 = 0x0000_0200;
@@ -62,6 +68,7 @@ pub const Cpu = struct {
6268
c.m68k_set_write8_callback(&self.core, cpuWrite8);
6369
c.m68k_set_write16_callback(&self.core, cpuWrite16);
6470
c.m68k_set_write32_callback(&self.core, cpuWrite32);
71+
c.m68k_set_int_ack_callback(&self.core, cpuIntAck);
6572

6673
return self;
6774
}
@@ -97,7 +104,7 @@ pub const Cpu = struct {
97104
}
98105

99106
pub fn runCycles(self: *Cpu, bus: *Bus, budget: u32) u32 {
100-
if (self.halted or budget == 0) return 0;
107+
if (budget == 0) return 0;
101108

102109
active_bus = bus;
103110
const ran = c.m68k_execute(&self.core, @intCast(budget));
@@ -108,7 +115,11 @@ pub const Cpu = struct {
108115
}
109116

110117
pub fn requestInterrupt(self: *Cpu, level: u3) void {
111-
c.m68k_set_irq(&self.core, @intCast(level));
118+
const current: c_int = @intCast(self.core.irq_level);
119+
const new_level: c_int = @intCast(level);
120+
if (new_level > current) {
121+
c.m68k_set_irq(&self.core, new_level);
122+
}
112123
}
113124

114125
pub fn debugDump(self: *const Cpu) void {

src/io.zig

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,12 @@ pub const Io = struct {
2828
}
2929

3030
pub fn read(self: *Io, address: u32) u8 {
31-
// ... (Keep existing switch dispatch)
3231
switch (address & 0xFF) {
33-
0x01 => return 0xA0, // Version: Export NTSC (Bit 7=1, Bit 6=0)
34-
0x03 => return self.readData(0), // Port A
35-
0x05 => return self.readData(1), // Port B
36-
0x09 => return self.ctrl[0],
37-
0x0B => return self.ctrl[1],
32+
0x01 => return 0x80, // Version register (Overseas NTSC, no Mega-CD), low byte
33+
0x03 => return self.readData(0), // Port A data, low byte
34+
0x05 => return self.readData(1), // Port B data, low byte
35+
0x09 => return self.ctrl[0], // Port A control, low byte
36+
0x0B => return self.ctrl[1], // Port B control, low byte
3837
else => return 0,
3938
}
4039
}

src/main.zig

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,8 @@ pub fn main() !void {
440440
const hblank_master_cycles = clock.ntsc_hblank_master_cycles;
441441
var m68k_sync = clock.M68kSync{};
442442
const target_frame_ns: u64 = 1_000_000_000 / 60;
443+
var frame_counter: u32 = 0;
444+
const uncapped_boot_frames: u32 = 240;
443445

444446
mainLoop: while (true) {
445447
const frame_start = std.time.nanoTimestamp();
@@ -517,24 +519,38 @@ pub fn main() !void {
517519
const active_budget = m68k_sync.budgetFromMaster(active_display_master_cycles);
518520
const active_ran = cpu.runCycles(&bus, active_budget);
519521
bus.stepMaster(m68k_sync.commitM68kCycles(active_ran));
520-
if (cpu.halted) break :mainLoop;
521522

522523
if (bus.vdp.consumeHintForLine(line, visible_lines)) {
523-
cpu.requestInterrupt(4); // H-BLANK interrupt
524+
cpu.requestInterrupt(4);
524525
}
525526

526527
bus.vdp.setHBlank(true);
527528
const hblank_budget = m68k_sync.budgetFromMaster(hblank_master_cycles);
528529
const hblank_ran = cpu.runCycles(&bus, hblank_budget);
529530
bus.stepMaster(m68k_sync.commitM68kCycles(hblank_ran));
530-
if (cpu.halted) break :mainLoop;
531531
bus.vdp.setHBlank(false);
532532

533533
if (line < visible_lines) {
534534
bus.vdp.renderScanline(line);
535535
}
536536
}
537537
bus.vdp.odd_frame = !bus.vdp.odd_frame;
538+
if ((frame_counter % 5) == 0) {
539+
var cram_nz: usize = 0;
540+
for (bus.vdp.cram) |b| {
541+
if (b != 0) cram_nz += 1;
542+
}
543+
std.debug.print("f={d} pc={X:0>8} f600={X:0>2} cram_nz={d} reg1={X:0>2} io_ver={X:0>2} dma={any}/{any}\n", .{
544+
frame_counter,
545+
cpu.core.pc,
546+
bus.ram[0xF600],
547+
cram_nz,
548+
bus.vdp.regs[1],
549+
bus.read8(0xA10001),
550+
bus.vdp.dma_active,
551+
bus.vdp.dma_fill,
552+
});
553+
}
538554
const audio_frames = bus.audio_timing.takePending();
539555
if (audio) |*a| {
540556
try a.output.pushPending(audio_frames, &bus.z80);
@@ -561,9 +577,12 @@ pub fn main() !void {
561577
try zsdl3.renderTexture(renderer, vdp_texture, null, null);
562578
zsdl3.renderPresent(renderer);
563579

564-
const frame_elapsed: u64 = @intCast(std.time.nanoTimestamp() - frame_start);
565-
if (frame_elapsed < target_frame_ns) {
566-
std.Thread.sleep(target_frame_ns - frame_elapsed);
580+
frame_counter += 1;
581+
if (frame_counter > uncapped_boot_frames) {
582+
const frame_elapsed: u64 = @intCast(std.time.nanoTimestamp() - frame_start);
583+
if (frame_elapsed < target_frame_ns) {
584+
std.Thread.sleep(target_frame_ns - frame_elapsed);
585+
}
567586
}
568587
}
569588
}

src/memory.zig

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,20 @@ pub const Bus = struct {
145145
self.ensureZ80HostWindow();
146146
const zaddr: u16 = @truncate(addr & 0xFFFF);
147147
return self.z80.readByte(zaddr);
148+
} else if (addr >= 0xC00000 and addr < 0xC00020) {
149+
if (addr < 0xC00004) {
150+
const word = self.vdp.readData();
151+
return if ((addr & 1) == 0) @intCast((word >> 8) & 0xFF) else @intCast(word & 0xFF);
152+
}
153+
if (addr < 0xC00008) {
154+
const word = self.vdp.readControl();
155+
return if ((addr & 1) == 0) @intCast((word >> 8) & 0xFF) else @intCast(word & 0xFF);
156+
}
157+
if (addr < 0xC00010) {
158+
const word = self.vdp.readHVCounter();
159+
return if ((addr & 1) == 0) @intCast((word >> 8) & 0xFF) else @intCast(word & 0xFF);
160+
}
161+
return 0;
148162
} else if (addr >= 0xA10000 and addr < 0xA10100) {
149163
// IO
150164
return self.io.read(addr);
@@ -156,10 +170,7 @@ pub const Bus = struct {
156170

157171
pub fn read16(self: *Bus, address: u32) u16 {
158172
const addr = address & 0xFFFFFF;
159-
if (addr >= 0xA10000 and addr < 0xA10100) {
160-
const val = self.io.read(addr);
161-
return @as(u16, val) << 8;
162-
} else if (addr == 0xA11100) { // Z80 Bus Request
173+
if (addr == 0xA11100) { // Z80 Bus Request
163174
return self.z80.readBusReq();
164175
} else if (addr == 0xA11200) { // Z80 Reset
165176
return 0; // Write only?
@@ -207,9 +218,13 @@ pub const Bus = struct {
207218
// IO
208219
self.io.write(addr, value);
209220
} else if (addr >= 0xC00000 and addr < 0xC00020) {
210-
// VDP (Byte writes to VDP are complex, usually MSB/LSB duplicated or ignored depending on port)
211-
// For now, ignore or implement as needed.
212-
// VDP Data port byte writes are valid?
221+
const word: u16 = if ((addr & 1) == 0) (@as(u16, value) << 8) else @as(u16, value);
222+
if (addr < 0xC00004) {
223+
self.vdp.writeData(word);
224+
} else if (addr < 0xC00008) {
225+
self.vdp.writeControl(word);
226+
}
227+
return;
213228
}
214229
}
215230

@@ -263,41 +278,36 @@ pub const Bus = struct {
263278
if (self.vdp.dma_active) {
264279
// Perform DMA
265280
if (self.vdp.dma_fill) {
266-
// DMA Fill (VRAM Fill)
267-
// Triggered by writing to the VDP data port, but the length/destination are set up here.
268-
// In a real VDP, the fill happens as the CPU writes to the data port?
269-
// Checks regs[23] bit 7 (DMA Type 1 = Fill).
270-
// But here we just want to ensure we don't crash or hang.
271-
// For now, let's acknowledge it and clear the active flag so we don't loop forever.
272-
self.vdp.dma_active = false;
281+
// DMA fill completes on the next VDP data port write.
282+
// Keep the DMA state armed until VDP.writeData consumes it.
273283
} else {
274284
// 68k -> VRAM Copy (DMA Mode 0 or 1)
275-
var len = @as(u32, self.vdp.dma_length);
276-
if (len == 0) len = 0xFFFF; // Usually 0 in reg means 64k word transfer (128kB)?
277-
// Docs say: Register value 0 = 0? Or 64k?
278-
// Reg 19/20 are 8-bit. reg19<<8 | reg20.
279-
// Usually 0 means 0x10000 words.
285+
var remaining = self.vdp.dma_remaining;
286+
if (remaining == 0) {
287+
self.vdp.dma_active = false;
288+
return;
289+
}
290+
// Incremental DMA: avoid stalling an entire frame on large transfers.
291+
// This keeps emulation responsive until cycle-accurate DMA timing is added.
292+
var budget_words = @as(u32, master_cycles / 8);
293+
if (budget_words == 0) budget_words = 1;
294+
if (budget_words > remaining) budget_words = remaining;
280295

281296
var src_addr = self.vdp.dma_source_addr;
282-
283-
// Transfer loop (16-bit words)
284-
// Note: Generic DMA is from 68k Bus to VDP Data Port.
285-
// It auto-increments VDP address.
286-
while (len > 0) {
297+
var words_done: u32 = 0;
298+
while (words_done < budget_words) : (words_done += 1) {
287299
const word = self.read16(src_addr);
288300
self.vdp.writeData(word);
289301
src_addr += 2; // Source address increments
290-
// Note: If Source is ROM/RAM, it increments.
291-
// If Source is fixed (not supported by standard DMA copy?), it wouldn't.
292-
// Standard 68k->VDP DMA increments source.
293-
294-
if (len == 0) break; // Overflow protection
295-
len -= 1;
296302
}
297303

298304
self.vdp.dma_source_addr = src_addr;
299-
self.vdp.dma_length = 0;
300-
self.vdp.dma_active = false;
305+
remaining -= words_done;
306+
self.vdp.dma_remaining = remaining;
307+
if (remaining == 0) {
308+
self.vdp.dma_length = 0;
309+
self.vdp.dma_active = false;
310+
}
301311
}
302312
}
303313
}

src/regression_tests.zig

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,51 @@ test "vdp control decode does not treat 0xA*** command word as register write" {
278278
try testing.expect(vdp.pending_command);
279279
}
280280

281+
test "sonic rom advances startup state across frames" {
282+
var bus = try Bus.init(testing.allocator, "roms/sn.smd");
283+
defer bus.deinit(testing.allocator);
284+
var cpu = Cpu.init();
285+
cpu.reset(&bus);
286+
287+
var m68k_sync = clock.M68kSync{};
288+
const visible_lines = clock.ntsc_visible_lines;
289+
const total_lines = clock.ntsc_lines_per_frame;
290+
291+
for (0..12) |_| {
292+
bus.vdp.beginFrame();
293+
for (0..total_lines) |line_idx| {
294+
const line: u16 = @intCast(line_idx);
295+
const entering_vblank = bus.vdp.setScanlineState(line, visible_lines, total_lines);
296+
if (entering_vblank and bus.vdp.isVBlankInterruptEnabled()) {
297+
cpu.requestInterrupt(6);
298+
}
299+
bus.vdp.setHBlank(false);
300+
301+
const active_budget = m68k_sync.budgetFromMaster(clock.ntsc_active_master_cycles);
302+
const active_ran = cpu.runCycles(&bus, active_budget);
303+
bus.stepMaster(m68k_sync.commitM68kCycles(active_ran));
304+
305+
if (bus.vdp.consumeHintForLine(line, visible_lines)) {
306+
cpu.requestInterrupt(4);
307+
}
308+
309+
bus.vdp.setHBlank(true);
310+
const hblank_budget = m68k_sync.budgetFromMaster(clock.ntsc_hblank_master_cycles);
311+
const hblank_ran = cpu.runCycles(&bus, hblank_budget);
312+
bus.stepMaster(m68k_sync.commitM68kCycles(hblank_ran));
313+
bus.vdp.setHBlank(false);
314+
315+
if (line < visible_lines) {
316+
bus.vdp.renderScanline(line);
317+
}
318+
}
319+
bus.vdp.odd_frame = !bus.vdp.odd_frame;
320+
}
321+
322+
try testing.expect(@as(u32, cpu.core.pc) != 0x0000_0200);
323+
try testing.expect(bus.vdp.regs[1] != 0 or bus.vdp.regs[2] != 0 or bus.vdp.regs[4] != 0);
324+
}
325+
281326
test "vdp renders plane B when plane A is transparent" {
282327
var vdp = Vdp.init();
283328
vdp.regs[2] = 0x00; // Plane A base 0x0000

0 commit comments

Comments
 (0)