Compare commits
No commits in common. "f130d1991c2483345039b340e12d418d215c8c90" and "a2d2a84850c198a8dd80322f4c3c01d412d6074c" have entirely different histories.
f130d1991c
...
a2d2a84850
216
src/apu.zig
216
src/apu.zig
|
@ -53,7 +53,7 @@ pub const Apu = struct {
|
||||||
.bias = .{ .raw = 0x0200 },
|
.bias = .{ .raw = 0x0200 },
|
||||||
|
|
||||||
.sampling_cycle = 0b00,
|
.sampling_cycle = 0b00,
|
||||||
.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, 1 << 15, SDL.AUDIO_U16, 2, host_sample_rate) orelse unreachable,
|
.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_F32, 2, 1 << 15, SDL.AUDIO_F32, 2, host_sample_rate) orelse unreachable,
|
||||||
.sched = sched,
|
.sched = sched,
|
||||||
|
|
||||||
.capacitor = 0,
|
.capacitor = 0,
|
||||||
|
@ -133,60 +133,64 @@ pub const Apu = struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sampleAudio(self: *Self, late: u64) void {
|
pub fn sampleAudio(self: *Self, late: u64) void {
|
||||||
var left: i16 = 0;
|
// zig fmt: off
|
||||||
var right: i16 = 0;
|
const any_ch_enabled = self.ch1.enabled
|
||||||
|
or self.ch2.enabled
|
||||||
|
or self.ch3.enabled
|
||||||
|
or self.ch4.enabled;
|
||||||
|
// zig fmt: on
|
||||||
|
|
||||||
// SOUNDCNT_L Channel Enable flags
|
|
||||||
const ch_left: u4 = self.psg_cnt.ch_left.read();
|
const ch_left: u4 = self.psg_cnt.ch_left.read();
|
||||||
const ch_right: u4 = self.psg_cnt.ch_right.read();
|
const ch_right: u4 = self.psg_cnt.ch_right.read();
|
||||||
|
|
||||||
// Determine SOUNDCNT_H volume modifications
|
// FIXME: Obscure behaviour?
|
||||||
const gba_vol: u4 = switch (self.dma_cnt.ch_vol.read()) {
|
// Apply NR50 Volume Modifications
|
||||||
0b00 => 2,
|
const left_master_vol = (@intToFloat(f32, self.psg_cnt.left_vol.read()) + 1.0) / 7.0;
|
||||||
0b01 => 1,
|
const right_master_vol = (@intToFloat(f32, self.psg_cnt.right_vol.read()) + 1.0) / 7.0;
|
||||||
else => 0,
|
|
||||||
|
// Apply SOUNDCNT_H Volume Modifications
|
||||||
|
const gba_vol: f32 = switch (self.dma_cnt.ch_vol.read()) {
|
||||||
|
0b00 => 0.25,
|
||||||
|
0b01 => 0.5,
|
||||||
|
0b10 => 0.75,
|
||||||
|
0b11 => 0.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add all PSG channels together
|
// Sample Channel 1
|
||||||
left += if (ch_left & 1 == 1) self.ch1.amplitude() else 0;
|
const ch1_sample = self.highPass(self.ch1.amplitude(), any_ch_enabled);
|
||||||
left += if (ch_left >> 1 & 1 == 1) self.ch2.amplitude() else 0;
|
const ch1_left = if (ch_left & 1 == 1) ch1_sample else 0;
|
||||||
left += if (ch_left >> 2 & 1 == 1) self.ch3.amplitude() else 0;
|
const ch1_right = if (ch_right & 1 == 1) ch1_sample else 0;
|
||||||
left += if (ch_left >> 3 == 1) self.ch4.amplitude() else 0;
|
|
||||||
|
|
||||||
right += if (ch_right & 1 == 1) self.ch1.amplitude() else 0;
|
// Sample Channel 2
|
||||||
right += if (ch_right >> 1 & 1 == 1) self.ch2.amplitude() else 0;
|
const ch2_sample = self.highPass(self.ch2.amplitude(), any_ch_enabled);
|
||||||
right += if (ch_right >> 2 & 1 == 1) self.ch3.amplitude() else 0;
|
const ch2_left = if (ch_left >> 1 & 1 == 1) ch2_sample else 0;
|
||||||
right += if (ch_right >> 3 == 1) self.ch4.amplitude() else 0;
|
const ch2_right = if (ch_right >> 1 & 1 == 1) ch2_sample else 0;
|
||||||
|
|
||||||
// Multiply by master channel volume
|
// Sample Channel 3
|
||||||
left *= 1 + @as(i16, self.psg_cnt.left_vol.read());
|
const ch3_sample = self.highPass(self.ch3.amplitude(), any_ch_enabled);
|
||||||
right *= 1 + @as(i16, self.psg_cnt.right_vol.read());
|
const ch3_left = if (ch_left >> 2 & 1 == 1) ch3_sample else 0;
|
||||||
|
const ch3_right = if (ch_right >> 2 & 1 == 1) ch3_sample else 0;
|
||||||
|
|
||||||
// Apply GBA volume modifications to PSG Channels
|
// Sample Channel 4
|
||||||
left >>= gba_vol;
|
const ch4_sample = self.highPass(self.ch4.amplitude(), any_ch_enabled);
|
||||||
right >>= gba_vol;
|
const ch4_left = if (ch_left >> 3 == 1) ch4_sample else 0;
|
||||||
|
const ch4_right = if (ch_right >> 3 == 1) ch4_sample else 0;
|
||||||
|
|
||||||
const chA_sample = self.chA.amplitude() << if (self.dma_cnt.chA_vol.read()) @as(u4, 2) else 1;
|
const psg_left = (ch1_left + ch2_left + ch3_left + ch4_left) * left_master_vol * gba_vol;
|
||||||
const chB_sample = self.chB.amplitude() << if (self.dma_cnt.chB_vol.read()) @as(u4, 2) else 1;
|
const psg_right = (ch1_right + ch2_right + ch3_right + ch4_right) * right_master_vol * gba_vol;
|
||||||
|
|
||||||
left += if (self.dma_cnt.chA_left.read()) chA_sample else 0;
|
// Sample Dma Channels
|
||||||
left += if (self.dma_cnt.chB_left.read()) chB_sample else 0;
|
const chA_sample = if (self.dma_cnt.chA_vol.read()) self.chA.amplitude() * 4 else self.chA.amplitude() * 2;
|
||||||
|
const chA_left = if (self.dma_cnt.chA_left.read()) chA_sample else 0;
|
||||||
|
const chA_right = if (self.dma_cnt.chA_right.read()) chA_sample else 0;
|
||||||
|
|
||||||
right += if (self.dma_cnt.chA_right.read()) chA_sample else 0;
|
const chB_sample = if (self.dma_cnt.chB_vol.read()) self.chB.amplitude() * 4 else self.chB.amplitude() * 2;
|
||||||
right += if (self.dma_cnt.chB_right.read()) chB_sample else 0;
|
const chB_left = if (self.dma_cnt.chB_left.read()) chB_sample else 0;
|
||||||
|
const chB_right = if (self.dma_cnt.chB_right.read()) chB_sample else 0;
|
||||||
|
|
||||||
// Add SOUNDBIAS
|
// Mix all Channels
|
||||||
// FIXME: Is SOUNDBIAS 9-bit or 10-bit?
|
const left = (chA_left + chB_left + psg_left) / 6.0;
|
||||||
const bias = @as(i16, self.bias.level.read()) << 1;
|
const right = (chA_right + chB_right + psg_right) / 6.0;
|
||||||
left += bias;
|
|
||||||
right += bias;
|
|
||||||
|
|
||||||
const tmp_left = std.math.clamp(@intCast(u16, left), std.math.minInt(u11), std.math.maxInt(u11));
|
|
||||||
const tmp_right = std.math.clamp(@intCast(u16, left), std.math.minInt(u11), std.math.maxInt(u11));
|
|
||||||
|
|
||||||
// Extend to 16-bit signed audio samples
|
|
||||||
const final_left = (tmp_left << 5) | (tmp_left >> 6);
|
|
||||||
const final_right = (tmp_right << 5) | (tmp_right >> 6);
|
|
||||||
|
|
||||||
if (self.sampling_cycle != self.bias.sampling_cycle.read()) {
|
if (self.sampling_cycle != self.bias.sampling_cycle.read()) {
|
||||||
log.info("Sampling Cycle changed from {} to {}", .{ self.sampling_cycle, self.bias.sampling_cycle.read() });
|
log.info("Sampling Cycle changed from {} to {}", .{ self.sampling_cycle, self.bias.sampling_cycle.read() });
|
||||||
|
@ -197,90 +201,15 @@ pub const Apu = struct {
|
||||||
defer SDL.SDL_FreeAudioStream(old);
|
defer SDL.SDL_FreeAudioStream(old);
|
||||||
|
|
||||||
self.sampling_cycle = self.bias.sampling_cycle.read();
|
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_F32, 2, @intCast(c_int, self.sampleRate()), SDL.AUDIO_F32, 2, host_sample_rate) orelse unreachable;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = SDL.SDL_AudioStreamPut(self.stream, &[2]u16{ final_left, final_right }, 2 * @sizeOf(u16));
|
while (SDL.SDL_AudioStreamAvailable(self.stream) > (@sizeOf(f32) * 2 * 0x800)) {}
|
||||||
|
|
||||||
|
_ = SDL.SDL_AudioStreamPut(self.stream, &[2]f32{ left, right }, 2 * @sizeOf(f32));
|
||||||
self.sched.push(.SampleAudio, self.sampleTicks() -| late);
|
self.sched.push(.SampleAudio, self.sampleTicks() -| late);
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub fn sampleAudio(self: *Self, late: u64) void {
|
|
||||||
// // zig fmt: off
|
|
||||||
// const any_ch_enabled = self.ch1.enabled
|
|
||||||
// or self.ch2.enabled
|
|
||||||
// or self.ch3.enabled
|
|
||||||
// or self.ch4.enabled;
|
|
||||||
// // zig fmt: on
|
|
||||||
|
|
||||||
// const ch_left: u4 = self.psg_cnt.ch_left.read();
|
|
||||||
// const ch_right: u4 = self.psg_cnt.ch_right.read();
|
|
||||||
|
|
||||||
// // FIXME: Obscure behaviour?
|
|
||||||
// // Apply NR50 Volume Modifications
|
|
||||||
// const left_master_vol = (@intToFloat(f32, self.psg_cnt.left_vol.read()) + 1.0) / 7.0;
|
|
||||||
// const right_master_vol = (@intToFloat(f32, self.psg_cnt.right_vol.read()) + 1.0) / 7.0;
|
|
||||||
|
|
||||||
// // Apply SOUNDCNT_H Volume Modifications
|
|
||||||
// const gba_vol: f32 = switch (self.dma_cnt.ch_vol.read()) {
|
|
||||||
// 0b00 => 0.25,
|
|
||||||
// 0b01 => 0.5,
|
|
||||||
// else => 1.0,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// // Sample Channel 1
|
|
||||||
// const ch1_sample = self.highPass(self.ch1.amplitude(), any_ch_enabled);
|
|
||||||
// const ch1_left = if (ch_left & 1 == 1) ch1_sample else 0;
|
|
||||||
// const ch1_right = if (ch_right & 1 == 1) ch1_sample else 0;
|
|
||||||
|
|
||||||
// // Sample Channel 2
|
|
||||||
// const ch2_sample = self.highPass(self.ch2.amplitude(), any_ch_enabled);
|
|
||||||
// const ch2_left = if (ch_left >> 1 & 1 == 1) ch2_sample else 0;
|
|
||||||
// const ch2_right = if (ch_right >> 1 & 1 == 1) ch2_sample else 0;
|
|
||||||
|
|
||||||
// // Sample Channel 3
|
|
||||||
// const ch3_sample = self.highPass(self.ch3.amplitude(), any_ch_enabled);
|
|
||||||
// const ch3_left = if (ch_left >> 2 & 1 == 1) ch3_sample else 0;
|
|
||||||
// const ch3_right = if (ch_right >> 2 & 1 == 1) ch3_sample else 0;
|
|
||||||
|
|
||||||
// // Sample Channel 4
|
|
||||||
// const ch4_sample = self.highPass(self.ch4.amplitude(), any_ch_enabled);
|
|
||||||
// const ch4_left = if (ch_left >> 3 == 1) ch4_sample else 0;
|
|
||||||
// const ch4_right = if (ch_right >> 3 == 1) ch4_sample else 0;
|
|
||||||
|
|
||||||
// const psg_left = (ch1_left + ch2_left + ch3_left + ch4_left) * left_master_vol * gba_vol;
|
|
||||||
// const psg_right = (ch1_right + ch2_right + ch3_right + ch4_right) * right_master_vol * gba_vol;
|
|
||||||
|
|
||||||
// // Sample Dma Channels
|
|
||||||
// const chA_sample = if (self.dma_cnt.chA_vol.read()) self.chA.amplitude() * 4 else self.chA.amplitude() * 2;
|
|
||||||
// const chA_left = if (self.dma_cnt.chA_left.read()) chA_sample else 0;
|
|
||||||
// const chA_right = if (self.dma_cnt.chA_right.read()) chA_sample else 0;
|
|
||||||
|
|
||||||
// const chB_sample = if (self.dma_cnt.chB_vol.read()) self.chB.amplitude() * 4 else self.chB.amplitude() * 2;
|
|
||||||
// const chB_left = if (self.dma_cnt.chB_left.read()) chB_sample else 0;
|
|
||||||
// const chB_right = if (self.dma_cnt.chB_right.read()) chB_sample else 0;
|
|
||||||
|
|
||||||
// // Mix all Channels
|
|
||||||
// const left = (chA_left + chB_left + psg_left) / 6.0;
|
|
||||||
// const right = (chA_right + chB_right + psg_right) / 6.0;
|
|
||||||
|
|
||||||
// if (self.sampling_cycle != self.bias.sampling_cycle.read()) {
|
|
||||||
// log.info("Sampling Cycle changed from {} to {}", .{ self.sampling_cycle, self.bias.sampling_cycle.read() });
|
|
||||||
|
|
||||||
// // Sample Rate Changed, Create a new Resampler since i can't figure out how to change
|
|
||||||
// // the parameters of the old one
|
|
||||||
// const old = self.stream;
|
|
||||||
// defer SDL.SDL_FreeAudioStream(old);
|
|
||||||
|
|
||||||
// self.sampling_cycle = self.bias.sampling_cycle.read();
|
|
||||||
// self.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_F32, 2, @intCast(c_int, self.sampleRate()), SDL.AUDIO_F32, 2, host_sample_rate) orelse unreachable;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// while (SDL.SDL_AudioStreamAvailable(self.stream) > (@sizeOf(f32) * 2 * 0x800)) {}
|
|
||||||
|
|
||||||
// _ = SDL.SDL_AudioStreamPut(self.stream, &[2]f32{ left, right }, 2 * @sizeOf(f32));
|
|
||||||
// self.sched.push(.SampleAudio, self.sampleTicks() -| late);
|
|
||||||
// }
|
|
||||||
|
|
||||||
fn sampleTicks(self: *const Self) u64 {
|
fn sampleTicks(self: *const Self) u64 {
|
||||||
return (1 << 24) / self.sampleRate();
|
return (1 << 24) / self.sampleRate();
|
||||||
}
|
}
|
||||||
|
@ -367,7 +296,7 @@ const ToneSweep = struct {
|
||||||
square: SquareWave,
|
square: SquareWave,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
|
|
||||||
sample: i8,
|
sample: u8,
|
||||||
|
|
||||||
const SweepDevice = struct {
|
const SweepDevice = struct {
|
||||||
const This = @This();
|
const This = @This();
|
||||||
|
@ -460,11 +389,11 @@ const ToneSweep = struct {
|
||||||
|
|
||||||
self.sample = 0;
|
self.sample = 0;
|
||||||
if (!self.isDacEnabled()) return;
|
if (!self.isDacEnabled()) return;
|
||||||
self.sample = if (self.enabled) self.square.sample(self.duty) * @as(i8, self.env_dev.vol) else 0;
|
self.sample = if (self.enabled) self.square.sample(self.duty) * self.env_dev.vol else 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn amplitude(self: *const Self) i16 {
|
fn amplitude(self: *const Self) f32 {
|
||||||
return @as(i16, self.sample);
|
return (@intToFloat(f32, self.sample) / 7.5) - 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// NR11, NR12
|
/// NR11, NR12
|
||||||
|
@ -555,7 +484,7 @@ const Tone = struct {
|
||||||
square: SquareWave,
|
square: SquareWave,
|
||||||
|
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
sample: i8,
|
sample: u8,
|
||||||
|
|
||||||
fn init(sched: *Scheduler) Self {
|
fn init(sched: *Scheduler) Self {
|
||||||
return .{
|
return .{
|
||||||
|
@ -594,11 +523,11 @@ const Tone = struct {
|
||||||
|
|
||||||
self.sample = 0;
|
self.sample = 0;
|
||||||
if (!self.isDacEnabled()) return;
|
if (!self.isDacEnabled()) return;
|
||||||
self.sample = if (self.enabled) self.square.sample(self.duty) * @as(i8, self.env_dev.vol) else 0;
|
self.sample = if (self.enabled) self.square.sample(self.duty) * self.env_dev.vol else 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn amplitude(self: *const Self) i16 {
|
fn amplitude(self: *const Self) f32 {
|
||||||
return @as(i16, self.sample);
|
return (@intToFloat(f32, self.sample) / 7.5) - 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// NR21, NR22
|
/// NR21, NR22
|
||||||
|
@ -680,7 +609,7 @@ const Wave = struct {
|
||||||
wave_dev: WaveDevice,
|
wave_dev: WaveDevice,
|
||||||
|
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
sample: i8,
|
sample: u8,
|
||||||
|
|
||||||
fn init(sched: *Scheduler) Self {
|
fn init(sched: *Scheduler) Self {
|
||||||
return .{
|
return .{
|
||||||
|
@ -767,12 +696,11 @@ const Wave = struct {
|
||||||
|
|
||||||
self.sample = 0;
|
self.sample = 0;
|
||||||
if (!self.select.enabled.read()) return;
|
if (!self.select.enabled.read()) return;
|
||||||
// Convert unsigned 4-bit wave sample to signed 8-bit sample
|
self.sample = if (self.enabled) self.wave_dev.sample(self.select) >> self.wave_dev.shift(self.vol) else 0;
|
||||||
self.sample = (2 * @as(i8, self.wave_dev.sample(self.select)) - 15) >> self.wave_dev.shift(self.vol);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn amplitude(self: *const Self) i16 {
|
fn amplitude(self: *const Self) f32 {
|
||||||
return @as(i16, self.sample);
|
return (@intToFloat(f32, self.sample) / 7.5) - 1.0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -799,7 +727,7 @@ const Noise = struct {
|
||||||
lfsr: Lfsr,
|
lfsr: Lfsr,
|
||||||
|
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
sample: i8,
|
sample: u8,
|
||||||
|
|
||||||
fn init(sched: *Scheduler) Self {
|
fn init(sched: *Scheduler) Self {
|
||||||
return .{
|
return .{
|
||||||
|
@ -893,11 +821,11 @@ const Noise = struct {
|
||||||
|
|
||||||
self.sample = 0;
|
self.sample = 0;
|
||||||
if (!self.isDacEnabled()) return;
|
if (!self.isDacEnabled()) return;
|
||||||
self.sample = if (self.enabled) self.lfsr.sample() * @as(i8, self.env_dev.vol) else 0;
|
self.sample = if (self.enabled) self.lfsr.sample() * self.env_dev.vol else 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn amplitude(self: *const Self) i16 {
|
fn amplitude(self: *const Self) f32 {
|
||||||
return @as(i16, self.sample);
|
return (@intToFloat(f32, self.sample) / 7.5) - 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn isDacEnabled(self: *const Self) bool {
|
fn isDacEnabled(self: *const Self) bool {
|
||||||
|
@ -933,8 +861,8 @@ pub fn DmaSound(comptime kind: DmaSoundKind) type {
|
||||||
if (self.fifo.readItem()) |sample| self.sample = @bitCast(i8, sample);
|
if (self.fifo.readItem()) |sample| self.sample = @bitCast(i8, sample);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn amplitude(self: *const Self) i16 {
|
pub fn amplitude(self: *const Self) f32 {
|
||||||
return @as(i16, self.sample);
|
return @intToFloat(f32, self.sample) / 127.5 - (1 / 255);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1172,7 +1100,7 @@ const SquareWave = struct {
|
||||||
self.sched.push(.{ .ApuChannel = if (kind == .Ch1) 0 else 1 }, @as(u64, self.timer) * tickInterval);
|
self.sched.push(.{ .ApuChannel = if (kind == .Ch1) 0 else 1 }, @as(u64, self.timer) * tickInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sample(self: *const Self, cnt: io.Duty) i8 {
|
fn sample(self: *const Self, cnt: io.Duty) u1 {
|
||||||
const pattern = cnt.pattern.read();
|
const pattern = cnt.pattern.read();
|
||||||
|
|
||||||
const i = self.pos ^ 7; // index of 0 should get highest bit
|
const i = self.pos ^ 7; // index of 0 should get highest bit
|
||||||
|
@ -1183,7 +1111,7 @@ const SquareWave = struct {
|
||||||
0b11 => @as(u8, 0b11111100) >> i, // 75%
|
0b11 => @as(u8, 0b11111100) >> i, // 75%
|
||||||
};
|
};
|
||||||
|
|
||||||
return if (result & 1 == 1) 1 else -1;
|
return @truncate(u1, result);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1205,8 +1133,8 @@ const Lfsr = struct {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sample(self: *const Self) i8 {
|
fn sample(self: *const Self) u1 {
|
||||||
return if ((~self.shift & 1) == 1) 1 else -1;
|
return @truncate(u1, ~self.shift);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn updateLength(_: *Self, fs: *const FrameSequencer, ch4: *Noise, new: io.NoiseControl) void {
|
fn updateLength(_: *Self, fs: *const FrameSequencer, ch4: *Noise, new: io.NoiseControl) void {
|
||||||
|
|
87
src/emu.zig
87
src/emu.zig
|
@ -1,17 +1,14 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const SDL = @import("sdl2");
|
|
||||||
|
|
||||||
const Bus = @import("Bus.zig");
|
const Bus = @import("Bus.zig");
|
||||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
const Scheduler = @import("scheduler.zig").Scheduler;
|
||||||
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
||||||
const FpsTracker = @import("util.zig").FpsTracker;
|
const EmulatorFps = @import("util.zig").EmulatorFps;
|
||||||
|
|
||||||
const Timer = std.time.Timer;
|
const Timer = std.time.Timer;
|
||||||
const Thread = std.Thread;
|
const Thread = std.Thread;
|
||||||
const Atomic = std.atomic.Atomic;
|
const Atomic = std.atomic.Atomic;
|
||||||
|
|
||||||
const audio_sync = true;
|
|
||||||
|
|
||||||
// 228 Lines which consist of 308 dots (which are 4 cycles long)
|
// 228 Lines which consist of 308 dots (which are 4 cycles long)
|
||||||
const cycles_per_frame: u64 = 228 * (308 * 4); //280896
|
const cycles_per_frame: u64 = 228 * (308 * 4); //280896
|
||||||
const clock_rate: u64 = 1 << 24; // 16.78MHz
|
const clock_rate: u64 = 1 << 24; // 16.78MHz
|
||||||
|
@ -35,9 +32,7 @@ const RunKind = enum {
|
||||||
LimitedBusy,
|
LimitedBusy,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn run(kind: RunKind, quit: *Atomic(bool), fps: *FpsTracker, sched: *Scheduler, cpu: *Arm7tdmi) void {
|
pub fn run(kind: RunKind, quit: *Atomic(bool), fps: *EmulatorFps, sched: *Scheduler, cpu: *Arm7tdmi) void {
|
||||||
if (audio_sync) log.info("Audio sync enabled", .{});
|
|
||||||
|
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
.Unlimited => runUnsynchronized(quit, sched, cpu, null),
|
.Unlimited => runUnsynchronized(quit, sched, cpu, null),
|
||||||
.Limited => runSynchronized(quit, sched, cpu, null),
|
.Limited => runSynchronized(quit, sched, cpu, null),
|
||||||
|
@ -61,78 +56,50 @@ pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn syncToAudio(cpu: *const Arm7tdmi) void {
|
pub fn runUnsynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*EmulatorFps) void {
|
||||||
const stream = cpu.bus.apu.stream;
|
|
||||||
const min_sample_count = 0x800;
|
|
||||||
|
|
||||||
// Busy Loop while we wait for the Audio system to catch up
|
|
||||||
while (SDL.SDL_AudioStreamAvailable(stream) > (@sizeOf(u16) * 2) * min_sample_count) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn runUnsynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*FpsTracker) void {
|
|
||||||
log.info("Emulation thread w/out video sync", .{});
|
|
||||||
|
|
||||||
if (fps) |tracker| {
|
if (fps) |tracker| {
|
||||||
log.info("FPS Tracking Enabled", .{});
|
log.info("Start unsynchronized emu thread w/ fps tracking", .{});
|
||||||
|
|
||||||
while (!quit.load(.SeqCst)) {
|
while (!quit.load(.SeqCst)) {
|
||||||
runFrame(sched, cpu);
|
runFrame(sched, cpu);
|
||||||
if (audio_sync) syncToAudio(cpu);
|
|
||||||
|
|
||||||
tracker.completeFrame();
|
tracker.completeFrame();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
while (!quit.load(.SeqCst)) {
|
log.info("Start unsynchronized emu thread", .{});
|
||||||
runFrame(sched, cpu);
|
while (!quit.load(.SeqCst)) runFrame(sched, cpu);
|
||||||
if (audio_sync) syncToAudio(cpu);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runSynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*FpsTracker) void {
|
pub fn runSynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*EmulatorFps) void {
|
||||||
log.info("Emulation thread w/ video 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;
|
||||||
|
|
||||||
if (fps) |tracker| {
|
if (fps) |tracker| {
|
||||||
log.info("FPS Tracking Enabled", .{});
|
log.info("Start synchronized emu thread w/ fps tracking", .{});
|
||||||
|
|
||||||
while (!quit.load(.SeqCst)) {
|
while (!quit.load(.SeqCst)) {
|
||||||
runFrame(sched, cpu);
|
runSyncCore(sched, cpu, &timer, &wake_time);
|
||||||
const new_wake_time = syncToVideo(&timer, wake_time);
|
|
||||||
|
|
||||||
// Spin to make up the difference of OS scheduler innacuracies
|
|
||||||
// If we happen to also be syncing to audio, we choose to spin on
|
|
||||||
// the amount of time needed for audio to catch up rather than
|
|
||||||
// our expected wake-up time
|
|
||||||
if (audio_sync) syncToAudio(cpu) else spinLoop(&timer, wake_time);
|
|
||||||
wake_time = new_wake_time;
|
|
||||||
|
|
||||||
tracker.completeFrame();
|
tracker.completeFrame();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
while (!quit.load(.SeqCst)) {
|
log.info("Start synchronized emu thread", .{});
|
||||||
runFrame(sched, cpu);
|
while (!quit.load(.SeqCst)) runSyncCore(sched, cpu, &timer, &wake_time);
|
||||||
const new_wake_time = syncToVideo(&timer, wake_time);
|
|
||||||
// see above comment
|
|
||||||
if (audio_sync) syncToAudio(cpu) else spinLoop(&timer, wake_time);
|
|
||||||
|
|
||||||
wake_time = new_wake_time;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fn syncToVideo(timer: *Timer, wake_time: u64) u64 {
|
inline fn runSyncCore(sched: *Scheduler, cpu: *Arm7tdmi, timer: *Timer, wake_time: *u64) void {
|
||||||
// Use the OS scheduler to put the emulation thread to sleep
|
runFrame(sched, cpu);
|
||||||
const maybe_recalc_wake_time = sleep(timer, wake_time);
|
|
||||||
|
|
||||||
// If sleep() determined we need to adjust our wake up time, do so
|
// Put the Thread to Sleep + Backup Spin Loop
|
||||||
// otherwise predict our next wake up time according to the frame period
|
// This saves on resource usage when frame limiting
|
||||||
return if (maybe_recalc_wake_time) |recalc| recalc else wake_time + frame_period;
|
sleep(timer, wake_time);
|
||||||
|
|
||||||
|
// Update to the new wake time
|
||||||
|
wake_time.* += frame_period;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runBusyLoop(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi) void {
|
pub fn runBusyLoop(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi) void {
|
||||||
log.info("Emulation thread with video sync using busy loop", .{});
|
log.info("Start synchronized emu thread using busy loop", .{});
|
||||||
var timer = Timer.start() catch unreachable;
|
var timer = Timer.start() catch unreachable;
|
||||||
var wake_time: u64 = frame_period;
|
var wake_time: u64 = frame_period;
|
||||||
|
|
||||||
|
@ -145,17 +112,21 @@ pub fn runBusyLoop(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi) void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sleep(timer: *Timer, wake_time: u64) ?u64 {
|
fn sleep(timer: *Timer, wake_time: *u64) void {
|
||||||
// const step = std.time.ns_per_ms * 10; // 10ms
|
// const step = std.time.ns_per_ms * 10; // 10ms
|
||||||
const timestamp = timer.read();
|
const timestamp = timer.read();
|
||||||
|
|
||||||
// ns_late is non zero if we are late.
|
// ns_late is non zero if we are late.
|
||||||
const ns_late = timestamp -| wake_time;
|
const ns_late = timestamp -| wake_time.*;
|
||||||
|
|
||||||
// If we're more than a frame late, skip the rest of this loop
|
// If we're more than a frame late, skip the rest of this loop
|
||||||
// Recalculate what our new wake time should be so that we can
|
// Recalculate what our new wake time should be so that we can
|
||||||
// get "back on track"
|
// get "back on track"
|
||||||
if (ns_late > frame_period) return timestamp + frame_period;
|
if (ns_late > frame_period) {
|
||||||
|
wake_time.* = timestamp + frame_period;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const sleep_for = frame_period - ns_late;
|
const sleep_for = frame_period - ns_late;
|
||||||
|
|
||||||
// // Employ several sleep calls in periods of 10ms
|
// // Employ several sleep calls in periods of 10ms
|
||||||
|
@ -168,7 +139,9 @@ fn sleep(timer: *Timer, wake_time: u64) ?u64 {
|
||||||
|
|
||||||
std.time.sleep(sleep_for);
|
std.time.sleep(sleep_for);
|
||||||
|
|
||||||
return null;
|
// Spin to make up the difference if there is a need
|
||||||
|
// Make sure that we're using the old wake time and not the onne we recalculated
|
||||||
|
spinLoop(timer, wake_time.*);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spinLoop(timer: *Timer, wake_time: u64) void {
|
fn spinLoop(timer: *Timer, wake_time: u64) void {
|
||||||
|
|
131
src/main.zig
131
src/main.zig
|
@ -9,7 +9,7 @@ const Bus = @import("Bus.zig");
|
||||||
const Apu = @import("apu.zig").Apu;
|
const Apu = @import("apu.zig").Apu;
|
||||||
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
||||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
const Scheduler = @import("scheduler.zig").Scheduler;
|
||||||
const FpsTracker = @import("util.zig").FpsTracker;
|
const EmulatorFps = @import("util.zig").EmulatorFps;
|
||||||
|
|
||||||
const Timer = std.time.Timer;
|
const Timer = std.time.Timer;
|
||||||
const Thread = std.Thread;
|
const Thread = std.Thread;
|
||||||
|
@ -31,61 +31,65 @@ pub const log_level = if (builtin.mode != .Debug) .info else std.log.default_lev
|
||||||
|
|
||||||
const asString = @import("util.zig").asString;
|
const asString = @import("util.zig").asString;
|
||||||
|
|
||||||
// CLI Arguments + Help Text
|
|
||||||
const params = clap.parseParamsComptime(
|
|
||||||
\\-h, --help Display this help and exit.
|
|
||||||
\\-b, --bios <str> Optional path to a GBA BIOS ROM.
|
|
||||||
\\<str> Path to the GBA GamePak ROM
|
|
||||||
\\
|
|
||||||
);
|
|
||||||
|
|
||||||
pub fn main() anyerror!void {
|
pub fn main() anyerror!void {
|
||||||
// Allocator for Emulator + CLI
|
// Allocator for Emulator + CLI
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
defer std.debug.assert(!gpa.deinit());
|
|
||||||
const alloc = gpa.allocator();
|
const alloc = gpa.allocator();
|
||||||
|
defer std.debug.assert(!gpa.deinit());
|
||||||
|
|
||||||
|
// CLI Arguments
|
||||||
|
const params = comptime clap.parseParamsComptime(
|
||||||
|
\\-h, --help Display this help and exit.
|
||||||
|
\\-b, --bios <str> Optional path to a GBA BIOS ROM.
|
||||||
|
\\<str> Path to the GBA GamePak ROM
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
|
||||||
// Setup CLI using zig-clap
|
|
||||||
var res = try clap.parse(clap.Help, ¶ms, clap.parsers.default, .{});
|
var res = try clap.parse(clap.Help, ¶ms, clap.parsers.default, .{});
|
||||||
defer res.deinit();
|
defer res.deinit();
|
||||||
|
|
||||||
const stderr = std.io.getStdErr();
|
const stderr = std.io.getStdErr();
|
||||||
defer stderr.close();
|
defer stderr.close();
|
||||||
|
|
||||||
// Display Help, if requested
|
|
||||||
// Grab ROM and BIOS paths if provided
|
|
||||||
if (res.args.help) return clap.help(stderr.writer(), clap.Help, ¶ms, .{});
|
if (res.args.help) return clap.help(stderr.writer(), clap.Help, ¶ms, .{});
|
||||||
const rom_path = try getRomPath(res, stderr);
|
|
||||||
const bios_path: ?[]const u8 = if (res.args.bios) |p| p else null;
|
const bios_path: ?[]const u8 = if (res.args.bios) |p| p else null;
|
||||||
|
|
||||||
|
const rom_path = switch (res.positionals.len) {
|
||||||
|
1 => res.positionals[0],
|
||||||
|
0 => {
|
||||||
|
try stderr.writeAll("ZBA requires a positional path to a GamePak ROM.\n");
|
||||||
|
return CliError.InsufficientOptions;
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
try stderr.writeAll("ZBA received too many arguments.\n");
|
||||||
|
return CliError.UnneededOptions;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// Determine Save Directory
|
// Determine Save Directory
|
||||||
const save_dir = try getSavePath(alloc);
|
const save_dir = try setupSavePath(alloc);
|
||||||
defer if (save_dir) |path| alloc.free(path);
|
defer if (save_dir) |path| alloc.free(path);
|
||||||
log.info("Found save directory: {s}", .{save_dir});
|
log.info("Found save directory: {s}", .{save_dir});
|
||||||
|
|
||||||
// Initialize Scheduler and ARM7TDMI Emulator
|
// Initialize SDL
|
||||||
// Provide GBA Bus (initialized with ARM7TDMI) with a valid ptr to ARM7TDMI
|
_ = initSdl2();
|
||||||
|
defer SDL.SDL_Quit();
|
||||||
|
|
||||||
|
// Initialize Emulator
|
||||||
var scheduler = Scheduler.init(alloc);
|
var scheduler = Scheduler.init(alloc);
|
||||||
defer scheduler.deinit();
|
defer scheduler.deinit();
|
||||||
|
|
||||||
const paths = .{ .bios = bios_path, .rom = rom_path, .save = save_dir };
|
const paths = .{ .bios = bios_path, .rom = rom_path, .save = save_dir };
|
||||||
var cpu = try Arm7tdmi.init(alloc, &scheduler, paths);
|
var cpu = try Arm7tdmi.init(alloc, &scheduler, paths);
|
||||||
defer cpu.deinit();
|
defer cpu.deinit();
|
||||||
|
|
||||||
cpu.bus.attach(&cpu);
|
cpu.bus.attach(&cpu);
|
||||||
// cpu.fastBoot(); // Uncomment to skip BIOS
|
// cpu.fastBoot();
|
||||||
|
|
||||||
// Copy ROM title while Emulator still belongs to this thread
|
// Initialize SDL Audio
|
||||||
const title = cpu.bus.pak.title;
|
const audio_dev = initAudio(&cpu.bus.apu);
|
||||||
|
defer SDL.SDL_CloseAudioDevice(audio_dev);
|
||||||
|
|
||||||
// Initialize SDL2
|
|
||||||
initSdl2();
|
|
||||||
defer SDL.SDL_Quit();
|
|
||||||
|
|
||||||
const dev = initAudio(&cpu.bus.apu);
|
|
||||||
defer SDL.SDL_CloseAudioDevice(dev);
|
|
||||||
|
|
||||||
// TODO: Refactor or delete this Logging code
|
|
||||||
// I probably still need logging in some form though (e.g. Golden Sun IIRC)
|
|
||||||
const log_file: ?File = if (enable_logging) blk: {
|
const log_file: ?File = if (enable_logging) blk: {
|
||||||
const file = try std.fs.cwd().createFile(if (is_binary) "zba.bin" else "zba.log", .{});
|
const file = try std.fs.cwd().createFile(if (is_binary) "zba.bin" else "zba.log", .{});
|
||||||
cpu.useLogger(&file, is_binary);
|
cpu.useLogger(&file, is_binary);
|
||||||
|
@ -93,17 +97,16 @@ pub fn main() anyerror!void {
|
||||||
} else null;
|
} else null;
|
||||||
defer if (log_file) |file| file.close();
|
defer if (log_file) |file| file.close();
|
||||||
|
|
||||||
|
// Init Atomics
|
||||||
var quit = Atomic(bool).init(false);
|
var quit = Atomic(bool).init(false);
|
||||||
var emu_rate = FpsTracker.init();
|
var emu_rate = EmulatorFps.init();
|
||||||
|
|
||||||
// Run Emulator in it's separate thread
|
// Create Emulator Thread
|
||||||
// From this point on, interacting with Arm7tdmi or Scheduler
|
|
||||||
// be justified, as it will require to be thread-afe
|
|
||||||
const emu_thread = try Thread.spawn(.{}, emu.run, .{ .LimitedFPS, &quit, &emu_rate, &scheduler, &cpu });
|
const emu_thread = try Thread.spawn(.{}, emu.run, .{ .LimitedFPS, &quit, &emu_rate, &scheduler, &cpu });
|
||||||
defer emu_thread.join();
|
defer emu_thread.join();
|
||||||
|
|
||||||
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}", .{asString(title)});
|
const window_title = try std.fmt.bufPrint(&title_buf, "ZBA | {s}", .{asString(cpu.bus.pak.title)});
|
||||||
|
|
||||||
const window = createWindow(window_title, gba_width, gba_height);
|
const window = createWindow(window_title, gba_width, gba_height);
|
||||||
defer SDL.SDL_DestroyWindow(window);
|
defer SDL.SDL_DestroyWindow(window);
|
||||||
|
@ -155,7 +158,7 @@ pub fn main() anyerror!void {
|
||||||
SDL.SDLK_s => io.keyinput.shoulder_r.set(),
|
SDL.SDLK_s => io.keyinput.shoulder_r.set(),
|
||||||
SDL.SDLK_RETURN => io.keyinput.start.set(),
|
SDL.SDLK_RETURN => io.keyinput.start.set(),
|
||||||
SDL.SDLK_RSHIFT => io.keyinput.select.set(),
|
SDL.SDLK_RSHIFT => io.keyinput.select.set(),
|
||||||
SDL.SDLK_i => std.debug.print("{} samples\n", .{@intCast(u32, SDL.SDL_AudioStreamAvailable(cpu.bus.apu.stream)) / (2 * @sizeOf(u16))}),
|
SDL.SDLK_i => std.debug.print("{} samples\n", .{@intCast(u32, SDL.SDL_AudioStreamAvailable(cpu.bus.apu.stream)) / (2 * @sizeOf(f32))}),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -186,9 +189,31 @@ fn sdlPanic() noreturn {
|
||||||
@panic(std.mem.sliceTo(str, 0));
|
@panic(std.mem.sliceTo(str, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initSdl2() void {
|
// FIXME: Superfluous allocations?
|
||||||
|
fn setupSavePath(alloc: std.mem.Allocator) !?[]const u8 {
|
||||||
|
const save_subpath = try std.fs.path.join(alloc, &[_][]const u8{ "zba", "save" });
|
||||||
|
defer alloc.free(save_subpath);
|
||||||
|
|
||||||
|
const maybe_data_path = try known_folders.getPath(alloc, .data);
|
||||||
|
defer if (maybe_data_path) |path| alloc.free(path);
|
||||||
|
|
||||||
|
const save_path = if (maybe_data_path) |base| try std.fs.path.join(alloc, &[_][]const u8{ base, save_subpath }) else null;
|
||||||
|
|
||||||
|
if (save_path) |_| {
|
||||||
|
// If we've determined what our save path should be, ensure the prereq directories
|
||||||
|
// are present so that we can successfully write to the path when necessary
|
||||||
|
const maybe_data_dir = try known_folders.open(alloc, .data, .{});
|
||||||
|
if (maybe_data_dir) |data_dir| try data_dir.makePath(save_subpath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return save_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initSdl2() c_int {
|
||||||
const status = SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO | SDL.SDL_INIT_GAMECONTROLLER);
|
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();
|
if (status < 0) sdlPanic();
|
||||||
|
|
||||||
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn createWindow(title: []u8, width: c_int, height: c_int) *SDL.SDL_Window {
|
fn createWindow(title: []u8, width: c_int, height: c_int) *SDL.SDL_Window {
|
||||||
|
@ -220,7 +245,7 @@ fn initAudio(apu: *Apu) SDL.SDL_AudioDeviceID {
|
||||||
var have: SDL.SDL_AudioSpec = undefined;
|
var have: SDL.SDL_AudioSpec = undefined;
|
||||||
var want: SDL.SDL_AudioSpec = .{
|
var want: SDL.SDL_AudioSpec = .{
|
||||||
.freq = sample_rate,
|
.freq = sample_rate,
|
||||||
.format = SDL.AUDIO_U16,
|
.format = SDL.AUDIO_F32,
|
||||||
.channels = 2,
|
.channels = 2,
|
||||||
.samples = 0x100,
|
.samples = 0x100,
|
||||||
.callback = audioCallback,
|
.callback = audioCallback,
|
||||||
|
@ -243,35 +268,3 @@ export fn audioCallback(userdata: ?*anyopaque, stream: [*c]u8, len: c_int) void
|
||||||
const apu = @ptrCast(*Apu, @alignCast(8, userdata));
|
const apu = @ptrCast(*Apu, @alignCast(8, userdata));
|
||||||
_ = SDL.SDL_AudioStreamGet(apu.stream, stream, len);
|
_ = SDL.SDL_AudioStreamGet(apu.stream, stream, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getSavePath(alloc: std.mem.Allocator) !?[]const u8 {
|
|
||||||
const save_subpath = "zba" ++ [_]u8{std.fs.path.sep} ++ "save";
|
|
||||||
|
|
||||||
const maybe_data_path = try known_folders.getPath(alloc, .data);
|
|
||||||
defer if (maybe_data_path) |path| alloc.free(path);
|
|
||||||
|
|
||||||
const save_path = if (maybe_data_path) |base| try std.fs.path.join(alloc, &[_][]const u8{ base, "zba", "save" }) else null;
|
|
||||||
|
|
||||||
if (save_path) |_| {
|
|
||||||
// If we've determined what our save path should be, ensure the prereq directories
|
|
||||||
// are present so that we can successfully write to the path when necessary
|
|
||||||
const maybe_data_dir = try known_folders.open(alloc, .data, .{});
|
|
||||||
if (maybe_data_dir) |data_dir| try data_dir.makePath(save_subpath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return save_path;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn getRomPath(res: clap.Result(clap.Help, ¶ms, clap.parsers.default), stderr: std.fs.File) ![]const u8 {
|
|
||||||
return switch (res.positionals.len) {
|
|
||||||
1 => res.positionals[0],
|
|
||||||
0 => {
|
|
||||||
try stderr.writeAll("ZBA requires a positional path to a GamePak ROM.\n");
|
|
||||||
return CliError.InsufficientOptions;
|
|
||||||
},
|
|
||||||
else => {
|
|
||||||
try stderr.writeAll("ZBA received too many arguments.\n");
|
|
||||||
return CliError.UnneededOptions;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ pub inline fn rotr(comptime T: type, x: T, r: anytype) T {
|
||||||
return x >> ar | x << (1 +% ~ar);
|
return x >> ar | x << (1 +% ~ar);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const FpsTracker = struct {
|
pub const EmulatorFps = struct {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
fps: u32,
|
fps: u32,
|
||||||
|
|
Loading…
Reference in New Issue