diff --git a/src/Bus.zig b/src/Bus.zig index c428fdf..818e7fc 100644 --- a/src/Bus.zig +++ b/src/Bus.zig @@ -1,5 +1,6 @@ const std = @import("std"); +const AudioDeviceId = @import("sdl2").SDL_AudioDeviceID; const Bios = @import("bus/Bios.zig"); const Ewram = @import("bus/Ewram.zig"); const GamePak = @import("bus/GamePak.zig"); @@ -10,6 +11,7 @@ const Apu = @import("apu.zig").Apu; const DmaControllers = @import("bus/dma.zig").DmaControllers; const Timers = @import("bus/timer.zig").Timers; const Scheduler = @import("scheduler.zig").Scheduler; +const FilePaths = @import("util.zig").FilePaths; const io = @import("bus/io.zig"); const Allocator = std.mem.Allocator; @@ -33,12 +35,12 @@ io: Io, sched: *Scheduler, -pub fn init(alloc: Allocator, sched: *Scheduler, rom_path: []const u8, bios_path: ?[]const u8, save_path: ?[]const u8) !Self { +pub fn init(alloc: Allocator, sched: *Scheduler, dev: AudioDeviceId, paths: FilePaths) !Self { return Self{ - .pak = try GamePak.init(alloc, rom_path, save_path), - .bios = try Bios.init(alloc, bios_path), + .pak = try GamePak.init(alloc, paths.rom, paths.save), + .bios = try Bios.init(alloc, paths.bios), .ppu = try Ppu.init(alloc, sched), - .apu = Apu.init(), + .apu = Apu.init(dev), .iwram = try Iwram.init(alloc), .ewram = try Ewram.init(alloc), .dma = DmaControllers.init(), @@ -56,6 +58,22 @@ pub fn deinit(self: Self) void { self.ppu.deinit(); } +pub fn handleDMATransfers(self: *Self) void { + while (self.isDmaRunning()) { + if (self.dma._1.step(self)) continue; + if (self.dma._0.step(self)) continue; + if (self.dma._2.step(self)) continue; + if (self.dma._3.step(self)) continue; + } +} + +fn isDmaRunning(self: *const Self) bool { + return self.dma._0.active or + self.dma._1.active or + self.dma._2.active or + self.dma._3.active; +} + pub fn read(self: *const Self, comptime T: type, address: u32) T { const page = @truncate(u8, address >> 24); const align_addr = alignAddress(T, address); diff --git a/src/apu.zig b/src/apu.zig index aa478c1..9b73d47 100644 --- a/src/apu.zig +++ b/src/apu.zig @@ -1,8 +1,13 @@ const std = @import("std"); +const SDL = @import("sdl2"); +const io = @import("bus/io.zig"); +const Arm7tdmi = @import("cpu.zig").Arm7tdmi; const SoundFifo = std.fifo.LinearFifo(u8, .{ .Static = 0x20 }); +const AudioDeviceId = SDL.SDL_AudioDeviceID; -const io = @import("bus/io.zig"); +const intToBytes = @import("util.zig").intToBytes; +const log = std.log.scoped(.APU); pub const Apu = struct { const Self = @This(); @@ -11,30 +16,45 @@ pub const Apu = struct { ch2: Tone, ch3: Wave, ch4: Noise, - chA: DmaSound, - chB: DmaSound, + chA: DmaSound(.A), + chB: DmaSound(.B), bias: io.SoundBias, ch_vol_cnt: io.ChannelVolumeControl, dma_cnt: io.DmaSoundControl, cnt: io.SoundControl, - pub fn init() Self { + dev: AudioDeviceId, + + pub fn init(dev: AudioDeviceId) Self { return .{ .ch1 = ToneSweep.init(), .ch2 = Tone.init(), .ch3 = Wave.init(), .ch4 = Noise.init(), - .chA = DmaSound.init(), - .chB = DmaSound.init(), + .chA = DmaSound(.A).init(), + .chB = DmaSound(.B).init(), .ch_vol_cnt = .{ .raw = 0 }, .dma_cnt = .{ .raw = 0 }, .cnt = .{ .raw = 0 }, .bias = .{ .raw = 0x0200 }, + + .dev = dev, }; } + pub fn setDmaCnt(self: *Self, value: u16) void { + const new: io.DmaSoundControl = .{ .raw = value }; + + // Reinitializing instead of resetting is fine because + // the FIFOs I'm using are stack allocated and 0x20 bytes big + if (new.sa_reset.read()) self.chA.fifo = SoundFifo.init(); + if (new.sb_reset.read()) self.chB.fifo = SoundFifo.init(); + + self.dma_cnt = new; + } + pub fn setSoundCntX(self: *Self, value: bool) void { self.cnt.apu_enable.write(value); } @@ -50,6 +70,21 @@ pub const Apu = struct { pub fn setBiasHigh(self: *Self, byte: u8) void { self.bias.raw = (@as(u16, byte) << 8) | (self.bias.raw & 0xFF); } + + pub fn handleTimerOverflow(self: *Self, kind: DmaSoundKind, cpu: *Arm7tdmi) void { + if (!self.cnt.apu_enable.read()) return; + + const samples = switch (kind) { + .A => blk: { + break :blk self.chA.handleTimerOverflow(cpu, self.dma_cnt); + }, + .B => blk: { + break :blk self.chB.handleTimerOverflow(cpu, self.dma_cnt); + }, + }; + + _ = SDL.SDL_QueueAudio(self.dev, &samples, 2); + } }; const ToneSweep = struct { @@ -162,16 +197,66 @@ const Noise = struct { }; } }; -const DmaSound = struct { - const Self = @This(); - a: SoundFifo, - b: SoundFifo, +pub fn DmaSound(comptime kind: DmaSoundKind) type { + return struct { + const Self = @This(); - fn init() Self { - return .{ - .a = SoundFifo.init(), - .b = SoundFifo.init(), - }; - } + fifo: SoundFifo, + + kind: DmaSoundKind, + + fn init() Self { + return .{ .fifo = SoundFifo.init(), .kind = kind }; + } + + pub fn push(self: *Self, value: u32) void { + self.fifo.write(&intToBytes(u32, value)) catch {}; + } + + pub fn pop(self: *Self) u8 { + return self.fifo.readItem() orelse 0; + } + + pub fn len(self: *const Self) usize { + return self.fifo.readableLength(); + } + + pub fn handleTimerOverflow(self: *Self, cpu: *Arm7tdmi, cnt: io.DmaSoundControl) [2]u8 { + const sample = self.pop(); + + var left: u8 = 0; + var right: u8 = 0; + var fifo_addr: u32 = undefined; + + switch (kind) { + .A => { + const vol = @boolToInt(!cnt.sa_vol.read()); // if unset, vol is 50% + if (cnt.sa_left_enable.read()) left = sample >> vol; + if (cnt.sa_right_enable.read()) right = sample >> vol; + + fifo_addr = 0x0400_00A0; + }, + .B => { + const vol = @boolToInt(!cnt.sb_vol.read()); // if unset, vol is 50% + if (cnt.sb_left_enable.read()) left = sample >> vol; + if (cnt.sb_right_enable.read()) right = sample >> vol; + + fifo_addr = 0x0400_00A4; + }, + } + + if (self.len() <= 15) { + cpu.bus.dma._1.enableSoundDma(fifo_addr); + cpu.bus.dma._2.enableSoundDma(fifo_addr); + } + + return .{ left, right }; + } + }; +} + +const DmaSoundKind = enum { + A, + B, }; diff --git a/src/bus/dma.zig b/src/bus/dma.zig index ddf6b95..f4758a4 100644 --- a/src/bus/dma.zig +++ b/src/bus/dma.zig @@ -53,10 +53,13 @@ fn DmaController(comptime id: u2) type { /// Internal. Word Count _word_count: if (id == 3) u16 else u14, + // Internal. FIFO Word Count + _fifo_word_count: u8, + /// Some DMA Transfers are enabled during Hblank / VBlank and / or /// have delays. Thefore bit 15 of DMACNT isn't actually something /// we can use to control when we do or do not execute a step in a DMA Transfer - enabled: bool, + active: bool, pub fn init() Self { return .{ @@ -70,7 +73,8 @@ fn DmaController(comptime id: u2) type { ._sad = 0, ._dad = 0, ._word_count = 0, - .enabled = false, + ._fifo_word_count = 4, + .active = false, }; } @@ -96,7 +100,7 @@ fn DmaController(comptime id: u2) type { self._word_count = if (self.word_count == 0) std.math.maxInt(@TypeOf(self._word_count)) else self.word_count; // Only a Start Timing of 00 has a DMA Transfer immediately begin - self.enabled = new.start_timing.read() == 0b00; + self.active = new.start_timing.read() == 0b00; } self.cnt.raw = halfword; @@ -108,27 +112,50 @@ fn DmaController(comptime id: u2) type { } pub inline fn check(self: *Self, bus: *Bus) bool { - if (!self.enabled) return false; // FIXME: Check CNT register? + if (!self.active) return false; // FIXME: Check CNT register? self.step(bus); return true; } - pub fn step(self: *Self, bus: *Bus) void { - @setCold(true); + pub fn step(self: *Self, bus: *Bus) bool { + if (!self.active) return false; const sad_adj = std.meta.intToEnum(Adjustment, self.cnt.sad_adj.read()) catch unreachable; const dad_adj = std.meta.intToEnum(Adjustment, self.cnt.dad_adj.read()) catch unreachable; + const is_fifo = (self.id == 1 or self.id == 2) and self.cnt.start_timing.read() == 0b11; - var offset: u32 = 0; - if (self.cnt.transfer_type.read()) { - offset = @sizeOf(u32); // 32-bit Transfer - const word = bus.read(u32, self._sad); - bus.write(u32, self._dad, word); + // // if (is_fifo) { + // // const offset = @sizeOf(u32); + // // bus.write(u32, self._dad, bus.read(u32, self._sad)); + + // // // TODO: Deduplicate + // // switch (sad_adj) { + // // .Increment => self._sad +%= offset, + // // .Decrement => self._sad -%= offset, + // // .Fixed => {}, + + // // // TODO: Figure out correct behaviour on Illegal Source Addr Control Type + // // .IncrementReload => std.debug.panic("panic(DmaTransfer): {} is an illegal src addr adjustment type", .{sad_adj}), + // // } + + // // self._fifo_word_count -= 1; + + // // if (self._fifo_word_count == 0) { + // // self._fifo_word_count = 4; + // // self.active = false; + // // } + + // // return true; + // // } + + const transfer_type = self.cnt.transfer_type.read() or is_fifo; + const offset: u32 = if (transfer_type) @sizeOf(u32) else @sizeOf(u16); + + if (transfer_type) { + bus.write(u32, self._dad, bus.read(u32, self._sad)); } else { - offset = @sizeOf(u16); // 16-bit Transfer - const halfword = bus.read(u16, self._sad); - bus.write(u16, self._dad, halfword); + bus.write(u16, self._dad, bus.read(u16, self._sad)); } switch (sad_adj) { @@ -140,10 +167,12 @@ fn DmaController(comptime id: u2) type { .IncrementReload => std.debug.panic("panic(DmaTransfer): {} is an illegal src addr adjustment type", .{sad_adj}), } - switch (dad_adj) { - .Increment, .IncrementReload => self._dad +%= offset, - .Decrement => self._dad -%= offset, - .Fixed => {}, + if (!is_fifo) { + switch (dad_adj) { + .Increment, .IncrementReload => self._dad +%= offset, + .Decrement => self._dad -%= offset, + .Fixed => {}, + } } self._word_count -= 1; @@ -165,8 +194,10 @@ fn DmaController(comptime id: u2) type { // We want to disable our internal enabled flag regardless of repeat // because we only want to step A DMA that repeats during it's specific // timing window - self.enabled = false; + self.active = false; } + + return true; } pub fn isBlocking(self: *const Self) bool { @@ -175,21 +206,31 @@ fn DmaController(comptime id: u2) type { } pub fn pollBlankingDma(self: *Self, comptime kind: DmaKind) void { - if (self.enabled) return; + if (self.active) return; switch (kind) { - .HBlank => self.enabled = self.cnt.enabled.read() and self.cnt.start_timing.read() == 0b10, - .VBlank => self.enabled = self.cnt.enabled.read() and self.cnt.start_timing.read() == 0b01, + .HBlank => self.active = self.cnt.enabled.read() and self.cnt.start_timing.read() == 0b10, + .VBlank => self.active = self.cnt.enabled.read() and self.cnt.start_timing.read() == 0b01, .Immediate, .Special => {}, } - if (self.cnt.repeat.read() and self.enabled) { + if (self.cnt.repeat.read() and self.active) { self._word_count = if (self.word_count == 0) std.math.maxInt(@TypeOf(self._word_count)) else self.word_count; const dad_adj = std.meta.intToEnum(Adjustment, self.cnt.dad_adj.read()) catch unreachable; if (dad_adj == .IncrementReload) self._dad = self.dad; } } + + pub fn enableSoundDma(self: *Self, fifo_addr: u32) void { + comptime std.debug.assert(id == 1 or id == 2); + + if (self.cnt.enabled.read() and self.cnt.start_timing.read() == 0b11 and self.dad == fifo_addr) { + self.active = true; + self._word_count = 4; + self.cnt.repeat.set(); + } + } }; } diff --git a/src/bus/io.zig b/src/bus/io.zig index 16c0eb6..78e4cae 100644 --- a/src/bus/io.zig +++ b/src/bus/io.zig @@ -143,8 +143,8 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void { 0x0400_001C => bus.ppu.setBgOffsets(3, value), // Sound - 0x0400_00A0 => log.warn("Wrote 0x{X:0>8} to FIFO_A", .{value}), - 0x0400_00A4 => log.warn("Wrote 0x{X:0>8} to FIFO_B", .{value}), + 0x0400_00A0 => bus.apu.chA.push(value), + 0x0400_00A4 => bus.apu.chB.push(value), // DMA Transfers 0x0400_00B0 => bus.dma._0.writeSad(value), @@ -208,7 +208,7 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void { // Sound 0x0400_0080 => bus.apu.ch_vol_cnt.raw = value, - 0x0400_0082 => bus.apu.dma_cnt.raw = value, + 0x0400_0082 => bus.apu.setDmaCnt(value), 0x0400_0084 => bus.apu.setSoundCntX(value >> 7 & 1 == 1), 0x0400_0088 => bus.apu.bias.raw = value, diff --git a/src/bus/timer.zig b/src/bus/timer.zig index 14c07c9..012e469 100644 --- a/src/bus/timer.zig +++ b/src/bus/timer.zig @@ -103,6 +103,17 @@ fn Timer(comptime id: u2) type { cpu.handleInterrupt(); } + // DMA Sound Things + if (id == 0 or id == 1) { + const apu = &cpu.bus.apu; + + const a_tim = @boolToInt(apu.dma_cnt.sa_timer.read()); + const b_tim = @boolToInt(apu.dma_cnt.sb_timer.read()); + + if (a_tim == id) apu.handleTimerOverflow(.A, cpu); + if (b_tim == id) apu.handleTimerOverflow(.B, cpu); + } + // Perform Cascade Behaviour switch (id) { 0 => if (tim._1.cnt.cascade.read()) { diff --git a/src/cpu.zig b/src/cpu.zig index 4f910bc..072fdf6 100644 --- a/src/cpu.zig +++ b/src/cpu.zig @@ -246,15 +246,6 @@ pub const Arm7tdmi = struct { } pub fn step(self: *Self) void { - // If we're processing a DMA (not Sound or Blanking) the CPU is disabled - if (self.handleDMATransfers()) return; - - // If we're halted, the cpu is disabled - if (self.bus.io.haltcnt == .Halt) { - self.sched.tick += 1; - return; - } - if (self.cpsr.t.read()) { const opcode = self.thumbFetch(); if (enable_logging) if (self.log_file) |file| self.debug_log(file, opcode); @@ -296,15 +287,6 @@ pub const Arm7tdmi = struct { } } - fn handleDMATransfers(self: *Self) bool { - if (self.bus.dma._0.check(self.bus)) return self.bus.dma._0.isBlocking(); - if (self.bus.dma._1.check(self.bus)) return self.bus.dma._1.isBlocking(); - if (self.bus.dma._2.check(self.bus)) return self.bus.dma._2.isBlocking(); - if (self.bus.dma._3.check(self.bus)) return self.bus.dma._3.isBlocking(); - - return false; - } - fn thumbFetch(self: *Self) u16 { defer self.r[15] += 2; return self.bus.read(u16, self.r[15]); diff --git a/src/emu.zig b/src/emu.zig index 58fecbf..087bed1 100644 --- a/src/emu.zig +++ b/src/emu.zig @@ -46,7 +46,9 @@ pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi, bus: *Bus) void { const frame_end = sched.tick + cycles_per_frame; while (sched.tick < frame_end) { - cpu.step(); + if (bus.io.haltcnt == .Halt) sched.tick += 1; + if (bus.io.haltcnt == .Execute) cpu.step(); + bus.handleDMATransfers(); while (sched.tick >= sched.nextTimestamp()) { sched.handleEvent(cpu, bus); diff --git a/src/main.zig b/src/main.zig index 0b9e224..4f97c40 100644 --- a/src/main.zig +++ b/src/main.zig @@ -66,11 +66,33 @@ pub fn main() anyerror!void { defer if (save_path) |path| alloc.free(path); log.info("Save Path: {s}", .{save_path}); + // Initialize SDL + const status = SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO | SDL.SDL_INIT_GAMECONTROLLER); + defer SDL.SDL_Quit(); + if (status < 0) sdlPanic(); + + // Initialize SDL Audio + var have: SDL.SDL_AudioSpec = undefined; + var want = std.mem.zeroes(SDL.SDL_AudioSpec); + want.freq = 32768; + want.format = SDL.AUDIO_S8; + want.channels = 2; + want.samples = 0x200; + want.callback = null; + + const audio_dev = SDL.SDL_OpenAudioDevice(null, 0, &want, &have, 0); + defer SDL.SDL_CloseAudioDevice(audio_dev); + if (audio_dev == 0) sdlPanic(); + + // Start Playback on the Audio evice + SDL.SDL_PauseAudioDevice(audio_dev, 0); + // Initialize Emulator var scheduler = Scheduler.init(alloc); defer scheduler.deinit(); - var bus = try Bus.init(alloc, &scheduler, rom_path, bios_path, save_path); + const paths = .{ .bios = bios_path, .rom = rom_path, .save = save_path }; + var bus = try Bus.init(alloc, &scheduler, audio_dev, paths); defer bus.deinit(); var cpu = Arm7tdmi.init(&scheduler, &bus); @@ -91,11 +113,6 @@ pub fn main() anyerror!void { const emu_thread = try Thread.spawn(.{}, emu.run, .{ .LimitedFPS, &quit, &emu_rate, &scheduler, &cpu, &bus }); defer emu_thread.join(); - // Initialize SDL - const status = SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO | SDL.SDL_INIT_GAMECONTROLLER); - if (status < 0) sdlPanic(); - defer SDL.SDL_Quit(); - const title = correctTitle(bus.pak.title); var title_buf: [0x20]u8 = std.mem.zeroes([0x20]u8); diff --git a/src/util.zig b/src/util.zig index 34f8ee1..f70df7c 100644 --- a/src/util.zig +++ b/src/util.zig @@ -101,3 +101,9 @@ pub fn fixTitle(alloc: std.mem.Allocator, title: [12]u8) ![]u8 { return buf; } + +pub const FilePaths = struct { + rom: []const u8, + bios: ?[]const u8, + save: ?[]const u8, +};