feat: implement ARM read open bus

This commit is contained in:
Rekai Nyangadzayi Musuka 2022-04-13 23:21:25 -03:00
parent 6d5c30ac25
commit 9b9b6c0d6f
8 changed files with 115 additions and 73 deletions

View File

@ -1,6 +1,7 @@
const std = @import("std"); const std = @import("std");
const AudioDeviceId = @import("sdl2").SDL_AudioDeviceID; const AudioDeviceId = @import("sdl2").SDL_AudioDeviceID;
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
const Bios = @import("bus/Bios.zig"); const Bios = @import("bus/Bios.zig");
const Ewram = @import("bus/Ewram.zig"); const Ewram = @import("bus/Ewram.zig");
const GamePak = @import("bus/GamePak.zig"); const GamePak = @import("bus/GamePak.zig");
@ -18,7 +19,6 @@ const Allocator = std.mem.Allocator;
const log = std.log.scoped(.Bus); const log = std.log.scoped(.Bus);
const rotr = @import("util.zig").rotr; const rotr = @import("util.zig").rotr;
const Self = @This(); const Self = @This();
const panic_on_und_bus: bool = false; const panic_on_und_bus: bool = false;
@ -33,19 +33,21 @@ iwram: Iwram,
ewram: Ewram, ewram: Ewram,
io: Io, io: Io,
cpu: ?*Arm7tdmi,
sched: *Scheduler, sched: *Scheduler,
pub fn init(alloc: Allocator, sched: *Scheduler, dev: AudioDeviceId, paths: FilePaths) !Self { pub fn init(alloc: Allocator, sched: *Scheduler, paths: FilePaths) !Self {
return Self{ return Self{
.pak = try GamePak.init(alloc, paths.rom, paths.save), .pak = try GamePak.init(alloc, paths.rom, paths.save),
.bios = try Bios.init(alloc, paths.bios), .bios = try Bios.init(alloc, paths.bios),
.ppu = try Ppu.init(alloc, sched), .ppu = try Ppu.init(alloc, sched),
.apu = Apu.init(dev), .apu = Apu.init(),
.iwram = try Iwram.init(alloc), .iwram = try Iwram.init(alloc),
.ewram = try Ewram.init(alloc), .ewram = try Ewram.init(alloc),
.dma = DmaControllers.init(), .dma = DmaControllers.init(),
.tim = Timers.init(sched), .tim = Timers.init(sched),
.io = Io.init(), .io = Io.init(),
.cpu = null,
.sched = sched, .sched = sched,
}; };
} }
@ -74,6 +76,29 @@ fn isDmaRunning(self: *const Self) bool {
self.dma._3.active; self.dma._3.active;
} }
pub fn debugRead(self: *const Self, comptime T: type, address: u32) T {
const cached = self.sched.tick;
defer self.sched.tick = cached;
return self.read(T, address);
}
fn readOpenBus(self: *const Self, comptime T: type, address: u32) T {
if (self.cpu.?.cpsr.t.read()) {
log.err("TODO: {} open bus read in THUMB", .{T});
return 0;
}
const word = self.debugRead(u32, self.cpu.?.r[15] + 4);
return @truncate(T, rotr(u32, word, 8 * (address & 3)));
}
fn readBios(self: *const Self, comptime T: type, address: u32) T {
if (address < Bios.size) return self.bios.read(T, alignAddress(T, address));
return self.readOpenBus(T, address);
}
pub fn read(self: *const Self, comptime T: type, address: u32) T { pub fn read(self: *const Self, comptime T: type, address: u32) T {
const page = @truncate(u8, address >> 24); const page = @truncate(u8, address >> 24);
const align_addr = alignAddress(T, address); const align_addr = alignAddress(T, address);
@ -81,7 +106,7 @@ pub fn read(self: *const Self, comptime T: type, address: u32) T {
return switch (page) { return switch (page) {
// General Internal Memory // General Internal Memory
0x00 => self.bios.read(T, align_addr), 0x00 => self.readBios(T, address),
0x02 => self.ewram.read(T, align_addr), 0x02 => self.ewram.read(T, align_addr),
0x03 => self.iwram.read(T, align_addr), 0x03 => self.iwram.read(T, align_addr),
0x04 => io.read(self, T, align_addr), 0x04 => io.read(self, T, align_addr),
@ -105,7 +130,7 @@ pub fn read(self: *const Self, comptime T: type, address: u32) T {
break :blk @as(T, value) * multiplier; break :blk @as(T, value) * multiplier;
}, },
else => undRead("Tried to read {} from 0x{X:0>8}", .{ T, address }), else => readOpenBus(self, T, address),
}; };
} }

View File

@ -24,9 +24,9 @@ pub const Apu = struct {
dma_cnt: io.DmaSoundControl, dma_cnt: io.DmaSoundControl,
cnt: io.SoundControl, cnt: io.SoundControl,
dev: AudioDeviceId, dev: ?AudioDeviceId,
pub fn init(dev: AudioDeviceId) Self { pub fn init() Self {
return .{ return .{
.ch1 = ToneSweep.init(), .ch1 = ToneSweep.init(),
.ch2 = Tone.init(), .ch2 = Tone.init(),
@ -40,10 +40,14 @@ pub const Apu = struct {
.cnt = .{ .raw = 0 }, .cnt = .{ .raw = 0 },
.bias = .{ .raw = 0x0200 }, .bias = .{ .raw = 0x0200 },
.dev = dev, .dev = null,
}; };
} }
pub fn attachAudioDevice(self: *Self, dev: AudioDeviceId) void {
self.dev = dev;
}
pub fn setDmaCnt(self: *Self, value: u16) void { pub fn setDmaCnt(self: *Self, value: u16) void {
const new: io.DmaSoundControl = .{ .raw = value }; const new: io.DmaSoundControl = .{ .raw = value };
@ -83,7 +87,7 @@ pub const Apu = struct {
}, },
}; };
_ = SDL.SDL_QueueAudio(self.dev, &samples, 2); if (self.dev) |dev| _ = SDL.SDL_QueueAudio(dev, &samples, 2);
} }
}; };

View File

@ -2,6 +2,9 @@ const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const log = std.log.scoped(.Bios); const log = std.log.scoped(.Bios);
/// Size of the BIOS in bytes
pub const size = 0x4000;
const Self = @This(); const Self = @This();
buf: ?[]u8, buf: ?[]u8,

View File

@ -5,7 +5,9 @@ const Bus = @import("Bus.zig");
const Bit = @import("bitfield").Bit; const Bit = @import("bitfield").Bit;
const Bitfield = @import("bitfield").Bitfield; const Bitfield = @import("bitfield").Bitfield;
const Scheduler = @import("scheduler.zig").Scheduler; const Scheduler = @import("scheduler.zig").Scheduler;
const FilePaths = @import("util.zig").FilePaths;
const Allocator = std.mem.Allocator;
const File = std.fs.File; const File = std.fs.File;
// ARM Instruction Groups // ARM Instruction Groups
@ -59,7 +61,7 @@ pub const Arm7tdmi = struct {
r: [16]u32, r: [16]u32,
sched: *Scheduler, sched: *Scheduler,
bus: *Bus, bus: Bus,
cpsr: PSR, cpsr: PSR,
spsr: PSR, spsr: PSR,
@ -77,11 +79,11 @@ pub const Arm7tdmi = struct {
log_buf: [0x100]u8, log_buf: [0x100]u8,
binary_log: bool, binary_log: bool,
pub fn init(sched: *Scheduler, bus: *Bus) Self { pub fn init(alloc: Allocator, sched: *Scheduler, paths: FilePaths) !Self {
return .{ var cpu: Arm7tdmi = .{
.r = [_]u32{0x00} ** 16, .r = [_]u32{0x00} ** 16,
.sched = sched, .sched = sched,
.bus = bus, .bus = try Bus.init(alloc, sched, paths),
.cpsr = .{ .raw = 0x0000_001F }, .cpsr = .{ .raw = 0x0000_001F },
.spsr = .{ .raw = 0x0000_0000 }, .spsr = .{ .raw = 0x0000_0000 },
.banked_fiq = [_]u32{0x00} ** 10, .banked_fiq = [_]u32{0x00} ** 10,
@ -91,6 +93,12 @@ pub const Arm7tdmi = struct {
.log_buf = undefined, .log_buf = undefined,
.binary_log = false, .binary_log = false,
}; };
cpu.bus.cpu = &cpu;
return cpu;
}
pub fn deinit(self: Self) void {
self.bus.deinit();
} }
pub fn useLogger(self: *Self, file: *const File, is_binary: bool) void { pub fn useLogger(self: *Self, file: *const File, is_binary: bool) void {
@ -250,13 +258,13 @@ pub const Arm7tdmi = struct {
const opcode = self.thumbFetch(); const opcode = self.thumbFetch();
if (enable_logging) if (self.log_file) |file| self.debug_log(file, opcode); if (enable_logging) if (self.log_file) |file| self.debug_log(file, opcode);
thumb_lut[thumbIdx(opcode)](self, self.bus, opcode); thumb_lut[thumbIdx(opcode)](self, &self.bus, opcode);
} else { } else {
const opcode = self.fetch(); const opcode = self.fetch();
if (enable_logging) if (self.log_file) |file| self.debug_log(file, opcode); if (enable_logging) if (self.log_file) |file| self.debug_log(file, opcode);
if (checkCond(self.cpsr, @truncate(u4, opcode >> 28))) { if (checkCond(self.cpsr, @truncate(u4, opcode >> 28))) {
arm_lut[armIdx(opcode)](self, self.bus, opcode); arm_lut[armIdx(opcode)](self, &self.bus, opcode);
} }
} }
} }

View File

@ -32,42 +32,42 @@ const RunKind = enum {
LimitedBusy, LimitedBusy,
}; };
pub fn run(kind: RunKind, quit: *Atomic(bool), fps: *FpsAverage, sched: *Scheduler, cpu: *Arm7tdmi, bus: *Bus) void { pub fn run(kind: RunKind, quit: *Atomic(bool), fps: *FpsAverage, sched: *Scheduler, cpu: *Arm7tdmi) void {
switch (kind) { switch (kind) {
.Unlimited => runUnsync(quit, sched, cpu, bus), .Unlimited => runUnsync(quit, sched, cpu),
.Limited => runSync(quit, sched, cpu, bus), .Limited => runSync(quit, sched, cpu),
.UnlimitedFPS => runUnsyncFps(quit, fps, sched, cpu, bus), .UnlimitedFPS => runUnsyncFps(quit, fps, sched, cpu),
.LimitedFPS => runSyncFps(quit, fps, sched, cpu, bus), .LimitedFPS => runSyncFps(quit, fps, sched, cpu),
.LimitedBusy => runBusyLoop(quit, sched, cpu, bus), .LimitedBusy => runBusyLoop(quit, sched, cpu),
} }
} }
pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi, bus: *Bus) void { pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
const frame_end = sched.tick + cycles_per_frame; const frame_end = sched.tick + cycles_per_frame;
while (sched.tick < frame_end) { while (sched.tick < frame_end) {
if (bus.io.haltcnt == .Halt) sched.tick += 1; if (cpu.bus.io.haltcnt == .Halt) sched.tick += 1;
if (bus.io.haltcnt == .Execute) cpu.step(); if (cpu.bus.io.haltcnt == .Execute) cpu.step();
bus.handleDMATransfers(); cpu.bus.handleDMATransfers();
while (sched.tick >= sched.nextTimestamp()) { while (sched.tick >= sched.nextTimestamp()) {
sched.handleEvent(cpu, bus); sched.handleEvent(cpu);
} }
} }
} }
pub fn runUnsync(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, bus: *Bus) void { pub fn runUnsync(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi) void {
log.info("Unsynchronized EmuThread has begun", .{}); log.info("Unsynchronized EmuThread has begun", .{});
while (!quit.load(.Unordered)) runFrame(sched, cpu, bus); while (!quit.load(.Unordered)) runFrame(sched, cpu);
} }
pub fn runSync(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, bus: *Bus) void { pub fn runSync(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi) void {
log.info("Synchronized EmuThread has begun", .{}); log.info("Synchronized EmuThread has begun", .{});
var timer = Timer.start() catch unreachable; var timer = Timer.start() catch unreachable;
var wake_time: u64 = frame_period; var wake_time: u64 = frame_period;
while (!quit.load(.Unordered)) { while (!quit.load(.Unordered)) {
runFrame(sched, cpu, bus); runFrame(sched, cpu);
// Put the Thread to Sleep + Backup Spin Loop // Put the Thread to Sleep + Backup Spin Loop
// This saves on resource usage when frame limiting // This saves on resource usage when frame limiting
@ -78,24 +78,24 @@ pub fn runSync(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, bus: *Bus
} }
} }
pub fn runUnsyncFps(quit: *Atomic(bool), fps: *FpsAverage, sched: *Scheduler, cpu: *Arm7tdmi, bus: *Bus) void { pub fn runUnsyncFps(quit: *Atomic(bool), fps: *FpsAverage, sched: *Scheduler, cpu: *Arm7tdmi) void {
log.info("Unsynchronized EmuThread with FPS Tracking has begun", .{}); log.info("Unsynchronized EmuThread with FPS Tracking has begun", .{});
var fps_timer = Timer.start() catch unreachable; var fps_timer = Timer.start() catch unreachable;
while (!quit.load(.Unordered)) { while (!quit.load(.Unordered)) {
runFrame(sched, cpu, bus); runFrame(sched, cpu);
fps.add(fps_timer.lap()); fps.add(fps_timer.lap());
} }
} }
pub fn runSyncFps(quit: *Atomic(bool), fps: *FpsAverage, sched: *Scheduler, cpu: *Arm7tdmi, bus: *Bus) void { pub fn runSyncFps(quit: *Atomic(bool), fps: *FpsAverage, sched: *Scheduler, cpu: *Arm7tdmi) void {
log.info("Synchronized EmuThread has begun", .{}); log.info("Synchronized EmuThread has begun", .{});
var timer = Timer.start() catch unreachable; var timer = Timer.start() catch unreachable;
var fps_timer = Timer.start() catch unreachable; var fps_timer = Timer.start() catch unreachable;
var wake_time: u64 = frame_period; var wake_time: u64 = frame_period;
while (!quit.load(.Unordered)) { while (!quit.load(.Unordered)) {
runFrame(sched, cpu, bus); runFrame(sched, cpu);
// Put the Thread to Sleep + Backup Spin Loop // Put the Thread to Sleep + Backup Spin Loop
// This saves on resource usage when frame limiting // This saves on resource usage when frame limiting
@ -109,13 +109,13 @@ pub fn runSyncFps(quit: *Atomic(bool), fps: *FpsAverage, sched: *Scheduler, cpu:
} }
} }
pub fn runBusyLoop(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, bus: *Bus) void { pub fn runBusyLoop(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi) void {
log.info("Run EmuThread with spin-loop sync", .{}); log.info("Run EmuThread with spin-loop sync", .{});
var timer = Timer.start() catch unreachable; var timer = Timer.start() catch unreachable;
var wake_time: u64 = frame_period; var wake_time: u64 = frame_period;
while (!quit.load(.Unordered)) { while (!quit.load(.Unordered)) {
runFrame(sched, cpu, bus); runFrame(sched, cpu);
spinLoop(&timer, wake_time); spinLoop(&timer, wake_time);
// Update to the new wake time // Update to the new wake time

View File

@ -92,10 +92,10 @@ pub fn main() anyerror!void {
defer scheduler.deinit(); defer scheduler.deinit();
const paths = .{ .bios = bios_path, .rom = rom_path, .save = save_path }; const paths = .{ .bios = bios_path, .rom = rom_path, .save = save_path };
var bus = try Bus.init(alloc, &scheduler, audio_dev, paths); var cpu = try Arm7tdmi.init(alloc, &scheduler, paths);
defer bus.deinit(); defer cpu.deinit();
var cpu = Arm7tdmi.init(&scheduler, &bus); cpu.bus.apu.attachAudioDevice(audio_dev);
cpu.fastBoot(); cpu.fastBoot();
const log_file: ?File = if (enable_logging) blk: { const log_file: ?File = if (enable_logging) blk: {
@ -110,10 +110,10 @@ pub fn main() anyerror!void {
var emu_rate = FpsAverage.init(); var emu_rate = FpsAverage.init();
// Create Emulator Thread // Create Emulator Thread
const emu_thread = try Thread.spawn(.{}, emu.run, .{ .LimitedFPS, &quit, &emu_rate, &scheduler, &cpu, &bus }); const emu_thread = try Thread.spawn(.{}, emu.run, .{ .LimitedFPS, &quit, &emu_rate, &scheduler, &cpu });
defer emu_thread.join(); defer emu_thread.join();
const title = correctTitle(bus.pak.title); const title = correctTitle(cpu.bus.pak.title);
var title_buf: [0x20]u8 = std.mem.zeroes([0x20]u8); var title_buf: [0x20]u8 = std.mem.zeroes([0x20]u8);
const window_title = try std.fmt.bufPrint(&title_buf, "ZBA | {s}", .{title}); const window_title = try std.fmt.bufPrint(&title_buf, "ZBA | {s}", .{title});
@ -145,36 +145,38 @@ pub fn main() anyerror!void {
switch (event.type) { switch (event.type) {
SDL.SDL_QUIT => break :emu_loop, SDL.SDL_QUIT => break :emu_loop,
SDL.SDL_KEYDOWN => { SDL.SDL_KEYDOWN => {
const io = &cpu.bus.io;
const key_code = event.key.keysym.sym; const key_code = event.key.keysym.sym;
switch (key_code) { switch (key_code) {
SDL.SDLK_UP => bus.io.keyinput.up.unset(), SDL.SDLK_UP => io.keyinput.up.unset(),
SDL.SDLK_DOWN => bus.io.keyinput.down.unset(), SDL.SDLK_DOWN => io.keyinput.down.unset(),
SDL.SDLK_LEFT => bus.io.keyinput.left.unset(), SDL.SDLK_LEFT => io.keyinput.left.unset(),
SDL.SDLK_RIGHT => bus.io.keyinput.right.unset(), SDL.SDLK_RIGHT => io.keyinput.right.unset(),
SDL.SDLK_x => bus.io.keyinput.a.unset(), SDL.SDLK_x => io.keyinput.a.unset(),
SDL.SDLK_z => bus.io.keyinput.b.unset(), SDL.SDLK_z => io.keyinput.b.unset(),
SDL.SDLK_a => bus.io.keyinput.shoulder_l.unset(), SDL.SDLK_a => io.keyinput.shoulder_l.unset(),
SDL.SDLK_s => bus.io.keyinput.shoulder_r.unset(), SDL.SDLK_s => io.keyinput.shoulder_r.unset(),
SDL.SDLK_RETURN => bus.io.keyinput.start.unset(), SDL.SDLK_RETURN => io.keyinput.start.unset(),
SDL.SDLK_RSHIFT => bus.io.keyinput.select.unset(), SDL.SDLK_RSHIFT => io.keyinput.select.unset(),
else => {}, else => {},
} }
}, },
SDL.SDL_KEYUP => { SDL.SDL_KEYUP => {
const io = &cpu.bus.io;
const key_code = event.key.keysym.sym; const key_code = event.key.keysym.sym;
switch (key_code) { switch (key_code) {
SDL.SDLK_UP => bus.io.keyinput.up.set(), SDL.SDLK_UP => io.keyinput.up.set(),
SDL.SDLK_DOWN => bus.io.keyinput.down.set(), SDL.SDLK_DOWN => io.keyinput.down.set(),
SDL.SDLK_LEFT => bus.io.keyinput.left.set(), SDL.SDLK_LEFT => io.keyinput.left.set(),
SDL.SDLK_RIGHT => bus.io.keyinput.right.set(), SDL.SDLK_RIGHT => io.keyinput.right.set(),
SDL.SDLK_x => bus.io.keyinput.a.set(), SDL.SDLK_x => io.keyinput.a.set(),
SDL.SDLK_z => bus.io.keyinput.b.set(), SDL.SDLK_z => io.keyinput.b.set(),
SDL.SDLK_a => bus.io.keyinput.shoulder_l.set(), SDL.SDLK_a => io.keyinput.shoulder_l.set(),
SDL.SDLK_s => bus.io.keyinput.shoulder_r.set(), SDL.SDLK_s => io.keyinput.shoulder_r.set(),
SDL.SDLK_RETURN => bus.io.keyinput.start.set(), SDL.SDLK_RETURN => io.keyinput.start.set(),
SDL.SDLK_RSHIFT => bus.io.keyinput.select.set(), SDL.SDLK_RSHIFT => io.keyinput.select.set(),
else => {}, else => {},
} }
}, },
@ -183,7 +185,7 @@ pub fn main() anyerror!void {
} }
// FIXME: Is it OK just to copy the Emulator's Frame Buffer to SDL? // FIXME: Is it OK just to copy the Emulator's Frame Buffer to SDL?
const buf_ptr = bus.ppu.framebuf.ptr; const buf_ptr = cpu.bus.ppu.framebuf.ptr;
_ = SDL.SDL_UpdateTexture(texture, null, buf_ptr, framebuf_pitch); _ = SDL.SDL_UpdateTexture(texture, null, buf_ptr, framebuf_pitch);
_ = SDL.SDL_RenderCopy(renderer, texture, null, null); _ = SDL.SDL_RenderCopy(renderer, texture, null, null);
SDL.SDL_RenderPresent(renderer); SDL.SDL_RenderPresent(renderer);

View File

@ -366,7 +366,7 @@ pub const Ppu = struct {
} }
// See if HBlank DMA is present and not enabled // See if HBlank DMA is present and not enabled
pollBlankingDma(cpu.bus, .HBlank); pollBlankingDma(&cpu.bus, .HBlank);
self.dispstat.hblank.set(); self.dispstat.hblank.set();
self.sched.push(.HBlank, self.sched.now() + (68 * 4) - late); self.sched.push(.HBlank, self.sched.now() + (68 * 4) - late);
@ -403,7 +403,7 @@ pub const Ppu = struct {
} }
// See if Vblank DMA is present and not enabled // See if Vblank DMA is present and not enabled
pollBlankingDma(cpu.bus, .VBlank); pollBlankingDma(&cpu.bus, .VBlank);
} }
if (scanline == 227) self.dispstat.vblank.unset(); if (scanline == 227) self.dispstat.vblank.unset();

View File

@ -29,7 +29,7 @@ pub const Scheduler = struct {
return self.tick; return self.tick;
} }
pub fn handleEvent(self: *Self, cpu: *Arm7tdmi, bus: *Bus) void { pub fn handleEvent(self: *Self, cpu: *Arm7tdmi) void {
if (self.queue.removeOrNull()) |event| { if (self.queue.removeOrNull()) |event| {
const late = self.tick - event.tick; const late = self.tick - event.tick;
@ -40,19 +40,19 @@ pub const Scheduler = struct {
}, },
.Draw => { .Draw => {
// The end of a VDraw // The end of a VDraw
bus.ppu.drawScanline(); cpu.bus.ppu.drawScanline();
bus.ppu.handleHDrawEnd(cpu, late); cpu.bus.ppu.handleHDrawEnd(cpu, late);
}, },
.TimerOverflow => |id| { .TimerOverflow => |id| {
switch (id) { switch (id) {
0 => bus.tim._0.handleOverflow(cpu, late), 0 => cpu.bus.tim._0.handleOverflow(cpu, late),
1 => bus.tim._1.handleOverflow(cpu, late), 1 => cpu.bus.tim._1.handleOverflow(cpu, late),
2 => bus.tim._2.handleOverflow(cpu, late), 2 => cpu.bus.tim._2.handleOverflow(cpu, late),
3 => bus.tim._3.handleOverflow(cpu, late), 3 => cpu.bus.tim._3.handleOverflow(cpu, late),
} }
}, },
.HBlank => bus.ppu.handleHBlankEnd(cpu, late), // The end of a HBlank .HBlank => cpu.bus.ppu.handleHBlankEnd(cpu, late), // The end of a HBlank
.VBlank => bus.ppu.handleHDrawEnd(cpu, late), // The end of a VBlank .VBlank => cpu.bus.ppu.handleHDrawEnd(cpu, late), // The end of a VBlank
} }
} }
} }