feat: Initial Implementation of DMA Audio

This commit is contained in:
Rekai Nyangadzayi Musuka 2022-04-10 04:28:05 -03:00
parent c100d64fcb
commit 76b4d56ca6
9 changed files with 233 additions and 71 deletions

View File

@ -1,5 +1,6 @@
const std = @import("std"); const std = @import("std");
const AudioDeviceId = @import("sdl2").SDL_AudioDeviceID;
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");
@ -10,6 +11,7 @@ const Apu = @import("apu.zig").Apu;
const DmaControllers = @import("bus/dma.zig").DmaControllers; const DmaControllers = @import("bus/dma.zig").DmaControllers;
const Timers = @import("bus/timer.zig").Timers; const Timers = @import("bus/timer.zig").Timers;
const Scheduler = @import("scheduler.zig").Scheduler; const Scheduler = @import("scheduler.zig").Scheduler;
const FilePaths = @import("util.zig").FilePaths;
const io = @import("bus/io.zig"); const io = @import("bus/io.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
@ -33,12 +35,12 @@ io: Io,
sched: *Scheduler, 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{ return Self{
.pak = try GamePak.init(alloc, rom_path, save_path), .pak = try GamePak.init(alloc, paths.rom, paths.save),
.bios = try Bios.init(alloc, bios_path), .bios = try Bios.init(alloc, paths.bios),
.ppu = try Ppu.init(alloc, sched), .ppu = try Ppu.init(alloc, sched),
.apu = Apu.init(), .apu = Apu.init(dev),
.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(),
@ -56,6 +58,22 @@ pub fn deinit(self: Self) void {
self.ppu.deinit(); 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 { 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);

View File

@ -1,8 +1,13 @@
const std = @import("std"); 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 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 { pub const Apu = struct {
const Self = @This(); const Self = @This();
@ -11,30 +16,45 @@ pub const Apu = struct {
ch2: Tone, ch2: Tone,
ch3: Wave, ch3: Wave,
ch4: Noise, ch4: Noise,
chA: DmaSound, chA: DmaSound(.A),
chB: DmaSound, chB: DmaSound(.B),
bias: io.SoundBias, bias: io.SoundBias,
ch_vol_cnt: io.ChannelVolumeControl, ch_vol_cnt: io.ChannelVolumeControl,
dma_cnt: io.DmaSoundControl, dma_cnt: io.DmaSoundControl,
cnt: io.SoundControl, cnt: io.SoundControl,
pub fn init() Self { dev: AudioDeviceId,
pub fn init(dev: AudioDeviceId) Self {
return .{ return .{
.ch1 = ToneSweep.init(), .ch1 = ToneSweep.init(),
.ch2 = Tone.init(), .ch2 = Tone.init(),
.ch3 = Wave.init(), .ch3 = Wave.init(),
.ch4 = Noise.init(), .ch4 = Noise.init(),
.chA = DmaSound.init(), .chA = DmaSound(.A).init(),
.chB = DmaSound.init(), .chB = DmaSound(.B).init(),
.ch_vol_cnt = .{ .raw = 0 }, .ch_vol_cnt = .{ .raw = 0 },
.dma_cnt = .{ .raw = 0 }, .dma_cnt = .{ .raw = 0 },
.cnt = .{ .raw = 0 }, .cnt = .{ .raw = 0 },
.bias = .{ .raw = 0x0200 }, .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 { pub fn setSoundCntX(self: *Self, value: bool) void {
self.cnt.apu_enable.write(value); self.cnt.apu_enable.write(value);
} }
@ -50,6 +70,21 @@ pub const Apu = struct {
pub fn setBiasHigh(self: *Self, byte: u8) void { pub fn setBiasHigh(self: *Self, byte: u8) void {
self.bias.raw = (@as(u16, byte) << 8) | (self.bias.raw & 0xFF); 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 { const ToneSweep = struct {
@ -162,16 +197,66 @@ const Noise = struct {
}; };
} }
}; };
const DmaSound = struct {
pub fn DmaSound(comptime kind: DmaSoundKind) type {
return struct {
const Self = @This(); const Self = @This();
a: SoundFifo, fifo: SoundFifo,
b: SoundFifo,
kind: DmaSoundKind,
fn init() Self { fn init() Self {
return .{ return .{ .fifo = SoundFifo.init(), .kind = kind };
.a = SoundFifo.init(), }
.b = SoundFifo.init(),
}; 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,
};

View File

@ -53,10 +53,13 @@ fn DmaController(comptime id: u2) type {
/// Internal. Word Count /// Internal. Word Count
_word_count: if (id == 3) u16 else u14, _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 /// Some DMA Transfers are enabled during Hblank / VBlank and / or
/// have delays. Thefore bit 15 of DMACNT isn't actually something /// 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 /// 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 { pub fn init() Self {
return .{ return .{
@ -70,7 +73,8 @@ fn DmaController(comptime id: u2) type {
._sad = 0, ._sad = 0,
._dad = 0, ._dad = 0,
._word_count = 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; 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 // 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; self.cnt.raw = halfword;
@ -108,27 +112,50 @@ fn DmaController(comptime id: u2) type {
} }
pub inline fn check(self: *Self, bus: *Bus) bool { 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); self.step(bus);
return true; return true;
} }
pub fn step(self: *Self, bus: *Bus) void { pub fn step(self: *Self, bus: *Bus) bool {
@setCold(true); if (!self.active) return false;
const sad_adj = std.meta.intToEnum(Adjustment, self.cnt.sad_adj.read()) catch unreachable; 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 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 (is_fifo) {
if (self.cnt.transfer_type.read()) { // // const offset = @sizeOf(u32);
offset = @sizeOf(u32); // 32-bit Transfer // // bus.write(u32, self._dad, bus.read(u32, self._sad));
const word = bus.read(u32, self._sad);
bus.write(u32, self._dad, word); // // // 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 { } else {
offset = @sizeOf(u16); // 16-bit Transfer bus.write(u16, self._dad, bus.read(u16, self._sad));
const halfword = bus.read(u16, self._sad);
bus.write(u16, self._dad, halfword);
} }
switch (sad_adj) { switch (sad_adj) {
@ -140,11 +167,13 @@ fn DmaController(comptime id: u2) type {
.IncrementReload => std.debug.panic("panic(DmaTransfer): {} is an illegal src addr adjustment type", .{sad_adj}), .IncrementReload => std.debug.panic("panic(DmaTransfer): {} is an illegal src addr adjustment type", .{sad_adj}),
} }
if (!is_fifo) {
switch (dad_adj) { switch (dad_adj) {
.Increment, .IncrementReload => self._dad +%= offset, .Increment, .IncrementReload => self._dad +%= offset,
.Decrement => self._dad -%= offset, .Decrement => self._dad -%= offset,
.Fixed => {}, .Fixed => {},
} }
}
self._word_count -= 1; 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 // 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 // because we only want to step A DMA that repeats during it's specific
// timing window // timing window
self.enabled = false; self.active = false;
} }
return true;
} }
pub fn isBlocking(self: *const Self) bool { 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 { pub fn pollBlankingDma(self: *Self, comptime kind: DmaKind) void {
if (self.enabled) return; if (self.active) return;
switch (kind) { switch (kind) {
.HBlank => self.enabled = self.cnt.enabled.read() and self.cnt.start_timing.read() == 0b10, .HBlank => self.active = 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, .VBlank => self.active = self.cnt.enabled.read() and self.cnt.start_timing.read() == 0b01,
.Immediate, .Special => {}, .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; 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; const dad_adj = std.meta.intToEnum(Adjustment, self.cnt.dad_adj.read()) catch unreachable;
if (dad_adj == .IncrementReload) self._dad = self.dad; 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();
}
}
}; };
} }

View File

@ -143,8 +143,8 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
0x0400_001C => bus.ppu.setBgOffsets(3, value), 0x0400_001C => bus.ppu.setBgOffsets(3, value),
// Sound // Sound
0x0400_00A0 => log.warn("Wrote 0x{X:0>8} to FIFO_A", .{value}), 0x0400_00A0 => bus.apu.chA.push(value),
0x0400_00A4 => log.warn("Wrote 0x{X:0>8} to FIFO_B", .{value}), 0x0400_00A4 => bus.apu.chB.push(value),
// DMA Transfers // DMA Transfers
0x0400_00B0 => bus.dma._0.writeSad(value), 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 // Sound
0x0400_0080 => bus.apu.ch_vol_cnt.raw = value, 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_0084 => bus.apu.setSoundCntX(value >> 7 & 1 == 1),
0x0400_0088 => bus.apu.bias.raw = value, 0x0400_0088 => bus.apu.bias.raw = value,

View File

@ -103,6 +103,17 @@ fn Timer(comptime id: u2) type {
cpu.handleInterrupt(); 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 // Perform Cascade Behaviour
switch (id) { switch (id) {
0 => if (tim._1.cnt.cascade.read()) { 0 => if (tim._1.cnt.cascade.read()) {

View File

@ -246,15 +246,6 @@ pub const Arm7tdmi = struct {
} }
pub fn step(self: *Self) void { 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()) { if (self.cpsr.t.read()) {
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);
@ -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 { fn thumbFetch(self: *Self) u16 {
defer self.r[15] += 2; defer self.r[15] += 2;
return self.bus.read(u16, self.r[15]); return self.bus.read(u16, self.r[15]);

View File

@ -46,7 +46,9 @@ pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi, bus: *Bus) 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) {
cpu.step(); if (bus.io.haltcnt == .Halt) sched.tick += 1;
if (bus.io.haltcnt == .Execute) cpu.step();
bus.handleDMATransfers();
while (sched.tick >= sched.nextTimestamp()) { while (sched.tick >= sched.nextTimestamp()) {
sched.handleEvent(cpu, bus); sched.handleEvent(cpu, bus);

View File

@ -66,11 +66,33 @@ pub fn main() anyerror!void {
defer if (save_path) |path| alloc.free(path); defer if (save_path) |path| alloc.free(path);
log.info("Save Path: {s}", .{save_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 // Initialize Emulator
var scheduler = Scheduler.init(alloc); var scheduler = Scheduler.init(alloc);
defer scheduler.deinit(); 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(); defer bus.deinit();
var cpu = Arm7tdmi.init(&scheduler, &bus); 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 }); const emu_thread = try Thread.spawn(.{}, emu.run, .{ .LimitedFPS, &quit, &emu_rate, &scheduler, &cpu, &bus });
defer emu_thread.join(); 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); const title = correctTitle(bus.pak.title);
var title_buf: [0x20]u8 = std.mem.zeroes([0x20]u8); var title_buf: [0x20]u8 = std.mem.zeroes([0x20]u8);

View File

@ -101,3 +101,9 @@ pub fn fixTitle(alloc: std.mem.Allocator, title: [12]u8) ![]u8 {
return buf; return buf;
} }
pub const FilePaths = struct {
rom: []const u8,
bios: ?[]const u8,
save: ?[]const u8,
};