Compare commits

..

8 Commits

6 changed files with 123 additions and 81 deletions

View File

@ -22,10 +22,10 @@ An in-progress Gameboy Advance Emulator written in Zig ⚡!
- [ ] `line_timing.gba`
- [ ] `lyc_midline.gba`
- [ ] `window_midframe.gba`
- [ ] [ladystarbreeze's GBA Test Collection](https://github.com/ladystarbreeze/GBA-Test-Collection)
- [x] [ladystarbreeze's GBA Test Collection](https://github.com/ladystarbreeze/GBA-Test-Collection)
- [x] `retAddr.gba`
- [x] `helloWorld.gba`
- [ ] `helloAudio.gba`
- [x] `helloAudio.gba`
- [x] [`armwrestler-gba-fixed.gba`](https://github.com/destoer/armwrestler-gba-fixed)
- [x] [FuzzARM](https://github.com/DenSinH/FuzzARM)
@ -36,7 +36,7 @@ An in-progress Gameboy Advance Emulator written in Zig ⚡!
* [ARM7TDMI Data Sheet](https://www.dca.fee.unicamp.br/cursos/EA871/references/ARM/ARM7TDMIDataSheet.pdf)
## Compiling
Most recently built on Zig [0.10.0-dev.2424+b3672e073](https://github.com/ziglang/zig/tree/b3672e073)
Most recently built on Zig [0.10.0-dev.2624+d506275a0](https://github.com/ziglang/zig/tree/d506275a0)
### Dependencies
* [SDL.zig](https://github.com/MasterQ32/SDL.zig)
@ -63,8 +63,8 @@ Key | Button
--- | ---
<kbd>X</kbd> | A
<kbd>Z</kbd> | B
<kbd>A</kbd> | Left Shoulder
<kbd>S</kbd> | Right Shoulder
<kbd>A</kbd> | L
<kbd>S</kbd> | R
<kbd>Return</kbd> | Start
<kbd>RShift</kbd> | Select
Arrow Keys | D-Pad

View File

@ -64,10 +64,16 @@ pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void {
switch (T) {
u32 => switch (byte) {
0x80 => {
apu.psg_cnt.raw = @truncate(u16, value); // SOUNDCNT_L
apu.dma_cnt.raw = @truncate(u16, value >> 16); // SOUNDCNT_H
},
0x60 => apu.ch1.setSoundCnt(value),
0x64 => apu.ch1.setSoundCntX(&apu.fs, @truncate(u16, value)),
0x68 => apu.ch2.setSoundCntL(@truncate(u16, value)),
0x6C => apu.ch2.setSoundCntH(&apu.fs, @truncate(u16, value)),
0x70 => apu.ch3.setSoundCnt(value),
0x74 => apu.ch3.setSoundCntX(&apu.fs, @truncate(u16, value)),
0x78 => apu.ch4.setSoundCntL(@truncate(u16, value)),
0x7C => apu.ch4.setSoundCntH(&apu.fs, @truncate(u16, value)),
0x80 => apu.setSoundCnt(value),
// WAVE_RAM
0x90...0x9F => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
0xA0 => apu.chA.push(value), // FIFO_A
@ -75,7 +81,7 @@ pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void {
else => writeUndefined(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
},
u16 => switch (byte) {
0x60 => apu.ch1.sweep.raw = @truncate(u8, value), // SOUND1CNT_L
0x60 => apu.ch1.setSoundCntL(@truncate(u8, value)), // SOUND1CNT_L
0x62 => apu.ch1.setSoundCntH(value),
0x64 => apu.ch1.setSoundCntX(&apu.fs, value),
@ -98,7 +104,7 @@ pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void {
else => writeUndefined(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
},
u8 => switch (byte) {
0x60 => apu.ch1.sweep.raw = value, // NR10
0x60 => apu.ch1.setSoundCntL(value),
0x62 => apu.ch1.setNr11(value),
0x63 => apu.ch1.setNr12(value),
0x64 => apu.ch1.setNr13(value),
@ -196,6 +202,12 @@ pub const Apu = struct {
self.ch4.reset();
}
/// SOUNDCNT
fn setSoundCnt(self: *Self, value: u32) void {
self.psg_cnt.raw = @truncate(u16, value);
self.setSoundCntH(@truncate(u16, value >> 16));
}
/// SOUNDCNT_H_L
fn setSoundCntHL(self: *Self, value: u8) void {
const merged = (self.dma_cnt.raw & 0xFF00) | value;
@ -208,6 +220,7 @@ pub const Apu = struct {
self.setSoundCntH(merged);
}
/// SOUNDCNT_H
pub fn setSoundCntH(self: *Self, value: u16) void {
const new: io.DmaSoundControl = .{ .raw = value };
@ -320,7 +333,8 @@ pub const Apu = struct {
const final_right = (tmp_right << 5) | (tmp_right >> 6);
if (self.sampling_cycle != self.bias.sampling_cycle.read()) {
log.info("Sampling Cycle changed from {} to {}", .{ self.sampling_cycle, self.bias.sampling_cycle.read() });
const new_sample_rate = Self.sampleRate(self.bias.sampling_cycle.read());
log.info("Sample Rate changed from {}Hz to {}Hz", .{ Self.sampleRate(self.sampling_cycle), new_sample_rate });
// Sample Rate Changed, Create a new Resampler since i can't figure out how to change
// the parameters of the old one
@ -328,7 +342,7 @@ pub const Apu = struct {
defer SDL.SDL_FreeAudioStream(old);
self.sampling_cycle = self.bias.sampling_cycle.read();
self.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, @intCast(c_int, self.sampleRate()), SDL.AUDIO_U16, 2, host_sample_rate) orelse unreachable;
self.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, @intCast(c_int, new_sample_rate), SDL.AUDIO_U16, 2, host_sample_rate) orelse unreachable;
}
_ = SDL.SDL_AudioStreamPut(self.stream, &[2]u16{ final_left, final_right }, 2 * @sizeOf(u16));
@ -336,11 +350,11 @@ pub const Apu = struct {
}
fn sampleTicks(self: *const Self) u64 {
return (1 << 24) / self.sampleRate();
return (1 << 24) / Self.sampleRate(self.bias.sampling_cycle.read());
}
fn sampleRate(self: *const Self) u64 {
return @as(u64, 1) << (15 + @as(u6, self.bias.sampling_cycle.read()));
fn sampleRate(cycle: u2) u64 {
return @as(u64, 1) << (15 + @as(u6, cycle));
}
pub fn tickFrameSequencer(self: *Self, late: u64) void {
@ -386,17 +400,6 @@ pub const Apu = struct {
if (self.chB.len() <= 15) cpu.bus.dma[2].requestSoundDma(0x0400_00A4);
}
}
fn highPass(self: *Self, sample: f32, enabled: bool) f32 {
return if (enabled) blk: {
const out = sample - self.capacitor;
const charge_factor =
std.math.pow(f32, 0.999958, @intToFloat(f32, (1 << 22) / self.sampleRate()));
self.capacitor = sample - out * charge_factor;
break :blk out;
} else 0.0;
}
};
const ToneSweep = struct {
@ -430,11 +433,14 @@ const ToneSweep = struct {
enabled: bool,
shadow: u11,
calc_performed: bool,
pub fn init() This {
return .{
.timer = 0,
.enabled = false,
.shadow = 0,
.calc_performed = false,
};
}
@ -444,6 +450,7 @@ const ToneSweep = struct {
if (this.timer == 0) {
const period = ch1.sweep.period.read();
this.timer = if (period == 0) 8 else period;
if (!this.calc_performed) this.calc_performed = true;
if (this.enabled and period != 0) {
const new_freq = this.calcFrequency(ch1);
@ -489,6 +496,8 @@ const ToneSweep = struct {
fn reset(self: *Self) void {
self.sweep.raw = 0;
self.sweep_dev.calc_performed = false;
self.duty.raw = 0;
self.envelope.raw = 0;
self.freq.raw = 0;
@ -521,11 +530,32 @@ const ToneSweep = struct {
return @as(i16, self.sample);
}
/// NR10, NR11, NR12
fn setSoundCnt(self: *Self, value: u32) void {
self.setSoundCntL(@truncate(u8, value));
self.setSoundCntH(@truncate(u16, value >> 16));
}
/// NR10
pub fn getSoundCntL(self: *const Self) u8 {
return self.sweep.raw & 0x7F;
}
/// NR10
fn setSoundCntL(self: *Self, value: u8) void {
const new = io.Sweep{ .raw = value };
if (self.sweep.direction.read() and !new.direction.read()) {
// Sweep Negate bit has been cleared
// If At least 1 Sweep Calculation has been made since
// the last trigger, the channel is immediately disabled
if (self.sweep_dev.calc_performed) self.enabled = false;
}
self.sweep.raw = value;
}
/// NR11, NR12
pub fn getSoundCntH(self: *const Self) u16 {
return @as(u16, self.envelope.raw) << 8 | (self.duty.raw & 0xC0);
@ -589,6 +619,7 @@ const ToneSweep = struct {
const sw_period = self.sweep.period.read();
const sw_shift = self.sweep.shift.read();
self.sweep_dev.calc_performed = false;
self.sweep_dev.shadow = self.freq.frequency.read();
self.sweep_dev.timer = if (sw_period == 0) 8 else sw_period;
self.sweep_dev.enabled = sw_period != 0 or sw_shift != 0;
@ -789,6 +820,12 @@ const Wave = struct {
self.len_dev.tick(self.freq.length_enable.read(), &self.enabled);
}
/// NR30, NR31, NR32
fn setSoundCnt(self: *Self, value: u32) void {
self.setSoundCntL(@truncate(u8, value));
self.setSoundCntH(@truncate(u16, value >> 16));
}
/// NR30
pub fn setSoundCntL(self: *Self, value: u8) void {
self.select.raw = value;

View File

@ -98,9 +98,6 @@ fn DmaController(comptime id: u2) type {
const sad_mask: u32 = if (id == 0) 0x07FF_FFFF else 0x0FFF_FFFF;
const dad_mask: u32 = if (id != 3) 0x07FF_FFFF else 0x0FFF_FFFF;
/// Determines whether DMAController is for DMA0, DMA1, DMA2 or DMA3
/// Note: Determined at comptime
id: u2,
/// Write-only. The first address in a DMA transfer. (DMASAD)
/// Note: use writeSrc instead of manipulating src_addr directly
sad: u32,
@ -126,11 +123,10 @@ fn DmaController(comptime id: u2) type {
/// 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
active: bool,
in_progress: bool,
pub fn init() Self {
return .{
.id = id,
.sad = 0,
.dad = 0,
.word_count = 0,
@ -141,7 +137,7 @@ fn DmaController(comptime id: u2) type {
._dad = 0,
._word_count = 0,
._fifo_word_count = 4,
.active = false,
.in_progress = false,
};
}
@ -167,7 +163,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.active = new.start_timing.read() == 0b00;
self.in_progress = new.start_timing.read() == 0b00;
}
self.cnt.raw = halfword;
@ -179,7 +175,7 @@ fn DmaController(comptime id: u2) type {
}
pub fn step(self: *Self, cpu: *Arm7tdmi) void {
const is_fifo = (self.id == 1 or self.id == 2) and self.cnt.start_timing.read() == 0b11;
const is_fifo = (id == 1 or id == 2) and self.cnt.start_timing.read() == 0b11;
const sad_adj = Self.adjustment(self.cnt.sad_adj.read());
const dad_adj = if (is_fifo) .Fixed else Self.adjustment(self.cnt.dad_adj.read());
@ -209,57 +205,63 @@ fn DmaController(comptime id: u2) type {
self._word_count -= 1;
if (self._word_count == 0) {
if (!self.cnt.repeat.read()) {
// If we're not repeating, Fire the IRQs and disable the DMA
if (self.cnt.irq.read()) {
switch (id) {
0 => cpu.bus.io.irq.dma0.set(),
1 => cpu.bus.io.irq.dma0.set(),
2 => cpu.bus.io.irq.dma0.set(),
3 => cpu.bus.io.irq.dma0.set(),
}
cpu.handleInterrupt();
if (self.cnt.irq.read()) {
switch (id) {
0 => cpu.bus.io.irq.dma0.set(),
1 => cpu.bus.io.irq.dma1.set(),
2 => cpu.bus.io.irq.dma2.set(),
3 => cpu.bus.io.irq.dma3.set(),
}
self.cnt.enabled.unset();
cpu.handleInterrupt();
}
// If we're not repeating, Fire the IRQs and disable the DMA
if (!self.cnt.repeat.read()) self.cnt.enabled.unset();
// 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.active = false;
self.in_progress = false;
}
}
pub fn pollBlankingDma(self: *Self, comptime kind: DmaKind) void {
if (self.active) return;
if (self.in_progress) return; // If there's an ongoing DMA Transfer, exit early
// No ongoing DMA Transfer, We want to check if we should repeat an existing one
// Determined by the repeat bit and whether the DMA is in the right start_timing
switch (kind) {
.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,
.VBlank => self.in_progress = self.cnt.enabled.read() and self.cnt.start_timing.read() == 0b01,
.HBlank => self.in_progress = self.cnt.enabled.read() and self.cnt.start_timing.read() == 0b10,
.Immediate, .Special => {},
}
if (self.cnt.repeat.read() and self.active) {
// If we determined that the repeat bit is set (and now the Hblank / Vblank DMA is now in progress)
// Reload internal word count latch
// Reload internal DAD latch if we are in IncrementRelaod
if (self.in_progress) {
self._word_count = if (self.word_count == 0) std.math.maxInt(@TypeOf(self._word_count)) else self.word_count;
if (Self.adjustment(self.cnt.dad_adj.read()) == .IncrementReload) self._dad = self.dad;
}
}
pub fn requestSoundDma(self: *Self, fifo_addr: u32) void {
pub fn requestSoundDma(self: *Self, _: u32) void {
comptime std.debug.assert(id == 1 or id == 2);
if (self.in_progress) return; // APU must wait their turn
const is_enabled = self.cnt.enabled.read();
const is_special = self.cnt.start_timing.read() == 0b11;
const is_repeating = self.cnt.repeat.read();
const is_fifo = self.dad == fifo_addr;
// DMA May not be configured for handling DMAs
if (self.cnt.start_timing.read() != 0b11) return;
if (is_enabled and is_special and is_repeating and is_fifo) {
self._word_count = 4;
self.cnt.transfer_type.set();
self.active = true;
}
// We Assume the Repeat Bit is Set
// We Assume that DAD is set to 0x0400_00A0 or 0x0400_00A4 (fifo_addr)
// We Assume DMACNT_L is set to 4
// FIXME: Safe to just assume whatever DAD is set to is the FIFO Address?
// self._dad = fifo_addr;
self.cnt.repeat.set();
self._word_count = 4;
self.in_progress = true;
}
fn adjustment(idx: u2) Adjustment {

View File

@ -52,7 +52,7 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) T {
0x0400_0006 => @as(T, bus.ppu.bg[0].cnt.raw) << 16 | bus.ppu.vcount.raw,
// DMA Transfers
0x0400_00B8...0x0400_00DC => dma.read(T, &bus.dma, address),
0x0400_00B0...0x0400_00DC => dma.read(T, &bus.dma, address),
// Timers
0x0400_0100...0x0400_010C => timer.read(T, &bus.tim, address),
@ -83,10 +83,10 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) T {
0x0400_004C => readTodo("Read {} from MOSAIC", .{T}),
// Sound
0x0400_0060...0x0400_009F => apu.read(T, &bus.apu, address),
0x0400_0060...0x0400_009E => apu.read(T, &bus.apu, address),
// DMA Transfers
0x0400_00BA...0x0400_00DE => dma.read(T, &bus.dma, address),
0x0400_00B0...0x0400_00DE => dma.read(T, &bus.dma, address),
// Timers
0x0400_0100...0x0400_010E => timer.read(T, &bus.tim, address),
@ -119,7 +119,7 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) T {
0x0400_000B => @truncate(T, bus.ppu.bg[1].cnt.raw >> 8),
// Sound
0x0400_0060...0x0400_0089 => apu.read(T, &bus.apu, address),
0x0400_0060...0x0400_00A7 => apu.read(T, &bus.apu, address),
// Serial Communication 1
0x0400_0128 => readTodo("Read {} from SIOCNT_L", .{T}),
@ -171,7 +171,7 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
0x0400_0058...0x0400_005C => {}, // Unused
// Sound
0x0400_0080...0x0400_00A4 => apu.write(T, &bus.apu, address, value),
0x0400_0060...0x0400_00A4 => apu.write(T, &bus.apu, address, value),
0x0400_00A8, 0x0400_00AC => {}, // Unused
// DMA Transfers
@ -190,6 +190,8 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
// Keypad Input
0x0400_0130 => log.debug("Wrote 0x{X:0>8} to KEYINPUT and KEYCNT", .{value}),
0x0400_0134 => log.debug("Wrote 0x{X:0>8} to RCNT and IR", .{value}),
0x0400_0138, 0x0400_013C => {}, // Unused
// Serial Communication 2
0x0400_0140 => log.debug("Wrote 0x{X:0>8} to JOYCNT", .{value}),
@ -197,6 +199,7 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
0x0400_0154 => log.debug("Wrote 0x{X:0>8} to JOY_TRANS", .{value}),
0x0400_0158 => log.debug("Wrote 0x{X:0>8} to JOYSTAT (?)", .{value}),
0x0400_0144...0x0400_014C, 0x0400_015C => {}, // Unused
0x0400_0160...0x0400_01FC => {},
// Interrupts
0x0400_0200 => bus.io.setIrqs(value),
@ -251,7 +254,7 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
0x0400_004E, 0x0400_0056 => {}, // Not used
// Sound
0x0400_0060...0x0400_009F => apu.write(T, &bus.apu, address, value),
0x0400_0060...0x0400_009E => apu.write(T, &bus.apu, address, value),
// Dma Transfers
0x0400_00B0...0x0400_00DE => dma.write(T, &bus.dma, address, value),
@ -301,7 +304,7 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
0x0400_0054 => log.debug("Wrote 0x{X:0>2} to BLDY_L", .{value}),
// Sound
0x0400_0060...0x0400_009F => apu.write(T, &bus.apu, address, value),
0x0400_0060...0x0400_00A7 => apu.write(T, &bus.apu, address, value),
// Serial Communication 1
0x0400_0120 => log.debug("Wrote 0x{X:0>2} to SIODATA32_L_L", .{value}),
@ -432,10 +435,10 @@ const InterruptRequest = extern union {
vblank: Bit(u16, 0),
hblank: Bit(u16, 1),
coincidence: Bit(u16, 2),
tim0_overflow: Bit(u16, 3),
tim1_overflow: Bit(u16, 4),
tim2_overflow: Bit(u16, 5),
tim3_overflow: Bit(u16, 6),
tim0: Bit(u16, 3),
tim1: Bit(u16, 4),
tim2: Bit(u16, 5),
tim3: Bit(u16, 6),
serial: Bit(u16, 7),
dma0: Bit(u16, 8),
dma1: Bit(u16, 9),

View File

@ -144,10 +144,10 @@ fn Timer(comptime id: u2) type {
if (self.cnt.irq.read()) {
switch (id) {
0 => io.irq.tim0_overflow.set(),
1 => io.irq.tim1_overflow.set(),
2 => io.irq.tim2_overflow.set(),
3 => io.irq.tim3_overflow.set(),
0 => io.irq.tim0.set(),
1 => io.irq.tim1.set(),
2 => io.irq.tim2.set(),
3 => io.irq.tim3.set(),
}
cpu.handleInterrupt();

View File

@ -277,22 +277,22 @@ pub const Arm7tdmi = struct {
const dma2 = &self.bus.dma[2];
const dma3 = &self.bus.dma[3];
if (dma0.active) {
if (dma0.in_progress) {
dma0.step(self);
return true;
}
if (dma1.active) {
if (dma1.in_progress) {
dma1.step(self);
return true;
}
if (dma2.active) {
if (dma2.in_progress) {
dma2.step(self);
return true;
}
if (dma3.active) {
if (dma3.in_progress) {
dma3.step(self);
return true;
}