Compare commits

..

21 Commits

Author SHA1 Message Date
Rekai Nyangadzayi Musuka 924729dcb1 fix(bios): set addr_latch even if bios is skipped 2022-10-09 14:11:18 -03:00
Rekai Nyangadzayi Musuka c42004a0b9 fix(bus): make open bus impl aware of CPU pipeline 2022-10-09 14:10:21 -03:00
Rekai Nyangadzayi Musuka b280dbfa0e style(bus): cpu ptr doesn't need to be optional 2022-10-09 13:20:19 -03:00
Rekai Nyangadzayi Musuka b81a31c2aa style: code cleanup 2022-10-01 13:17:57 -03:00
Rekai Nyangadzayi Musuka 3e25a7f595 fix: resolve timing regressions
make sure to use fetch timings when fetching instructions
2022-09-28 17:25:21 -03:00
Rekai Nyangadzayi Musuka e16c2df8d0 fix: rename Pipline to Pipeline 2022-09-28 16:37:40 -03:00
Rekai Nyangadzayi Musuka 14ba0dbca9 feat: working pipeline implementation 2022-09-28 16:11:25 -03:00
Rekai Nyangadzayi Musuka f466ae2ae2 chore: refactor ARM/THUMB data processing instructions 2022-09-28 12:47:44 -03:00
Rekai Nyangadzayi Musuka f616ed29d1 fix: don't flush pipeline when reloading CPSR in ARM Data Processing 2022-09-28 12:47:44 -03:00
Rekai Nyangadzayi Musuka f13b08b527 chore: don't write to CPSR + swap with SPSR at the same time 2022-09-28 10:01:52 -03:00
Rekai Nyangadzayi Musuka 750fdb65ae fix: account for pipeline in obscure bios behaviour 2022-09-26 16:26:45 -03:00
Rekai Nyangadzayi Musuka 05b5081351 chore: update README.md 2022-09-26 16:26:45 -03:00
Rekai Nyangadzayi Musuka be3ce71a96 fix: advance r15, even when the pipeline is reloaded from the scheduler
The PC would fall behind whenever an IRQ was called because the pipeline
was reloaded (+8 to PC), however that was never actually done by any code

Now, the PC is always incremented when the pipeline is reloaded
2022-09-26 16:26:45 -03:00
Rekai Nyangadzayi Musuka 8e2022dffd chore: dump pipeline state on cpu panic 2022-09-26 16:26:45 -03:00
Rekai Nyangadzayi Musuka 0fe164db11 fix: reimpl THUMB.5 instructions
pipeline branch now passes arm.gba and thumb.gba again

(TODO: Stop rewriting my commits away)
2022-09-26 16:26:45 -03:00
Rekai Nyangadzayi Musuka 41f10c6c12 fix: impl workaround for stage2 miscompilation 2022-09-26 16:26:45 -03:00
Rekai Nyangadzayi Musuka d5acfb6eb0 chore: instantly refill the pipeline on flush
I believe this to be necessary in order to get hardware interrupts
working.

thumb.gba test 108 fails but I'm committing anyways (despite the
regression) because this is kind of rebase/merge hell and I have
something that at least sort of works rn
2022-09-26 16:26:45 -03:00
Rekai Nyangadzayi Musuka 6428be5f22 fix: reimpl handleInterrupt code 2022-09-26 16:26:45 -03:00
Rekai Nyangadzayi Musuka 1d09554ea4 feat: implement basic pipeline
passes arm.gba, thumb.gb and armwrestler, fails in actual games
TODO: run FuzzARM debug specific titles
2022-09-26 16:26:45 -03:00
Rekai Nyangadzayi Musuka 66ad155a76 feat: resolve off-by-{word, halfword} errors when printing debug info 2022-09-26 16:26:45 -03:00
Rekai Nyangadzayi Musuka 1fa855d8b5 feat: reimplement cpu logging 2022-09-26 16:26:45 -03:00
34 changed files with 1898 additions and 2005 deletions

3
.gitmodules vendored
View File

@ -10,6 +10,3 @@
[submodule "lib/zig-datetime"] [submodule "lib/zig-datetime"]
path = lib/zig-datetime path = lib/zig-datetime
url = https://github.com/frmdstryr/zig-datetime url = https://github.com/frmdstryr/zig-datetime
[submodule "lib/zig-toml"]
path = lib/zig-toml
url = https://github.com/aeronavery/zig-toml

View File

@ -28,9 +28,6 @@ pub fn build(b: *std.build.Builder) void {
// Argument Parsing Library // Argument Parsing Library
exe.addPackagePath("clap", "lib/zig-clap/clap.zig"); exe.addPackagePath("clap", "lib/zig-clap/clap.zig");
// TOML Library
exe.addPackagePath("toml", "lib/zig-toml/src/toml.zig");
// Zig SDL Bindings: https://github.com/MasterQ32/SDL.zig // Zig SDL Bindings: https://github.com/MasterQ32/SDL.zig
const sdk = Sdk.init(b); const sdk = Sdk.init(b);
sdk.link(exe, .dynamic); sdk.link(exe, .dynamic);

@ -1 +1 @@
Subproject commit e5d09c4b2d121025ad7195b2de704451e6306807 Subproject commit 4f4196fc3bc95c4bd3b12ce2e4a5f1050742cd3c

@ -1 +0,0 @@
Subproject commit 5dfa919e03b446c66b295c04bef9bdecabd4276f

View File

@ -1,74 +0,0 @@
const std = @import("std");
const toml = @import("toml");
const Allocator = std.mem.Allocator;
var state: Config = .{};
const Config = struct {
host: Host = .{},
guest: Guest = .{},
debug: Debug = .{},
/// Settings related to the Computer the Emulator is being run on
const Host = struct {
/// Using Nearest-Neighbor, multiply the resolution of the GBA Window
win_scale: i64 = 3,
/// Enable Vsync
///
/// Note: This does not affect whether Emulation is synced to 59Hz
vsync: bool = true,
};
// Settings realted to the emulation itself
const Guest = struct {
/// Whether Emulation thread to sync to Audio Callbacks
audio_sync: bool = true,
/// Whether Emulation thread should sync to 59Hz
video_sync: bool = true,
/// Whether RTC I/O should always be enabled
force_rtc: bool = false,
};
/// Settings related to debugging ZBA
const Debug = struct {
/// Enable CPU Trace logs
cpu_trace: bool = false,
/// If false and ZBA is build in debug mode, ZBA will panic on unhandled I/O
unhandled_io: bool = true,
};
};
pub fn config() *const Config {
return &state;
}
/// Reads a config file and then loads it into the global state
pub fn load(allocator: Allocator, config_path: []const u8) !void {
var config_file = try std.fs.cwd().openFile(config_path, .{});
defer config_file.close();
const contents = try config_file.readToEndAlloc(allocator, try config_file.getEndPos());
defer allocator.free(contents);
const table = try toml.parseContents(allocator, contents, null);
defer table.deinit();
// TODO: Report unknown config options
if (table.keys.get("Host")) |host| {
if (host.Table.keys.get("win_scale")) |scale| state.host.win_scale = scale.Integer;
if (host.Table.keys.get("vsync")) |vsync| state.host.vsync = vsync.Boolean;
}
if (table.keys.get("Guest")) |guest| {
if (guest.Table.keys.get("audio_sync")) |sync| state.guest.audio_sync = sync.Boolean;
if (guest.Table.keys.get("video_sync")) |sync| state.guest.video_sync = sync.Boolean;
if (guest.Table.keys.get("force_rtc")) |forced| state.guest.force_rtc = forced.Boolean;
}
if (table.keys.get("Debug")) |debug| {
if (debug.Table.keys.get("cpu_trace")) |trace| state.debug.cpu_trace = trace.Boolean;
if (debug.Table.keys.get("unhandled_io")) |unhandled| state.debug.unhandled_io = unhandled.Boolean;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,142 +0,0 @@
const io = @import("../bus/io.zig");
const util = @import("../../util.zig");
const Scheduler = @import("../scheduler.zig").Scheduler;
const FrameSequencer = @import("../apu.zig").FrameSequencer;
const Tick = @import("../apu.zig").Apu.Tick;
const Envelope = @import("device/Envelope.zig");
const Length = @import("device/Length.zig");
const Lfsr = @import("signal/Lfsr.zig");
const Self = @This();
/// Write-only
/// NR41
len: u6,
/// NR42
envelope: io.Envelope,
/// NR43
poly: io.PolyCounter,
/// NR44
cnt: io.NoiseControl,
/// Length Functionarlity
len_dev: Length,
/// Envelope Functionality
env_dev: Envelope,
// Linear Feedback Shift Register
lfsr: Lfsr,
enabled: bool,
sample: i8,
pub fn init(sched: *Scheduler) Self {
return .{
.len = 0,
.envelope = .{ .raw = 0 },
.poly = .{ .raw = 0 },
.cnt = .{ .raw = 0 },
.enabled = false,
.len_dev = Length.create(),
.env_dev = Envelope.create(),
.lfsr = Lfsr.create(sched),
.sample = 0,
};
}
pub fn reset(self: *Self) void {
self.len = 0;
self.envelope.raw = 0;
self.poly.raw = 0;
self.cnt.raw = 0;
self.sample = 0;
self.enabled = false;
}
pub fn tick(self: *Self, comptime kind: Tick) void {
switch (kind) {
.Length => self.len_dev.tick(self.cnt.length_enable.read(), &self.enabled),
.Envelope => self.env_dev.tick(self.envelope),
.Sweep => @compileError("Channel 4 does not implement Sweep"),
}
}
/// NR41, NR42
pub fn sound4CntL(self: *const Self) u16 {
return @as(u16, self.envelope.raw) << 8;
}
/// NR41, NR42
pub fn setSound4CntL(self: *Self, value: u16) void {
self.setNr41(@truncate(u8, value));
self.setNr42(@truncate(u8, value >> 8));
}
/// NR41
pub fn setNr41(self: *Self, len: u8) void {
self.len = @truncate(u6, len);
self.len_dev.timer = @as(u7, 64) - @truncate(u6, len);
}
/// NR42
pub fn setNr42(self: *Self, value: u8) void {
self.envelope.raw = value;
if (!self.isDacEnabled()) self.enabled = false;
}
/// NR43, NR44
pub fn sound4CntH(self: *const Self) u16 {
return @as(u16, self.poly.raw & 0x40) << 8 | self.cnt.raw;
}
/// NR43, NR44
pub fn setSound4CntH(self: *Self, fs: *const FrameSequencer, value: u16) void {
self.poly.raw = @truncate(u8, value);
self.setNr44(fs, @truncate(u8, value >> 8));
}
/// NR44
pub fn setNr44(self: *Self, fs: *const FrameSequencer, byte: u8) void {
var new: io.NoiseControl = .{ .raw = byte };
if (new.trigger.read()) {
self.enabled = true;
if (self.len_dev.timer == 0) {
self.len_dev.timer =
if (!fs.isLengthNext() and new.length_enable.read()) 63 else 64;
}
// Update The Frequency Timer
self.lfsr.reload(self.poly);
self.lfsr.shift = 0x7FFF;
// Update Envelope and Volume
self.env_dev.timer = self.envelope.period.read();
if (fs.isEnvelopeNext() and self.env_dev.timer != 0b111) self.env_dev.timer += 1;
self.env_dev.vol = self.envelope.init_vol.read();
self.enabled = self.isDacEnabled();
}
util.audio.length.ch4.update(self, fs, new);
self.cnt = new;
}
pub fn onNoiseEvent(self: *Self, late: u64) void {
self.lfsr.onLfsrTimerExpire(self.poly, late);
self.sample = 0;
if (!self.isDacEnabled()) return;
self.sample = if (self.enabled) self.lfsr.sample() * @as(i8, self.env_dev.vol) else 0;
}
fn isDacEnabled(self: *const Self) bool {
return self.envelope.raw & 0xF8 != 0x00;
}

View File

@ -1,138 +0,0 @@
const io = @import("../bus/io.zig");
const util = @import("../../util.zig");
const Scheduler = @import("../scheduler.zig").Scheduler;
const FrameSequencer = @import("../apu.zig").FrameSequencer;
const Tick = @import("../apu.zig").Apu.Tick;
const Length = @import("device/Length.zig");
const Envelope = @import("device/Envelope.zig");
const Square = @import("signal/Square.zig");
const Self = @This();
/// NR21
duty: io.Duty,
/// NR22
envelope: io.Envelope,
/// NR23, NR24
freq: io.Frequency,
/// Length Functionarlity
len_dev: Length,
/// Envelope Functionality
env_dev: Envelope,
/// FrequencyTimer Functionality
square: Square,
enabled: bool,
sample: i8,
pub fn init(sched: *Scheduler) Self {
return .{
.duty = .{ .raw = 0 },
.envelope = .{ .raw = 0 },
.freq = .{ .raw = 0 },
.enabled = false,
.square = Square.init(sched),
.len_dev = Length.create(),
.env_dev = Envelope.create(),
.sample = 0,
};
}
pub fn reset(self: *Self) void {
self.duty.raw = 0;
self.envelope.raw = 0;
self.freq.raw = 0;
self.sample = 0;
self.enabled = false;
}
pub fn tick(self: *Self, comptime kind: Tick) void {
switch (kind) {
.Length => self.len_dev.tick(self.freq.length_enable.read(), &self.enabled),
.Envelope => self.env_dev.tick(self.envelope),
.Sweep => @compileError("Channel 2 does not implement Sweep"),
}
}
pub fn onToneEvent(self: *Self, late: u64) void {
self.square.onSquareTimerExpire(Self, self.freq, late);
self.sample = 0;
if (!self.isDacEnabled()) return;
self.sample = if (self.enabled) self.square.sample(self.duty) * @as(i8, self.env_dev.vol) else 0;
}
/// NR21, NR22
pub fn sound2CntL(self: *const Self) u16 {
return @as(u16, self.envelope.raw) << 8 | (self.duty.raw & 0xC0);
}
/// NR21, NR22
pub fn setSound2CntL(self: *Self, value: u16) void {
self.setNr21(@truncate(u8, value));
self.setNr22(@truncate(u8, value >> 8));
}
/// NR21
pub fn setNr21(self: *Self, value: u8) void {
self.duty.raw = value;
self.len_dev.timer = @as(u7, 64) - @truncate(u6, value);
}
/// NR22
pub fn setNr22(self: *Self, value: u8) void {
self.envelope.raw = value;
if (!self.isDacEnabled()) self.enabled = false;
}
/// NR23, NR24
pub fn sound2CntH(self: *const Self) u16 {
return self.freq.raw & 0x4000;
}
/// NR23, NR24
pub fn setSound2CntH(self: *Self, fs: *const FrameSequencer, value: u16) void {
self.setNr23(@truncate(u8, value));
self.setNr24(fs, @truncate(u8, value >> 8));
}
/// NR23
pub fn setNr23(self: *Self, byte: u8) void {
self.freq.raw = (self.freq.raw & 0xFF00) | byte;
}
/// NR24
pub fn setNr24(self: *Self, fs: *const FrameSequencer, byte: u8) void {
var new: io.Frequency = .{ .raw = (@as(u16, byte) << 8) | (self.freq.raw & 0xFF) };
if (new.trigger.read()) {
self.enabled = true;
if (self.len_dev.timer == 0) {
self.len_dev.timer =
if (!fs.isLengthNext() and new.length_enable.read()) 63 else 64;
}
self.square.reload(Self, self.freq.frequency.read());
// Reload Envelope period and timer
self.env_dev.timer = self.envelope.period.read();
if (fs.isEnvelopeNext() and self.env_dev.timer != 0b111) self.env_dev.timer += 1;
self.env_dev.vol = self.envelope.init_vol.read();
self.enabled = self.isDacEnabled();
}
util.audio.length.update(Self, self, fs, new);
self.freq = new;
}
fn isDacEnabled(self: *const Self) bool {
return self.envelope.raw & 0xF8 != 0;
}

View File

@ -1,184 +0,0 @@
const io = @import("../bus/io.zig");
const util = @import("../../util.zig");
const Scheduler = @import("../scheduler.zig").Scheduler;
const FrameSequencer = @import("../apu.zig").FrameSequencer;
const Length = @import("device/Length.zig");
const Envelope = @import("device/Envelope.zig");
const Sweep = @import("device/Sweep.zig");
const Square = @import("signal/Square.zig");
const Tick = @import("../apu.zig").Apu.Tick;
const Self = @This();
/// NR10
sweep: io.Sweep,
/// NR11
duty: io.Duty,
/// NR12
envelope: io.Envelope,
/// NR13, NR14
freq: io.Frequency,
/// Length Functionality
len_dev: Length,
/// Sweep Functionality
sweep_dev: Sweep,
/// Envelope Functionality
env_dev: Envelope,
/// Frequency Timer Functionality
square: Square,
enabled: bool,
sample: i8,
pub fn init(sched: *Scheduler) Self {
return .{
.sweep = .{ .raw = 0 },
.duty = .{ .raw = 0 },
.envelope = .{ .raw = 0 },
.freq = .{ .raw = 0 },
.sample = 0,
.enabled = false,
.square = Square.init(sched),
.len_dev = Length.create(),
.sweep_dev = Sweep.create(),
.env_dev = Envelope.create(),
};
}
pub 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;
self.sample = 0;
self.enabled = false;
}
pub fn tick(self: *Self, comptime kind: Tick) void {
switch (kind) {
.Length => self.len_dev.tick(self.freq.length_enable.read(), &self.enabled),
.Envelope => self.env_dev.tick(self.envelope),
.Sweep => self.sweep_dev.tick(self),
}
}
pub fn onToneSweepEvent(self: *Self, late: u64) void {
self.square.onSquareTimerExpire(Self, self.freq, late);
self.sample = 0;
if (!self.isDacEnabled()) return;
self.sample = if (self.enabled) self.square.sample(self.duty) * @as(i8, self.env_dev.vol) else 0;
}
/// NR10, NR11, NR12
pub fn setSound1Cnt(self: *Self, value: u32) void {
self.setSound1CntL(@truncate(u8, value));
self.setSound1CntH(@truncate(u16, value >> 16));
}
/// NR10
pub fn sound1CntL(self: *const Self) u8 {
return self.sweep.raw & 0x7F;
}
/// NR10
pub fn setSound1CntL(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 sound1CntH(self: *const Self) u16 {
return @as(u16, self.envelope.raw) << 8 | (self.duty.raw & 0xC0);
}
/// NR11, NR12
pub fn setSound1CntH(self: *Self, value: u16) void {
self.setNr11(@truncate(u8, value));
self.setNr12(@truncate(u8, value >> 8));
}
/// NR11
pub fn setNr11(self: *Self, value: u8) void {
self.duty.raw = value;
self.len_dev.timer = @as(u7, 64) - @truncate(u6, value);
}
/// NR12
pub fn setNr12(self: *Self, value: u8) void {
self.envelope.raw = value;
if (!self.isDacEnabled()) self.enabled = false;
}
/// NR13, NR14
pub fn sound1CntX(self: *const Self) u16 {
return self.freq.raw & 0x4000;
}
/// NR13, NR14
pub fn setSound1CntX(self: *Self, fs: *const FrameSequencer, value: u16) void {
self.setNr13(@truncate(u8, value));
self.setNr14(fs, @truncate(u8, value >> 8));
}
/// NR13
pub fn setNr13(self: *Self, byte: u8) void {
self.freq.raw = (self.freq.raw & 0xFF00) | byte;
}
/// NR14
pub fn setNr14(self: *Self, fs: *const FrameSequencer, byte: u8) void {
var new: io.Frequency = .{ .raw = (@as(u16, byte) << 8) | (self.freq.raw & 0xFF) };
if (new.trigger.read()) {
self.enabled = true;
if (self.len_dev.timer == 0) {
self.len_dev.timer =
if (!fs.isLengthNext() and new.length_enable.read()) 63 else 64;
}
self.square.reload(Self, self.freq.frequency.read());
// Reload Envelope period and timer
self.env_dev.timer = self.envelope.period.read();
if (fs.isEnvelopeNext() and self.env_dev.timer != 0b111) self.env_dev.timer += 1;
self.env_dev.vol = self.envelope.init_vol.read();
// Sweep Trigger Behaviour
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;
if (sw_shift != 0) _ = self.sweep_dev.calculate(self.sweep, &self.enabled);
self.enabled = self.isDacEnabled();
}
util.audio.length.update(Self, self, fs, new);
self.freq = new;
}
fn isDacEnabled(self: *const Self) bool {
return self.envelope.raw & 0xF8 != 0;
}

View File

@ -1,132 +0,0 @@
const io = @import("../bus/io.zig");
const util = @import("../../util.zig");
const Scheduler = @import("../scheduler.zig").Scheduler;
const FrameSequencer = @import("../apu.zig").FrameSequencer;
const Tick = @import("../apu.zig").Apu.Tick;
const Length = @import("device/Length.zig");
const Wave = @import("signal/Wave.zig");
const Self = @This();
/// Write-only
/// NR30
select: io.WaveSelect,
/// NR31
length: u8,
/// NR32
vol: io.WaveVolume,
/// NR33, NR34
freq: io.Frequency,
/// Length Functionarlity
len_dev: Length,
wave_dev: Wave,
enabled: bool,
sample: i8,
pub fn init(sched: *Scheduler) Self {
return .{
.select = .{ .raw = 0 },
.vol = .{ .raw = 0 },
.freq = .{ .raw = 0 },
.length = 0,
.len_dev = Length.create(),
.wave_dev = Wave.init(sched),
.enabled = false,
.sample = 0,
};
}
pub fn reset(self: *Self) void {
self.select.raw = 0;
self.length = 0;
self.vol.raw = 0;
self.freq.raw = 0;
self.sample = 0;
self.enabled = false;
}
pub fn tick(self: *Self, comptime kind: Tick) void {
switch (kind) {
.Length => self.len_dev.tick(self.freq.length_enable.read(), &self.enabled),
.Envelope => @compileError("Channel 3 does not implement Envelope"),
.Sweep => @compileError("Channel 3 does not implement Sweep"),
}
}
/// NR30, NR31, NR32
pub fn setSound3Cnt(self: *Self, value: u32) void {
self.setSound3CntL(@truncate(u8, value));
self.setSound3CntH(@truncate(u16, value >> 16));
}
/// NR30
pub fn setSound3CntL(self: *Self, value: u8) void {
self.select.raw = value;
if (!self.select.enabled.read()) self.enabled = false;
}
/// NR31, NR32
pub fn sound3CntH(self: *const Self) u16 {
return @as(u16, self.length & 0xE0) << 8;
}
/// NR31, NR32
pub fn setSound3CntH(self: *Self, value: u16) void {
self.setNr31(@truncate(u8, value));
self.vol.raw = (@truncate(u8, value >> 8));
}
/// NR31
pub fn setNr31(self: *Self, len: u8) void {
self.length = len;
self.len_dev.timer = 256 - @as(u9, len);
}
/// NR33, NR34
pub fn setSound3CntX(self: *Self, fs: *const FrameSequencer, value: u16) void {
self.setNr33(@truncate(u8, value));
self.setNr34(fs, @truncate(u8, value >> 8));
}
/// NR33
pub fn setNr33(self: *Self, byte: u8) void {
self.freq.raw = (self.freq.raw & 0xFF00) | byte;
}
/// NR34
pub fn setNr34(self: *Self, fs: *const FrameSequencer, byte: u8) void {
var new: io.Frequency = .{ .raw = (@as(u16, byte) << 8) | (self.freq.raw & 0xFF) };
if (new.trigger.read()) {
self.enabled = true;
if (self.len_dev.timer == 0) {
self.len_dev.timer =
if (!fs.isLengthNext() and new.length_enable.read()) 255 else 256;
}
// Update The Frequency Timer
self.wave_dev.reload(self.freq.frequency.read());
self.wave_dev.offset = 0;
self.enabled = self.select.enabled.read();
}
util.audio.length.update(Self, self, fs, new);
self.freq = new;
}
pub fn onWaveEvent(self: *Self, late: u64) void {
self.wave_dev.onWaveTimerExpire(self.freq, self.select, late);
self.sample = 0;
if (!self.select.enabled.read()) return;
// Convert unsigned 4-bit wave sample to signed 8-bit sample
self.sample = (2 * @as(i8, self.wave_dev.sample(self.select)) - 15) >> self.wave_dev.shift(self.vol);
}

View File

@ -1,28 +0,0 @@
const io = @import("../../bus/io.zig");
const Self = @This();
/// Period Timer
timer: u3,
/// Current Volume
vol: u4,
pub fn create() Self {
return .{ .timer = 0, .vol = 0 };
}
pub fn tick(self: *Self, nrx2: io.Envelope) void {
if (nrx2.period.read() != 0) {
if (self.timer != 0) self.timer -= 1;
if (self.timer == 0) {
self.timer = nrx2.period.read();
if (nrx2.direction.read()) {
if (self.vol < 0xF) self.vol += 1;
} else {
if (self.vol > 0x0) self.vol -= 1;
}
}
}
}

View File

@ -1,18 +0,0 @@
const Self = @This();
timer: u9,
pub fn create() Self {
return .{ .timer = 0 };
}
pub fn tick(self: *Self, enabled: bool, ch_enable: *bool) void {
if (enabled) {
if (self.timer == 0) return;
self.timer -= 1;
// By returning early if timer == 0, this is only
// true if timer == 0 because of the decrement we just did
if (self.timer == 0) ch_enable.* = false;
}
}

View File

@ -1,52 +0,0 @@
const io = @import("../../bus/io.zig");
const ToneSweep = @import("../ToneSweep.zig");
const Self = @This();
timer: u8,
enabled: bool,
shadow: u11,
calc_performed: bool,
pub fn create() Self {
return .{
.timer = 0,
.enabled = false,
.shadow = 0,
.calc_performed = false,
};
}
pub fn tick(self: *Self, ch1: *ToneSweep) void {
if (self.timer != 0) self.timer -= 1;
if (self.timer == 0) {
const period = ch1.sweep.period.read();
self.timer = if (period == 0) 8 else period;
if (!self.calc_performed) self.calc_performed = true;
if (self.enabled and period != 0) {
const new_freq = self.calculate(ch1.sweep, &ch1.enabled);
if (new_freq <= 0x7FF and ch1.sweep.shift.read() != 0) {
ch1.freq.frequency.write(@truncate(u11, new_freq));
self.shadow = @truncate(u11, new_freq);
_ = self.calculate(ch1.sweep, &ch1.enabled);
}
}
}
}
/// Calculates the Sweep Frequency
pub fn calculate(self: *Self, sweep: io.Sweep, ch_enable: *bool) u12 {
const shadow = @as(u12, self.shadow);
const shadow_shifted = shadow >> sweep.shift.read();
const decrease = sweep.direction.read();
const freq = if (decrease) shadow - shadow_shifted else shadow + shadow_shifted;
if (freq > 0x7FF) ch_enable.* = false;
return freq;
}

View File

@ -1,59 +0,0 @@
const io = @import("../../bus/io.zig");
/// Linear Feedback Shift Register
const Scheduler = @import("../../scheduler.zig").Scheduler;
const FrameSequencer = @import("../../apu.zig").FrameSequencer;
const Noise = @import("../Noise.zig");
const Self = @This();
pub const interval: u64 = (1 << 24) / (1 << 22);
shift: u15,
timer: u16,
sched: *Scheduler,
pub fn create(sched: *Scheduler) Self {
return .{
.shift = 0,
.timer = 0,
.sched = sched,
};
}
pub fn sample(self: *const Self) i8 {
return if ((~self.shift & 1) == 1) 1 else -1;
}
/// Reload LFSR Timer
pub fn reload(self: *Self, poly: io.PolyCounter) void {
self.sched.removeScheduledEvent(.{ .ApuChannel = 3 });
const div = Self.divisor(poly.div_ratio.read());
const timer = div << poly.shift.read();
self.sched.push(.{ .ApuChannel = 3 }, @as(u64, timer) * interval);
}
/// Scheduler Event Handler for LFSR Timer Expire
/// FIXME: This gets called a lot, clogging up the Scheduler
pub fn onLfsrTimerExpire(self: *Self, poly: io.PolyCounter, late: u64) void {
// Obscure: "Using a noise channel clock shift of 14 or 15
// results in the LFSR receiving no clocks."
if (poly.shift.read() >= 14) return;
const div = Self.divisor(poly.div_ratio.read());
const timer = div << poly.shift.read();
const tmp = (self.shift & 1) ^ ((self.shift & 2) >> 1);
self.shift = (self.shift >> 1) | (tmp << 14);
if (poly.width.read())
self.shift = (self.shift & ~@as(u15, 0x40)) | tmp << 6;
self.sched.push(.{ .ApuChannel = 3 }, @as(u64, timer) * interval -| late);
}
fn divisor(code: u3) u16 {
if (code == 0) return 8;
return @as(u16, code) << 4;
}

View File

@ -1,58 +0,0 @@
const std = @import("std");
const io = @import("../../bus/io.zig");
const Scheduler = @import("../../scheduler.zig").Scheduler;
const FrameSequencer = @import("../../apu.zig").FrameSequencer;
const ToneSweep = @import("../ToneSweep.zig");
const Tone = @import("../Tone.zig");
const Self = @This();
pub const interval: u64 = (1 << 24) / (1 << 22);
pos: u3,
sched: *Scheduler,
timer: u16,
pub fn init(sched: *Scheduler) Self {
return .{
.timer = 0,
.pos = 0,
.sched = sched,
};
}
/// Scheduler Event Handler for Square Synth Timer Expire
pub fn onSquareTimerExpire(self: *Self, comptime T: type, nrx34: io.Frequency, late: u64) void {
comptime std.debug.assert(T == ToneSweep or T == Tone);
self.pos +%= 1;
self.timer = (@as(u16, 2048) - nrx34.frequency.read()) * 4;
self.sched.push(.{ .ApuChannel = if (T == ToneSweep) 0 else 1 }, @as(u64, self.timer) * interval -| late);
}
/// Reload Square Wave Timer
pub fn reload(self: *Self, comptime T: type, value: u11) void {
comptime std.debug.assert(T == ToneSweep or T == Tone);
const channel = if (T == ToneSweep) 0 else 1;
self.sched.removeScheduledEvent(.{ .ApuChannel = channel });
const tmp = (@as(u16, 2048) - value) * 4; // What Freq Timer should be assuming no weird behaviour
self.timer = (tmp & ~@as(u16, 0x3)) | self.timer & 0x3; // Keep the last two bits from the old timer;
self.sched.push(.{ .ApuChannel = channel }, @as(u64, self.timer) * interval);
}
pub fn sample(self: *const Self, nrx1: io.Duty) i8 {
const pattern = nrx1.pattern.read();
const i = self.pos ^ 7; // index of 0 should get highest bit
const result = switch (pattern) {
0b00 => @as(u8, 0b00000001) >> i, // 12.5%
0b01 => @as(u8, 0b00000011) >> i, // 25%
0b10 => @as(u8, 0b00001111) >> i, // 50%
0b11 => @as(u8, 0b11111100) >> i, // 75%
};
return if (result & 1 == 1) 1 else -1;
}

View File

@ -1,79 +0,0 @@
const std = @import("std");
const io = @import("../../bus/io.zig");
const Scheduler = @import("../../scheduler.zig").Scheduler;
const FrameSequencer = @import("../../apu.zig").FrameSequencer;
const Wave = @import("../Wave.zig");
const buf_len = 0x20;
pub const interval: u64 = (1 << 24) / (1 << 22);
const Self = @This();
buf: [buf_len]u8,
timer: u16,
offset: u12,
sched: *Scheduler,
pub fn read(self: *const Self, comptime T: type, nr30: io.WaveSelect, addr: u32) T {
// TODO: Handle reads when Channel 3 is disabled
const base = if (!nr30.bank.read()) @as(u32, 0x10) else 0; // Read from the Opposite Bank in Use
const i = base + addr - 0x0400_0090;
return std.mem.readIntSliceLittle(T, self.buf[i..][0..@sizeOf(T)]);
}
pub fn write(self: *Self, comptime T: type, nr30: io.WaveSelect, addr: u32, value: T) void {
// TODO: Handle writes when Channel 3 is disabled
const base = if (!nr30.bank.read()) @as(u32, 0x10) else 0; // Write to the Opposite Bank in Use
const i = base + addr - 0x0400_0090;
std.mem.writeIntSliceLittle(T, self.buf[i..][0..@sizeOf(T)], value);
}
pub fn init(sched: *Scheduler) Self {
return .{
.buf = [_]u8{0x00} ** buf_len,
.timer = 0,
.offset = 0,
.sched = sched,
};
}
/// Reload internal Wave Timer
pub fn reload(self: *Self, value: u11) void {
self.sched.removeScheduledEvent(.{ .ApuChannel = 2 });
self.timer = (@as(u16, 2048) - value) * 2;
self.sched.push(.{ .ApuChannel = 2 }, @as(u64, self.timer) * interval);
}
/// Scheduler Event Handler
pub fn onWaveTimerExpire(self: *Self, nrx34: io.Frequency, nr30: io.WaveSelect, late: u64) void {
if (nr30.dimension.read()) {
self.offset = (self.offset + 1) % 0x40; // 0x20 bytes (both banks), which contain 2 samples each
} else {
self.offset = (self.offset + 1) % 0x20; // 0x10 bytes, which contain 2 samples each
}
self.timer = (@as(u16, 2048) - nrx34.frequency.read()) * 2;
self.sched.push(.{ .ApuChannel = 2 }, @as(u64, self.timer) * interval -| late);
}
/// Generate Sample from Wave Synth
pub fn sample(self: *const Self, nr30: io.WaveSelect) u4 {
const base = if (nr30.bank.read()) @as(u32, 0x10) else 0;
const value = self.buf[base + self.offset / 2];
return if (self.offset & 1 == 0) @truncate(u4, value >> 4) else @truncate(u4, value);
}
/// TODO: Write comment
pub fn shift(_: *const Self, nr32: io.WaveVolume) u2 {
return switch (nr32.kind.read()) {
0b00 => 3, // Mute / Zero
0b01 => 0, // 100% Volume
0b10 => 1, // 50% Volume
0b11 => 2, // 25% Volume
};
}

View File

@ -12,36 +12,6 @@ allocator: Allocator,
addr_latch: u32, addr_latch: u32,
pub fn read(self: *Self, comptime T: type, r15: u32, addr: u32) T {
if (r15 < Self.size) {
self.addr_latch = addr;
return self._read(T, addr);
}
log.debug("Rejected read since r15=0x{X:0>8}", .{r15});
return @truncate(T, self._read(T, self.addr_latch + 8));
}
pub fn dbgRead(self: *const Self, comptime T: type, r15: u32, addr: u32) T {
if (r15 < Self.size) return self._read(T, addr);
return @truncate(T, self._read(T, self.addr_latch + 8));
}
/// Read without the GBA safety checks
fn _read(self: *const Self, comptime T: type, addr: u32) T {
const buf = self.buf orelse std.debug.panic("[BIOS] ZBA tried to read {} from 0x{X:0>8} but not BIOS was present", .{ T, addr });
return switch (T) {
u32, u16, u8 => std.mem.readIntSliceLittle(T, buf[addr..][0..@sizeOf(T)]),
else => @compileError("BIOS: Unsupported read width"),
};
}
pub fn write(_: *Self, comptime T: type, addr: u32, value: T) void {
@setCold(true);
log.debug("Tried to write {} 0x{X:} to 0x{X:0>8} ", .{ T, value, addr });
}
pub fn init(allocator: Allocator, maybe_path: ?[]const u8) !Self { pub fn init(allocator: Allocator, maybe_path: ?[]const u8) !Self {
const buf: ?[]u8 = if (maybe_path) |path| blk: { const buf: ?[]u8 = if (maybe_path) |path| blk: {
const file = try std.fs.cwd().openFile(path, .{}); const file = try std.fs.cwd().openFile(path, .{});
@ -61,3 +31,34 @@ pub fn deinit(self: *Self) void {
if (self.buf) |buf| self.allocator.free(buf); if (self.buf) |buf| self.allocator.free(buf);
self.* = undefined; self.* = undefined;
} }
pub fn read(self: *Self, comptime T: type, r15: u32, addr: u32) T {
if (r15 < Self.size) {
self.addr_latch = addr;
return self.uncheckedRead(T, addr);
}
log.debug("Rejected read since r15=0x{X:0>8}", .{r15});
return @truncate(T, self.uncheckedRead(T, self.addr_latch));
}
pub fn dbgRead(self: *const Self, comptime T: type, r15: u32, addr: u32) T {
if (r15 < Self.size) return self.uncheckedRead(T, addr);
return @truncate(T, self.uncheckedRead(T, self.addr_latch));
}
fn uncheckedRead(self: *const Self, comptime T: type, addr: u32) T {
if (self.buf) |buf| {
return switch (T) {
u32, u16, u8 => std.mem.readIntSliceLittle(T, buf[addr..][0..@sizeOf(T)]),
else => @compileError("BIOS: Unsupported read width"),
};
}
std.debug.panic("[BIOS] ZBA tried to read {} from 0x{X:0>8} but not BIOS was present", .{ T, addr });
}
pub fn write(_: *Self, comptime T: type, addr: u32, value: T) void {
@setCold(true);
log.debug("Tried to write {} 0x{X:} to 0x{X:0>8} ", .{ T, value, addr });
}

View File

@ -7,6 +7,21 @@ const Self = @This();
buf: []u8, buf: []u8,
allocator: Allocator, allocator: Allocator,
pub fn init(allocator: Allocator) !Self {
const buf = try allocator.alloc(u8, ewram_size);
std.mem.set(u8, buf, 0);
return Self{
.buf = buf,
.allocator = allocator,
};
}
pub fn deinit(self: *Self) void {
self.allocator.free(self.buf);
self.* = undefined;
}
pub fn read(self: *const Self, comptime T: type, address: usize) T { pub fn read(self: *const Self, comptime T: type, address: usize) T {
const addr = address & 0x3FFFF; const addr = address & 0x3FFFF;
@ -24,18 +39,3 @@ pub fn write(self: *const Self, comptime T: type, address: usize, value: T) void
else => @compileError("EWRAM: Unsupported write width"), else => @compileError("EWRAM: Unsupported write width"),
}; };
} }
pub fn init(allocator: Allocator) !Self {
const buf = try allocator.alloc(u8, ewram_size);
std.mem.set(u8, buf, 0);
return Self{
.buf = buf,
.allocator = allocator,
};
}
pub fn deinit(self: *Self) void {
self.allocator.free(self.buf);
self.* = undefined;
}

View File

@ -1,6 +1,4 @@
const std = @import("std"); const std = @import("std");
const config = @import("../../config.zig");
const Bit = @import("bitfield").Bit; const Bit = @import("bitfield").Bit;
const Bitfield = @import("bitfield").Bitfield; const Bitfield = @import("bitfield").Bitfield;
const DateTime = @import("datetime").datetime.Datetime; const DateTime = @import("datetime").datetime.Datetime;
@ -10,6 +8,7 @@ const Backup = @import("backup.zig").Backup;
const Gpio = @import("gpio.zig").Gpio; const Gpio = @import("gpio.zig").Gpio;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const force_rtc = @import("../emu.zig").force_rtc;
const log = std.log.scoped(.GamePak); const log = std.log.scoped(.GamePak);
const Self = @This(); const Self = @This();
@ -20,11 +19,78 @@ allocator: Allocator,
backup: Backup, backup: Backup,
gpio: *Gpio, gpio: *Gpio,
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, rom_path: []const u8, save_path: ?[]const u8) !Self {
const file = try std.fs.cwd().openFile(rom_path, .{});
defer file.close();
const file_buf = try file.readToEndAlloc(allocator, try file.getEndPos());
const title = file_buf[0xA0..0xAC].*;
const kind = Backup.guessKind(file_buf);
const device = if (force_rtc) .Rtc else guessDevice(file_buf);
logHeader(file_buf, &title);
return .{
.buf = file_buf,
.allocator = allocator,
.title = title,
.backup = try Backup.init(allocator, kind, title, save_path),
.gpio = try Gpio.init(allocator, cpu, device),
};
}
/// Searches the ROM to see if it can determine whether the ROM it's searching uses
/// any GPIO device, like a RTC for example.
fn guessDevice(buf: []const u8) Gpio.Device.Kind {
// Try to Guess if ROM uses RTC
const needle = "RTC_V"; // I was told SIIRTC_V, though Pokemen Firered (USA) is a false negative
var i: usize = 0;
while ((i + needle.len) < buf.len) : (i += 1) {
if (std.mem.eql(u8, needle, buf[i..(i + needle.len)])) return .Rtc;
}
// TODO: Detect other GPIO devices
return .None;
}
fn logHeader(buf: []const u8, title: *const [12]u8) void {
const code = buf[0xAC..0xB0];
const maker = buf[0xB0..0xB2];
const version = buf[0xBC];
log.info("Title: {s}", .{title});
if (version != 0) log.info("Version: {}", .{version});
log.info("Game Code: {s}", .{code});
if (lookupMaker(maker)) |c| log.info("Maker: {s}", .{c}) else log.info("Maker Code: {s}", .{maker});
}
fn lookupMaker(slice: *const [2]u8) ?[]const u8 {
const id = @as(u16, slice[1]) << 8 | @as(u16, slice[0]);
return switch (id) {
0x3130 => "Nintendo",
else => null,
};
}
inline fn isLarge(self: *const Self) bool {
return self.buf.len > 0x100_0000;
}
pub fn deinit(self: *Self) void {
self.backup.deinit();
self.gpio.deinit(self.allocator);
self.allocator.destroy(self.gpio);
self.allocator.free(self.buf);
self.* = undefined;
}
pub fn read(self: *Self, comptime T: type, address: u32) T { pub fn read(self: *Self, comptime T: type, address: u32) T {
const addr = address & 0x1FF_FFFF; const addr = address & 0x1FF_FFFF;
if (self.backup.kind == .Eeprom) { if (self.backup.kind == .Eeprom) {
if (self.buf.len > 0x100_0000) { // Large if (self.isLarge()) {
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if // Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
// * Backup type is EEPROM // * Backup type is EEPROM
// * Large ROM (Size is greater than 16MB) // * Large ROM (Size is greater than 16MB)
@ -76,19 +142,11 @@ pub fn read(self: *Self, comptime T: type, address: u32) T {
}; };
} }
inline fn get(self: *const Self, i: u32) u8 {
@setRuntimeSafety(false);
if (i < self.buf.len) return self.buf[i];
const lhs = i >> 1 & 0xFFFF;
return @truncate(u8, lhs >> 8 * @truncate(u5, i & 1));
}
pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T { pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
const addr = address & 0x1FF_FFFF; const addr = address & 0x1FF_FFFF;
if (self.backup.kind == .Eeprom) { if (self.backup.kind == .Eeprom) {
if (self.buf.len > 0x100_0000) { // Large if (self.isLarge()) {
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if // Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
// * Backup type is EEPROM // * Backup type is EEPROM
// * Large ROM (Size is greater than 16MB) // * Large ROM (Size is greater than 16MB)
@ -103,35 +161,6 @@ pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
} }
} }
if (self.gpio.cnt == 1) {
// GPIO Can be read from
// We assume that this will only be true when a ROM actually does want something from GPIO
switch (T) {
u32 => switch (address) {
// TODO: Do I even need to implement these?
0x0800_00C4 => std.debug.panic("Handle 32-bit GPIO Data/Direction Reads", .{}),
0x0800_00C6 => std.debug.panic("Handle 32-bit GPIO Direction/Control Reads", .{}),
0x0800_00C8 => std.debug.panic("Handle 32-bit GPIO Control Reads", .{}),
else => {},
},
u16 => switch (address) {
// FIXME: What do 16-bit GPIO Reads look like?
0x0800_00C4 => return self.gpio.read(.Data),
0x0800_00C6 => return self.gpio.read(.Direction),
0x0800_00C8 => return self.gpio.read(.Control),
else => {},
},
u8 => switch (address) {
0x0800_00C4 => return self.gpio.read(.Data),
0x0800_00C6 => return self.gpio.read(.Direction),
0x0800_00C8 => return self.gpio.read(.Control),
else => {},
},
else => @compileError("GamePak[GPIO]: Unsupported read width"),
}
}
return switch (T) { return switch (T) {
u32 => (@as(T, self.get(addr + 3)) << 24) | (@as(T, self.get(addr + 2)) << 16) | (@as(T, self.get(addr + 1)) << 8) | (@as(T, self.get(addr))), u32 => (@as(T, self.get(addr + 3)) << 24) | (@as(T, self.get(addr + 2)) << 16) | (@as(T, self.get(addr + 1)) << 8) | (@as(T, self.get(addr))),
u16 => (@as(T, self.get(addr + 1)) << 8) | @as(T, self.get(addr)), u16 => (@as(T, self.get(addr + 1)) << 8) | @as(T, self.get(addr)),
@ -146,7 +175,7 @@ pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value
if (self.backup.kind == .Eeprom) { if (self.backup.kind == .Eeprom) {
const bit = @truncate(u1, value); const bit = @truncate(u1, value);
if (self.buf.len > 0x100_0000) { // Large if (self.isLarge()) {
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if // Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
// * Backup type is EEPROM // * Backup type is EEPROM
// * Large ROM (Size is greater than 16MB) // * Large ROM (Size is greater than 16MB)
@ -184,59 +213,12 @@ pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value
} }
} }
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, rom_path: []const u8, save_path: ?[]const u8) !Self { fn get(self: *const Self, i: u32) u8 {
const file = try std.fs.cwd().openFile(rom_path, .{}); @setRuntimeSafety(false);
defer file.close(); if (i < self.buf.len) return self.buf[i];
const file_buf = try file.readToEndAlloc(allocator, try file.getEndPos()); const lhs = i >> 1 & 0xFFFF;
const title = file_buf[0xA0..0xAC].*; return @truncate(u8, lhs >> 8 * @truncate(u5, i & 1));
const kind = Backup.guess(file_buf);
const device = if (config.config().guest.force_rtc) .Rtc else guessDevice(file_buf);
logHeader(file_buf, &title);
return .{
.buf = file_buf,
.allocator = allocator,
.title = title,
.backup = try Backup.init(allocator, kind, title, save_path),
.gpio = try Gpio.init(allocator, cpu, device),
};
}
pub fn deinit(self: *Self) void {
self.backup.deinit();
self.gpio.deinit(self.allocator);
self.allocator.destroy(self.gpio);
self.allocator.free(self.buf);
self.* = undefined;
}
/// Searches the ROM to see if it can determine whether the ROM it's searching uses
/// any GPIO device, like a RTC for example.
fn guessDevice(buf: []const u8) Gpio.Device.Kind {
// Try to Guess if ROM uses RTC
const needle = "RTC_V"; // I was told SIIRTC_V, though Pokemen Firered (USA) is a false negative
var i: usize = 0;
while ((i + needle.len) < buf.len) : (i += 1) {
if (std.mem.eql(u8, needle, buf[i..(i + needle.len)])) return .Rtc;
}
// TODO: Detect other GPIO devices
return .None;
}
fn logHeader(buf: []const u8, title: *const [12]u8) void {
const code = buf[0xAC..0xB0];
const maker = buf[0xB0..0xB2];
const version = buf[0xBC];
log.info("Title: {s}", .{title});
if (version != 0) log.info("Version: {}", .{version});
log.info("Game Code: {s}", .{code});
log.info("Maker Code: {s}", .{maker});
} }
test "OOB Access" { test "OOB Access" {

View File

@ -7,6 +7,21 @@ const Self = @This();
buf: []u8, buf: []u8,
allocator: Allocator, allocator: Allocator,
pub fn init(allocator: Allocator) !Self {
const buf = try allocator.alloc(u8, iwram_size);
std.mem.set(u8, buf, 0);
return Self{
.buf = buf,
.allocator = allocator,
};
}
pub fn deinit(self: *Self) void {
self.allocator.free(self.buf);
self.* = undefined;
}
pub fn read(self: *const Self, comptime T: type, address: usize) T { pub fn read(self: *const Self, comptime T: type, address: usize) T {
const addr = address & 0x7FFF; const addr = address & 0x7FFF;
@ -24,18 +39,3 @@ pub fn write(self: *const Self, comptime T: type, address: usize, value: T) void
else => @compileError("IWRAM: Unsupported write width"), else => @compileError("IWRAM: Unsupported write width"),
}; };
} }
pub fn init(allocator: Allocator) !Self {
const buf = try allocator.alloc(u8, iwram_size);
std.mem.set(u8, buf, 0);
return Self{
.buf = buf,
.allocator = allocator,
};
}
pub fn deinit(self: *Self) void {
self.allocator.free(self.buf);
self.* = undefined;
}

View File

@ -2,13 +2,9 @@ const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const log = std.log.scoped(.Backup); const log = std.log.scoped(.Backup);
const Eeprom = @import("backup/eeprom.zig").Eeprom;
const Flash = @import("backup/Flash.zig");
const escape = @import("../../util.zig").escape; const escape = @import("../../util.zig").escape;
const span = @import("../../util.zig").span; const span = @import("../../util.zig").span;
const Needle = struct { str: []const u8, kind: Backup.Kind };
const backup_kinds = [6]Needle{ const backup_kinds = [6]Needle{
.{ .str = "EEPROM_V", .kind = .Eeprom }, .{ .str = "EEPROM_V", .kind = .Eeprom },
.{ .str = "SRAM_V", .kind = .Sram }, .{ .str = "SRAM_V", .kind = .Sram },
@ -18,8 +14,6 @@ const backup_kinds = [6]Needle{
.{ .str = "FLASH1M_V", .kind = .Flash1M }, .{ .str = "FLASH1M_V", .kind = .Flash1M },
}; };
const SaveError = error{Unsupported};
pub const Backup = struct { pub const Backup = struct {
const Self = @This(); const Self = @This();
@ -41,65 +35,6 @@ pub const Backup = struct {
None, None,
}; };
pub fn read(self: *const Self, address: usize) u8 {
const addr = address & 0xFFFF;
switch (self.kind) {
.Flash => {
switch (addr) {
0x0000 => if (self.flash.id_mode) return 0x32, // Panasonic manufacturer ID
0x0001 => if (self.flash.id_mode) return 0x1B, // Panasonic device ID
else => {},
}
return self.flash.read(self.buf, addr);
},
.Flash1M => {
switch (addr) {
0x0000 => if (self.flash.id_mode) return 0x62, // Sanyo manufacturer ID
0x0001 => if (self.flash.id_mode) return 0x13, // Sanyo device ID
else => {},
}
return self.flash.read(self.buf, addr);
},
.Sram => return self.buf[addr & 0x7FFF], // 32K SRAM chip is mirrored
.None, .Eeprom => return 0xFF,
}
}
pub fn write(self: *Self, address: usize, byte: u8) void {
const addr = address & 0xFFFF;
switch (self.kind) {
.Flash, .Flash1M => {
if (self.flash.prep_write) return self.flash.write(self.buf, addr, byte);
if (self.flash.shouldEraseSector(addr, byte)) return self.flash.erase(self.buf, addr);
switch (addr) {
0x0000 => if (self.kind == .Flash1M and self.flash.set_bank) {
self.flash.bank = @truncate(u1, byte);
},
0x5555 => {
if (self.flash.state == .Command) {
self.flash.handleCommand(self.buf, byte);
} else if (byte == 0xAA and self.flash.state == .Ready) {
self.flash.state = .Set;
} else if (byte == 0xF0) {
self.flash.state = .Ready;
}
},
0x2AAA => if (byte == 0x55 and self.flash.state == .Set) {
self.flash.state = .Command;
},
else => {},
}
},
.Sram => self.buf[addr & 0x7FFF] = byte,
.None, .Eeprom => {},
}
}
pub fn init(allocator: Allocator, kind: Kind, title: [12]u8, path: ?[]const u8) !Self { pub fn init(allocator: Allocator, kind: Kind, title: [12]u8, path: ?[]const u8) !Self {
log.info("Kind: {}", .{kind}); log.info("Kind: {}", .{kind});
@ -119,22 +54,15 @@ pub const Backup = struct {
.kind = kind, .kind = kind,
.title = title, .title = title,
.save_path = path, .save_path = path,
.flash = Flash.create(), .flash = Flash.init(),
.eeprom = Eeprom.create(allocator), .eeprom = Eeprom.init(allocator),
}; };
if (backup.save_path) |p| backup.readSave(allocator, p) catch |e| log.err("Failed to load save: {}", .{e}); if (backup.save_path) |p| backup.loadSaveFromDisk(allocator, p) catch |e| log.err("Failed to load save: {}", .{e});
return backup; return backup;
} }
pub fn deinit(self: *Self) void { pub fn guessKind(rom: []const u8) Kind {
if (self.save_path) |path| self.writeSave(self.allocator, path) catch |e| log.err("Failed to write save: {}", .{e});
self.allocator.free(self.buf);
self.* = undefined;
}
/// Guesses the Backup Kind of a GBA ROM
pub fn guess(rom: []const u8) Kind {
for (backup_kinds) |needle| { for (backup_kinds) |needle| {
const needle_len = needle.str.len; const needle_len = needle.str.len;
@ -147,8 +75,14 @@ pub const Backup = struct {
return .None; return .None;
} }
fn readSave(self: *Self, allocator: Allocator, path: []const u8) !void { pub fn deinit(self: *Self) void {
const file_path = try self.savePath(allocator, path); if (self.save_path) |path| self.writeSaveToDisk(self.allocator, path) catch |e| log.err("Failed to write save: {}", .{e});
self.allocator.free(self.buf);
self.* = undefined;
}
fn loadSaveFromDisk(self: *Self, allocator: Allocator, path: []const u8) !void {
const file_path = try self.getSaveFilePath(allocator, path);
defer allocator.free(file_path); defer allocator.free(file_path);
// FIXME: Don't rely on this lol // FIXME: Don't rely on this lol
@ -183,26 +117,26 @@ pub const Backup = struct {
file_buf.len, file_buf.len,
}); });
}, },
.None => return SaveError.Unsupported, .None => return SaveError.UnsupportedBackupKind,
} }
} }
fn savePath(self: *const Self, allocator: Allocator, path: []const u8) ![]const u8 { fn getSaveFilePath(self: *const Self, allocator: Allocator, path: []const u8) ![]const u8 {
const filename = try self.saveName(allocator); const filename = try self.getSaveFilename(allocator);
defer allocator.free(filename); defer allocator.free(filename);
return try std.fs.path.join(allocator, &[_][]const u8{ path, filename }); return try std.fs.path.join(allocator, &[_][]const u8{ path, filename });
} }
fn saveName(self: *const Self, allocator: Allocator) ![]const u8 { fn getSaveFilename(self: *const Self, allocator: Allocator) ![]const u8 {
const title_str = span(&escape(self.title)); const title_str = span(&escape(self.title));
const name = if (title_str.len != 0) title_str else "untitled"; const name = if (title_str.len != 0) title_str else "untitled";
return try std.mem.concat(allocator, u8, &[_][]const u8{ name, ".sav" }); return try std.mem.concat(allocator, u8, &[_][]const u8{ name, ".sav" });
} }
fn writeSave(self: Self, allocator: Allocator, path: []const u8) !void { fn writeSaveToDisk(self: Self, allocator: Allocator, path: []const u8) !void {
const file_path = try self.savePath(allocator, path); const file_path = try self.getSaveFilePath(allocator, path);
defer allocator.free(file_path); defer allocator.free(file_path);
switch (self.kind) { switch (self.kind) {
@ -213,7 +147,420 @@ pub const Backup = struct {
try file.writeAll(self.buf); try file.writeAll(self.buf);
log.info("Wrote Save to {s}", .{file_path}); log.info("Wrote Save to {s}", .{file_path});
}, },
else => return SaveError.Unsupported, else => return SaveError.UnsupportedBackupKind,
}
}
pub fn read(self: *const Self, address: usize) u8 {
const addr = address & 0xFFFF;
switch (self.kind) {
.Flash => {
switch (addr) {
0x0000 => if (self.flash.id_mode) return 0x32, // Panasonic manufacturer ID
0x0001 => if (self.flash.id_mode) return 0x1B, // Panasonic device ID
else => {},
}
return self.flash.read(self.buf, addr);
},
.Flash1M => {
switch (addr) {
0x0000 => if (self.flash.id_mode) return 0x62, // Sanyo manufacturer ID
0x0001 => if (self.flash.id_mode) return 0x13, // Sanyo device ID
else => {},
}
return self.flash.read(self.buf, addr);
},
.Sram => return self.buf[addr & 0x7FFF], // 32K SRAM chip is mirrored
.None, .Eeprom => return 0xFF,
}
}
pub fn write(self: *Self, address: usize, byte: u8) void {
const addr = address & 0xFFFF;
switch (self.kind) {
.Flash, .Flash1M => {
if (self.flash.prep_write) return self.flash.write(self.buf, addr, byte);
if (self.flash.shouldEraseSector(addr, byte)) return self.flash.eraseSector(self.buf, addr);
switch (addr) {
0x0000 => if (self.kind == .Flash1M and self.flash.set_bank) {
self.flash.bank = @truncate(u1, byte);
},
0x5555 => {
if (self.flash.state == .Command) {
self.flash.handleCommand(self.buf, byte);
} else if (byte == 0xAA and self.flash.state == .Ready) {
self.flash.state = .Set;
} else if (byte == 0xF0) {
self.flash.state = .Ready;
}
},
0x2AAA => if (byte == 0x55 and self.flash.state == .Set) {
self.flash.state = .Command;
},
else => {},
}
},
.Sram => self.buf[addr & 0x7FFF] = byte,
.None, .Eeprom => {},
} }
} }
}; };
const Needle = struct {
const Self = @This();
str: []const u8,
kind: Backup.Kind,
fn init(str: []const u8, kind: Backup.Kind) Self {
return .{
.str = str,
.kind = kind,
};
}
};
const SaveError = error{
UnsupportedBackupKind,
};
const Flash = struct {
const Self = @This();
state: State,
id_mode: bool,
set_bank: bool,
prep_erase: bool,
prep_write: bool,
bank: u1,
const State = enum {
Ready,
Set,
Command,
};
fn init() Self {
return .{
.state = .Ready,
.id_mode = false,
.set_bank = false,
.prep_erase = false,
.prep_write = false,
.bank = 0,
};
}
fn handleCommand(self: *Self, buf: []u8, byte: u8) void {
switch (byte) {
0x90 => self.id_mode = true,
0xF0 => self.id_mode = false,
0xB0 => self.set_bank = true,
0x80 => self.prep_erase = true,
0x10 => {
std.mem.set(u8, buf, 0xFF);
self.prep_erase = false;
},
0xA0 => self.prep_write = true,
else => std.debug.panic("Unhandled Flash Command: 0x{X:0>2}", .{byte}),
}
self.state = .Ready;
}
fn shouldEraseSector(self: *const Self, addr: usize, byte: u8) bool {
return self.state == .Command and self.prep_erase and byte == 0x30 and addr & 0xFFF == 0x000;
}
fn write(self: *Self, buf: []u8, idx: usize, byte: u8) void {
buf[self.baseAddress() + idx] = byte;
self.prep_write = false;
}
fn read(self: *const Self, buf: []u8, idx: usize) u8 {
return buf[self.baseAddress() + idx];
}
fn eraseSector(self: *Self, buf: []u8, idx: usize) void {
const start = self.baseAddress() + (idx & 0xF000);
std.mem.set(u8, buf[start..][0..0x1000], 0xFF);
self.prep_erase = false;
self.state = .Ready;
}
inline fn baseAddress(self: *const Self) usize {
return if (self.bank == 1) 0x10000 else @as(usize, 0);
}
};
const Eeprom = struct {
const Self = @This();
addr: u14,
kind: Kind,
state: State,
writer: Writer,
reader: Reader,
allocator: Allocator,
const Kind = enum {
Unknown,
Small, // 512B
Large, // 8KB
};
const State = enum {
Ready,
Read,
Write,
WriteTransfer,
RequestEnd,
};
fn init(allocator: Allocator) Self {
return .{
.kind = .Unknown,
.state = .Ready,
.writer = Writer.init(),
.reader = Reader.init(),
.addr = 0,
.allocator = allocator,
};
}
pub fn read(self: *Self) u1 {
return self.reader.read();
}
pub fn dbgRead(self: *const Self) u1 {
return self.reader.dbgRead();
}
pub fn write(self: *Self, word_count: u16, buf: *[]u8, bit: u1) void {
if (self.guessKind(word_count)) |found| {
log.info("EEPROM Kind: {}", .{found});
self.kind = found;
// buf.len will not equal zero when a save file was found and loaded.
// Right now, we assume that the save file is of the correct size which
// isn't necessarily true, since we can't trust anything a user can influence
// TODO: use ?[]u8 instead of a 0-sized slice?
if (buf.len == 0) {
const len: usize = switch (found) {
.Small => 0x200,
.Large => 0x2000,
else => unreachable,
};
buf.* = self.allocator.alloc(u8, len) catch |e| {
log.err("Failed to resize EEPROM buf to {} bytes", .{len});
std.debug.panic("EEPROM entered irrecoverable state {}", .{e});
};
std.mem.set(u8, buf.*, 0xFF);
}
}
if (self.state == .RequestEnd) {
if (bit != 0) log.debug("EEPROM Request did not end in 0u1. TODO: is this ok?", .{});
self.state = .Ready;
return;
}
switch (self.state) {
.Ready => self.writer.requestWrite(bit),
.Read, .Write => self.writer.addressWrite(self.kind, bit),
.WriteTransfer => self.writer.dataWrite(bit),
.RequestEnd => unreachable, // We return early just above this block
}
self.tick(buf.*);
}
fn guessKind(self: *const Self, word_count: u16) ?Kind {
if (self.kind != .Unknown or self.state != .Read) return null;
return switch (word_count) {
17 => .Large,
9 => .Small,
else => blk: {
log.err("Unexpected length of DMA3 Transfer upon initial EEPROM read: {}", .{word_count});
break :blk null;
},
};
}
fn tick(self: *Self, buf: []u8) void {
switch (self.state) {
.Ready => {
if (self.writer.len() == 2) {
const req = @intCast(u2, self.writer.finish());
switch (req) {
0b11 => self.state = .Read,
0b10 => self.state = .Write,
else => log.err("Unknown EEPROM Request 0b{b:0>2}", .{req}),
}
}
},
.Read => {
switch (self.kind) {
.Large => {
if (self.writer.len() == 14) {
const addr = @intCast(u10, self.writer.finish());
const value = std.mem.readIntSliceLittle(u64, buf[@as(u13, addr) * 8 ..][0..8]);
self.reader.configure(value);
self.state = .RequestEnd;
}
},
.Small => {
if (self.writer.len() == 6) {
// FIXME: Duplicated code from above
const addr = @intCast(u6, self.writer.finish());
const value = std.mem.readIntSliceLittle(u64, buf[@as(u13, addr) * 8 ..][0..8]);
self.reader.configure(value);
self.state = .RequestEnd;
}
},
else => log.err("Unable to calculate EEPROM read address. EEPROM size UNKNOWN", .{}),
}
},
.Write => {
switch (self.kind) {
.Large => {
if (self.writer.len() == 14) {
self.addr = @intCast(u10, self.writer.finish());
self.state = .WriteTransfer;
}
},
.Small => {
if (self.writer.len() == 6) {
self.addr = @intCast(u6, self.writer.finish());
self.state = .WriteTransfer;
}
},
else => log.err("Unable to calculate EEPROM write address. EEPROM size UNKNOWN", .{}),
}
},
.WriteTransfer => {
if (self.writer.len() == 64) {
std.mem.writeIntSliceLittle(u64, buf[self.addr * 8 ..][0..8], self.writer.finish());
self.state = .RequestEnd;
}
},
.RequestEnd => unreachable, // We return early in write() if state is .RequestEnd
}
}
const Reader = struct {
const This = @This();
data: u64,
i: u8,
enabled: bool,
fn init() This {
return .{
.data = 0,
.i = 0,
.enabled = false,
};
}
fn configure(self: *This, value: u64) void {
self.data = value;
self.i = 0;
self.enabled = true;
}
fn read(self: *This) u1 {
if (!self.enabled) return 1;
const bit = if (self.i < 4) blk: {
break :blk 0;
} else blk: {
const idx = @intCast(u6, 63 - (self.i - 4));
break :blk @truncate(u1, self.data >> idx);
};
self.i = (self.i + 1) % (64 + 4);
if (self.i == 0) self.enabled = false;
return bit;
}
fn dbgRead(self: *const This) u1 {
if (!self.enabled) return 1;
const bit = if (self.i < 4) blk: {
break :blk 0;
} else blk: {
const idx = @intCast(u6, 63 - (self.i - 4));
break :blk @truncate(u1, self.data >> idx);
};
return bit;
}
};
const Writer = struct {
const This = @This();
data: u64,
i: u8,
fn init() This {
return .{ .data = 0, .i = 0 };
}
fn requestWrite(self: *This, bit: u1) void {
const idx = @intCast(u1, 1 - self.i);
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
self.i += 1;
}
fn addressWrite(self: *This, kind: Eeprom.Kind, bit: u1) void {
if (kind == .Unknown) return;
const size: u4 = switch (kind) {
.Large => 13,
.Small => 5,
.Unknown => unreachable,
};
const idx = @intCast(u4, size - self.i);
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
self.i += 1;
}
fn dataWrite(self: *This, bit: u1) void {
const idx = @intCast(u6, 63 - self.i);
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
self.i += 1;
}
fn len(self: *const This) u8 {
return self.i;
}
fn finish(self: *This) u64 {
defer self.reset();
return self.data;
}
fn reset(self: *This) void {
self.i = 0;
self.data = 0;
}
};
};

View File

@ -1,72 +0,0 @@
const std = @import("std");
const Self = @This();
state: State,
id_mode: bool,
set_bank: bool,
prep_erase: bool,
prep_write: bool,
bank: u1,
const State = enum {
Ready,
Set,
Command,
};
pub fn read(self: *const Self, buf: []u8, idx: usize) u8 {
return buf[self.address() + idx];
}
pub fn write(self: *Self, buf: []u8, idx: usize, byte: u8) void {
buf[self.address() + idx] = byte;
self.prep_write = false;
}
pub fn create() Self {
return .{
.state = .Ready,
.id_mode = false,
.set_bank = false,
.prep_erase = false,
.prep_write = false,
.bank = 0,
};
}
pub fn handleCommand(self: *Self, buf: []u8, byte: u8) void {
switch (byte) {
0x90 => self.id_mode = true,
0xF0 => self.id_mode = false,
0xB0 => self.set_bank = true,
0x80 => self.prep_erase = true,
0x10 => {
std.mem.set(u8, buf, 0xFF);
self.prep_erase = false;
},
0xA0 => self.prep_write = true,
else => std.debug.panic("Unhandled Flash Command: 0x{X:0>2}", .{byte}),
}
self.state = .Ready;
}
pub fn shouldEraseSector(self: *const Self, addr: usize, byte: u8) bool {
return self.state == .Command and self.prep_erase and byte == 0x30 and addr & 0xFFF == 0x000;
}
pub fn erase(self: *Self, buf: []u8, sector: usize) void {
const start = self.address() + (sector & 0xF000);
std.mem.set(u8, buf[start..][0..0x1000], 0xFF);
self.prep_erase = false;
self.state = .Ready;
}
/// Base Address
inline fn address(self: *const Self) usize {
return if (self.bank == 1) 0x10000 else @as(usize, 0);
}

View File

@ -1,269 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const log = std.log.scoped(.Eeprom);
pub const Eeprom = struct {
const Self = @This();
addr: u14,
kind: Kind,
state: State,
writer: Writer,
reader: Reader,
allocator: Allocator,
const Kind = enum {
Unknown,
Small, // 512B
Large, // 8KB
};
const State = enum {
Ready,
Read,
Write,
WriteTransfer,
RequestEnd,
};
pub fn read(self: *Self) u1 {
return self.reader.read();
}
pub fn dbgRead(self: *const Self) u1 {
return self.reader.dbgRead();
}
pub fn write(self: *Self, word_count: u16, buf: *[]u8, bit: u1) void {
if (self.guessKind(word_count)) |found| {
log.info("EEPROM Kind: {}", .{found});
self.kind = found;
// buf.len will not equal zero when a save file was found and loaded.
// Right now, we assume that the save file is of the correct size which
// isn't necessarily true, since we can't trust anything a user can influence
// TODO: use ?[]u8 instead of a 0-sized slice?
if (buf.len == 0) {
const len: usize = switch (found) {
.Small => 0x200,
.Large => 0x2000,
else => unreachable,
};
buf.* = self.allocator.alloc(u8, len) catch |e| {
log.err("Failed to resize EEPROM buf to {} bytes", .{len});
std.debug.panic("EEPROM entered irrecoverable state {}", .{e});
};
std.mem.set(u8, buf.*, 0xFF);
}
}
if (self.state == .RequestEnd) {
if (bit != 0) log.debug("EEPROM Request did not end in 0u1. TODO: is this ok?", .{});
self.state = .Ready;
return;
}
switch (self.state) {
.Ready => self.writer.requestWrite(bit),
.Read, .Write => self.writer.addressWrite(self.kind, bit),
.WriteTransfer => self.writer.dataWrite(bit),
.RequestEnd => unreachable, // We return early just above this block
}
self.tick(buf.*);
}
pub fn create(allocator: Allocator) Self {
return .{
.kind = .Unknown,
.state = .Ready,
.writer = Writer.create(),
.reader = Reader.create(),
.addr = 0,
.allocator = allocator,
};
}
fn guessKind(self: *const Self, word_count: u16) ?Kind {
if (self.kind != .Unknown or self.state != .Read) return null;
return switch (word_count) {
17 => .Large,
9 => .Small,
else => blk: {
log.err("Unexpected length of DMA3 Transfer upon initial EEPROM read: {}", .{word_count});
break :blk null;
},
};
}
fn tick(self: *Self, buf: []u8) void {
switch (self.state) {
.Ready => {
if (self.writer.len() == 2) {
const req = @intCast(u2, self.writer.finish());
switch (req) {
0b11 => self.state = .Read,
0b10 => self.state = .Write,
else => log.err("Unknown EEPROM Request 0b{b:0>2}", .{req}),
}
}
},
.Read => {
switch (self.kind) {
.Large => {
if (self.writer.len() == 14) {
const addr = @intCast(u10, self.writer.finish());
const value = std.mem.readIntSliceLittle(u64, buf[@as(u13, addr) * 8 ..][0..8]);
self.reader.configure(value);
self.state = .RequestEnd;
}
},
.Small => {
if (self.writer.len() == 6) {
// FIXME: Duplicated code from above
const addr = @intCast(u6, self.writer.finish());
const value = std.mem.readIntSliceLittle(u64, buf[@as(u13, addr) * 8 ..][0..8]);
self.reader.configure(value);
self.state = .RequestEnd;
}
},
else => log.err("Unable to calculate EEPROM read address. EEPROM size UNKNOWN", .{}),
}
},
.Write => {
switch (self.kind) {
.Large => {
if (self.writer.len() == 14) {
self.addr = @intCast(u10, self.writer.finish());
self.state = .WriteTransfer;
}
},
.Small => {
if (self.writer.len() == 6) {
self.addr = @intCast(u6, self.writer.finish());
self.state = .WriteTransfer;
}
},
else => log.err("Unable to calculate EEPROM write address. EEPROM size UNKNOWN", .{}),
}
},
.WriteTransfer => {
if (self.writer.len() == 64) {
std.mem.writeIntSliceLittle(u64, buf[self.addr * 8 ..][0..8], self.writer.finish());
self.state = .RequestEnd;
}
},
.RequestEnd => unreachable, // We return early in write() if state is .RequestEnd
}
}
};
const Reader = struct {
const Self = @This();
data: u64,
i: u8,
enabled: bool,
fn create() Self {
return .{
.data = 0,
.i = 0,
.enabled = false,
};
}
fn read(self: *Self) u1 {
if (!self.enabled) return 1;
const bit = if (self.i < 4) blk: {
break :blk 0;
} else blk: {
const idx = @intCast(u6, 63 - (self.i - 4));
break :blk @truncate(u1, self.data >> idx);
};
self.i = (self.i + 1) % (64 + 4);
if (self.i == 0) self.enabled = false;
return bit;
}
fn dbgRead(self: *const Self) u1 {
if (!self.enabled) return 1;
const bit = if (self.i < 4) blk: {
break :blk 0;
} else blk: {
const idx = @intCast(u6, 63 - (self.i - 4));
break :blk @truncate(u1, self.data >> idx);
};
return bit;
}
fn configure(self: *Self, value: u64) void {
self.data = value;
self.i = 0;
self.enabled = true;
}
};
const Writer = struct {
const Self = @This();
data: u64,
i: u8,
fn create() Self {
return .{ .data = 0, .i = 0 };
}
fn requestWrite(self: *Self, bit: u1) void {
const idx = @intCast(u1, 1 - self.i);
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
self.i += 1;
}
fn addressWrite(self: *Self, kind: Eeprom.Kind, bit: u1) void {
if (kind == .Unknown) return;
const size: u4 = switch (kind) {
.Large => 13,
.Small => 5,
.Unknown => unreachable,
};
const idx = @intCast(u4, size - self.i);
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
self.i += 1;
}
fn dataWrite(self: *Self, bit: u1) void {
const idx = @intCast(u6, 63 - self.i);
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
self.i += 1;
}
fn len(self: *const Self) u8 {
return self.i;
}
fn finish(self: *Self) u64 {
defer self.reset();
return self.data;
}
fn reset(self: *Self) void {
self.i = 0;
self.data = 0;
}
};

View File

@ -8,9 +8,6 @@ const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
pub const DmaTuple = std.meta.Tuple(&[_]type{ DmaController(0), DmaController(1), DmaController(2), DmaController(3) }); pub const DmaTuple = std.meta.Tuple(&[_]type{ DmaController(0), DmaController(1), DmaController(2), DmaController(3) });
const log = std.log.scoped(.DmaTransfer); const log = std.log.scoped(.DmaTransfer);
const setHi = util.setHi;
const setLo = util.setLo;
pub fn create() DmaTuple { pub fn create() DmaTuple {
return .{ DmaController(0).init(), DmaController(1).init(), DmaController(2).init(), DmaController(3).init() }; return .{ DmaController(0).init(), DmaController(1).init(), DmaController(2).init(), DmaController(3).init() };
} }
@ -43,48 +40,48 @@ pub fn write(comptime T: type, dma: *DmaTuple, addr: u32, value: T) void {
switch (T) { switch (T) {
u32 => switch (byte) { u32 => switch (byte) {
0xB0 => dma.*[0].setDmasad(value), 0xB0 => dma.*[0].setSad(value),
0xB4 => dma.*[0].setDmadad(value), 0xB4 => dma.*[0].setDad(value),
0xB8 => dma.*[0].setDmacnt(value), 0xB8 => dma.*[0].setCnt(value),
0xBC => dma.*[1].setDmasad(value), 0xBC => dma.*[1].setSad(value),
0xC0 => dma.*[1].setDmadad(value), 0xC0 => dma.*[1].setDad(value),
0xC4 => dma.*[1].setDmacnt(value), 0xC4 => dma.*[1].setCnt(value),
0xC8 => dma.*[2].setDmasad(value), 0xC8 => dma.*[2].setSad(value),
0xCC => dma.*[2].setDmadad(value), 0xCC => dma.*[2].setDad(value),
0xD0 => dma.*[2].setDmacnt(value), 0xD0 => dma.*[2].setCnt(value),
0xD4 => dma.*[3].setDmasad(value), 0xD4 => dma.*[3].setSad(value),
0xD8 => dma.*[3].setDmadad(value), 0xD8 => dma.*[3].setDad(value),
0xDC => dma.*[3].setDmacnt(value), 0xDC => dma.*[3].setCnt(value),
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }), else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
}, },
u16 => switch (byte) { u16 => switch (byte) {
0xB0 => dma.*[0].setDmasad(setLo(u32, dma.*[0].sad, value)), 0xB0 => dma.*[0].setSad(setU32L(dma.*[0].sad, value)),
0xB2 => dma.*[0].setDmasad(setHi(u32, dma.*[0].sad, value)), 0xB2 => dma.*[0].setSad(setU32H(dma.*[0].sad, value)),
0xB4 => dma.*[0].setDmadad(setLo(u32, dma.*[0].dad, value)), 0xB4 => dma.*[0].setDad(setU32L(dma.*[0].dad, value)),
0xB6 => dma.*[0].setDmadad(setHi(u32, dma.*[0].dad, value)), 0xB6 => dma.*[0].setDad(setU32H(dma.*[0].dad, value)),
0xB8 => dma.*[0].setDmacntL(value), 0xB8 => dma.*[0].setCntL(value),
0xBA => dma.*[0].setDmacntH(value), 0xBA => dma.*[0].setCntH(value),
0xBC => dma.*[1].setDmasad(setLo(u32, dma.*[1].sad, value)), 0xBC => dma.*[1].setSad(setU32L(dma.*[1].sad, value)),
0xBE => dma.*[1].setDmasad(setHi(u32, dma.*[1].sad, value)), 0xBE => dma.*[1].setSad(setU32H(dma.*[1].sad, value)),
0xC0 => dma.*[1].setDmadad(setLo(u32, dma.*[1].dad, value)), 0xC0 => dma.*[1].setDad(setU32L(dma.*[1].dad, value)),
0xC2 => dma.*[1].setDmadad(setHi(u32, dma.*[1].dad, value)), 0xC2 => dma.*[1].setDad(setU32H(dma.*[1].dad, value)),
0xC4 => dma.*[1].setDmacntL(value), 0xC4 => dma.*[1].setCntL(value),
0xC6 => dma.*[1].setDmacntH(value), 0xC6 => dma.*[1].setCntH(value),
0xC8 => dma.*[2].setDmasad(setLo(u32, dma.*[2].sad, value)), 0xC8 => dma.*[2].setSad(setU32L(dma.*[2].sad, value)),
0xCA => dma.*[2].setDmasad(setHi(u32, dma.*[2].sad, value)), 0xCA => dma.*[2].setSad(setU32H(dma.*[2].sad, value)),
0xCC => dma.*[2].setDmadad(setLo(u32, dma.*[2].dad, value)), 0xCC => dma.*[2].setDad(setU32L(dma.*[2].dad, value)),
0xCE => dma.*[2].setDmadad(setHi(u32, dma.*[2].dad, value)), 0xCE => dma.*[2].setDad(setU32H(dma.*[2].dad, value)),
0xD0 => dma.*[2].setDmacntL(value), 0xD0 => dma.*[2].setCntL(value),
0xD2 => dma.*[2].setDmacntH(value), 0xD2 => dma.*[2].setCntH(value),
0xD4 => dma.*[3].setDmasad(setLo(u32, dma.*[3].sad, value)), 0xD4 => dma.*[3].setSad(setU32L(dma.*[3].sad, value)),
0xD6 => dma.*[3].setDmasad(setHi(u32, dma.*[3].sad, value)), 0xD6 => dma.*[3].setSad(setU32H(dma.*[3].sad, value)),
0xD8 => dma.*[3].setDmadad(setLo(u32, dma.*[3].dad, value)), 0xD8 => dma.*[3].setDad(setU32L(dma.*[3].dad, value)),
0xDA => dma.*[3].setDmadad(setHi(u32, dma.*[3].dad, value)), 0xDA => dma.*[3].setDad(setU32H(dma.*[3].dad, value)),
0xDC => dma.*[3].setDmacntL(value), 0xDC => dma.*[3].setCntL(value),
0xDE => dma.*[3].setDmacntH(value), 0xDE => dma.*[3].setCntH(value),
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }), else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
}, },
u8 => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }), u8 => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
@ -113,12 +110,15 @@ fn DmaController(comptime id: u2) type {
cnt: DmaControl, cnt: DmaControl,
/// Internal. Currrent Source Address /// Internal. Currrent Source Address
sad_latch: u32, _sad: u32,
/// Internal. Current Destination Address /// Internal. Current Destination Address
dad_latch: u32, _dad: u32,
/// 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
@ -132,32 +132,33 @@ fn DmaController(comptime id: u2) type {
.cnt = .{ .raw = 0x000 }, .cnt = .{ .raw = 0x000 },
// Internals // Internals
.sad_latch = 0, ._sad = 0,
.dad_latch = 0, ._dad = 0,
._word_count = 0, ._word_count = 0,
._fifo_word_count = 4,
.in_progress = false, .in_progress = false,
}; };
} }
pub fn setDmasad(self: *Self, addr: u32) void { pub fn setSad(self: *Self, addr: u32) void {
self.sad = addr & sad_mask; self.sad = addr & sad_mask;
} }
pub fn setDmadad(self: *Self, addr: u32) void { pub fn setDad(self: *Self, addr: u32) void {
self.dad = addr & dad_mask; self.dad = addr & dad_mask;
} }
pub fn setDmacntL(self: *Self, halfword: u16) void { pub fn setCntL(self: *Self, halfword: u16) void {
self.word_count = @truncate(@TypeOf(self.word_count), halfword); self.word_count = @truncate(@TypeOf(self.word_count), halfword);
} }
pub fn setDmacntH(self: *Self, halfword: u16) void { pub fn setCntH(self: *Self, halfword: u16) void {
const new = DmaControl{ .raw = halfword }; const new = DmaControl{ .raw = halfword };
if (!self.cnt.enabled.read() and new.enabled.read()) { if (!self.cnt.enabled.read() and new.enabled.read()) {
// Reload Internals on Rising Edge. // Reload Internals on Rising Edge.
self.sad_latch = self.sad; self._sad = self.sad;
self.dad_latch = self.dad; self._dad = self.dad;
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
@ -167,15 +168,15 @@ fn DmaController(comptime id: u2) type {
self.cnt.raw = halfword; self.cnt.raw = halfword;
} }
pub fn setDmacnt(self: *Self, word: u32) void { pub fn setCnt(self: *Self, word: u32) void {
self.setDmacntL(@truncate(u16, word)); self.setCntL(@truncate(u16, word));
self.setDmacntH(@truncate(u16, word >> 16)); self.setCntH(@truncate(u16, word >> 16));
} }
pub fn step(self: *Self, cpu: *Arm7tdmi) void { pub fn step(self: *Self, cpu: *Arm7tdmi) void {
const is_fifo = (id == 1 or 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 = @intToEnum(Adjustment, self.cnt.sad_adj.read()); const sad_adj = Self.adjustment(self.cnt.sad_adj.read());
const dad_adj = if (is_fifo) .Fixed else @intToEnum(Adjustment, self.cnt.dad_adj.read()); const dad_adj = if (is_fifo) .Fixed else Self.adjustment(self.cnt.dad_adj.read());
const transfer_type = is_fifo or self.cnt.transfer_type.read(); const transfer_type = is_fifo or self.cnt.transfer_type.read();
const offset: u32 = if (transfer_type) @sizeOf(u32) else @sizeOf(u16); const offset: u32 = if (transfer_type) @sizeOf(u32) else @sizeOf(u16);
@ -183,22 +184,22 @@ fn DmaController(comptime id: u2) type {
const mask = if (transfer_type) ~@as(u32, 3) else ~@as(u32, 1); const mask = if (transfer_type) ~@as(u32, 3) else ~@as(u32, 1);
if (transfer_type) { if (transfer_type) {
cpu.bus.write(u32, self.dad_latch & mask, cpu.bus.read(u32, self.sad_latch & mask)); cpu.bus.write(u32, self._dad & mask, cpu.bus.read(u32, self._sad & mask));
} else { } else {
cpu.bus.write(u16, self.dad_latch & mask, cpu.bus.read(u16, self.sad_latch & mask)); cpu.bus.write(u16, self._dad & mask, cpu.bus.read(u16, self._sad & mask));
} }
switch (sad_adj) { switch (sad_adj) {
.Increment => self.sad_latch +%= offset, .Increment => self._sad +%= offset,
.Decrement => self.sad_latch -%= offset, .Decrement => self._sad -%= offset,
// FIXME: Is just ignoring this ok? // TODO: Is just ignoring this ok?
.IncrementReload => log.err("{} is a prohibited adjustment on SAD", .{sad_adj}), .IncrementReload => log.err("{} is a prohibited adjustment on SAD", .{sad_adj}),
.Fixed => {}, .Fixed => {},
} }
switch (dad_adj) { switch (dad_adj) {
.Increment, .IncrementReload => self.dad_latch +%= offset, .Increment, .IncrementReload => self._dad +%= offset,
.Decrement => self.dad_latch -%= offset, .Decrement => self._dad -%= offset,
.Fixed => {}, .Fixed => {},
} }
@ -226,7 +227,7 @@ fn DmaController(comptime id: u2) type {
} }
} }
fn poll(self: *Self, comptime kind: DmaKind) void { pub fn pollBlankingDma(self: *Self, comptime kind: DmaKind) void {
if (self.in_progress) return; // If there's an ongoing DMA Transfer, exit early 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 // No ongoing DMA Transfer, We want to check if we should repeat an existing one
@ -242,11 +243,11 @@ fn DmaController(comptime id: u2) type {
// Reload internal DAD latch if we are in IncrementRelaod // Reload internal DAD latch if we are in IncrementRelaod
if (self.in_progress) { if (self.in_progress) {
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;
if (@intToEnum(Adjustment, self.cnt.dad_adj.read()) == .IncrementReload) self.dad_latch = self.dad; if (Self.adjustment(self.cnt.dad_adj.read()) == .IncrementReload) self._dad = self.dad;
} }
} }
pub fn requestAudio(self: *Self, _: u32) void { pub fn requestSoundDma(self: *Self, _: u32) void {
comptime std.debug.assert(id == 1 or id == 2); comptime std.debug.assert(id == 1 or id == 2);
if (self.in_progress) return; // APU must wait their turn if (self.in_progress) return; // APU must wait their turn
@ -258,19 +259,23 @@ fn DmaController(comptime id: u2) type {
// We Assume DMACNT_L is set to 4 // We Assume DMACNT_L is set to 4
// FIXME: Safe to just assume whatever DAD is set to is the FIFO Address? // FIXME: Safe to just assume whatever DAD is set to is the FIFO Address?
// self.dad_latch = fifo_addr; // self._dad = fifo_addr;
self.cnt.repeat.set(); self.cnt.repeat.set();
self._word_count = 4; self._word_count = 4;
self.in_progress = true; self.in_progress = true;
} }
fn adjustment(idx: u2) Adjustment {
return std.meta.intToEnum(Adjustment, idx) catch unreachable;
}
}; };
} }
pub fn pollDmaOnBlank(bus: *Bus, comptime kind: DmaKind) void { pub fn pollBlankingDma(bus: *Bus, comptime kind: DmaKind) void {
bus.dma[0].poll(kind); bus.dma[0].pollBlankingDma(kind);
bus.dma[1].poll(kind); bus.dma[1].pollBlankingDma(kind);
bus.dma[2].poll(kind); bus.dma[2].pollBlankingDma(kind);
bus.dma[3].poll(kind); bus.dma[3].pollBlankingDma(kind);
} }
const Adjustment = enum(u2) { const Adjustment = enum(u2) {
@ -286,3 +291,11 @@ const DmaKind = enum(u2) {
VBlank, VBlank,
Special, Special,
}; };
fn setU32L(left: u32, right: u16) u32 {
return (left & 0xFFFF_0000) | right;
}
fn setU32H(left: u32, right: u16) u32 {
return (left & 0x0000_FFFF) | (@as(u32, right) << 16);
}

View File

@ -288,7 +288,7 @@ pub const Clock = struct {
cpu.sched.push(.RealTimeClock, 1 << 24); // Every Second cpu.sched.push(.RealTimeClock, 1 << 24); // Every Second
} }
pub fn onClockUpdate(self: *Self, late: u64) void { pub fn updateTime(self: *Self, late: u64) void {
self.cpu.sched.push(.RealTimeClock, (1 << 24) -| late); // Reschedule self.cpu.sched.push(.RealTimeClock, (1 << 24) -| late); // Reschedule
const now = DateTime.now(); const now = DateTime.now();

View File

@ -11,9 +11,6 @@ const Bus = @import("../Bus.zig");
const DmaController = @import("dma.zig").DmaController; const DmaController = @import("dma.zig").DmaController;
const Scheduler = @import("../scheduler.zig").Scheduler; const Scheduler = @import("../scheduler.zig").Scheduler;
const setHi = util.setLo;
const setLo = util.setHi;
const log = std.log.scoped(.@"I/O"); const log = std.log.scoped(.@"I/O");
pub const Io = struct { pub const Io = struct {
@ -236,18 +233,18 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
0x0400_0022 => bus.ppu.aff_bg[0].pb = @bitCast(i16, value), 0x0400_0022 => bus.ppu.aff_bg[0].pb = @bitCast(i16, value),
0x0400_0024 => bus.ppu.aff_bg[0].pc = @bitCast(i16, value), 0x0400_0024 => bus.ppu.aff_bg[0].pc = @bitCast(i16, value),
0x0400_0026 => bus.ppu.aff_bg[0].pd = @bitCast(i16, value), 0x0400_0026 => bus.ppu.aff_bg[0].pd = @bitCast(i16, value),
0x0400_0028 => bus.ppu.aff_bg[0].x = @bitCast(i32, setLo(u32, @bitCast(u32, bus.ppu.aff_bg[0].x), value)), 0x0400_0028 => bus.ppu.aff_bg[0].x = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[0].x) & 0xFFFF_0000 | value),
0x0400_002A => bus.ppu.aff_bg[0].x = @bitCast(i32, setHi(u32, @bitCast(u32, bus.ppu.aff_bg[0].x), value)), 0x0400_002A => bus.ppu.aff_bg[0].x = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[0].x) & 0x0000_FFFF | (@as(u32, value) << 16)),
0x0400_002C => bus.ppu.aff_bg[0].y = @bitCast(i32, setLo(u32, @bitCast(u32, bus.ppu.aff_bg[0].y), value)), 0x0400_002C => bus.ppu.aff_bg[0].y = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[0].y) & 0xFFFF_0000 | value),
0x0400_002E => bus.ppu.aff_bg[0].y = @bitCast(i32, setHi(u32, @bitCast(u32, bus.ppu.aff_bg[0].y), value)), 0x0400_002E => bus.ppu.aff_bg[0].y = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[0].y) & 0x0000_FFFF | (@as(u32, value) << 16)),
0x0400_0030 => bus.ppu.aff_bg[1].pa = @bitCast(i16, value), 0x0400_0030 => bus.ppu.aff_bg[1].pa = @bitCast(i16, value),
0x0400_0032 => bus.ppu.aff_bg[1].pb = @bitCast(i16, value), 0x0400_0032 => bus.ppu.aff_bg[1].pb = @bitCast(i16, value),
0x0400_0034 => bus.ppu.aff_bg[1].pc = @bitCast(i16, value), 0x0400_0034 => bus.ppu.aff_bg[1].pc = @bitCast(i16, value),
0x0400_0036 => bus.ppu.aff_bg[1].pd = @bitCast(i16, value), 0x0400_0036 => bus.ppu.aff_bg[1].pd = @bitCast(i16, value),
0x0400_0038 => bus.ppu.aff_bg[1].x = @bitCast(i32, setLo(u32, @bitCast(u32, bus.ppu.aff_bg[1].x), value)), 0x0400_0038 => bus.ppu.aff_bg[1].x = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[1].x) & 0xFFFF_0000 | value),
0x0400_003A => bus.ppu.aff_bg[1].x = @bitCast(i32, setHi(u32, @bitCast(u32, bus.ppu.aff_bg[1].x), value)), 0x0400_003A => bus.ppu.aff_bg[1].x = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[1].x) & 0x0000_FFFF | (@as(u32, value) << 16)),
0x0400_003C => bus.ppu.aff_bg[1].y = @bitCast(i32, setLo(u32, @bitCast(u32, bus.ppu.aff_bg[1].y), value)), 0x0400_003C => bus.ppu.aff_bg[1].y = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[1].y) & 0xFFFF_0000 | value),
0x0400_003E => bus.ppu.aff_bg[1].y = @bitCast(i32, setHi(u32, @bitCast(u32, bus.ppu.aff_bg[1].y), value)), 0x0400_003E => bus.ppu.aff_bg[1].y = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[1].y) & 0x0000_FFFF | (@as(u32, value) << 16)),
0x0400_0040 => bus.ppu.win.h[0].raw = value, 0x0400_0040 => bus.ppu.win.h[0].raw = value,
0x0400_0042 => bus.ppu.win.h[1].raw = value, 0x0400_0042 => bus.ppu.win.h[1].raw = value,
0x0400_0044 => bus.ppu.win.v[0].raw = value, 0x0400_0044 => bus.ppu.win.v[0].raw = value,
@ -299,16 +296,16 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
}, },
u8 => switch (address) { u8 => switch (address) {
// Display // Display
0x0400_0004 => bus.ppu.dispstat.raw = setLo(u16, bus.ppu.dispstat.raw, value), 0x0400_0004 => bus.ppu.dispstat.raw = (bus.ppu.dispstat.raw & 0xFF00) | value,
0x0400_0005 => bus.ppu.dispstat.raw = setHi(u16, bus.ppu.dispstat.raw, value), 0x0400_0005 => bus.ppu.dispstat.raw = (@as(u16, value) << 8) | (bus.ppu.dispstat.raw & 0xFF),
0x0400_0008 => bus.ppu.bg[0].cnt.raw = setLo(u16, bus.ppu.bg[0].cnt.raw, value), 0x0400_0008 => bus.ppu.bg[0].cnt.raw = (bus.ppu.bg[0].cnt.raw & 0xFF00) | value,
0x0400_0009 => bus.ppu.bg[0].cnt.raw = setHi(u16, bus.ppu.bg[0].cnt.raw, value), 0x0400_0009 => bus.ppu.bg[0].cnt.raw = (@as(u16, value) << 8) | (bus.ppu.bg[0].cnt.raw & 0xFF),
0x0400_000A => bus.ppu.bg[1].cnt.raw = setLo(u16, bus.ppu.bg[1].cnt.raw, value), 0x0400_000A => bus.ppu.bg[1].cnt.raw = (bus.ppu.bg[1].cnt.raw & 0xFF00) | value,
0x0400_000B => bus.ppu.bg[1].cnt.raw = setHi(u16, bus.ppu.bg[1].cnt.raw, value), 0x0400_000B => bus.ppu.bg[1].cnt.raw = (@as(u16, value) << 8) | (bus.ppu.bg[1].cnt.raw & 0xFF),
0x0400_0048 => bus.ppu.win.in.raw = setLo(u16, bus.ppu.win.in.raw, value), 0x0400_0048 => bus.ppu.win.setInL(value),
0x0400_0049 => bus.ppu.win.in.raw = setHi(u16, bus.ppu.win.in.raw, value), 0x0400_0049 => bus.ppu.win.setInH(value),
0x0400_004A => bus.ppu.win.out.raw = setLo(u16, bus.ppu.win.out.raw, value), 0x0400_004A => bus.ppu.win.setOutL(value),
0x0400_0054 => bus.ppu.bldy.raw = setLo(u16, bus.ppu.bldy.raw, value), 0x0400_0054 => bus.ppu.bldy.raw = (bus.ppu.bldy.raw & 0xFF00) | value,
// Sound // Sound
0x0400_0060...0x0400_00A7 => apu.write(T, &bus.apu, address, value), 0x0400_0060...0x0400_00A7 => apu.write(T, &bus.apu, address, value),

View File

@ -19,20 +19,20 @@ pub fn read(comptime T: type, tim: *const TimerTuple, addr: u32) ?T {
return switch (T) { return switch (T) {
u32 => switch (nybble) { u32 => switch (nybble) {
0x0 => @as(T, tim.*[0].cnt.raw) << 16 | tim.*[0].timcntL(), 0x0 => @as(T, tim.*[0].cnt.raw) << 16 | tim.*[0].getCntL(),
0x4 => @as(T, tim.*[1].cnt.raw) << 16 | tim.*[1].timcntL(), 0x4 => @as(T, tim.*[1].cnt.raw) << 16 | tim.*[1].getCntL(),
0x8 => @as(T, tim.*[2].cnt.raw) << 16 | tim.*[2].timcntL(), 0x8 => @as(T, tim.*[2].cnt.raw) << 16 | tim.*[2].getCntL(),
0xC => @as(T, tim.*[3].cnt.raw) << 16 | tim.*[3].timcntL(), 0xC => @as(T, tim.*[3].cnt.raw) << 16 | tim.*[3].getCntL(),
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }), else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
}, },
u16 => switch (nybble) { u16 => switch (nybble) {
0x0 => tim.*[0].timcntL(), 0x0 => tim.*[0].getCntL(),
0x2 => tim.*[0].cnt.raw, 0x2 => tim.*[0].cnt.raw,
0x4 => tim.*[1].timcntL(), 0x4 => tim.*[1].getCntL(),
0x6 => tim.*[1].cnt.raw, 0x6 => tim.*[1].cnt.raw,
0x8 => tim.*[2].timcntL(), 0x8 => tim.*[2].getCntL(),
0xA => tim.*[2].cnt.raw, 0xA => tim.*[2].cnt.raw,
0xC => tim.*[3].timcntL(), 0xC => tim.*[3].getCntL(),
0xE => tim.*[3].cnt.raw, 0xE => tim.*[3].cnt.raw,
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }), else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
}, },
@ -46,21 +46,21 @@ pub fn write(comptime T: type, tim: *TimerTuple, addr: u32, value: T) void {
return switch (T) { return switch (T) {
u32 => switch (nybble) { u32 => switch (nybble) {
0x0 => tim.*[0].setTimcnt(value), 0x0 => tim.*[0].setCnt(value),
0x4 => tim.*[1].setTimcnt(value), 0x4 => tim.*[1].setCnt(value),
0x8 => tim.*[2].setTimcnt(value), 0x8 => tim.*[2].setCnt(value),
0xC => tim.*[3].setTimcnt(value), 0xC => tim.*[3].setCnt(value),
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }), else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
}, },
u16 => switch (nybble) { u16 => switch (nybble) {
0x0 => tim.*[0].setTimcntL(value), 0x0 => tim.*[0].setCntL(value),
0x2 => tim.*[0].setTimcntH(value), 0x2 => tim.*[0].setCntH(value),
0x4 => tim.*[1].setTimcntL(value), 0x4 => tim.*[1].setCntL(value),
0x6 => tim.*[1].setTimcntH(value), 0x6 => tim.*[1].setCntH(value),
0x8 => tim.*[2].setTimcntL(value), 0x8 => tim.*[2].setCntL(value),
0xA => tim.*[2].setTimcntH(value), 0xA => tim.*[2].setCntH(value),
0xC => tim.*[3].setTimcntL(value), 0xC => tim.*[3].setCntL(value),
0xE => tim.*[3].setTimcntH(value), 0xE => tim.*[3].setCntH(value),
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }), else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
}, },
u8 => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }), u8 => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
@ -72,13 +72,13 @@ fn Timer(comptime id: u2) type {
return struct { return struct {
const Self = @This(); const Self = @This();
/// Read Only, Internal. Please use self.timcntL() /// Read Only, Internal. Please use self.getCntL()
_counter: u16, _counter: u16,
/// Write Only, Internal. Please use self.setTimcntL() /// Write Only, Internal. Please use self.setCntL()
_reload: u16, _reload: u16,
/// Write Only, Internal. Please use self.setTimcntH() /// Write Only, Internal. Please use self.setCntH()
cnt: TimerControl, cnt: TimerControl,
/// Internal. /// Internal.
@ -97,26 +97,26 @@ fn Timer(comptime id: u2) type {
}; };
} }
/// TIMCNT_L Getter /// TIMCNT_L
pub fn timcntL(self: *const Self) u16 { pub fn getCntL(self: *const Self) u16 {
if (self.cnt.cascade.read() or !self.cnt.enabled.read()) return self._counter; if (self.cnt.cascade.read() or !self.cnt.enabled.read()) return self._counter;
return self._counter +% @truncate(u16, (self.sched.now() - self._start_timestamp) / self.frequency()); return self._counter +% @truncate(u16, (self.sched.now() - self._start_timestamp) / self.frequency());
} }
/// TIMCNT_L Setter /// TIMCNT_L
pub fn setTimcntL(self: *Self, halfword: u16) void { pub fn setCntL(self: *Self, halfword: u16) void {
self._reload = halfword; self._reload = halfword;
} }
/// TIMCNT_L & TIMCNT_H /// TIMCNT_L & TIMCNT_H
pub fn setTimcnt(self: *Self, word: u32) void { pub fn setCnt(self: *Self, word: u32) void {
self.setTimcntL(@truncate(u16, word)); self.setCntL(@truncate(u16, word));
self.setTimcntH(@truncate(u16, word >> 16)); self.setCntH(@truncate(u16, word >> 16));
} }
/// TIMCNT_H /// TIMCNT_H
pub fn setTimcntH(self: *Self, halfword: u16) void { pub fn setCntH(self: *Self, halfword: u16) void {
const new = TimerControl{ .raw = halfword }; const new = TimerControl{ .raw = halfword };
// If Timer happens to be enabled, It will either be resheduled or disabled // If Timer happens to be enabled, It will either be resheduled or disabled
@ -132,12 +132,12 @@ fn Timer(comptime id: u2) type {
if (!self.cnt.enabled.read() and new.enabled.read()) self._counter = self._reload; if (!self.cnt.enabled.read() and new.enabled.read()) self._counter = self._reload;
// If Timer is enabled and we're not cascading, we need to schedule an overflow event // If Timer is enabled and we're not cascading, we need to schedule an overflow event
if (new.enabled.read() and !new.cascade.read()) self.rescheduleTimerExpire(0); if (new.enabled.read() and !new.cascade.read()) self.scheduleOverflow(0);
self.cnt.raw = halfword; self.cnt.raw = halfword;
} }
pub fn onTimerExpire(self: *Self, cpu: *Arm7tdmi, late: u64) void { pub fn handleOverflow(self: *Self, cpu: *Arm7tdmi, late: u64) void {
// Fire IRQ if enabled // Fire IRQ if enabled
const io = &cpu.bus.io; const io = &cpu.bus.io;
@ -154,22 +154,22 @@ fn Timer(comptime id: u2) type {
// DMA Sound Things // DMA Sound Things
if (id == 0 or id == 1) { if (id == 0 or id == 1) {
cpu.bus.apu.onDmaAudioSampleRequest(cpu, id); cpu.bus.apu.handleTimerOverflow(cpu, id);
} }
// Perform Cascade Behaviour // Perform Cascade Behaviour
switch (id) { switch (id) {
0 => if (cpu.bus.tim[1].cnt.cascade.read()) { 0 => if (cpu.bus.tim[1].cnt.cascade.read()) {
cpu.bus.tim[1]._counter +%= 1; cpu.bus.tim[1]._counter +%= 1;
if (cpu.bus.tim[1]._counter == 0) cpu.bus.tim[1].onTimerExpire(cpu, late); if (cpu.bus.tim[1]._counter == 0) cpu.bus.tim[1].handleOverflow(cpu, late);
}, },
1 => if (cpu.bus.tim[2].cnt.cascade.read()) { 1 => if (cpu.bus.tim[2].cnt.cascade.read()) {
cpu.bus.tim[2]._counter +%= 1; cpu.bus.tim[2]._counter +%= 1;
if (cpu.bus.tim[2]._counter == 0) cpu.bus.tim[2].onTimerExpire(cpu, late); if (cpu.bus.tim[2]._counter == 0) cpu.bus.tim[2].handleOverflow(cpu, late);
}, },
2 => if (cpu.bus.tim[3].cnt.cascade.read()) { 2 => if (cpu.bus.tim[3].cnt.cascade.read()) {
cpu.bus.tim[3]._counter +%= 1; cpu.bus.tim[3]._counter +%= 1;
if (cpu.bus.tim[3]._counter == 0) cpu.bus.tim[3].onTimerExpire(cpu, late); if (cpu.bus.tim[3]._counter == 0) cpu.bus.tim[3].handleOverflow(cpu, late);
}, },
3 => {}, // There is no Timer for TIM3 to "cascade" to, 3 => {}, // There is no Timer for TIM3 to "cascade" to,
} }
@ -177,11 +177,11 @@ fn Timer(comptime id: u2) type {
// Reschedule Timer if we're not cascading // Reschedule Timer if we're not cascading
if (!self.cnt.cascade.read()) { if (!self.cnt.cascade.read()) {
self._counter = self._reload; self._counter = self._reload;
self.rescheduleTimerExpire(late); self.scheduleOverflow(late);
} }
} }
fn rescheduleTimerExpire(self: *Self, late: u64) void { fn scheduleOverflow(self: *Self, late: u64) void {
const when = (@as(u64, 0x10000) - self._counter) * self.frequency(); const when = (@as(u64, 0x10000) - self._counter) * self.frequency();
self._start_timestamp = self.sched.now(); self._start_timestamp = self.sched.now();

View File

@ -236,6 +236,7 @@ pub const thumb = struct {
} }
}; };
const cpu_logging = @import("emu.zig").cpu_logging;
const log = std.log.scoped(.Arm7Tdmi); const log = std.log.scoped(.Arm7Tdmi);
pub const Arm7tdmi = struct { pub const Arm7tdmi = struct {
@ -443,12 +444,12 @@ pub const Arm7tdmi = struct {
if (self.cpsr.t.read()) { if (self.cpsr.t.read()) {
const opcode = @truncate(u16, self.pipe.step(self, u16) orelse return); const opcode = @truncate(u16, self.pipe.step(self, u16) orelse return);
if (self.logger) |*trace| trace.mgbaLog(self, opcode); if (cpu_logging) self.logger.?.mgbaLog(self, opcode);
thumb.lut[thumb.idx(opcode)](self, self.bus, opcode); thumb.lut[thumb.idx(opcode)](self, self.bus, opcode);
} else { } else {
const opcode = self.pipe.step(self, u32) orelse return; const opcode = self.pipe.step(self, u32) orelse return;
if (self.logger) |*trace| trace.mgbaLog(self, opcode); if (cpu_logging) self.logger.?.mgbaLog(self, opcode);
if (checkCond(self.cpsr, @truncate(u4, opcode >> 28))) { if (checkCond(self.cpsr, @truncate(u4, opcode >> 28))) {
arm.lut[arm.idx(opcode)](self, self.bus, opcode); arm.lut[arm.idx(opcode)](self, self.bus, opcode);

View File

@ -1,6 +1,5 @@
const std = @import("std"); const std = @import("std");
const SDL = @import("sdl2"); const SDL = @import("sdl2");
const config = @import("../config.zig");
const Bus = @import("Bus.zig"); const Bus = @import("Bus.zig");
const Scheduler = @import("scheduler.zig").Scheduler; const Scheduler = @import("scheduler.zig").Scheduler;
@ -13,6 +12,14 @@ const Thread = std.Thread;
const Atomic = std.atomic.Atomic; const Atomic = std.atomic.Atomic;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
// TODO: Move these to a TOML File
const sync_audio = false; // Enable Audio Sync
const sync_video: RunKind = .LimitedFPS; // Configure Video Sync
pub const win_scale = 4; // 1x, 2x, 3x, etc. Window Scaling
pub const cpu_logging = false; // Enable detailed CPU logging
pub const allow_unhandled_io = true; // Only relevant in Debug Builds
pub const force_rtc = false;
// 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
@ -33,57 +40,18 @@ const RunKind = enum {
UnlimitedFPS, UnlimitedFPS,
Limited, Limited,
LimitedFPS, LimitedFPS,
LimitedBusy,
}; };
pub fn run(quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: *FpsTracker) void { pub fn run(quit: *Atomic(bool), fps: *FpsTracker, sched: *Scheduler, cpu: *Arm7tdmi) void {
const audio_sync = config.config().guest.audio_sync; if (sync_audio) log.info("Audio sync enabled", .{});
if (audio_sync) log.info("Audio sync enabled", .{});
if (config.config().guest.video_sync) { switch (sync_video) {
inner(.LimitedFPS, audio_sync, quit, scheduler, cpu, tracker); .Unlimited => runUnsynchronized(quit, sched, cpu, null),
} else { .Limited => runSynchronized(quit, sched, cpu, null),
inner(.UnlimitedFPS, audio_sync, quit, scheduler, cpu, tracker); .UnlimitedFPS => runUnsynchronized(quit, sched, cpu, fps),
} .LimitedFPS => runSynchronized(quit, sched, cpu, fps),
} .LimitedBusy => runBusyLoop(quit, sched, cpu),
fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: ?*FpsTracker) void {
if (kind == .UnlimitedFPS or kind == .LimitedFPS) {
std.debug.assert(tracker != null);
log.info("FPS tracking enabled", .{});
}
switch (kind) {
.Unlimited, .UnlimitedFPS => {
log.info("Emulation w/out video sync", .{});
while (!quit.load(.SeqCst)) {
runFrame(scheduler, cpu);
audioSync(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
if (kind == .UnlimitedFPS) tracker.?.tick();
}
},
.Limited, .LimitedFPS => {
log.info("Emulation w/ video sync", .{});
var timer = Timer.start() catch @panic("failed to initalize std.timer.Timer");
var wake_time: u64 = frame_period;
while (!quit.load(.SeqCst)) {
runFrame(scheduler, cpu);
const new_wake_time = videoSync(&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
audioSync(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
if (!audio_sync) spinLoop(&timer, wake_time);
wake_time = new_wake_time;
if (kind == .LimitedFPS) tracker.?.tick();
}
},
} }
} }
@ -104,7 +72,7 @@ pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
} }
} }
fn audioSync(audio_sync: bool, stream: *SDL.SDL_AudioStream, is_buffer_full: *bool) void { fn syncToAudio(stream: *SDL.SDL_AudioStream, is_buffer_full: *bool) void {
const sample_size = 2 * @sizeOf(u16); const sample_size = 2 * @sizeOf(u16);
const max_buf_size: c_int = 0x400; const max_buf_size: c_int = 0x400;
@ -117,20 +85,90 @@ fn audioSync(audio_sync: bool, stream: *SDL.SDL_AudioStream, is_buffer_full: *bo
while (true) { while (true) {
still_full = SDL.SDL_AudioStreamAvailable(stream) > sample_size * max_buf_size >> 1; still_full = SDL.SDL_AudioStreamAvailable(stream) > sample_size * max_buf_size >> 1;
if (!audio_sync or !still_full) break; if (!sync_audio or !still_full) break;
} }
} }
fn videoSync(timer: *Timer, wake_time: u64) u64 { pub fn runUnsynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*FpsTracker) void {
log.info("Emulation thread w/out video sync", .{});
if (fps) |tracker| {
log.info("FPS Tracking Enabled", .{});
while (!quit.load(.SeqCst)) {
runFrame(sched, cpu);
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
tracker.tick();
}
} else {
while (!quit.load(.SeqCst)) {
runFrame(sched, cpu);
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
}
}
}
pub fn runSynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*FpsTracker) void {
log.info("Emulation thread w/ video sync", .{});
var timer = Timer.start() catch std.debug.panic("Failed to initialize std.timer.Timer", .{});
var wake_time: u64 = frame_period;
if (fps) |tracker| {
log.info("FPS Tracking Enabled", .{});
while (!quit.load(.SeqCst)) {
runFrame(sched, cpu);
const new_wake_time = blockOnVideo(&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
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
if (!sync_audio) spinLoop(&timer, wake_time);
wake_time = new_wake_time;
tracker.tick();
}
} else {
while (!quit.load(.SeqCst)) {
runFrame(sched, cpu);
const new_wake_time = blockOnVideo(&timer, wake_time);
// see above comment
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
if (!sync_audio) spinLoop(&timer, wake_time);
wake_time = new_wake_time;
}
}
}
inline fn blockOnVideo(timer: *Timer, wake_time: u64) u64 {
// Use the OS scheduler to put the emulation thread to sleep // Use the OS scheduler to put the emulation thread to sleep
const recalculated = sleep(timer, wake_time); const maybe_recalc_wake_time = sleep(timer, wake_time);
// If sleep() determined we need to adjust our wake up time, do so // If sleep() determined we need to adjust our wake up time, do so
// otherwise predict our next wake up time according to the frame period // otherwise predict our next wake up time according to the frame period
return recalculated orelse wake_time + frame_period; return if (maybe_recalc_wake_time) |recalc| recalc else wake_time + frame_period;
}
pub fn runBusyLoop(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi) void {
log.info("Emulation thread with video sync using busy loop", .{});
var timer = Timer.start() catch unreachable;
var wake_time: u64 = frame_period;
while (!quit.load(.SeqCst)) {
runFrame(sched, cpu);
spinLoop(&timer, wake_time);
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
// Update to the new wake time
wake_time += frame_period;
}
} }
// TODO: Better sleep impl?
fn sleep(timer: *Timer, wake_time: u64) ?u64 { fn sleep(timer: *Timer, wake_time: u64) ?u64 {
// 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();

View File

@ -10,7 +10,7 @@ const Bitfield = @import("bitfield").Bitfield;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const log = std.log.scoped(.PPU); const log = std.log.scoped(.PPU);
const pollDmaOnBlank = @import("bus/dma.zig").pollDmaOnBlank; const pollBlankingDma = @import("bus/dma.zig").pollBlankingDma;
/// This is used to generate byuu / Talurabi's Color Correction algorithm /// This is used to generate byuu / Talurabi's Color Correction algorithm
const COLOUR_LUT = genColourLut(); const COLOUR_LUT = genColourLut();
@ -562,7 +562,7 @@ pub const Ppu = struct {
}; };
} }
pub fn onHdrawEnd(self: *Self, cpu: *Arm7tdmi, late: u64) void { pub fn handleHDrawEnd(self: *Self, cpu: *Arm7tdmi, late: u64) void {
// Transitioning to a Hblank // Transitioning to a Hblank
if (self.dispstat.hblank_irq.read()) { if (self.dispstat.hblank_irq.read()) {
cpu.bus.io.irq.hblank.set(); cpu.bus.io.irq.hblank.set();
@ -572,13 +572,13 @@ pub const Ppu = struct {
// See if HBlank DMA is present and not enabled // See if HBlank DMA is present and not enabled
if (!self.dispstat.vblank.read()) if (!self.dispstat.vblank.read())
pollDmaOnBlank(cpu.bus, .HBlank); pollBlankingDma(cpu.bus, .HBlank);
self.dispstat.hblank.set(); self.dispstat.hblank.set();
self.sched.push(.HBlank, 68 * 4 -| late); self.sched.push(.HBlank, 68 * 4 -| late);
} }
pub fn onHblankEnd(self: *Self, cpu: *Arm7tdmi, late: u64) void { pub fn handleHBlankEnd(self: *Self, cpu: *Arm7tdmi, late: u64) void {
// The End of a Hblank (During Draw or Vblank) // The End of a Hblank (During Draw or Vblank)
const old_scanline = self.vcount.scanline.read(); const old_scanline = self.vcount.scanline.read();
const scanline = (old_scanline + 1) % 228; const scanline = (old_scanline + 1) % 228;
@ -614,7 +614,7 @@ pub const Ppu = struct {
self.aff_bg[1].latchRefPoints(); self.aff_bg[1].latchRefPoints();
// See if Vblank DMA is present and not enabled // See if Vblank DMA is present and not enabled
pollDmaOnBlank(cpu.bus, .VBlank); pollBlankingDma(cpu.bus, .VBlank);
} }
if (scanline == 227) self.dispstat.vblank.unset(); if (scanline == 227) self.dispstat.vblank.unset();
@ -808,6 +808,18 @@ const Window = struct {
self.in.raw = @truncate(u16, value); self.in.raw = @truncate(u16, value);
self.out.raw = @truncate(u16, value >> 16); self.out.raw = @truncate(u16, value >> 16);
} }
pub fn setInL(self: *Self, value: u8) void {
self.in.raw = (self.in.raw & 0xFF00) | value;
}
pub fn setInH(self: *Self, value: u8) void {
self.in.raw = (self.in.raw & 0x00FF) | (@as(u16, value) << 8);
}
pub fn setOutL(self: *Self, value: u8) void {
self.out.raw = (self.out.raw & 0xFF00) | value;
}
}; };
const Background = struct { const Background = struct {

View File

@ -43,22 +43,22 @@ pub const Scheduler = struct {
.Draw => { .Draw => {
// The end of a VDraw // The end of a VDraw
cpu.bus.ppu.drawScanline(); cpu.bus.ppu.drawScanline();
cpu.bus.ppu.onHdrawEnd(cpu, late); cpu.bus.ppu.handleHDrawEnd(cpu, late);
}, },
.TimerOverflow => |id| { .TimerOverflow => |id| {
switch (id) { switch (id) {
0 => cpu.bus.tim[0].onTimerExpire(cpu, late), 0 => cpu.bus.tim[0].handleOverflow(cpu, late),
1 => cpu.bus.tim[1].onTimerExpire(cpu, late), 1 => cpu.bus.tim[1].handleOverflow(cpu, late),
2 => cpu.bus.tim[2].onTimerExpire(cpu, late), 2 => cpu.bus.tim[2].handleOverflow(cpu, late),
3 => cpu.bus.tim[3].onTimerExpire(cpu, late), 3 => cpu.bus.tim[3].handleOverflow(cpu, late),
} }
}, },
.ApuChannel => |id| { .ApuChannel => |id| {
switch (id) { switch (id) {
0 => cpu.bus.apu.ch1.onToneSweepEvent(late), 0 => cpu.bus.apu.ch1.channelTimerOverflow(late),
1 => cpu.bus.apu.ch2.onToneEvent(late), 1 => cpu.bus.apu.ch2.channelTimerOverflow(late),
2 => cpu.bus.apu.ch3.onWaveEvent(late), 2 => cpu.bus.apu.ch3.channelTimerOverflow(late),
3 => cpu.bus.apu.ch4.onNoiseEvent(late), 3 => cpu.bus.apu.ch4.channelTimerOverflow(late),
} }
}, },
.RealTimeClock => { .RealTimeClock => {
@ -66,12 +66,12 @@ pub const Scheduler = struct {
if (device.kind != .Rtc or device.ptr == null) return; if (device.kind != .Rtc or device.ptr == null) return;
const clock = @ptrCast(*Clock, @alignCast(@alignOf(*Clock), device.ptr.?)); const clock = @ptrCast(*Clock, @alignCast(@alignOf(*Clock), device.ptr.?));
clock.onClockUpdate(late); clock.updateTime(late);
}, },
.FrameSequencer => cpu.bus.apu.onSequencerTick(late), .FrameSequencer => cpu.bus.apu.tickFrameSequencer(late),
.SampleAudio => cpu.bus.apu.sampleAudio(late), .SampleAudio => cpu.bus.apu.sampleAudio(late),
.HBlank => cpu.bus.ppu.onHblankEnd(cpu, late), // The end of a HBlank .HBlank => cpu.bus.ppu.handleHBlankEnd(cpu, late), // The end of a HBlank
.VBlank => cpu.bus.ppu.onHdrawEnd(cpu, late), // The end of a VBlank .VBlank => cpu.bus.ppu.handleHDrawEnd(cpu, late), // The end of a VBlank
} }
} }
} }

View File

@ -1,10 +1,9 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const known_folders = @import("known_folders"); const known_folders = @import("known_folders");
const clap = @import("clap"); const clap = @import("clap");
const config = @import("config.zig");
const Gui = @import("platform.zig").Gui; const Gui = @import("platform.zig").Gui;
const Bus = @import("core/Bus.zig"); const Bus = @import("core/Bus.zig");
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi; const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
@ -15,8 +14,11 @@ const Allocator = std.mem.Allocator;
const log = std.log.scoped(.Cli); const log = std.log.scoped(.Cli);
const width = @import("core/ppu.zig").width; const width = @import("core/ppu.zig").width;
const height = @import("core/ppu.zig").height; const height = @import("core/ppu.zig").height;
const cpu_logging = @import("core/emu.zig").cpu_logging;
pub const log_level = if (builtin.mode != .Debug) .info else std.log.default_level; pub const log_level = if (builtin.mode != .Debug) .info else std.log.default_level;
// TODO: Reimpl Logging
// CLI Arguments + Help Text // CLI Arguments + Help Text
const params = clap.parseParamsComptime( const params = clap.parseParamsComptime(
\\-h, --help Display this help and exit. \\-h, --help Display this help and exit.
@ -31,27 +33,14 @@ pub fn main() anyerror!void {
defer std.debug.assert(!gpa.deinit()); defer std.debug.assert(!gpa.deinit());
const allocator = gpa.allocator(); const allocator = gpa.allocator();
// TODO: Make Error message not Linux Specific
const data_path = try known_folders.getPath(allocator, .data) orelse exit("Unable to Determine XDG Data Path", .{});
defer allocator.free(data_path);
const config_path = try configFilePath(allocator, data_path);
defer allocator.free(config_path);
const save_path = try savePath(allocator, data_path);
defer allocator.free(save_path);
try config.load(allocator, config_path);
// Handle CLI Input // Handle CLI Input
const result = try clap.parse(clap.Help, &params, clap.parsers.default, .{}); const result = try clap.parse(clap.Help, &params, clap.parsers.default, .{});
defer result.deinit(); defer result.deinit();
const paths = try handleArguments(allocator, data_path, &result); const paths = try handleArguments(allocator, &result);
defer if (paths.save) |path| allocator.free(path); defer if (paths.save) |path| allocator.free(path);
const cpu_trace = config.config().debug.cpu_trace; const log_file: ?std.fs.File = if (cpu_logging) try std.fs.cwd().createFile("zba.log", .{}) else null;
const log_file: ?std.fs.File = if (cpu_trace) try std.fs.cwd().createFile("zba.log", .{}) else null;
defer if (log_file) |file| file.close(); defer if (log_file) |file| file.close();
// TODO: Take Emulator Init Code out of main.zig // TODO: Take Emulator Init Code out of main.zig
@ -72,15 +61,39 @@ pub fn main() anyerror!void {
try gui.run(&cpu, &scheduler); try gui.run(&cpu, &scheduler);
} }
pub fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const clap.Result(clap.Help, &params, clap.parsers.default)) !FilePaths { fn getSavePath(allocator: Allocator) !?[]const u8 {
const rom_path = romPath(result); const save_subpath = "zba" ++ [_]u8{std.fs.path.sep} ++ "save";
log.info("ROM path: {s}", .{rom_path});
const maybe_data_path = try known_folders.getPath(allocator, .data);
defer if (maybe_data_path) |path| allocator.free(path);
const save_path = if (maybe_data_path) |base| try std.fs.path.join(allocator, &[_][]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(allocator, .data, .{});
if (maybe_data_dir) |data_dir| try data_dir.makePath(save_subpath);
}
return save_path;
}
fn getRomPath(result: *const clap.Result(clap.Help, &params, clap.parsers.default)) ![]const u8 {
return switch (result.positionals.len) {
1 => result.positionals[0],
0 => std.debug.panic("ZBA requires a positional path to a GamePak ROM.\n", .{}),
else => std.debug.panic("ZBA received too many arguments.\n", .{}),
};
}
pub fn handleArguments(allocator: Allocator, result: *const clap.Result(clap.Help, &params, clap.parsers.default)) !FilePaths {
const rom_path = try getRomPath(result);
log.info("ROM path: {s}", .{rom_path});
const bios_path = result.args.bios; const bios_path = result.args.bios;
if (bios_path) |path| log.info("BIOS path: {s}", .{path}) else log.info("No BIOS provided", .{}); if (bios_path) |path| log.info("BIOS path: {s}", .{path}) else log.info("No BIOS provided", .{});
const save_path = try getSavePath(allocator);
const save_path = try savePath(allocator, data_path); if (save_path) |path| log.info("Save path: {s}", .{path});
log.info("Save path: {s}", .{save_path});
return FilePaths{ return FilePaths{
.rom = rom_path, .rom = rom_path,
@ -88,42 +101,3 @@ pub fn handleArguments(allocator: Allocator, data_path: []const u8, result: *con
.save = save_path, .save = save_path,
}; };
} }
fn configFilePath(allocator: Allocator, data_path: []const u8) ![]const u8 {
const path = try std.fs.path.join(allocator, &[_][]const u8{ data_path, "zba", "config.toml" });
// We try to create the file exclusively, meaning that we err out if the file already exists.
// All we care about is a file being there so we can just ignore that error in particular and
// continue down the happy pathj
std.fs.accessAbsolute(path, .{}) catch {
const file_handle = try std.fs.createFileAbsolute(path, .{});
file_handle.close();
};
return path;
}
fn savePath(allocator: Allocator, data_path: []const u8) ![]const u8 {
var dir = try std.fs.openDirAbsolute(data_path, .{});
defer dir.close();
// Will either make the path recursively, or just exit early since it already exists
try dir.makePath("zba" ++ [_]u8{std.fs.path.sep} ++ "save");
// FIXME: Do we have to allocate? :sad:
return try std.fs.path.join(allocator, &[_][]const u8{ data_path, "zba", "save" });
}
fn romPath(result: *const clap.Result(clap.Help, &params, clap.parsers.default)) []const u8 {
return switch (result.positionals.len) {
1 => result.positionals[0],
0 => exit("ZBA requires a path to a GamePak ROM\n", .{}),
else => exit("ZBA received too many positional arguments. \n", .{}),
};
}
fn exit(comptime format: []const u8, args: anytype) noreturn {
const stderr = std.io.getStdErr().writer();
stderr.print(format, args) catch {}; // Just exit already...
std.os.exit(1);
}

View File

@ -1,7 +1,6 @@
const std = @import("std"); const std = @import("std");
const SDL = @import("sdl2"); const SDL = @import("sdl2");
const emu = @import("core/emu.zig"); const emu = @import("core/emu.zig");
const config = @import("config.zig");
const Apu = @import("core/apu.zig").Apu; const Apu = @import("core/apu.zig").Apu;
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi; const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
@ -11,6 +10,8 @@ const FpsTracker = @import("util.zig").FpsTracker;
const span = @import("util.zig").span; const span = @import("util.zig").span;
const pitch = @import("core/ppu.zig").framebuf_pitch; const pitch = @import("core/ppu.zig").framebuf_pitch;
const scale = @import("core/emu.zig").win_scale;
const default_title: []const u8 = "ZBA"; const default_title: []const u8 = "ZBA";
pub const Gui = struct { pub const Gui = struct {
@ -27,19 +28,16 @@ pub const Gui = struct {
const ret = SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO | SDL.SDL_INIT_GAMECONTROLLER); const ret = SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO | SDL.SDL_INIT_GAMECONTROLLER);
if (ret < 0) panic(); if (ret < 0) panic();
const win_scale = @intCast(c_int, config.config().host.win_scale);
const window = SDL.SDL_CreateWindow( const window = SDL.SDL_CreateWindow(
default_title.ptr, default_title.ptr,
SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED,
SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED,
@as(c_int, width * win_scale), @as(c_int, width * scale),
@as(c_int, height * win_scale), @as(c_int, height * scale),
SDL.SDL_WINDOW_SHOWN, SDL.SDL_WINDOW_SHOWN,
) orelse panic(); ) orelse panic();
const renderer_flags = SDL.SDL_RENDERER_ACCELERATED | if (config.config().host.vsync) SDL.SDL_RENDERER_PRESENTVSYNC else 0; const renderer = SDL.SDL_CreateRenderer(window, -1, SDL.SDL_RENDERER_ACCELERATED | SDL.SDL_RENDERER_PRESENTVSYNC) orelse panic();
const renderer = SDL.SDL_CreateRenderer(window, -1, @bitCast(u32, renderer_flags)) orelse panic();
const texture = SDL.SDL_CreateTexture( const texture = SDL.SDL_CreateTexture(
renderer, renderer,
@ -60,9 +58,9 @@ pub const Gui = struct {
pub fn run(self: *Self, cpu: *Arm7tdmi, scheduler: *Scheduler) !void { pub fn run(self: *Self, cpu: *Arm7tdmi, scheduler: *Scheduler) !void {
var quit = std.atomic.Atomic(bool).init(false); var quit = std.atomic.Atomic(bool).init(false);
var tracker = FpsTracker.init(); var frame_rate = FpsTracker.init();
const thread = try std.Thread.spawn(.{}, emu.run, .{ &quit, scheduler, cpu, &tracker }); const thread = try std.Thread.spawn(.{}, emu.run, .{ &quit, &frame_rate, scheduler, cpu });
defer thread.join(); defer thread.join();
var title_buf: [0x100]u8 = [_]u8{0} ** 0x100; var title_buf: [0x100]u8 = [_]u8{0} ** 0x100;
@ -129,7 +127,7 @@ pub const Gui = struct {
_ = SDL.SDL_RenderCopy(self.renderer, self.texture, null, null); _ = SDL.SDL_RenderCopy(self.renderer, self.texture, null, null);
SDL.SDL_RenderPresent(self.renderer); SDL.SDL_RenderPresent(self.renderer);
const dyn_title = std.fmt.bufPrint(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, tracker.value() }) catch unreachable; const dyn_title = std.fmt.bufPrint(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, frame_rate.value() }) catch unreachable;
SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr); SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr);
} }

View File

@ -1,10 +1,10 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const config = @import("config.zig");
const Log2Int = std.math.Log2Int; const Log2Int = std.math.Log2Int;
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi; const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
const allow_unhandled_io = @import("core/emu.zig").allow_unhandled_io;
// Sign-Extend value of type `T` to type `U` // Sign-Extend value of type `T` to type `U`
pub fn sext(comptime T: type, comptime U: type, value: T) T { pub fn sext(comptime T: type, comptime U: type, value: T) T {
// U must have less bits than T // U must have less bits than T
@ -144,10 +144,8 @@ pub const io = struct {
} }
pub fn undef(comptime T: type, log: anytype, comptime format: []const u8, args: anytype) ?T { pub fn undef(comptime T: type, log: anytype, comptime format: []const u8, args: anytype) ?T {
const unhandled_io = config.config().debug.unhandled_io;
log.warn(format, args); log.warn(format, args);
if (builtin.mode == .Debug and !unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{}); if (builtin.mode == .Debug and !allow_unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{});
return null; return null;
} }
@ -155,13 +153,22 @@ pub const io = struct {
pub const write = struct { pub const write = struct {
pub fn undef(log: anytype, comptime format: []const u8, args: anytype) void { pub fn undef(log: anytype, comptime format: []const u8, args: anytype) void {
const unhandled_io = config.config().debug.unhandled_io;
log.warn(format, args); log.warn(format, args);
if (builtin.mode == .Debug and !unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{}); if (builtin.mode == .Debug and !allow_unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{});
} }
}; };
}; };
pub fn readUndefined(log: anytype, comptime format: []const u8, args: anytype) u8 {
log.warn(format, args);
if (builtin.mode == .Debug) std.debug.panic("TODO: Implement I/O Register", .{});
return 0;
}
pub fn writeUndefined(log: anytype, comptime format: []const u8, args: anytype) void {
log.warn(format, args);
if (builtin.mode == .Debug) std.debug.panic("TODO: Implement I/O Register", .{});
}
pub const Logger = struct { pub const Logger = struct {
const Self = @This(); const Self = @This();
@ -224,81 +231,3 @@ pub const Logger = struct {
}; };
const FmtArgTuple = std.meta.Tuple(&.{ u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32 }); const FmtArgTuple = std.meta.Tuple(&.{ u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32 });
pub const audio = struct {
const _io = @import("core/bus/io.zig");
const ToneSweep = @import("core/apu/ToneSweep.zig");
const Tone = @import("core/apu/Tone.zig");
const Wave = @import("core/apu/Wave.zig");
const Noise = @import("core/apu/Noise.zig");
pub const length = struct {
const FrameSequencer = @import("core/apu.zig").FrameSequencer;
/// Update State of Ch1, Ch2 and Ch3 length timer
pub fn update(comptime T: type, self: *T, fs: *const FrameSequencer, nrx34: _io.Frequency) void {
comptime std.debug.assert(T == ToneSweep or T == Tone or T == Wave);
// Write to NRx4 when FS's next step is not one that clocks the length counter
if (!fs.isLengthNext()) {
// If length_enable was disabled but is now enabled and length timer is not 0 already,
// decrement the length timer
if (!self.freq.length_enable.read() and nrx34.length_enable.read() and self.len_dev.timer != 0) {
self.len_dev.timer -= 1;
// If Length Timer is now 0 and trigger is clear, disable the channel
if (self.len_dev.timer == 0 and !nrx34.trigger.read()) self.enabled = false;
}
}
}
pub const ch4 = struct {
/// update state of ch4 length timer
pub fn update(self: *Noise, fs: *const FrameSequencer, nr44: _io.NoiseControl) void {
// Write to NRx4 when FS's next step is not one that clocks the length counter
if (!fs.isLengthNext()) {
// If length_enable was disabled but is now enabled and length timer is not 0 already,
// decrement the length timer
if (!self.cnt.length_enable.read() and nr44.length_enable.read() and self.len_dev.timer != 0) {
self.len_dev.timer -= 1;
// If Length Timer is now 0 and trigger is clear, disable the channel
if (self.len_dev.timer == 0 and !nr44.trigger.read()) self.enabled = false;
}
}
}
};
};
};
/// Sets the high bits of an integer to a value
pub inline fn setHi(comptime T: type, left: T, right: HalfInt(T)) T {
return switch (T) {
u32 => (left & 0xFFFF_0000) | right,
u16 => (left & 0xFF00) | right,
u8 => (left & 0xF0) | right,
else => @compileError("unsupported type"),
};
}
/// sets the low bits of an integer to a value
pub inline fn setLo(comptime T: type, left: T, right: HalfInt(T)) T {
return switch (T) {
u32 => (left & 0x0000_FFFF) | @as(u32, right) << 16,
u16 => (left & 0x00FF) | @as(u16, right) << 8,
u8 => (left & 0x0F) | @as(u8, right) << 4,
else => @compileError("unsupported type"),
};
}
/// The Integer type which corresponds to T with exactly half the amount of bits
fn HalfInt(comptime T: type) type {
const type_info = @typeInfo(T);
comptime std.debug.assert(type_info == .Int); // Type must be an integer
comptime std.debug.assert(type_info.Int.bits % 2 == 0); // Type must have an even amount of bits
return std.meta.Int(type_info.Int.signedness, type_info.Int.bits >> 1);
}