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 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);

View File

@ -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,
};

View File

@ -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();
}
}
};
}

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),
// 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,

View File

@ -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()) {

View File

@ -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]);

View File

@ -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);

View File

@ -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);

View File

@ -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,
};