Compare commits
524 Commits
1f4e01bd32
...
f96e26eb76
|
@ -1,7 +1,7 @@
|
||||||
/.vscode
|
/.vscode
|
||||||
/bin
|
/bin
|
||||||
/zig-cache
|
**/zig-cache
|
||||||
/zig-out
|
**/zig-out
|
||||||
/docs
|
/docs
|
||||||
**/*.log
|
**/*.log
|
||||||
**/*.bin
|
**/*.bin
|
||||||
|
|
|
@ -10,3 +10,6 @@
|
||||||
[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
|
||||||
|
|
29
README.md
29
README.md
|
@ -1,14 +1,29 @@
|
||||||
# ZBA (working title)
|
# ZBA (working title)
|
||||||
An in-progress Game Boy Advance Emulator written in Zig ⚡!
|
A Game Boy Advance Emulator written in Zig ⚡!
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
I'm hardly the first to write a Game Boy Advance Emulator nor will I be the last. This project isn't going to compete with the GOATs like
|
||||||
|
[mGBA](https://github.com/mgba-emu) or [NanoBoyAdvance](https://github.com/nba-emu/NanoBoyAdvance). There aren't any interesting
|
||||||
|
ideas either like in [DSHBA](https://github.com/DenSinH/DSHBA).
|
||||||
|
|
||||||
|
This is a simple (read: incomplete) for-fun long-term project. I hope to get "mostly there", which to me means that I'm not missing any major hardware
|
||||||
|
features and the set of possible improvements would be in memory timing or in UI/UX. With respect to that goal, here's what's outstanding:
|
||||||
|
|
||||||
|
### TODO
|
||||||
|
- [ ] Affine Sprites
|
||||||
|
- [ ] Windowing (see [this branch](https://git.musuka.dev/paoda/zba/src/branch/window))
|
||||||
|
- [ ] Audio Resampler (Having issues with SDL2's)
|
||||||
|
- [ ] Immediate Mode GUI
|
||||||
|
- [ ] Refactoring for easy-ish perf boosts
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
- [ ] [jsmolka's GBA Test Collection](https://github.com/jsmolka/gba-tests)
|
- [x] [jsmolka's GBA Test Collection](https://github.com/jsmolka/gba-tests)
|
||||||
- [x] `arm.gba` and `thumb.gba`
|
- [x] `arm.gba` and `thumb.gba`
|
||||||
- [x] `flash64.gba`, `flash128.gba`, `none.gba`, and `sram.gba`
|
- [x] `flash64.gba`, `flash128.gba`, `none.gba`, and `sram.gba`
|
||||||
- [x] `hello.gba`, `shades.gba`, and `stripes.gba`
|
- [x] `hello.gba`, `shades.gba`, and `stripes.gba`
|
||||||
- [x] `memory.gba`
|
- [x] `memory.gba`
|
||||||
- [x] `bios.gba`
|
- [x] `bios.gba`
|
||||||
- [ ] `nes.gba`
|
- [x] `nes.gba`
|
||||||
- [ ] [DenSinH's GBA ROMs](https://github.com/DenSinH/GBARoms)
|
- [ ] [DenSinH's GBA ROMs](https://github.com/DenSinH/GBARoms)
|
||||||
- [x] `eeprom-test` and `flash-test`
|
- [x] `eeprom-test` and `flash-test`
|
||||||
- [x] `midikey2freq`
|
- [x] `midikey2freq`
|
||||||
|
@ -35,18 +50,20 @@ An in-progress Game Boy Advance Emulator written in Zig ⚡!
|
||||||
* [ARM7TDMI Data Sheet](https://www.dca.fee.unicamp.br/cursos/EA871/references/ARM/ARM7TDMIDataSheet.pdf)
|
* [ARM7TDMI Data Sheet](https://www.dca.fee.unicamp.br/cursos/EA871/references/ARM/ARM7TDMIDataSheet.pdf)
|
||||||
|
|
||||||
## Compiling
|
## Compiling
|
||||||
Most recently built on Zig [0.10.0-dev.3900+ab4b26d8a](https://github.com/ziglang/zig/tree/ab4b26d8a)
|
Most recently built on Zig [0.10.0-dev.4474+b41b35f57](https://github.com/ziglang/zig/tree/b41b35f57)
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
* [SDL.zig](https://github.com/MasterQ32/SDL.zig)
|
* [SDL.zig](https://github.com/MasterQ32/SDL.zig)
|
||||||
* [SDL2](https://www.libsdl.org/download-2.0.php)
|
* [SDL2](https://www.libsdl.org/download-2.0.php)
|
||||||
* [zig-clap](https://github.com/Hejsil/zig-clap)
|
* [zig-clap](https://github.com/Hejsil/zig-clap)
|
||||||
* [known-folders](https://github.com/ziglibs/known-folders)
|
* [known-folders](https://github.com/ziglibs/known-folders)
|
||||||
* [`bitfields.zig`](https://github.com/FlorenceOS/Florence/blob/aaa5a9e568197ad24780ec9adb421217530d4466/lib/util/bitfields.zig)
|
* [zig-toml](https://github.com/aeronavery/zig-toml)
|
||||||
|
* [zig-datetime](https://github.com/frmdstryr/zig-datetime)
|
||||||
|
* [`bitfields.zig`](https://github.com/FlorenceOS/Florence/blob/aaa5a9e568/lib/util/bitfields.zig)
|
||||||
|
|
||||||
`bitfields.zig` from [FlorenceOS](https://github.com/FlorenceOS) is included under `lib/util/bitfield.zig`.
|
`bitfields.zig` from [FlorenceOS](https://github.com/FlorenceOS) is included under `lib/util/bitfield.zig`.
|
||||||
|
|
||||||
Use `git submodule update --init` from the project root to pull the git submodules `SDL.zig`, `zig-clap`, and `known-folders`
|
Use `git submodule update --init` from the project root to pull the git submodules `SDL.zig`, `zig-clap`, `known-folders`, `zig-toml` and `zig-datetime`
|
||||||
|
|
||||||
Be sure to provide SDL2 using:
|
Be sure to provide SDL2 using:
|
||||||
* Linux: Your distro's package manager
|
* Linux: Your distro's package manager
|
||||||
|
|
11
build.zig
11
build.zig
|
@ -13,6 +13,9 @@ pub fn build(b: *std.build.Builder) void {
|
||||||
const mode = b.standardReleaseOptions();
|
const mode = b.standardReleaseOptions();
|
||||||
|
|
||||||
const exe = b.addExecutable("zba", "src/main.zig");
|
const exe = b.addExecutable("zba", "src/main.zig");
|
||||||
|
exe.setMainPkgPath("."); // Necessary so that src/main.zig can embed example.toml
|
||||||
|
exe.setTarget(target);
|
||||||
|
|
||||||
// Known Folders (%APPDATA%, XDG, etc.)
|
// Known Folders (%APPDATA%, XDG, etc.)
|
||||||
exe.addPackagePath("known_folders", "lib/known-folders/known-folders.zig");
|
exe.addPackagePath("known_folders", "lib/known-folders/known-folders.zig");
|
||||||
|
|
||||||
|
@ -26,13 +29,17 @@ 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");
|
||||||
|
|
||||||
|
// OpenGL 3.3 Bindings
|
||||||
|
exe.addPackagePath("gl", "lib/gl.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);
|
||||||
|
|
||||||
exe.addPackage(sdk.getNativePackage("sdl2"));
|
exe.addPackage(sdk.getNativePackage("sdl2"));
|
||||||
|
|
||||||
exe.setTarget(target);
|
|
||||||
exe.setBuildMode(mode);
|
exe.setBuildMode(mode);
|
||||||
exe.install();
|
exe.install();
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
[Host]
|
||||||
|
# Using nearest-neighbour scaling, how many times the native resolution
|
||||||
|
# of the game bow should the screen be?
|
||||||
|
win_scale = 4
|
||||||
|
# Enable VSYNC on the UI thread
|
||||||
|
vsync = true
|
||||||
|
# Mute ZBA
|
||||||
|
mute = false
|
||||||
|
|
||||||
|
[Guest]
|
||||||
|
# Sync Emulation to Audio
|
||||||
|
audio_sync = false
|
||||||
|
# Sync Emulation to Video
|
||||||
|
video_sync = false
|
||||||
|
# Force RTC support
|
||||||
|
force_rtc = false
|
||||||
|
# Skip BIOS
|
||||||
|
skip_bios = false
|
||||||
|
|
||||||
|
[Debug]
|
||||||
|
# Enable detailed CPU logs
|
||||||
|
cpu_trace = false
|
||||||
|
# When false and builtin.mode == .Debug, ZBA will panic
|
||||||
|
# on unknown I/O reads
|
||||||
|
unhandled_io = true
|
File diff suppressed because it is too large
Load Diff
|
@ -1 +1 @@
|
||||||
Subproject commit 4f4196fc3bc95c4bd3b12ce2e4a5f1050742cd3c
|
Subproject commit e5d09c4b2d121025ad7195b2de704451e6306807
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 5dfa919e03b446c66b295c04bef9bdecabd4276f
|
|
@ -0,0 +1,83 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const toml = @import("toml");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const log = std.log.scoped(.Config);
|
||||||
|
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,
|
||||||
|
/// Mute ZBA
|
||||||
|
mute: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
/// Skip BIOS
|
||||||
|
skip_bios: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Settings related to debugging ZBA
|
||||||
|
const Debug = struct {
|
||||||
|
/// Enable CPU Trace logs
|
||||||
|
cpu_trace: bool = false,
|
||||||
|
/// If false and ZBA is built 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();
|
||||||
|
|
||||||
|
log.info("loaded from {s}", .{config_path});
|
||||||
|
|
||||||
|
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 (host.Table.keys.get("mute")) |mute| state.host.mute = mute.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 (guest.Table.keys.get("skip_bios")) |skip| state.guest.skip_bios = skip.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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -46,7 +46,7 @@ iwram: Iwram,
|
||||||
ewram: Ewram,
|
ewram: Ewram,
|
||||||
io: Io,
|
io: Io,
|
||||||
|
|
||||||
cpu: ?*Arm7tdmi,
|
cpu: *Arm7tdmi,
|
||||||
sched: *Scheduler,
|
sched: *Scheduler,
|
||||||
|
|
||||||
pub fn init(self: *Self, allocator: Allocator, sched: *Scheduler, cpu: *Arm7tdmi, paths: FilePaths) !void {
|
pub fn init(self: *Self, allocator: Allocator, sched: *Scheduler, cpu: *Arm7tdmi, paths: FilePaths) !void {
|
||||||
|
@ -82,9 +82,9 @@ pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
|
||||||
// General Internal Memory
|
// General Internal Memory
|
||||||
0x00 => blk: {
|
0x00 => blk: {
|
||||||
if (address < Bios.size)
|
if (address < Bios.size)
|
||||||
break :blk self.bios.dbgRead(T, self.cpu.?.r[15], aligned_addr);
|
break :blk self.bios.dbgRead(T, self.cpu.r[15], aligned_addr);
|
||||||
|
|
||||||
break :blk self.readOpenBus(T, address);
|
break :blk self.openBus(T, address);
|
||||||
},
|
},
|
||||||
0x02 => self.ewram.read(T, aligned_addr),
|
0x02 => self.ewram.read(T, aligned_addr),
|
||||||
0x03 => self.iwram.read(T, aligned_addr),
|
0x03 => self.iwram.read(T, aligned_addr),
|
||||||
|
@ -109,48 +109,63 @@ pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
|
||||||
|
|
||||||
break :blk @as(T, value) * multiplier;
|
break :blk @as(T, value) * multiplier;
|
||||||
},
|
},
|
||||||
else => self.readOpenBus(T, address),
|
else => self.openBus(T, address),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn readIo(self: *const Self, comptime T: type, unaligned_address: u32) T {
|
fn readIo(self: *const Self, comptime T: type, unaligned_address: u32) T {
|
||||||
const maybe_value = io.read(self, T, forceAlign(T, unaligned_address));
|
const maybe_value = io.read(self, T, forceAlign(T, unaligned_address));
|
||||||
return if (maybe_value) |value| value else self.readOpenBus(T, unaligned_address);
|
return if (maybe_value) |value| value else self.openBus(T, unaligned_address);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn readOpenBus(self: *const Self, comptime T: type, address: u32) T {
|
fn openBus(self: *const Self, comptime T: type, address: u32) T {
|
||||||
const r15 = self.cpu.?.r[15];
|
const r15 = self.cpu.r[15];
|
||||||
|
|
||||||
const word = blk: {
|
const word = blk: {
|
||||||
// If u32 Open Bus, read recently fetched opcode (PC + 8)
|
// If Arm, get the most recently fetched instruction (PC + 8)
|
||||||
if (!self.cpu.?.cpsr.t.read()) break :blk self.dbgRead(u32, r15 + 4);
|
if (!self.cpu.cpsr.t.read()) break :blk self.cpu.pipe.stage[1].?;
|
||||||
|
|
||||||
const page = @truncate(u8, r15 >> 24);
|
const page = @truncate(u8, r15 >> 24);
|
||||||
|
|
||||||
|
// PC + 2 = stage[0]
|
||||||
|
// PC + 4 = stage[1]
|
||||||
|
// PC + 6 = Need a Debug Read for this?
|
||||||
|
|
||||||
switch (page) {
|
switch (page) {
|
||||||
// EWRAM, PALRAM, VRAM, and Game ROM (16-bit)
|
// EWRAM, PALRAM, VRAM, and Game ROM (16-bit)
|
||||||
0x02, 0x05, 0x06, 0x08...0x0D => {
|
0x02, 0x05, 0x06, 0x08...0x0D => {
|
||||||
// (PC + 4)
|
const halfword: u32 = @truncate(u16, self.cpu.pipe.stage[1].?);
|
||||||
const halfword = self.dbgRead(u16, r15 + 2);
|
break :blk halfword << 16 | halfword;
|
||||||
|
|
||||||
break :blk @as(u32, halfword) << 16 | halfword;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// BIOS or OAM (32-bit)
|
// BIOS or OAM (32-bit)
|
||||||
0x00, 0x07 => {
|
0x00, 0x07 => {
|
||||||
// Aligned: (PC + 6) | (PC + 4)
|
// Aligned: (PC + 6) | (PC + 4)
|
||||||
// Unaligned: (PC + 4) | (PC + 2)
|
// Unaligned: (PC + 4) | (PC + 2)
|
||||||
const offset: u32 = if (address & 3 == 0b00) 2 else 0;
|
const aligned = address & 3 == 0b00;
|
||||||
|
|
||||||
break :blk @as(u32, self.dbgRead(u16, r15 + 2 + offset)) << 16 | self.dbgRead(u16, r15 + offset);
|
// TODO: What to do on PC + 6?
|
||||||
|
const high: u32 = if (aligned) self.dbgRead(u16, r15 + 4) else @truncate(u16, self.cpu.pipe.stage[1].?);
|
||||||
|
const low: u32 = @truncate(u16, self.cpu.pipe.stage[@boolToInt(aligned)].?);
|
||||||
|
|
||||||
|
break :blk high << 16 | low;
|
||||||
},
|
},
|
||||||
|
|
||||||
// IWRAM (16-bit but special)
|
// IWRAM (16-bit but special)
|
||||||
0x03 => {
|
0x03 => {
|
||||||
// Aligned: (PC + 2) | (PC + 4)
|
// Aligned: (PC + 2) | (PC + 4)
|
||||||
// Unaligned: (PC + 4) | (PC + 2)
|
// Unaligned: (PC + 4) | (PC + 2)
|
||||||
const offset: u32 = if (address & 3 == 0b00) 2 else 0;
|
const aligned = address & 3 == 0b00;
|
||||||
|
|
||||||
break :blk @as(u32, self.dbgRead(u16, r15 + 2 - offset)) << 16 | self.dbgRead(u16, r15 + offset);
|
const high: u32 = @truncate(u16, self.cpu.pipe.stage[1 - @boolToInt(aligned)].?);
|
||||||
|
const low: u32 = @truncate(u16, self.cpu.pipe.stage[@boolToInt(aligned)].?);
|
||||||
|
|
||||||
|
break :blk high << 16 | low;
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
log.err("THUMB open bus read from 0x{X:0>2} page @0x{X:0>8}", .{ page, address });
|
||||||
|
@panic("invariant most-likely broken");
|
||||||
},
|
},
|
||||||
else => unreachable,
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -167,9 +182,9 @@ pub fn read(self: *Self, comptime T: type, address: u32) T {
|
||||||
// General Internal Memory
|
// General Internal Memory
|
||||||
0x00 => blk: {
|
0x00 => blk: {
|
||||||
if (address < Bios.size)
|
if (address < Bios.size)
|
||||||
break :blk self.bios.read(T, self.cpu.?.r[15], aligned_addr);
|
break :blk self.bios.read(T, self.cpu.r[15], aligned_addr);
|
||||||
|
|
||||||
break :blk self.readOpenBus(T, address);
|
break :blk self.openBus(T, address);
|
||||||
},
|
},
|
||||||
0x02 => self.ewram.read(T, aligned_addr),
|
0x02 => self.ewram.read(T, aligned_addr),
|
||||||
0x03 => self.iwram.read(T, aligned_addr),
|
0x03 => self.iwram.read(T, aligned_addr),
|
||||||
|
@ -194,7 +209,7 @@ pub fn read(self: *Self, comptime T: type, address: u32) T {
|
||||||
|
|
||||||
break :blk @as(T, value) * multiplier;
|
break :blk @as(T, value) * multiplier;
|
||||||
},
|
},
|
||||||
else => self.readOpenBus(T, address),
|
else => self.openBus(T, address),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
1097
src/core/apu.zig
1097
src/core/apu.zig
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,142 @@
|
||||||
|
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;
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
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;
|
||||||
|
}
|
|
@ -0,0 +1,184 @@
|
||||||
|
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;
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
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);
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
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;
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
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;
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
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;
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
|
@ -12,6 +12,36 @@ 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, .{});
|
||||||
|
@ -31,34 +61,3 @@ 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 + 8));
|
|
||||||
}
|
|
||||||
|
|
||||||
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 + 8));
|
|
||||||
}
|
|
||||||
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,21 +7,6 @@ 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;
|
||||||
|
|
||||||
|
@ -39,3 +24,18 @@ 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;
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
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;
|
||||||
|
@ -8,7 +10,6 @@ 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();
|
||||||
|
@ -19,78 +20,11 @@ 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.isLarge()) {
|
if (self.buf.len > 0x100_0000) { // Large
|
||||||
// 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)
|
||||||
|
@ -142,11 +76,19 @@ 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.isLarge()) {
|
if (self.buf.len > 0x100_0000) { // Large
|
||||||
// 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)
|
||||||
|
@ -161,6 +103,35 @@ 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)),
|
||||||
|
@ -175,7 +146,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.isLarge()) {
|
if (self.buf.len > 0x100_0000) { // Large
|
||||||
// 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)
|
||||||
|
@ -213,12 +184,59 @@ pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(self: *const Self, i: u32) u8 {
|
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, rom_path: []const u8, save_path: ?[]const u8) !Self {
|
||||||
@setRuntimeSafety(false);
|
const file = try std.fs.cwd().openFile(rom_path, .{});
|
||||||
if (i < self.buf.len) return self.buf[i];
|
defer file.close();
|
||||||
|
|
||||||
const lhs = i >> 1 & 0xFFFF;
|
const file_buf = try file.readToEndAlloc(allocator, try file.getEndPos());
|
||||||
return @truncate(u8, lhs >> 8 * @truncate(u5, i & 1));
|
const title = file_buf[0xA0..0xAC].*;
|
||||||
|
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" {
|
||||||
|
|
|
@ -7,21 +7,6 @@ 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;
|
||||||
|
|
||||||
|
@ -39,3 +24,18 @@ 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;
|
||||||
|
}
|
||||||
|
|
|
@ -2,9 +2,13 @@ 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 },
|
||||||
|
@ -14,6 +18,8 @@ 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();
|
||||||
|
|
||||||
|
@ -35,122 +41,6 @@ pub const Backup = struct {
|
||||||
None,
|
None,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, kind: Kind, title: [12]u8, path: ?[]const u8) !Self {
|
|
||||||
log.info("Kind: {}", .{kind});
|
|
||||||
|
|
||||||
const buf_size: usize = switch (kind) {
|
|
||||||
.Sram => 0x8000, // 32K
|
|
||||||
.Flash => 0x10000, // 64K
|
|
||||||
.Flash1M => 0x20000, // 128K
|
|
||||||
.None, .Eeprom => 0, // EEPROM is handled upon first Read Request to it
|
|
||||||
};
|
|
||||||
|
|
||||||
const buf = try allocator.alloc(u8, buf_size);
|
|
||||||
std.mem.set(u8, buf, 0xFF);
|
|
||||||
|
|
||||||
var backup = Self{
|
|
||||||
.buf = buf,
|
|
||||||
.allocator = allocator,
|
|
||||||
.kind = kind,
|
|
||||||
.title = title,
|
|
||||||
.save_path = path,
|
|
||||||
.flash = Flash.init(),
|
|
||||||
.eeprom = Eeprom.init(allocator),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (backup.save_path) |p| backup.loadSaveFromDisk(allocator, p) catch |e| log.err("Failed to load save: {}", .{e});
|
|
||||||
return backup;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn guessKind(rom: []const u8) Kind {
|
|
||||||
for (backup_kinds) |needle| {
|
|
||||||
const needle_len = needle.str.len;
|
|
||||||
|
|
||||||
var i: usize = 0;
|
|
||||||
while ((i + needle_len) < rom.len) : (i += 1) {
|
|
||||||
if (std.mem.eql(u8, needle.str, rom[i..][0..needle_len])) return needle.kind;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return .None;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
|
||||||
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);
|
|
||||||
|
|
||||||
// FIXME: Don't rely on this lol
|
|
||||||
if (std.mem.eql(u8, file_path[file_path.len - 12 .. file_path.len], "untitled.sav")) {
|
|
||||||
return log.err("ROM header lacks title, no save loaded", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
const file: std.fs.File = try std.fs.openFileAbsolute(file_path, .{});
|
|
||||||
const file_buf = try file.readToEndAlloc(allocator, try file.getEndPos());
|
|
||||||
defer allocator.free(file_buf);
|
|
||||||
|
|
||||||
switch (self.kind) {
|
|
||||||
.Sram, .Flash, .Flash1M => {
|
|
||||||
if (self.buf.len == file_buf.len) {
|
|
||||||
std.mem.copy(u8, self.buf, file_buf);
|
|
||||||
return log.info("Loaded Save from {s}", .{file_path});
|
|
||||||
}
|
|
||||||
|
|
||||||
log.err("{s} is {} bytes, but we expected {} bytes", .{ file_path, file_buf.len, self.buf.len });
|
|
||||||
},
|
|
||||||
.Eeprom => {
|
|
||||||
if (file_buf.len == 0x200 or file_buf.len == 0x2000) {
|
|
||||||
self.eeprom.kind = if (file_buf.len == 0x200) .Small else .Large;
|
|
||||||
|
|
||||||
self.buf = try allocator.alloc(u8, file_buf.len);
|
|
||||||
std.mem.copy(u8, self.buf, file_buf);
|
|
||||||
return log.info("Loaded Save from {s}", .{file_path});
|
|
||||||
}
|
|
||||||
|
|
||||||
log.err("EEPROM can either be 0x200 bytes or 0x2000 byes, but {s} was {X:} bytes", .{
|
|
||||||
file_path,
|
|
||||||
file_buf.len,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
.None => return SaveError.UnsupportedBackupKind,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn getSaveFilePath(self: *const Self, allocator: Allocator, path: []const u8) ![]const u8 {
|
|
||||||
const filename = try self.getSaveFilename(allocator);
|
|
||||||
defer allocator.free(filename);
|
|
||||||
|
|
||||||
return try std.fs.path.join(allocator, &[_][]const u8{ path, filename });
|
|
||||||
}
|
|
||||||
|
|
||||||
fn getSaveFilename(self: *const Self, allocator: Allocator) ![]const u8 {
|
|
||||||
const title_str = span(&escape(self.title));
|
|
||||||
const name = if (title_str.len != 0) title_str else "untitled";
|
|
||||||
|
|
||||||
return try std.mem.concat(allocator, u8, &[_][]const u8{ name, ".sav" });
|
|
||||||
}
|
|
||||||
|
|
||||||
fn writeSaveToDisk(self: Self, allocator: Allocator, path: []const u8) !void {
|
|
||||||
const file_path = try self.getSaveFilePath(allocator, path);
|
|
||||||
defer allocator.free(file_path);
|
|
||||||
|
|
||||||
switch (self.kind) {
|
|
||||||
.Sram, .Flash, .Flash1M, .Eeprom => {
|
|
||||||
const file = try std.fs.createFileAbsolute(file_path, .{});
|
|
||||||
defer file.close();
|
|
||||||
|
|
||||||
try file.writeAll(self.buf);
|
|
||||||
log.info("Wrote Save to {s}", .{file_path});
|
|
||||||
},
|
|
||||||
else => return SaveError.UnsupportedBackupKind,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read(self: *const Self, address: usize) u8 {
|
pub fn read(self: *const Self, address: usize) u8 {
|
||||||
const addr = address & 0xFFFF;
|
const addr = address & 0xFFFF;
|
||||||
|
|
||||||
|
@ -184,7 +74,7 @@ pub const Backup = struct {
|
||||||
switch (self.kind) {
|
switch (self.kind) {
|
||||||
.Flash, .Flash1M => {
|
.Flash, .Flash1M => {
|
||||||
if (self.flash.prep_write) return self.flash.write(self.buf, addr, byte);
|
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);
|
if (self.flash.shouldEraseSector(addr, byte)) return self.flash.erase(self.buf, addr);
|
||||||
|
|
||||||
switch (addr) {
|
switch (addr) {
|
||||||
0x0000 => if (self.kind == .Flash1M and self.flash.set_bank) {
|
0x0000 => if (self.kind == .Flash1M and self.flash.set_bank) {
|
||||||
|
@ -209,358 +99,121 @@ pub const Backup = struct {
|
||||||
.None, .Eeprom => {},
|
.None, .Eeprom => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const Needle = struct {
|
pub fn init(allocator: Allocator, kind: Kind, title: [12]u8, path: ?[]const u8) !Self {
|
||||||
const Self = @This();
|
log.info("Kind: {}", .{kind});
|
||||||
|
|
||||||
str: []const u8,
|
const buf_size: usize = switch (kind) {
|
||||||
kind: Backup.Kind,
|
.Sram => 0x8000, // 32K
|
||||||
|
.Flash => 0x10000, // 64K
|
||||||
fn init(str: []const u8, kind: Backup.Kind) Self {
|
.Flash1M => 0x20000, // 128K
|
||||||
return .{
|
.None, .Eeprom => 0, // EEPROM is handled upon first Read Request to it
|
||||||
.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 {
|
const buf = try allocator.alloc(u8, buf_size);
|
||||||
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);
|
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;
|
var backup = Self{
|
||||||
}
|
.buf = buf,
|
||||||
|
|
||||||
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,
|
.allocator = allocator,
|
||||||
};
|
.kind = kind,
|
||||||
}
|
.title = title,
|
||||||
|
.save_path = path,
|
||||||
pub fn read(self: *Self) u1 {
|
.flash = Flash.create(),
|
||||||
return self.reader.read();
|
.eeprom = Eeprom.create(allocator),
|
||||||
}
|
|
||||||
|
|
||||||
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| {
|
if (backup.save_path) |p| backup.readSave(allocator, p) catch |e| log.err("Failed to load save: {}", .{e});
|
||||||
log.err("Failed to resize EEPROM buf to {} bytes", .{len});
|
return backup;
|
||||||
std.debug.panic("EEPROM entered irrecoverable state {}", .{e});
|
}
|
||||||
};
|
|
||||||
std.mem.set(u8, buf.*, 0xFF);
|
pub fn deinit(self: *Self) void {
|
||||||
|
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| {
|
||||||
|
const needle_len = needle.str.len;
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
while ((i + needle_len) < rom.len) : (i += 1) {
|
||||||
|
if (std.mem.eql(u8, needle.str, rom[i..][0..needle_len])) return needle.kind;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.state == .RequestEnd) {
|
return .None;
|
||||||
if (bit != 0) log.debug("EEPROM Request did not end in 0u1. TODO: is this ok?", .{});
|
|
||||||
self.state = .Ready;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (self.state) {
|
fn readSave(self: *Self, allocator: Allocator, path: []const u8) !void {
|
||||||
.Ready => self.writer.requestWrite(bit),
|
const file_path = try self.savePath(allocator, path);
|
||||||
.Read, .Write => self.writer.addressWrite(self.kind, bit),
|
defer allocator.free(file_path);
|
||||||
.WriteTransfer => self.writer.dataWrite(bit),
|
|
||||||
.RequestEnd => unreachable, // We return early just above this block
|
// FIXME: Don't rely on this lol
|
||||||
|
if (std.mem.eql(u8, file_path[file_path.len - 12 .. file_path.len], "untitled.sav")) {
|
||||||
|
return log.err("ROM header lacks title, no save loaded", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
self.tick(buf.*);
|
const file: std.fs.File = try std.fs.openFileAbsolute(file_path, .{});
|
||||||
}
|
const file_buf = try file.readToEndAlloc(allocator, try file.getEndPos());
|
||||||
|
defer allocator.free(file_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) {
|
switch (self.kind) {
|
||||||
.Large => {
|
.Sram, .Flash, .Flash1M => {
|
||||||
if (self.writer.len() == 14) {
|
if (self.buf.len == file_buf.len) {
|
||||||
const addr = @intCast(u10, self.writer.finish());
|
std.mem.copy(u8, self.buf, file_buf);
|
||||||
const value = std.mem.readIntSliceLittle(u64, buf[@as(u13, addr) * 8 ..][0..8]);
|
return log.info("Loaded Save from {s}", .{file_path});
|
||||||
|
}
|
||||||
|
|
||||||
self.reader.configure(value);
|
log.err("{s} is {} bytes, but we expected {} bytes", .{ file_path, file_buf.len, self.buf.len });
|
||||||
self.state = .RequestEnd;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
.Small => {
|
.Eeprom => {
|
||||||
if (self.writer.len() == 6) {
|
if (file_buf.len == 0x200 or file_buf.len == 0x2000) {
|
||||||
// FIXME: Duplicated code from above
|
self.eeprom.kind = if (file_buf.len == 0x200) .Small else .Large;
|
||||||
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.buf = try allocator.alloc(u8, file_buf.len);
|
||||||
self.state = .RequestEnd;
|
std.mem.copy(u8, self.buf, file_buf);
|
||||||
|
return log.info("Loaded Save from {s}", .{file_path});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.err("EEPROM can either be 0x200 bytes or 0x2000 byes, but {s} was {X:} bytes", .{
|
||||||
|
file_path,
|
||||||
|
file_buf.len,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
else => log.err("Unable to calculate EEPROM read address. EEPROM size UNKNOWN", .{}),
|
.None => return SaveError.Unsupported,
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
.Write => {
|
|
||||||
|
fn savePath(self: *const Self, allocator: Allocator, path: []const u8) ![]const u8 {
|
||||||
|
const filename = try self.saveName(allocator);
|
||||||
|
defer allocator.free(filename);
|
||||||
|
|
||||||
|
return try std.fs.path.join(allocator, &[_][]const u8{ path, filename });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn saveName(self: *const Self, allocator: Allocator) ![]const u8 {
|
||||||
|
const title_str = span(&escape(self.title));
|
||||||
|
const name = if (title_str.len != 0) title_str else "untitled";
|
||||||
|
|
||||||
|
return try std.mem.concat(allocator, u8, &[_][]const u8{ name, ".sav" });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeSave(self: Self, allocator: Allocator, path: []const u8) !void {
|
||||||
|
const file_path = try self.savePath(allocator, path);
|
||||||
|
defer allocator.free(file_path);
|
||||||
|
|
||||||
switch (self.kind) {
|
switch (self.kind) {
|
||||||
.Large => {
|
.Sram, .Flash, .Flash1M, .Eeprom => {
|
||||||
if (self.writer.len() == 14) {
|
const file = try std.fs.createFileAbsolute(file_path, .{});
|
||||||
self.addr = @intCast(u10, self.writer.finish());
|
defer file.close();
|
||||||
self.state = .WriteTransfer;
|
|
||||||
}
|
try file.writeAll(self.buf);
|
||||||
|
log.info("Wrote Save to {s}", .{file_path});
|
||||||
},
|
},
|
||||||
.Small => {
|
else => return SaveError.Unsupported,
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
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);
|
||||||
|
}
|
|
@ -0,0 +1,269 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -8,6 +8,9 @@ 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() };
|
||||||
}
|
}
|
||||||
|
@ -40,48 +43,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].setSad(value),
|
0xB0 => dma.*[0].setDmasad(value),
|
||||||
0xB4 => dma.*[0].setDad(value),
|
0xB4 => dma.*[0].setDmadad(value),
|
||||||
0xB8 => dma.*[0].setCnt(value),
|
0xB8 => dma.*[0].setDmacnt(value),
|
||||||
0xBC => dma.*[1].setSad(value),
|
0xBC => dma.*[1].setDmasad(value),
|
||||||
0xC0 => dma.*[1].setDad(value),
|
0xC0 => dma.*[1].setDmadad(value),
|
||||||
0xC4 => dma.*[1].setCnt(value),
|
0xC4 => dma.*[1].setDmacnt(value),
|
||||||
0xC8 => dma.*[2].setSad(value),
|
0xC8 => dma.*[2].setDmasad(value),
|
||||||
0xCC => dma.*[2].setDad(value),
|
0xCC => dma.*[2].setDmadad(value),
|
||||||
0xD0 => dma.*[2].setCnt(value),
|
0xD0 => dma.*[2].setDmacnt(value),
|
||||||
0xD4 => dma.*[3].setSad(value),
|
0xD4 => dma.*[3].setDmasad(value),
|
||||||
0xD8 => dma.*[3].setDad(value),
|
0xD8 => dma.*[3].setDmadad(value),
|
||||||
0xDC => dma.*[3].setCnt(value),
|
0xDC => dma.*[3].setDmacnt(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].setSad(setU32L(dma.*[0].sad, value)),
|
0xB0 => dma.*[0].setDmasad(setLo(u32, dma.*[0].sad, value)),
|
||||||
0xB2 => dma.*[0].setSad(setU32H(dma.*[0].sad, value)),
|
0xB2 => dma.*[0].setDmasad(setHi(u32, dma.*[0].sad, value)),
|
||||||
0xB4 => dma.*[0].setDad(setU32L(dma.*[0].dad, value)),
|
0xB4 => dma.*[0].setDmadad(setLo(u32, dma.*[0].dad, value)),
|
||||||
0xB6 => dma.*[0].setDad(setU32H(dma.*[0].dad, value)),
|
0xB6 => dma.*[0].setDmadad(setHi(u32, dma.*[0].dad, value)),
|
||||||
0xB8 => dma.*[0].setCntL(value),
|
0xB8 => dma.*[0].setDmacntL(value),
|
||||||
0xBA => dma.*[0].setCntH(value),
|
0xBA => dma.*[0].setDmacntH(value),
|
||||||
|
|
||||||
0xBC => dma.*[1].setSad(setU32L(dma.*[1].sad, value)),
|
0xBC => dma.*[1].setDmasad(setLo(u32, dma.*[1].sad, value)),
|
||||||
0xBE => dma.*[1].setSad(setU32H(dma.*[1].sad, value)),
|
0xBE => dma.*[1].setDmasad(setHi(u32, dma.*[1].sad, value)),
|
||||||
0xC0 => dma.*[1].setDad(setU32L(dma.*[1].dad, value)),
|
0xC0 => dma.*[1].setDmadad(setLo(u32, dma.*[1].dad, value)),
|
||||||
0xC2 => dma.*[1].setDad(setU32H(dma.*[1].dad, value)),
|
0xC2 => dma.*[1].setDmadad(setHi(u32, dma.*[1].dad, value)),
|
||||||
0xC4 => dma.*[1].setCntL(value),
|
0xC4 => dma.*[1].setDmacntL(value),
|
||||||
0xC6 => dma.*[1].setCntH(value),
|
0xC6 => dma.*[1].setDmacntH(value),
|
||||||
|
|
||||||
0xC8 => dma.*[2].setSad(setU32L(dma.*[2].sad, value)),
|
0xC8 => dma.*[2].setDmasad(setLo(u32, dma.*[2].sad, value)),
|
||||||
0xCA => dma.*[2].setSad(setU32H(dma.*[2].sad, value)),
|
0xCA => dma.*[2].setDmasad(setHi(u32, dma.*[2].sad, value)),
|
||||||
0xCC => dma.*[2].setDad(setU32L(dma.*[2].dad, value)),
|
0xCC => dma.*[2].setDmadad(setLo(u32, dma.*[2].dad, value)),
|
||||||
0xCE => dma.*[2].setDad(setU32H(dma.*[2].dad, value)),
|
0xCE => dma.*[2].setDmadad(setHi(u32, dma.*[2].dad, value)),
|
||||||
0xD0 => dma.*[2].setCntL(value),
|
0xD0 => dma.*[2].setDmacntL(value),
|
||||||
0xD2 => dma.*[2].setCntH(value),
|
0xD2 => dma.*[2].setDmacntH(value),
|
||||||
|
|
||||||
0xD4 => dma.*[3].setSad(setU32L(dma.*[3].sad, value)),
|
0xD4 => dma.*[3].setDmasad(setLo(u32, dma.*[3].sad, value)),
|
||||||
0xD6 => dma.*[3].setSad(setU32H(dma.*[3].sad, value)),
|
0xD6 => dma.*[3].setDmasad(setHi(u32, dma.*[3].sad, value)),
|
||||||
0xD8 => dma.*[3].setDad(setU32L(dma.*[3].dad, value)),
|
0xD8 => dma.*[3].setDmadad(setLo(u32, dma.*[3].dad, value)),
|
||||||
0xDA => dma.*[3].setDad(setU32H(dma.*[3].dad, value)),
|
0xDA => dma.*[3].setDmadad(setHi(u32, dma.*[3].dad, value)),
|
||||||
0xDC => dma.*[3].setCntL(value),
|
0xDC => dma.*[3].setDmacntL(value),
|
||||||
0xDE => dma.*[3].setCntH(value),
|
0xDE => dma.*[3].setDmacntH(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 }),
|
||||||
|
@ -110,15 +113,12 @@ fn DmaController(comptime id: u2) type {
|
||||||
cnt: DmaControl,
|
cnt: DmaControl,
|
||||||
|
|
||||||
/// Internal. Currrent Source Address
|
/// Internal. Currrent Source Address
|
||||||
_sad: u32,
|
sad_latch: u32,
|
||||||
/// Internal. Current Destination Address
|
/// Internal. Current Destination Address
|
||||||
_dad: u32,
|
dad_latch: 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,33 +132,32 @@ fn DmaController(comptime id: u2) type {
|
||||||
.cnt = .{ .raw = 0x000 },
|
.cnt = .{ .raw = 0x000 },
|
||||||
|
|
||||||
// Internals
|
// Internals
|
||||||
._sad = 0,
|
.sad_latch = 0,
|
||||||
._dad = 0,
|
.dad_latch = 0,
|
||||||
._word_count = 0,
|
._word_count = 0,
|
||||||
._fifo_word_count = 4,
|
|
||||||
.in_progress = false,
|
.in_progress = false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setSad(self: *Self, addr: u32) void {
|
pub fn setDmasad(self: *Self, addr: u32) void {
|
||||||
self.sad = addr & sad_mask;
|
self.sad = addr & sad_mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setDad(self: *Self, addr: u32) void {
|
pub fn setDmadad(self: *Self, addr: u32) void {
|
||||||
self.dad = addr & dad_mask;
|
self.dad = addr & dad_mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setCntL(self: *Self, halfword: u16) void {
|
pub fn setDmacntL(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 setCntH(self: *Self, halfword: u16) void {
|
pub fn setDmacntH(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 = self.sad;
|
self.sad_latch = self.sad;
|
||||||
self._dad = self.dad;
|
self.dad_latch = 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
|
||||||
|
@ -168,15 +167,15 @@ fn DmaController(comptime id: u2) type {
|
||||||
self.cnt.raw = halfword;
|
self.cnt.raw = halfword;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setCnt(self: *Self, word: u32) void {
|
pub fn setDmacnt(self: *Self, word: u32) void {
|
||||||
self.setCntL(@truncate(u16, word));
|
self.setDmacntL(@truncate(u16, word));
|
||||||
self.setCntH(@truncate(u16, word >> 16));
|
self.setDmacntH(@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 = Self.adjustment(self.cnt.sad_adj.read());
|
const sad_adj = @intToEnum(Adjustment, self.cnt.sad_adj.read());
|
||||||
const dad_adj = if (is_fifo) .Fixed else Self.adjustment(self.cnt.dad_adj.read());
|
const dad_adj = if (is_fifo) .Fixed else @intToEnum(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);
|
||||||
|
@ -184,22 +183,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 & mask, cpu.bus.read(u32, self._sad & mask));
|
cpu.bus.write(u32, self.dad_latch & mask, cpu.bus.read(u32, self.sad_latch & mask));
|
||||||
} else {
|
} else {
|
||||||
cpu.bus.write(u16, self._dad & mask, cpu.bus.read(u16, self._sad & mask));
|
cpu.bus.write(u16, self.dad_latch & mask, cpu.bus.read(u16, self.sad_latch & mask));
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (sad_adj) {
|
switch (sad_adj) {
|
||||||
.Increment => self._sad +%= offset,
|
.Increment => self.sad_latch +%= offset,
|
||||||
.Decrement => self._sad -%= offset,
|
.Decrement => self.sad_latch -%= offset,
|
||||||
// TODO: Is just ignoring this ok?
|
// FIXME: 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 +%= offset,
|
.Increment, .IncrementReload => self.dad_latch +%= offset,
|
||||||
.Decrement => self._dad -%= offset,
|
.Decrement => self.dad_latch -%= offset,
|
||||||
.Fixed => {},
|
.Fixed => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,7 +226,7 @@ fn DmaController(comptime id: u2) type {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pollBlanking(self: *Self, comptime kind: DmaKind) void {
|
fn poll(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
|
||||||
|
@ -243,11 +242,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 (Self.adjustment(self.cnt.dad_adj.read()) == .IncrementReload) self._dad = self.dad;
|
if (@intToEnum(Adjustment, self.cnt.dad_adj.read()) == .IncrementReload) self.dad_latch = self.dad;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn requestSoundDma(self: *Self, _: u32) void {
|
pub fn requestAudio(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
|
||||||
|
|
||||||
|
@ -259,23 +258,19 @@ 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 = fifo_addr;
|
// self.dad_latch = 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 onBlanking(bus: *Bus, comptime kind: DmaKind) void {
|
pub fn onBlanking(bus: *Bus, comptime kind: DmaKind) void {
|
||||||
bus.dma[0].pollBlanking(kind);
|
bus.dma[0].poll(kind);
|
||||||
bus.dma[1].pollBlanking(kind);
|
bus.dma[1].poll(kind);
|
||||||
bus.dma[2].pollBlanking(kind);
|
bus.dma[2].poll(kind);
|
||||||
bus.dma[3].pollBlanking(kind);
|
bus.dma[3].poll(kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Adjustment = enum(u2) {
|
const Adjustment = enum(u2) {
|
||||||
|
@ -291,11 +286,3 @@ 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);
|
|
||||||
}
|
|
||||||
|
|
|
@ -68,6 +68,8 @@ pub const Gpio = struct {
|
||||||
log.info("Device: {}", .{kind});
|
log.info("Device: {}", .{kind});
|
||||||
|
|
||||||
const self = try allocator.create(Self);
|
const self = try allocator.create(Self);
|
||||||
|
errdefer allocator.destroy(self);
|
||||||
|
|
||||||
self.* = .{
|
self.* = .{
|
||||||
.data = 0b0000,
|
.data = 0b0000,
|
||||||
.direction = 0b1111, // TODO: What is GPIO DIrection set to by default?
|
.direction = 0b1111, // TODO: What is GPIO DIrection set to by default?
|
||||||
|
@ -288,7 +290,7 @@ pub const Clock = struct {
|
||||||
cpu.sched.push(.RealTimeClock, 1 << 24); // Every Second
|
cpu.sched.push(.RealTimeClock, 1 << 24); // Every Second
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn updateTime(self: *Self, late: u64) void {
|
pub fn onClockUpdate(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();
|
||||||
|
|
|
@ -11,6 +11,9 @@ 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 {
|
||||||
|
@ -233,18 +236,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, @bitCast(u32, bus.ppu.aff_bg[0].x) & 0xFFFF_0000 | value),
|
0x0400_0028 => bus.ppu.aff_bg[0].x = @bitCast(i32, setLo(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_002A => bus.ppu.aff_bg[0].x = @bitCast(i32, setHi(u32, @bitCast(u32, bus.ppu.aff_bg[0].x), value)),
|
||||||
0x0400_002C => bus.ppu.aff_bg[0].y = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[0].y) & 0xFFFF_0000 | value),
|
0x0400_002C => bus.ppu.aff_bg[0].y = @bitCast(i32, setLo(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_002E => bus.ppu.aff_bg[0].y = @bitCast(i32, setHi(u32, @bitCast(u32, bus.ppu.aff_bg[0].y), value)),
|
||||||
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, @bitCast(u32, bus.ppu.aff_bg[1].x) & 0xFFFF_0000 | value),
|
0x0400_0038 => bus.ppu.aff_bg[1].x = @bitCast(i32, setLo(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_003A => bus.ppu.aff_bg[1].x = @bitCast(i32, setHi(u32, @bitCast(u32, bus.ppu.aff_bg[1].x), value)),
|
||||||
0x0400_003C => bus.ppu.aff_bg[1].y = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[1].y) & 0xFFFF_0000 | value),
|
0x0400_003C => bus.ppu.aff_bg[1].y = @bitCast(i32, setLo(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_003E => bus.ppu.aff_bg[1].y = @bitCast(i32, setHi(u32, @bitCast(u32, bus.ppu.aff_bg[1].y), value)),
|
||||||
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,
|
||||||
|
@ -296,24 +299,26 @@ 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 = (bus.ppu.dispstat.raw & 0xFF00) | value,
|
0x0400_0004 => bus.ppu.dispstat.raw = setLo(u16, bus.ppu.dispstat.raw, value),
|
||||||
0x0400_0005 => bus.ppu.dispstat.raw = (@as(u16, value) << 8) | (bus.ppu.dispstat.raw & 0xFF),
|
0x0400_0005 => bus.ppu.dispstat.raw = setHi(u16, bus.ppu.dispstat.raw, value),
|
||||||
0x0400_0008 => bus.ppu.bg[0].cnt.raw = (bus.ppu.bg[0].cnt.raw & 0xFF00) | value,
|
0x0400_0008 => bus.ppu.bg[0].cnt.raw = setLo(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_0009 => bus.ppu.bg[0].cnt.raw = setHi(u16, bus.ppu.bg[0].cnt.raw, value),
|
||||||
0x0400_000A => bus.ppu.bg[1].cnt.raw = (bus.ppu.bg[1].cnt.raw & 0xFF00) | value,
|
0x0400_000A => bus.ppu.bg[1].cnt.raw = setLo(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_000B => bus.ppu.bg[1].cnt.raw = setHi(u16, bus.ppu.bg[1].cnt.raw, value),
|
||||||
0x0400_0040 => bus.ppu.win.h[0].set(.Lo, value),
|
0x0400_0040 => bus.ppu.win.h[0].raw = setLo(u16, bus.ppu.win.h[0].raw, value),
|
||||||
0x0400_0041 => bus.ppu.win.h[0].set(.Hi, value),
|
0x0400_0041 => bus.ppu.win.h[0].raw = setHi(u16, bus.ppu.win.h[0].raw, value),
|
||||||
0x0400_0042 => bus.ppu.win.h[1].set(.Lo, value),
|
0x0400_0042 => bus.ppu.win.h[1].raw = setLo(u16, bus.ppu.win.h[1].raw, value),
|
||||||
0x0400_0043 => bus.ppu.win.h[1].set(.Hi, value),
|
0x0400_0043 => bus.ppu.win.h[1].raw = setHi(u16, bus.ppu.win.h[1].raw, value),
|
||||||
0x0400_0044 => bus.ppu.win.v[0].set(.Lo, value),
|
0x0400_0044 => bus.ppu.win.v[0].raw = setLo(u16, bus.ppu.win.v[0].raw, value),
|
||||||
0x0400_0045 => bus.ppu.win.v[0].set(.Hi, value),
|
0x0400_0045 => bus.ppu.win.v[0].raw = setHi(u16, bus.ppu.win.v[0].raw, value),
|
||||||
0x0400_0046 => bus.ppu.win.v[1].set(.Lo, value),
|
0x0400_0046 => bus.ppu.win.v[1].raw = setLo(u16, bus.ppu.win.v[1].raw, value),
|
||||||
0x0400_0047 => bus.ppu.win.v[1].set(.Hi, value),
|
0x0400_0047 => bus.ppu.win.v[1].raw = setHi(u16, bus.ppu.win.v[1].raw, value),
|
||||||
0x0400_0048 => bus.ppu.win.setInL(value),
|
0x0400_0048 => bus.ppu.win.in.raw = setLo(u16, bus.ppu.win.in.raw, value),
|
||||||
0x0400_0049 => bus.ppu.win.setInH(value),
|
0x0400_0049 => bus.ppu.win.in.raw = setHi(u16, bus.ppu.win.in.raw, value),
|
||||||
0x0400_004A => bus.ppu.win.setOutL(value),
|
0x0400_004A => bus.ppu.win.out.raw = setLo(u16, bus.ppu.win.out.raw, value),
|
||||||
0x0400_0054 => bus.ppu.bldy.raw = (bus.ppu.bldy.raw & 0xFF00) | value,
|
0x0400_0054 => bus.ppu.bldy.raw = setLo(u16, bus.ppu.bldy.raw, 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),
|
||||||
|
@ -476,13 +481,6 @@ pub const WinH = extern union {
|
||||||
x2: Bitfield(u16, 0, 8),
|
x2: Bitfield(u16, 0, 8),
|
||||||
x1: Bitfield(u16, 8, 8),
|
x1: Bitfield(u16, 8, 8),
|
||||||
raw: u16,
|
raw: u16,
|
||||||
|
|
||||||
pub fn set(self: *Self, comptime K: u8WriteKind, value: u8) void {
|
|
||||||
self.raw = switch (K) {
|
|
||||||
.Hi => (@as(u16, value) << 8) | self.raw & 0xFF,
|
|
||||||
.Lo => (self.raw & 0xFF00) | value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Write-only
|
/// Write-only
|
||||||
|
|
|
@ -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].getCntL(),
|
0x0 => @as(T, tim.*[0].cnt.raw) << 16 | tim.*[0].timcntL(),
|
||||||
0x4 => @as(T, tim.*[1].cnt.raw) << 16 | tim.*[1].getCntL(),
|
0x4 => @as(T, tim.*[1].cnt.raw) << 16 | tim.*[1].timcntL(),
|
||||||
0x8 => @as(T, tim.*[2].cnt.raw) << 16 | tim.*[2].getCntL(),
|
0x8 => @as(T, tim.*[2].cnt.raw) << 16 | tim.*[2].timcntL(),
|
||||||
0xC => @as(T, tim.*[3].cnt.raw) << 16 | tim.*[3].getCntL(),
|
0xC => @as(T, tim.*[3].cnt.raw) << 16 | tim.*[3].timcntL(),
|
||||||
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].getCntL(),
|
0x0 => tim.*[0].timcntL(),
|
||||||
0x2 => tim.*[0].cnt.raw,
|
0x2 => tim.*[0].cnt.raw,
|
||||||
0x4 => tim.*[1].getCntL(),
|
0x4 => tim.*[1].timcntL(),
|
||||||
0x6 => tim.*[1].cnt.raw,
|
0x6 => tim.*[1].cnt.raw,
|
||||||
0x8 => tim.*[2].getCntL(),
|
0x8 => tim.*[2].timcntL(),
|
||||||
0xA => tim.*[2].cnt.raw,
|
0xA => tim.*[2].cnt.raw,
|
||||||
0xC => tim.*[3].getCntL(),
|
0xC => tim.*[3].timcntL(),
|
||||||
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].setCnt(value),
|
0x0 => tim.*[0].setTimcnt(value),
|
||||||
0x4 => tim.*[1].setCnt(value),
|
0x4 => tim.*[1].setTimcnt(value),
|
||||||
0x8 => tim.*[2].setCnt(value),
|
0x8 => tim.*[2].setTimcnt(value),
|
||||||
0xC => tim.*[3].setCnt(value),
|
0xC => tim.*[3].setTimcnt(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].setCntL(value),
|
0x0 => tim.*[0].setTimcntL(value),
|
||||||
0x2 => tim.*[0].setCntH(value),
|
0x2 => tim.*[0].setTimcntH(value),
|
||||||
0x4 => tim.*[1].setCntL(value),
|
0x4 => tim.*[1].setTimcntL(value),
|
||||||
0x6 => tim.*[1].setCntH(value),
|
0x6 => tim.*[1].setTimcntH(value),
|
||||||
0x8 => tim.*[2].setCntL(value),
|
0x8 => tim.*[2].setTimcntL(value),
|
||||||
0xA => tim.*[2].setCntH(value),
|
0xA => tim.*[2].setTimcntH(value),
|
||||||
0xC => tim.*[3].setCntL(value),
|
0xC => tim.*[3].setTimcntL(value),
|
||||||
0xE => tim.*[3].setCntH(value),
|
0xE => tim.*[3].setTimcntH(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.getCntL()
|
/// Read Only, Internal. Please use self.timcntL()
|
||||||
_counter: u16,
|
_counter: u16,
|
||||||
|
|
||||||
/// Write Only, Internal. Please use self.setCntL()
|
/// Write Only, Internal. Please use self.setTimcntL()
|
||||||
_reload: u16,
|
_reload: u16,
|
||||||
|
|
||||||
/// Write Only, Internal. Please use self.setCntH()
|
/// Write Only, Internal. Please use self.setTimcntH()
|
||||||
cnt: TimerControl,
|
cnt: TimerControl,
|
||||||
|
|
||||||
/// Internal.
|
/// Internal.
|
||||||
|
@ -97,26 +97,26 @@ fn Timer(comptime id: u2) type {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TIMCNT_L
|
/// TIMCNT_L Getter
|
||||||
pub fn getCntL(self: *const Self) u16 {
|
pub fn timcntL(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
|
/// TIMCNT_L Setter
|
||||||
pub fn setCntL(self: *Self, halfword: u16) void {
|
pub fn setTimcntL(self: *Self, halfword: u16) void {
|
||||||
self._reload = halfword;
|
self._reload = halfword;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TIMCNT_L & TIMCNT_H
|
/// TIMCNT_L & TIMCNT_H
|
||||||
pub fn setCnt(self: *Self, word: u32) void {
|
pub fn setTimcnt(self: *Self, word: u32) void {
|
||||||
self.setCntL(@truncate(u16, word));
|
self.setTimcntL(@truncate(u16, word));
|
||||||
self.setCntH(@truncate(u16, word >> 16));
|
self.setTimcntH(@truncate(u16, word >> 16));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TIMCNT_H
|
/// TIMCNT_H
|
||||||
pub fn setCntH(self: *Self, halfword: u16) void {
|
pub fn setTimcntH(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.scheduleOverflow(0);
|
if (new.enabled.read() and !new.cascade.read()) self.rescheduleTimerExpire(0);
|
||||||
|
|
||||||
self.cnt.raw = halfword;
|
self.cnt.raw = halfword;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handleOverflow(self: *Self, cpu: *Arm7tdmi, late: u64) void {
|
pub fn onTimerExpire(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.handleTimerOverflow(cpu, id);
|
cpu.bus.apu.onDmaAudioSampleRequest(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].handleOverflow(cpu, late);
|
if (cpu.bus.tim[1]._counter == 0) cpu.bus.tim[1].onTimerExpire(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].handleOverflow(cpu, late);
|
if (cpu.bus.tim[2]._counter == 0) cpu.bus.tim[2].onTimerExpire(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].handleOverflow(cpu, late);
|
if (cpu.bus.tim[3]._counter == 0) cpu.bus.tim[3].onTimerExpire(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.scheduleOverflow(late);
|
self.rescheduleTimerExpire(late);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scheduleOverflow(self: *Self, late: u64) void {
|
fn rescheduleTimerExpire(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();
|
||||||
|
|
125
src/core/cpu.zig
125
src/core/cpu.zig
|
@ -236,13 +236,13 @@ 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 {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
r: [16]u32,
|
r: [16]u32,
|
||||||
|
pipe: Pipeline,
|
||||||
sched: *Scheduler,
|
sched: *Scheduler,
|
||||||
bus: *Bus,
|
bus: *Bus,
|
||||||
cpsr: PSR,
|
cpsr: PSR,
|
||||||
|
@ -263,6 +263,7 @@ pub const Arm7tdmi = struct {
|
||||||
pub fn init(sched: *Scheduler, bus: *Bus, log_file: ?std.fs.File) Self {
|
pub fn init(sched: *Scheduler, bus: *Bus, log_file: ?std.fs.File) Self {
|
||||||
return Self{
|
return Self{
|
||||||
.r = [_]u32{0x00} ** 16,
|
.r = [_]u32{0x00} ** 16,
|
||||||
|
.pipe = Pipeline.init(),
|
||||||
.sched = sched,
|
.sched = sched,
|
||||||
.bus = bus,
|
.bus = bus,
|
||||||
.cpsr = .{ .raw = 0x0000_001F },
|
.cpsr = .{ .raw = 0x0000_001F },
|
||||||
|
@ -411,29 +412,43 @@ pub const Arm7tdmi = struct {
|
||||||
self.cpsr.mode.write(@enumToInt(next));
|
self.cpsr.mode.write(@enumToInt(next));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Advances state so that the BIOS is skipped
|
||||||
|
///
|
||||||
|
/// Note: This accesses the CPU's bus ptr so it only may be called
|
||||||
|
/// once the Bus has been properly initialized
|
||||||
|
///
|
||||||
|
/// TODO: Make above notice impossible to do in code
|
||||||
pub fn fastBoot(self: *Self) void {
|
pub fn fastBoot(self: *Self) void {
|
||||||
self.r = std.mem.zeroes([16]u32);
|
self.r = std.mem.zeroes([16]u32);
|
||||||
|
|
||||||
self.r[0] = 0x08000000;
|
// self.r[0] = 0x08000000;
|
||||||
self.r[1] = 0x000000EA;
|
// self.r[1] = 0x000000EA;
|
||||||
self.r[13] = 0x0300_7F00;
|
self.r[13] = 0x0300_7F00;
|
||||||
self.r[15] = 0x0800_0000;
|
self.r[15] = 0x0800_0000;
|
||||||
|
|
||||||
self.banked_r[bankedIdx(.Irq, .R13)] = 0x0300_7FA0;
|
self.banked_r[bankedIdx(.Irq, .R13)] = 0x0300_7FA0;
|
||||||
self.banked_r[bankedIdx(.Supervisor, .R13)] = 0x0300_7FE0;
|
self.banked_r[bankedIdx(.Supervisor, .R13)] = 0x0300_7FE0;
|
||||||
|
|
||||||
self.cpsr.raw = 0x6000001F;
|
// self.cpsr.raw = 0x6000001F;
|
||||||
|
self.cpsr.raw = 0x0000_001F;
|
||||||
|
|
||||||
|
self.bus.bios.addr_latch = 0x0000_00DC + 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn step(self: *Self) void {
|
pub fn step(self: *Self) void {
|
||||||
|
defer {
|
||||||
|
if (!self.pipe.flushed) self.r[15] += if (self.cpsr.t.read()) 2 else @as(u32, 4);
|
||||||
|
self.pipe.flushed = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (self.cpsr.t.read()) {
|
if (self.cpsr.t.read()) {
|
||||||
const opcode = self.fetch(u16);
|
const opcode = @truncate(u16, self.pipe.step(self, u16) orelse return);
|
||||||
if (cpu_logging) self.logger.?.mgbaLog(self, opcode);
|
if (self.logger) |*trace| trace.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.fetch(u32);
|
const opcode = self.pipe.step(self, u32) orelse return;
|
||||||
if (cpu_logging) self.logger.?.mgbaLog(self, opcode);
|
if (self.logger) |*trace| trace.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);
|
||||||
|
@ -473,42 +488,41 @@ pub const Arm7tdmi = struct {
|
||||||
pub fn handleInterrupt(self: *Self) void {
|
pub fn handleInterrupt(self: *Self) void {
|
||||||
const should_handle = self.bus.io.ie.raw & self.bus.io.irq.raw;
|
const should_handle = self.bus.io.ie.raw & self.bus.io.irq.raw;
|
||||||
|
|
||||||
if (should_handle != 0) {
|
// Return if IME is disabled, CPSR I is set or there is nothing to handle
|
||||||
|
if (!self.bus.io.ime or self.cpsr.i.read() or should_handle == 0) return;
|
||||||
|
|
||||||
|
// If Pipeline isn't full, we have a bug
|
||||||
|
std.debug.assert(self.pipe.isFull());
|
||||||
|
|
||||||
|
// log.debug("Handling Interrupt!", .{});
|
||||||
self.bus.io.haltcnt = .Execute;
|
self.bus.io.haltcnt = .Execute;
|
||||||
// log.debug("An Interrupt was Fired!", .{});
|
|
||||||
|
|
||||||
// Either IME is not true or I in CPSR is true
|
// FIXME: This seems weird, but retAddr.gba suggests I need to make these changes
|
||||||
// Don't handle interrupts
|
const ret_addr = self.r[15] - if (self.cpsr.t.read()) 0 else @as(u32, 4);
|
||||||
if (!self.bus.io.ime or self.cpsr.i.read()) return;
|
const new_spsr = self.cpsr.raw;
|
||||||
// log.debug("An interrupt was Handled!", .{});
|
|
||||||
|
|
||||||
// retAddr.gba says r15 on it's own is off by -04h in both ARM and THUMB mode
|
|
||||||
const r15 = self.r[15] + 4;
|
|
||||||
const cpsr = self.cpsr.raw;
|
|
||||||
|
|
||||||
self.changeMode(.Irq);
|
self.changeMode(.Irq);
|
||||||
self.cpsr.t.write(false);
|
self.cpsr.t.write(false);
|
||||||
self.cpsr.i.write(true);
|
self.cpsr.i.write(true);
|
||||||
|
|
||||||
self.r[14] = r15;
|
self.r[14] = ret_addr;
|
||||||
self.spsr.raw = cpsr;
|
self.spsr.raw = new_spsr;
|
||||||
self.r[15] = 0x000_0018;
|
self.r[15] = 0x0000_0018;
|
||||||
}
|
self.pipe.reload(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fn fetch(self: *Self, comptime T: type) T {
|
inline fn fetch(self: *Self, comptime T: type, address: u32) T {
|
||||||
comptime std.debug.assert(T == u32 or T == u16); // Opcode may be 32-bit (ARM) or 16-bit (THUMB)
|
comptime std.debug.assert(T == u32 or T == u16); // Opcode may be 32-bit (ARM) or 16-bit (THUMB)
|
||||||
defer self.r[15] += if (T == u32) 4 else 2;
|
|
||||||
|
|
||||||
// FIXME: You better hope this is optimized out
|
// Bus.read will advance the scheduler. There are different timings for CPU fetches,
|
||||||
|
// so we want to undo what Bus.read will apply. We can do this by caching the current tick
|
||||||
|
// This is very dumb.
|
||||||
|
//
|
||||||
|
// FIXME: Please rework this
|
||||||
const tick_cache = self.sched.tick;
|
const tick_cache = self.sched.tick;
|
||||||
defer self.sched.tick = tick_cache + Bus.fetch_timings[@boolToInt(T == u32)][@truncate(u4, self.r[15] >> 24)];
|
defer self.sched.tick = tick_cache + Bus.fetch_timings[@boolToInt(T == u32)][@truncate(u4, address >> 24)];
|
||||||
|
|
||||||
return self.bus.read(T, self.r[15]);
|
return self.bus.read(T, address);
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fakePC(self: *const Self) u32 {
|
|
||||||
return self.r[15] + 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn panic(self: *const Self, comptime format: []const u8, args: anytype) noreturn {
|
pub fn panic(self: *const Self, comptime format: []const u8, args: anytype) noreturn {
|
||||||
|
@ -525,6 +539,8 @@ pub const Arm7tdmi = struct {
|
||||||
std.debug.print("spsr: 0x{X:0>8} ", .{self.spsr.raw});
|
std.debug.print("spsr: 0x{X:0>8} ", .{self.spsr.raw});
|
||||||
prettyPrintPsr(&self.spsr);
|
prettyPrintPsr(&self.spsr);
|
||||||
|
|
||||||
|
std.debug.print("pipeline: {??X:0>8}\n", .{self.pipe.stage});
|
||||||
|
|
||||||
if (self.cpsr.t.read()) {
|
if (self.cpsr.t.read()) {
|
||||||
const opcode = self.bus.dbgRead(u16, self.r[15] - 4);
|
const opcode = self.bus.dbgRead(u16, self.r[15] - 4);
|
||||||
const id = thumb.idx(opcode);
|
const id = thumb.idx(opcode);
|
||||||
|
@ -588,7 +604,7 @@ pub const Arm7tdmi = struct {
|
||||||
const r12 = self.r[12];
|
const r12 = self.r[12];
|
||||||
const r13 = self.r[13];
|
const r13 = self.r[13];
|
||||||
const r14 = self.r[14];
|
const r14 = self.r[14];
|
||||||
const r15 = self.r[15];
|
const r15 = self.r[15] -| if (self.cpsr.t.read()) 2 else @as(u32, 4);
|
||||||
|
|
||||||
const c_psr = self.cpsr.raw;
|
const c_psr = self.cpsr.raw;
|
||||||
|
|
||||||
|
@ -596,7 +612,7 @@ pub const Arm7tdmi = struct {
|
||||||
if (self.cpsr.t.read()) {
|
if (self.cpsr.t.read()) {
|
||||||
if (opcode >> 11 == 0x1E) {
|
if (opcode >> 11 == 0x1E) {
|
||||||
// Instruction 1 of a BL Opcode, print in ARM mode
|
// Instruction 1 of a BL Opcode, print in ARM mode
|
||||||
const other_half = self.bus.dbgRead(u16, self.r[15]);
|
const other_half = self.bus.debugRead(u16, self.r[15] - 2);
|
||||||
const bl_opcode = @as(u32, opcode) << 16 | other_half;
|
const bl_opcode = @as(u32, opcode) << 16 | other_half;
|
||||||
|
|
||||||
log_str = try std.fmt.bufPrint(&buf, arm_fmt, .{ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, c_psr, bl_opcode });
|
log_str = try std.fmt.bufPrint(&buf, arm_fmt, .{ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, c_psr, bl_opcode });
|
||||||
|
@ -632,6 +648,49 @@ pub fn checkCond(cpsr: PSR, cond: u4) bool {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Pipeline = struct {
|
||||||
|
const Self = @This();
|
||||||
|
stage: [2]?u32,
|
||||||
|
flushed: bool,
|
||||||
|
|
||||||
|
fn init() Self {
|
||||||
|
return .{
|
||||||
|
.stage = [_]?u32{null} ** 2,
|
||||||
|
.flushed = false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isFull(self: *const Self) bool {
|
||||||
|
return self.stage[0] != null and self.stage[1] != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step(self: *Self, cpu: *Arm7tdmi, comptime T: type) ?u32 {
|
||||||
|
comptime std.debug.assert(T == u32 or T == u16);
|
||||||
|
|
||||||
|
// FIXME: https://github.com/ziglang/zig/issues/12642
|
||||||
|
var opcode = self.stage[0];
|
||||||
|
|
||||||
|
self.stage[0] = self.stage[1];
|
||||||
|
self.stage[1] = cpu.fetch(T, cpu.r[15]);
|
||||||
|
|
||||||
|
return opcode;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reload(self: *Self, cpu: *Arm7tdmi) void {
|
||||||
|
if (cpu.cpsr.t.read()) {
|
||||||
|
self.stage[0] = cpu.fetch(u16, cpu.r[15]);
|
||||||
|
self.stage[1] = cpu.fetch(u16, cpu.r[15] + 2);
|
||||||
|
cpu.r[15] += 4;
|
||||||
|
} else {
|
||||||
|
self.stage[0] = cpu.fetch(u32, cpu.r[15]);
|
||||||
|
self.stage[1] = cpu.fetch(u32, cpu.r[15] + 4);
|
||||||
|
cpu.r[15] += 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.flushed = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub const PSR = extern union {
|
pub const PSR = extern union {
|
||||||
mode: Bitfield(u32, 0, 5),
|
mode: Bitfield(u32, 0, 5),
|
||||||
t: Bit(u32, 5),
|
t: Bit(u32, 5),
|
||||||
|
|
|
@ -55,8 +55,10 @@ pub fn blockDataTransfer(comptime P: bool, comptime U: bool, comptime S: bool, c
|
||||||
|
|
||||||
if (L) {
|
if (L) {
|
||||||
cpu.r[15] = bus.read(u32, und_addr);
|
cpu.r[15] = bus.read(u32, und_addr);
|
||||||
|
cpu.pipe.reload(cpu);
|
||||||
} else {
|
} else {
|
||||||
bus.write(u32, und_addr, cpu.r[15] + 8);
|
// FIXME: Should r15 on write be +12 ahead?
|
||||||
|
bus.write(u32, und_addr, cpu.r[15] + 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
cpu.r[rn] = if (U) cpu.r[rn] + 0x40 else cpu.r[rn] - 0x40;
|
cpu.r[rn] = if (U) cpu.r[rn] + 0x40 else cpu.r[rn] - 0x40;
|
||||||
|
@ -86,17 +88,23 @@ pub fn blockDataTransfer(comptime P: bool, comptime U: bool, comptime S: bool, c
|
||||||
cpu.setUserModeRegister(i, bus.read(u32, address));
|
cpu.setUserModeRegister(i, bus.read(u32, address));
|
||||||
} else {
|
} else {
|
||||||
const value = bus.read(u32, address);
|
const value = bus.read(u32, address);
|
||||||
cpu.r[i] = if (i == 0xF) value & 0xFFFF_FFFC else value;
|
|
||||||
if (S and i == 0xF) cpu.setCpsr(cpu.spsr.raw);
|
cpu.r[i] = value;
|
||||||
|
if (i == 0xF) {
|
||||||
|
cpu.r[i] &= ~@as(u32, 3); // Align r15
|
||||||
|
cpu.pipe.reload(cpu);
|
||||||
|
|
||||||
|
if (S) cpu.setCpsr(cpu.spsr.raw);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (S) {
|
if (S) {
|
||||||
// Always Transfer User mode Registers
|
// Always Transfer User mode Registers
|
||||||
// This happens regardless if r15 is in the list
|
// This happens regardless if r15 is in the list
|
||||||
const value = cpu.getUserModeRegister(i);
|
const value = cpu.getUserModeRegister(i);
|
||||||
bus.write(u32, address, value + if (i == 0xF) 8 else @as(u32, 0)); // PC is already 4 ahead to make 12
|
bus.write(u32, address, value + if (i == 0xF) 4 else @as(u32, 0)); // PC is already 8 ahead to make 12
|
||||||
} else {
|
} else {
|
||||||
bus.write(u32, address, cpu.r[i] + if (i == 0xF) 8 else @as(u32, 0));
|
bus.write(u32, address, cpu.r[i] + if (i == 0xF) 4 else @as(u32, 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,14 +9,20 @@ const sext = @import("../../../util.zig").sext;
|
||||||
pub fn branch(comptime L: bool) InstrFn {
|
pub fn branch(comptime L: bool) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
||||||
if (L) cpu.r[14] = cpu.r[15];
|
if (L) cpu.r[14] = cpu.r[15] - 4;
|
||||||
cpu.r[15] = cpu.fakePC() +% (sext(u32, u24, opcode) << 2);
|
|
||||||
|
cpu.r[15] +%= sext(u32, u24, opcode) << 2;
|
||||||
|
cpu.pipe.reload(cpu);
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn branchAndExchange(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
pub fn branchAndExchange(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
||||||
const rn = opcode & 0xF;
|
const rn = opcode & 0xF;
|
||||||
cpu.cpsr.t.write(cpu.r[rn] & 1 == 1);
|
|
||||||
cpu.r[15] = cpu.r[rn] & 0xFFFF_FFFE;
|
const thumb = cpu.r[rn] & 1 == 1;
|
||||||
|
cpu.r[15] = cpu.r[rn] & if (thumb) ~@as(u32, 1) else ~@as(u32, 3);
|
||||||
|
|
||||||
|
cpu.cpsr.t.write(thumb);
|
||||||
|
cpu.pipe.reload(cpu);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,10 @@ const Bus = @import("../../Bus.zig");
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||||
|
|
||||||
const rotateRight = @import("../barrel_shifter.zig").rotateRight;
|
const exec = @import("../barrel_shifter.zig").exec;
|
||||||
const execute = @import("../barrel_shifter.zig").execute;
|
const ror = @import("../barrel_shifter.zig").ror;
|
||||||
|
|
||||||
pub fn dataProcessing(comptime I: bool, comptime S: bool, comptime instrKind: u4) InstrFn {
|
pub fn dataProcessing(comptime I: bool, comptime S: bool, comptime kind: u4) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
||||||
const rd = @truncate(u4, opcode >> 12 & 0xF);
|
const rd = @truncate(u4, opcode >> 12 & 0xF);
|
||||||
|
@ -13,269 +13,168 @@ pub fn dataProcessing(comptime I: bool, comptime S: bool, comptime instrKind: u4
|
||||||
const old_carry = @boolToInt(cpu.cpsr.c.read());
|
const old_carry = @boolToInt(cpu.cpsr.c.read());
|
||||||
|
|
||||||
// If certain conditions are met, PC is 12 ahead instead of 8
|
// If certain conditions are met, PC is 12 ahead instead of 8
|
||||||
|
// TODO: Why these conditions?
|
||||||
if (!I and opcode >> 4 & 1 == 1) cpu.r[15] += 4;
|
if (!I and opcode >> 4 & 1 == 1) cpu.r[15] += 4;
|
||||||
|
const op1 = cpu.r[rn];
|
||||||
|
|
||||||
const op1 = if (rn == 0xF) cpu.fakePC() else cpu.r[rn];
|
|
||||||
|
|
||||||
var op2: u32 = undefined;
|
|
||||||
if (I) {
|
|
||||||
const amount = @truncate(u8, (opcode >> 8 & 0xF) << 1);
|
const amount = @truncate(u8, (opcode >> 8 & 0xF) << 1);
|
||||||
op2 = rotateRight(S, &cpu.cpsr, opcode & 0xFF, amount);
|
const op2 = if (I) ror(S, &cpu.cpsr, opcode & 0xFF, amount) else exec(S, cpu, opcode);
|
||||||
} else {
|
|
||||||
op2 = execute(S, cpu, opcode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Undo special condition from above
|
// Undo special condition from above
|
||||||
if (!I and opcode >> 4 & 1 == 1) cpu.r[15] -= 4;
|
if (!I and opcode >> 4 & 1 == 1) cpu.r[15] -= 4;
|
||||||
|
|
||||||
switch (instrKind) {
|
var result: u32 = undefined;
|
||||||
0x0 => {
|
var overflow: bool = undefined;
|
||||||
// AND
|
|
||||||
const result = op1 & op2;
|
// Perform Data Processing Logic
|
||||||
cpu.r[rd] = result;
|
switch (kind) {
|
||||||
setArmLogicOpFlags(S, cpu, rd, result);
|
0x0 => result = op1 & op2, // AND
|
||||||
},
|
0x1 => result = op1 ^ op2, // EOR
|
||||||
0x1 => {
|
0x2 => result = op1 -% op2, // SUB
|
||||||
// EOR
|
0x3 => result = op2 -% op1, // RSB
|
||||||
const result = op1 ^ op2;
|
0x4 => result = add(&overflow, op1, op2), // ADD
|
||||||
cpu.r[rd] = result;
|
0x5 => result = adc(&overflow, op1, op2, old_carry), // ADC
|
||||||
setArmLogicOpFlags(S, cpu, rd, result);
|
0x6 => result = sbc(op1, op2, old_carry), // SBC
|
||||||
},
|
0x7 => result = sbc(op2, op1, old_carry), // RSC
|
||||||
0x2 => {
|
|
||||||
// SUB
|
|
||||||
cpu.r[rd] = armSub(S, cpu, rd, op1, op2);
|
|
||||||
},
|
|
||||||
0x3 => {
|
|
||||||
// RSB
|
|
||||||
cpu.r[rd] = armSub(S, cpu, rd, op2, op1);
|
|
||||||
},
|
|
||||||
0x4 => {
|
|
||||||
// ADD
|
|
||||||
cpu.r[rd] = armAdd(S, cpu, rd, op1, op2);
|
|
||||||
},
|
|
||||||
0x5 => {
|
|
||||||
// ADC
|
|
||||||
cpu.r[rd] = armAdc(S, cpu, rd, op1, op2, old_carry);
|
|
||||||
},
|
|
||||||
0x6 => {
|
|
||||||
// SBC
|
|
||||||
cpu.r[rd] = armSbc(S, cpu, rd, op1, op2, old_carry);
|
|
||||||
},
|
|
||||||
0x7 => {
|
|
||||||
// RSC
|
|
||||||
cpu.r[rd] = armSbc(S, cpu, rd, op2, op1, old_carry);
|
|
||||||
},
|
|
||||||
0x8 => {
|
0x8 => {
|
||||||
// TST
|
// TST
|
||||||
if (rd == 0xF) {
|
if (rd == 0xF)
|
||||||
undefinedTestBehaviour(cpu);
|
return undefinedTestBehaviour(cpu);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = op1 & op2;
|
result = op1 & op2;
|
||||||
setTestOpFlags(S, cpu, opcode, result);
|
|
||||||
},
|
},
|
||||||
0x9 => {
|
0x9 => {
|
||||||
// TEQ
|
// TEQ
|
||||||
if (rd == 0xF) {
|
if (rd == 0xF)
|
||||||
undefinedTestBehaviour(cpu);
|
return undefinedTestBehaviour(cpu);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = op1 ^ op2;
|
result = op1 ^ op2;
|
||||||
setTestOpFlags(S, cpu, opcode, result);
|
|
||||||
},
|
},
|
||||||
0xA => {
|
0xA => {
|
||||||
// CMP
|
// CMP
|
||||||
if (rd == 0xF) {
|
if (rd == 0xF)
|
||||||
undefinedTestBehaviour(cpu);
|
return undefinedTestBehaviour(cpu);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cmp(cpu, op1, op2);
|
result = op1 -% op2;
|
||||||
},
|
},
|
||||||
0xB => {
|
0xB => {
|
||||||
// CMN
|
// CMN
|
||||||
if (rd == 0xF) {
|
if (rd == 0xF)
|
||||||
undefinedTestBehaviour(cpu);
|
return undefinedTestBehaviour(cpu);
|
||||||
return;
|
|
||||||
|
overflow = @addWithOverflow(u32, op1, op2, &result);
|
||||||
|
},
|
||||||
|
0xC => result = op1 | op2, // ORR
|
||||||
|
0xD => result = op2, // MOV
|
||||||
|
0xE => result = op1 & ~op2, // BIC
|
||||||
|
0xF => result = ~op2, // MVN
|
||||||
}
|
}
|
||||||
|
|
||||||
cmn(cpu, op1, op2);
|
// Write to Destination Register
|
||||||
},
|
switch (kind) {
|
||||||
0xC => {
|
0x8, 0x9, 0xA, 0xB => {}, // Test Operations
|
||||||
// ORR
|
else => {
|
||||||
const result = op1 | op2;
|
|
||||||
cpu.r[rd] = result;
|
cpu.r[rd] = result;
|
||||||
setArmLogicOpFlags(S, cpu, rd, result);
|
if (rd == 0xF) {
|
||||||
|
if (S) cpu.setCpsr(cpu.spsr.raw);
|
||||||
|
cpu.pipe.reload(cpu);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
0xD => {
|
}
|
||||||
// MOV
|
|
||||||
cpu.r[rd] = op2;
|
// Write Flags
|
||||||
setArmLogicOpFlags(S, cpu, rd, op2);
|
switch (kind) {
|
||||||
|
0x0, 0x1, 0xC, 0xD, 0xE, 0xF => if (S and rd != 0xF) {
|
||||||
|
// Logic Operation Flags
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
// C set by Barrel Shifter, V is unaffected
|
||||||
|
|
||||||
},
|
},
|
||||||
0xE => {
|
0x2, 0x3 => if (S and rd != 0xF) {
|
||||||
// BIC
|
// SUB, RSB Flags
|
||||||
const result = op1 & ~op2;
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
cpu.r[rd] = result;
|
cpu.cpsr.z.write(result == 0);
|
||||||
setArmLogicOpFlags(S, cpu, rd, result);
|
|
||||||
|
if (kind == 0x2) {
|
||||||
|
// SUB specific
|
||||||
|
cpu.cpsr.c.write(op2 <= op1);
|
||||||
|
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
||||||
|
} else {
|
||||||
|
// RSB Specific
|
||||||
|
cpu.cpsr.c.write(op1 <= op2);
|
||||||
|
cpu.cpsr.v.write(((op2 ^ result) & (~op1 ^ result)) >> 31 & 1 == 1);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
0xF => {
|
0x4, 0x5 => if (S and rd != 0xF) {
|
||||||
// MVN
|
// ADD, ADC Flags
|
||||||
const result = ~op2;
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
cpu.r[rd] = result;
|
cpu.cpsr.z.write(result == 0);
|
||||||
setArmLogicOpFlags(S, cpu, rd, result);
|
cpu.cpsr.c.write(overflow);
|
||||||
|
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
|
||||||
|
},
|
||||||
|
0x6, 0x7 => if (S and rd != 0xF) {
|
||||||
|
// SBC, RSC Flags
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
|
||||||
|
if (kind == 0x6) {
|
||||||
|
// SBC specific
|
||||||
|
const subtrahend = @as(u64, op2) -% old_carry +% 1;
|
||||||
|
cpu.cpsr.c.write(subtrahend <= op1);
|
||||||
|
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
||||||
|
} else {
|
||||||
|
// RSC Specific
|
||||||
|
const subtrahend = @as(u64, op1) -% old_carry +% 1;
|
||||||
|
cpu.cpsr.c.write(subtrahend <= op2);
|
||||||
|
cpu.cpsr.v.write(((op2 ^ result) & (~op1 ^ result)) >> 31 & 1 == 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x8, 0x9, 0xA, 0xB => {
|
||||||
|
// Test Operation Flags
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
|
||||||
|
if (kind == 0xA) {
|
||||||
|
// CMP specific
|
||||||
|
cpu.cpsr.c.write(op2 <= op1);
|
||||||
|
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
||||||
|
} else if (kind == 0xB) {
|
||||||
|
// CMN specific
|
||||||
|
cpu.cpsr.c.write(overflow);
|
||||||
|
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
|
||||||
|
} else {
|
||||||
|
// TST, TEQ specific
|
||||||
|
// Barrel Shifter should always calc CPSR C in TST
|
||||||
|
if (!S) _ = exec(true, cpu, opcode);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn armSbc(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32, old_carry: u1) u32 {
|
pub fn sbc(left: u32, right: u32, old_carry: u1) u32 {
|
||||||
var result: u32 = undefined;
|
|
||||||
if (S and rd == 0xF) {
|
|
||||||
result = sbc(false, cpu, left, right, old_carry);
|
|
||||||
cpu.setCpsr(cpu.spsr.raw);
|
|
||||||
} else {
|
|
||||||
result = sbc(S, cpu, left, right, old_carry);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sbc(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32, old_carry: u1) u32 {
|
|
||||||
// TODO: Make your own version (thanks peach.bot)
|
// TODO: Make your own version (thanks peach.bot)
|
||||||
const subtrahend = @as(u64, right) -% old_carry +% 1;
|
const subtrahend = @as(u64, right) -% old_carry +% 1;
|
||||||
const result = @truncate(u32, left -% subtrahend);
|
const ret = @truncate(u32, left -% subtrahend);
|
||||||
|
|
||||||
if (S) {
|
return ret;
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
cpu.cpsr.c.write(subtrahend <= left);
|
|
||||||
cpu.cpsr.v.write(((left ^ result) & (~right ^ result)) >> 31 & 1 == 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn armSub(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32) u32 {
|
pub fn add(overflow: *bool, left: u32, right: u32) u32 {
|
||||||
var result: u32 = undefined;
|
var ret: u32 = undefined;
|
||||||
if (S and rd == 0xF) {
|
overflow.* = @addWithOverflow(u32, left, right, &ret);
|
||||||
result = sub(false, cpu, left, right);
|
return ret;
|
||||||
cpu.setCpsr(cpu.spsr.raw);
|
|
||||||
} else {
|
|
||||||
result = sub(S, cpu, left, right);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sub(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32) u32 {
|
pub fn adc(overflow: *bool, left: u32, right: u32, old_carry: u1) u32 {
|
||||||
const result = left -% right;
|
var ret: u32 = undefined;
|
||||||
|
const first = @addWithOverflow(u32, left, right, &ret);
|
||||||
|
const second = @addWithOverflow(u32, ret, old_carry, &ret);
|
||||||
|
|
||||||
if (S) {
|
overflow.* = first or second;
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
return ret;
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
cpu.cpsr.c.write(right <= left);
|
|
||||||
cpu.cpsr.v.write(((left ^ result) & (~right ^ result)) >> 31 & 1 == 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn armAdd(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32) u32 {
|
|
||||||
var result: u32 = undefined;
|
|
||||||
if (S and rd == 0xF) {
|
|
||||||
result = add(false, cpu, left, right);
|
|
||||||
cpu.setCpsr(cpu.spsr.raw);
|
|
||||||
} else {
|
|
||||||
result = add(S, cpu, left, right);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32) u32 {
|
|
||||||
var result: u32 = undefined;
|
|
||||||
const didOverflow = @addWithOverflow(u32, left, right, &result);
|
|
||||||
|
|
||||||
if (S) {
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
cpu.cpsr.c.write(didOverflow);
|
|
||||||
cpu.cpsr.v.write(((left ^ result) & (right ^ result)) >> 31 & 1 == 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn armAdc(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32, old_carry: u1) u32 {
|
|
||||||
var result: u32 = undefined;
|
|
||||||
if (S and rd == 0xF) {
|
|
||||||
result = adc(false, cpu, left, right, old_carry);
|
|
||||||
cpu.setCpsr(cpu.spsr.raw);
|
|
||||||
} else {
|
|
||||||
result = adc(S, cpu, left, right, old_carry);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn adc(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32, old_carry: u1) u32 {
|
|
||||||
var result: u32 = undefined;
|
|
||||||
const did = @addWithOverflow(u32, left, right, &result);
|
|
||||||
const overflow = @addWithOverflow(u32, result, old_carry, &result);
|
|
||||||
|
|
||||||
if (S) {
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
cpu.cpsr.c.write(did or overflow);
|
|
||||||
cpu.cpsr.v.write(((left ^ result) & (right ^ result)) >> 31 & 1 == 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cmp(cpu: *Arm7tdmi, left: u32, right: u32) void {
|
|
||||||
const result = left -% right;
|
|
||||||
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
cpu.cpsr.c.write(right <= left);
|
|
||||||
cpu.cpsr.v.write(((left ^ result) & (~right ^ result)) >> 31 & 1 == 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cmn(cpu: *Arm7tdmi, left: u32, right: u32) void {
|
|
||||||
var result: u32 = undefined;
|
|
||||||
const didOverflow = @addWithOverflow(u32, left, right, &result);
|
|
||||||
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
cpu.cpsr.c.write(didOverflow);
|
|
||||||
cpu.cpsr.v.write(((left ^ result) & (right ^ result)) >> 31 & 1 == 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setArmLogicOpFlags(comptime S: bool, cpu: *Arm7tdmi, rd: u4, result: u32) void {
|
|
||||||
if (S and rd == 0xF) {
|
|
||||||
cpu.setCpsr(cpu.spsr.raw);
|
|
||||||
} else {
|
|
||||||
setLogicOpFlags(S, cpu, result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setLogicOpFlags(comptime S: bool, cpu: *Arm7tdmi, result: u32) void {
|
|
||||||
if (S) {
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
// C set by Barrel Shifter, V is unaffected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setTestOpFlags(comptime S: bool, cpu: *Arm7tdmi, opcode: u32, result: u32) void {
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
// Barrel Shifter should always calc CPSR C in TST
|
|
||||||
if (!S) _ = execute(true, cpu, opcode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn undefinedTestBehaviour(cpu: *Arm7tdmi) void {
|
fn undefinedTestBehaviour(cpu: *Arm7tdmi) void {
|
||||||
|
|
|
@ -15,20 +15,8 @@ pub fn halfAndSignedDataTransfer(comptime P: bool, comptime U: bool, comptime I:
|
||||||
const rm = opcode & 0xF;
|
const rm = opcode & 0xF;
|
||||||
const imm_offset_high = opcode >> 8 & 0xF;
|
const imm_offset_high = opcode >> 8 & 0xF;
|
||||||
|
|
||||||
var base: u32 = undefined;
|
const base = cpu.r[rn] + if (!L and rn == 0xF) 4 else @as(u32, 0);
|
||||||
if (rn == 0xF) {
|
const offset = if (I) imm_offset_high << 4 | rm else cpu.r[rm];
|
||||||
base = cpu.fakePC();
|
|
||||||
if (!L) base += 4;
|
|
||||||
} else {
|
|
||||||
base = cpu.r[rn];
|
|
||||||
}
|
|
||||||
|
|
||||||
var offset: u32 = undefined;
|
|
||||||
if (I) {
|
|
||||||
offset = imm_offset_high << 4 | rm;
|
|
||||||
} else {
|
|
||||||
offset = cpu.r[rm];
|
|
||||||
}
|
|
||||||
|
|
||||||
const modified_base = if (U) base +% offset else base -% offset;
|
const modified_base = if (U) base +% offset else base -% offset;
|
||||||
var address = if (P) modified_base else base;
|
var address = if (P) modified_base else base;
|
||||||
|
|
|
@ -14,15 +14,10 @@ pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool,
|
||||||
const rn = opcode >> 16 & 0xF;
|
const rn = opcode >> 16 & 0xF;
|
||||||
const rd = opcode >> 12 & 0xF;
|
const rd = opcode >> 12 & 0xF;
|
||||||
|
|
||||||
var base: u32 = undefined;
|
// rn is r15 and L is not set, the PC is 12 ahead
|
||||||
if (rn == 0xF) {
|
const base = cpu.r[rn] + if (!L and rn == 0xF) 4 else @as(u32, 0);
|
||||||
base = cpu.fakePC();
|
|
||||||
if (!L) base += 4; // Offset of 12
|
|
||||||
} else {
|
|
||||||
base = cpu.r[rn];
|
|
||||||
}
|
|
||||||
|
|
||||||
const offset = if (I) shifter.immShift(false, cpu, opcode) else opcode & 0xFFF;
|
const offset = if (I) shifter.immediate(false, cpu, opcode) else opcode & 0xFFF;
|
||||||
|
|
||||||
const modified_base = if (U) base +% offset else base -% offset;
|
const modified_base = if (U) base +% offset else base -% offset;
|
||||||
var address = if (P) modified_base else base;
|
var address = if (P) modified_base else base;
|
||||||
|
@ -40,18 +35,26 @@ pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool,
|
||||||
} else {
|
} else {
|
||||||
if (B) {
|
if (B) {
|
||||||
// STRB
|
// STRB
|
||||||
const value = if (rd == 0xF) cpu.r[rd] + 8 else cpu.r[rd];
|
const value = cpu.r[rd] + if (rd == 0xF) 4 else @as(u32, 0); // PC is 12 ahead
|
||||||
bus.write(u8, address, @truncate(u8, value));
|
bus.write(u8, address, @truncate(u8, value));
|
||||||
} else {
|
} else {
|
||||||
// STR
|
// STR
|
||||||
const value = if (rd == 0xF) cpu.r[rd] + 8 else cpu.r[rd];
|
const value = cpu.r[rd] + if (rd == 0xF) 4 else @as(u32, 0);
|
||||||
bus.write(u32, address, value);
|
bus.write(u32, address, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
address = modified_base;
|
address = modified_base;
|
||||||
if (W and P or !P) cpu.r[rn] = address;
|
if (W and P or !P) {
|
||||||
if (L) cpu.r[rd] = result; // This emulates the LDR rd == rn behaviour
|
cpu.r[rn] = address;
|
||||||
|
if (rn == 0xF) cpu.pipe.reload(cpu);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (L) {
|
||||||
|
// This emulates the LDR rd == rn behaviour
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
if (rd == 0xF) cpu.pipe.reload(cpu);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ pub fn armSoftwareInterrupt() InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, _: u32) void {
|
fn inner(cpu: *Arm7tdmi, _: *Bus, _: u32) void {
|
||||||
// Copy Values from Current Mode
|
// Copy Values from Current Mode
|
||||||
const r15 = cpu.r[15];
|
const ret_addr = cpu.r[15] - 4;
|
||||||
const cpsr = cpu.cpsr.raw;
|
const cpsr = cpu.cpsr.raw;
|
||||||
|
|
||||||
// Switch Mode
|
// Switch Mode
|
||||||
|
@ -14,9 +14,10 @@ pub fn armSoftwareInterrupt() InstrFn {
|
||||||
cpu.cpsr.t.write(false); // Force ARM Mode
|
cpu.cpsr.t.write(false); // Force ARM Mode
|
||||||
cpu.cpsr.i.write(true); // Disable normal interrupts
|
cpu.cpsr.i.write(true); // Disable normal interrupts
|
||||||
|
|
||||||
cpu.r[14] = r15; // Resume Execution
|
cpu.r[14] = ret_addr; // Resume Execution
|
||||||
cpu.spsr.raw = cpsr; // Previous mode CPSR
|
cpu.spsr.raw = cpsr; // Previous mode CPSR
|
||||||
cpu.r[15] = 0x0000_0008;
|
cpu.r[15] = 0x0000_0008;
|
||||||
|
cpu.pipe.reload(cpu);
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,37 +5,33 @@ const CPSR = @import("../cpu.zig").PSR;
|
||||||
|
|
||||||
const rotr = @import("../../util.zig").rotr;
|
const rotr = @import("../../util.zig").rotr;
|
||||||
|
|
||||||
pub fn execute(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
pub fn exec(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
||||||
var result: u32 = undefined;
|
var result: u32 = undefined;
|
||||||
if (opcode >> 4 & 1 == 1) {
|
if (opcode >> 4 & 1 == 1) {
|
||||||
result = registerShift(S, cpu, opcode);
|
result = register(S, cpu, opcode);
|
||||||
} else {
|
} else {
|
||||||
result = immShift(S, cpu, opcode);
|
result = immediate(S, cpu, opcode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn registerShift(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
fn register(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
||||||
const rs_idx = opcode >> 8 & 0xF;
|
const rs_idx = opcode >> 8 & 0xF;
|
||||||
|
const rm = cpu.r[opcode & 0xF];
|
||||||
const rs = @truncate(u8, cpu.r[rs_idx]);
|
const rs = @truncate(u8, cpu.r[rs_idx]);
|
||||||
|
|
||||||
const rm_idx = opcode & 0xF;
|
|
||||||
const rm = if (rm_idx == 0xF) cpu.fakePC() else cpu.r[rm_idx];
|
|
||||||
|
|
||||||
return switch (@truncate(u2, opcode >> 5)) {
|
return switch (@truncate(u2, opcode >> 5)) {
|
||||||
0b00 => logicalLeft(S, &cpu.cpsr, rm, rs),
|
0b00 => lsl(S, &cpu.cpsr, rm, rs),
|
||||||
0b01 => logicalRight(S, &cpu.cpsr, rm, rs),
|
0b01 => lsr(S, &cpu.cpsr, rm, rs),
|
||||||
0b10 => arithmeticRight(S, &cpu.cpsr, rm, rs),
|
0b10 => asr(S, &cpu.cpsr, rm, rs),
|
||||||
0b11 => rotateRight(S, &cpu.cpsr, rm, rs),
|
0b11 => ror(S, &cpu.cpsr, rm, rs),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn immShift(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
pub fn immediate(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
||||||
const amount = @truncate(u8, opcode >> 7 & 0x1F);
|
const amount = @truncate(u8, opcode >> 7 & 0x1F);
|
||||||
|
const rm = cpu.r[opcode & 0xF];
|
||||||
const rm_idx = opcode & 0xF;
|
|
||||||
const rm = if (rm_idx == 0xF) cpu.fakePC() else cpu.r[rm_idx];
|
|
||||||
|
|
||||||
var result: u32 = undefined;
|
var result: u32 = undefined;
|
||||||
if (amount == 0) {
|
if (amount == 0) {
|
||||||
|
@ -64,17 +60,17 @@ pub fn immShift(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch (@truncate(u2, opcode >> 5)) {
|
switch (@truncate(u2, opcode >> 5)) {
|
||||||
0b00 => result = logicalLeft(S, &cpu.cpsr, rm, amount),
|
0b00 => result = lsl(S, &cpu.cpsr, rm, amount),
|
||||||
0b01 => result = logicalRight(S, &cpu.cpsr, rm, amount),
|
0b01 => result = lsr(S, &cpu.cpsr, rm, amount),
|
||||||
0b10 => result = arithmeticRight(S, &cpu.cpsr, rm, amount),
|
0b10 => result = asr(S, &cpu.cpsr, rm, amount),
|
||||||
0b11 => result = rotateRight(S, &cpu.cpsr, rm, amount),
|
0b11 => result = ror(S, &cpu.cpsr, rm, amount),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn logicalLeft(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
pub fn lsl(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
||||||
const amount = @truncate(u5, total_amount);
|
const amount = @truncate(u5, total_amount);
|
||||||
const bit_count: u8 = @typeInfo(u32).Int.bits;
|
const bit_count: u8 = @typeInfo(u32).Int.bits;
|
||||||
|
|
||||||
|
@ -101,7 +97,7 @@ pub fn logicalLeft(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn logicalRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u32) u32 {
|
pub fn lsr(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u32) u32 {
|
||||||
const amount = @truncate(u5, total_amount);
|
const amount = @truncate(u5, total_amount);
|
||||||
const bit_count: u8 = @typeInfo(u32).Int.bits;
|
const bit_count: u8 = @typeInfo(u32).Int.bits;
|
||||||
|
|
||||||
|
@ -125,7 +121,7 @@ pub fn logicalRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u32) u
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn arithmeticRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
pub fn asr(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
||||||
const amount = @truncate(u5, total_amount);
|
const amount = @truncate(u5, total_amount);
|
||||||
const bit_count: u8 = @typeInfo(u32).Int.bits;
|
const bit_count: u8 = @typeInfo(u32).Int.bits;
|
||||||
|
|
||||||
|
@ -142,7 +138,7 @@ pub fn arithmeticRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8)
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rotateRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
pub fn ror(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
||||||
const result = rotr(u32, rm, total_amount);
|
const result = rotr(u32, rm, total_amount);
|
||||||
|
|
||||||
if (S and total_amount != 0) {
|
if (S and total_amount != 0) {
|
||||||
|
|
|
@ -4,16 +4,11 @@ const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
||||||
|
|
||||||
const adc = @import("../arm/data_processing.zig").adc;
|
const adc = @import("../arm/data_processing.zig").adc;
|
||||||
const sbc = @import("../arm/data_processing.zig").sbc;
|
const sbc = @import("../arm/data_processing.zig").sbc;
|
||||||
const sub = @import("../arm/data_processing.zig").sub;
|
|
||||||
const cmp = @import("../arm/data_processing.zig").cmp;
|
|
||||||
const cmn = @import("../arm/data_processing.zig").cmn;
|
|
||||||
const setTestOpFlags = @import("../arm/data_processing.zig").setTestOpFlags;
|
|
||||||
const setLogicOpFlags = @import("../arm/data_processing.zig").setLogicOpFlags;
|
|
||||||
|
|
||||||
const logicalLeft = @import("../barrel_shifter.zig").logicalLeft;
|
const lsl = @import("../barrel_shifter.zig").lsl;
|
||||||
const logicalRight = @import("../barrel_shifter.zig").logicalRight;
|
const lsr = @import("../barrel_shifter.zig").lsr;
|
||||||
const arithmeticRight = @import("../barrel_shifter.zig").arithmeticRight;
|
const asr = @import("../barrel_shifter.zig").asr;
|
||||||
const rotateRight = @import("../barrel_shifter.zig").rotateRight;
|
const ror = @import("../barrel_shifter.zig").ror;
|
||||||
|
|
||||||
pub fn fmt4(comptime op: u4) InstrFn {
|
pub fn fmt4(comptime op: u4) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
|
@ -22,96 +17,85 @@ pub fn fmt4(comptime op: u4) InstrFn {
|
||||||
const rd = opcode & 0x7;
|
const rd = opcode & 0x7;
|
||||||
const carry = @boolToInt(cpu.cpsr.c.read());
|
const carry = @boolToInt(cpu.cpsr.c.read());
|
||||||
|
|
||||||
|
const op1 = cpu.r[rd];
|
||||||
|
const op2 = cpu.r[rs];
|
||||||
|
|
||||||
|
var result: u32 = undefined;
|
||||||
|
var overflow: bool = undefined;
|
||||||
switch (op) {
|
switch (op) {
|
||||||
0x0 => {
|
0x0 => result = op1 & op2, // AND
|
||||||
// AND
|
0x1 => result = op1 ^ op2, // EOR
|
||||||
const result = cpu.r[rd] & cpu.r[rs];
|
0x2 => result = lsl(true, &cpu.cpsr, op1, @truncate(u8, op2)), // LSL
|
||||||
cpu.r[rd] = result;
|
0x3 => result = lsr(true, &cpu.cpsr, op1, @truncate(u8, op2)), // LSR
|
||||||
setLogicOpFlags(true, cpu, result);
|
0x4 => result = asr(true, &cpu.cpsr, op1, @truncate(u8, op2)), // ASR
|
||||||
|
0x5 => result = adc(&overflow, op1, op2, carry), // ADC
|
||||||
|
0x6 => result = sbc(op1, op2, carry), // SBC
|
||||||
|
0x7 => result = ror(true, &cpu.cpsr, op1, @truncate(u8, op2)), // ROR
|
||||||
|
0x8 => result = op1 & op2, // TST
|
||||||
|
0x9 => result = 0 -% op2, // NEG
|
||||||
|
0xA => result = op1 -% op2, // CMP
|
||||||
|
0xB => overflow = @addWithOverflow(u32, op1, op2, &result), // CMN
|
||||||
|
0xC => result = op1 | op2, // ORR
|
||||||
|
0xD => result = @truncate(u32, @as(u64, op2) * @as(u64, op1)),
|
||||||
|
0xE => result = op1 & ~op2,
|
||||||
|
0xF => result = ~op2,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to Destination Register
|
||||||
|
switch (op) {
|
||||||
|
0x8, 0xA, 0xB => {},
|
||||||
|
else => cpu.r[rd] = result,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write Flags
|
||||||
|
switch (op) {
|
||||||
|
0x0, 0x1, 0x2, 0x3, 0x4, 0x7, 0xC, 0xE, 0xF => {
|
||||||
|
// Logic Operations
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
// C set by Barrel Shifter, V is unaffected
|
||||||
},
|
},
|
||||||
0x1 => {
|
0x8, 0xA => {
|
||||||
// EOR
|
// Test Flags
|
||||||
const result = cpu.r[rd] ^ cpu.r[rs];
|
// CMN (0xB) is handled with ADC
|
||||||
cpu.r[rd] = result;
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
setLogicOpFlags(true, cpu, result);
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
|
||||||
|
if (op == 0xA) {
|
||||||
|
// CMP specific
|
||||||
|
cpu.cpsr.c.write(op2 <= op1);
|
||||||
|
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
0x2 => {
|
0x5, 0xB => {
|
||||||
// LSL
|
// ADC, CMN
|
||||||
const result = logicalLeft(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
cpu.r[rd] = result;
|
cpu.cpsr.z.write(result == 0);
|
||||||
setLogicOpFlags(true, cpu, result);
|
cpu.cpsr.c.write(overflow);
|
||||||
},
|
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
|
||||||
0x3 => {
|
|
||||||
// LSR
|
|
||||||
const result = logicalRight(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
setLogicOpFlags(true, cpu, result);
|
|
||||||
},
|
|
||||||
0x4 => {
|
|
||||||
// ASR
|
|
||||||
const result = arithmeticRight(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
setLogicOpFlags(true, cpu, result);
|
|
||||||
},
|
|
||||||
0x5 => {
|
|
||||||
// ADC
|
|
||||||
cpu.r[rd] = adc(true, cpu, cpu.r[rd], cpu.r[rs], carry);
|
|
||||||
},
|
},
|
||||||
0x6 => {
|
0x6 => {
|
||||||
// SBC
|
// SBC
|
||||||
cpu.r[rd] = sbc(true, cpu, cpu.r[rd], cpu.r[rs], carry);
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
},
|
cpu.cpsr.z.write(result == 0);
|
||||||
0x7 => {
|
|
||||||
// ROR
|
const subtrahend = @as(u64, op2) -% carry +% 1;
|
||||||
const result = rotateRight(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
|
cpu.cpsr.c.write(subtrahend <= op1);
|
||||||
cpu.r[rd] = result;
|
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
||||||
setLogicOpFlags(true, cpu, result);
|
|
||||||
},
|
|
||||||
0x8 => {
|
|
||||||
// TST
|
|
||||||
const result = cpu.r[rd] & cpu.r[rs];
|
|
||||||
setLogicOpFlags(true, cpu, result);
|
|
||||||
},
|
},
|
||||||
0x9 => {
|
0x9 => {
|
||||||
// NEG
|
// NEG
|
||||||
cpu.r[rd] = sub(true, cpu, 0, cpu.r[rs]);
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
},
|
cpu.cpsr.z.write(result == 0);
|
||||||
0xA => {
|
cpu.cpsr.c.write(op2 <= 0);
|
||||||
// CMP
|
cpu.cpsr.v.write(((0 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
||||||
cmp(cpu, cpu.r[rd], cpu.r[rs]);
|
|
||||||
},
|
|
||||||
0xB => {
|
|
||||||
// CMN
|
|
||||||
cmn(cpu, cpu.r[rd], cpu.r[rs]);
|
|
||||||
},
|
|
||||||
0xC => {
|
|
||||||
// ORR
|
|
||||||
const result = cpu.r[rd] | cpu.r[rs];
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
setLogicOpFlags(true, cpu, result);
|
|
||||||
},
|
},
|
||||||
0xD => {
|
0xD => {
|
||||||
// MUL
|
// Multiplication
|
||||||
const temp = @as(u64, cpu.r[rs]) * @as(u64, cpu.r[rd]);
|
|
||||||
const result = @truncate(u32, temp);
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
cpu.cpsr.z.write(result == 0);
|
cpu.cpsr.z.write(result == 0);
|
||||||
// V is unaffected, assuming similar behaviour to ARMv4 MUL C is undefined
|
// V is unaffected, assuming similar behaviour to ARMv4 MUL C is undefined
|
||||||
},
|
},
|
||||||
0xE => {
|
|
||||||
// BIC
|
|
||||||
const result = cpu.r[rd] & ~cpu.r[rs];
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
setLogicOpFlags(true, cpu, result);
|
|
||||||
},
|
|
||||||
0xF => {
|
|
||||||
// MVN
|
|
||||||
const result = ~cpu.r[rs];
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
setLogicOpFlags(true, cpu, result);
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
|
|
|
@ -33,7 +33,8 @@ pub fn fmt14(comptime L: bool, comptime R: bool) InstrFn {
|
||||||
if (R) {
|
if (R) {
|
||||||
if (L) {
|
if (L) {
|
||||||
const value = bus.read(u32, address);
|
const value = bus.read(u32, address);
|
||||||
cpu.r[15] = value & 0xFFFF_FFFE;
|
cpu.r[15] = value & ~@as(u32, 1);
|
||||||
|
cpu.pipe.reload(cpu);
|
||||||
} else {
|
} else {
|
||||||
bus.write(u32, address, cpu.r[14]);
|
bus.write(u32, address, cpu.r[14]);
|
||||||
}
|
}
|
||||||
|
@ -52,7 +53,13 @@ pub fn fmt15(comptime L: bool, comptime rb: u3) InstrFn {
|
||||||
const end_address = cpu.r[rb] + 4 * countRlist(opcode);
|
const end_address = cpu.r[rb] + 4 * countRlist(opcode);
|
||||||
|
|
||||||
if (opcode & 0xFF == 0) {
|
if (opcode & 0xFF == 0) {
|
||||||
if (L) cpu.r[15] = bus.read(u32, address) else bus.write(u32, address, cpu.r[15] + 4);
|
if (L) {
|
||||||
|
cpu.r[15] = bus.read(u32, address);
|
||||||
|
cpu.pipe.reload(cpu);
|
||||||
|
} else {
|
||||||
|
bus.write(u32, address, cpu.r[15] + 2);
|
||||||
|
}
|
||||||
|
|
||||||
cpu.r[rb] += 0x40;
|
cpu.r[rb] += 0x40;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,16 +9,13 @@ pub fn fmt16(comptime cond: u4) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||||
// B
|
// B
|
||||||
const offset = sext(u32, u8, opcode & 0xFF) << 1;
|
if (cond == 0xE or cond == 0xF)
|
||||||
|
cpu.panic("[CPU/THUMB.16] Undefined conditional branch with condition {}", .{cond});
|
||||||
|
|
||||||
const should_execute = switch (cond) {
|
if (!checkCond(cpu.cpsr, cond)) return;
|
||||||
0xE, 0xF => cpu.panic("[CPU/THUMB.16] Undefined conditional branch with condition {}", .{cond}),
|
|
||||||
else => checkCond(cpu.cpsr, cond),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (should_execute) {
|
cpu.r[15] +%= sext(u32, u8, opcode & 0xFF) << 1;
|
||||||
cpu.r[15] = (cpu.r[15] + 2) +% offset;
|
cpu.pipe.reload(cpu);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
}
|
}
|
||||||
|
@ -27,8 +24,8 @@ pub fn fmt18() InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
// B but conditional
|
// B but conditional
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||||
const offset = sext(u32, u11, opcode & 0x7FF) << 1;
|
cpu.r[15] +%= sext(u32, u11, opcode & 0x7FF) << 1;
|
||||||
cpu.r[15] = (cpu.r[15] + 2) +% offset;
|
cpu.pipe.reload(cpu);
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
}
|
}
|
||||||
|
@ -41,13 +38,16 @@ pub fn fmt19(comptime is_low: bool) InstrFn {
|
||||||
|
|
||||||
if (is_low) {
|
if (is_low) {
|
||||||
// Instruction 2
|
// Instruction 2
|
||||||
const old_pc = cpu.r[15];
|
const next_opcode = cpu.r[15] - 2;
|
||||||
|
|
||||||
cpu.r[15] = cpu.r[14] +% (offset << 1);
|
cpu.r[15] = cpu.r[14] +% (offset << 1);
|
||||||
cpu.r[14] = old_pc | 1;
|
cpu.r[14] = next_opcode | 1;
|
||||||
|
|
||||||
|
cpu.pipe.reload(cpu);
|
||||||
} else {
|
} else {
|
||||||
// Instruction 1
|
// Instruction 1
|
||||||
cpu.r[14] = (cpu.r[15] + 2) +% (sext(u32, u11, offset) << 12);
|
const lr_offset = sext(u32, u11, offset) << 12;
|
||||||
|
cpu.r[14] = (cpu.r[15] +% lr_offset) & ~@as(u32, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
|
|
|
@ -3,14 +3,12 @@ const std = @import("std");
|
||||||
const Bus = @import("../../Bus.zig");
|
const Bus = @import("../../Bus.zig");
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
||||||
const shifter = @import("../barrel_shifter.zig");
|
|
||||||
|
|
||||||
const add = @import("../arm/data_processing.zig").add;
|
const add = @import("../arm/data_processing.zig").add;
|
||||||
const sub = @import("../arm/data_processing.zig").sub;
|
|
||||||
const cmp = @import("../arm/data_processing.zig").cmp;
|
|
||||||
const setLogicOpFlags = @import("../arm/data_processing.zig").setLogicOpFlags;
|
|
||||||
|
|
||||||
const log = std.log.scoped(.Thumb1);
|
const lsl = @import("../barrel_shifter.zig").lsl;
|
||||||
|
const lsr = @import("../barrel_shifter.zig").lsr;
|
||||||
|
const asr = @import("../barrel_shifter.zig").asr;
|
||||||
|
|
||||||
pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
|
pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
|
@ -24,7 +22,7 @@ pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
|
||||||
if (offset == 0) {
|
if (offset == 0) {
|
||||||
break :blk cpu.r[rs];
|
break :blk cpu.r[rs];
|
||||||
} else {
|
} else {
|
||||||
break :blk shifter.logicalLeft(true, &cpu.cpsr, cpu.r[rs], offset);
|
break :blk lsl(true, &cpu.cpsr, cpu.r[rs], offset);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
0b01 => blk: {
|
0b01 => blk: {
|
||||||
|
@ -33,7 +31,7 @@ pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
|
||||||
cpu.cpsr.c.write(cpu.r[rs] >> 31 & 1 == 1);
|
cpu.cpsr.c.write(cpu.r[rs] >> 31 & 1 == 1);
|
||||||
break :blk @as(u32, 0);
|
break :blk @as(u32, 0);
|
||||||
} else {
|
} else {
|
||||||
break :blk shifter.logicalRight(true, &cpu.cpsr, cpu.r[rs], offset);
|
break :blk lsr(true, &cpu.cpsr, cpu.r[rs], offset);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
0b10 => blk: {
|
0b10 => blk: {
|
||||||
|
@ -42,7 +40,7 @@ pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
|
||||||
cpu.cpsr.c.write(cpu.r[rs] >> 31 & 1 == 1);
|
cpu.cpsr.c.write(cpu.r[rs] >> 31 & 1 == 1);
|
||||||
break :blk @bitCast(u32, @bitCast(i32, cpu.r[rs]) >> 31);
|
break :blk @bitCast(u32, @bitCast(i32, cpu.r[rs]) >> 31);
|
||||||
} else {
|
} else {
|
||||||
break :blk shifter.arithmeticRight(true, &cpu.cpsr, cpu.r[rs], offset);
|
break :blk asr(true, &cpu.cpsr, cpu.r[rs], offset);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
else => cpu.panic("[CPU/THUMB.1] 0b{b:0>2} is not a valid op", .{op}),
|
else => cpu.panic("[CPU/THUMB.1] 0b{b:0>2} is not a valid op", .{op}),
|
||||||
|
@ -50,7 +48,10 @@ pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
|
||||||
|
|
||||||
// Equivalent to an ARM MOVS
|
// Equivalent to an ARM MOVS
|
||||||
cpu.r[rd] = result;
|
cpu.r[rd] = result;
|
||||||
setLogicOpFlags(true, cpu, result);
|
|
||||||
|
// Write Flags
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
}
|
}
|
||||||
|
@ -58,28 +59,51 @@ pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
|
||||||
pub fn fmt5(comptime op: u2, comptime h1: u1, comptime h2: u1) InstrFn {
|
pub fn fmt5(comptime op: u2, comptime h1: u1, comptime h2: u1) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||||
const src_idx = @as(u4, h2) << 3 | (opcode >> 3 & 0x7);
|
const rs = @as(u4, h2) << 3 | (opcode >> 3 & 0x7);
|
||||||
const dst_idx = @as(u4, h1) << 3 | (opcode & 0x7);
|
const rd = @as(u4, h1) << 3 | (opcode & 0x7);
|
||||||
|
|
||||||
const src = if (src_idx == 0xF) (cpu.r[src_idx] + 2) & 0xFFFF_FFFE else cpu.r[src_idx];
|
const op1 = cpu.r[rd];
|
||||||
const dst = if (dst_idx == 0xF) (cpu.r[dst_idx] + 2) & 0xFFFF_FFFE else cpu.r[dst_idx];
|
const op2 = cpu.r[rs];
|
||||||
|
|
||||||
|
var result: u32 = undefined;
|
||||||
|
var overflow: bool = undefined;
|
||||||
switch (op) {
|
switch (op) {
|
||||||
0b00 => {
|
0b00 => result = add(&overflow, op1, op2), // ADD
|
||||||
// ADD
|
0b01 => result = op1 -% op2, // CMP
|
||||||
const sum = add(false, cpu, dst, src);
|
0b10 => result = op2, // MOV
|
||||||
cpu.r[dst_idx] = if (dst_idx == 0xF) sum & 0xFFFF_FFFE else sum;
|
0b11 => {},
|
||||||
},
|
}
|
||||||
0b01 => cmp(cpu, dst, src), // CMP
|
|
||||||
0b10 => {
|
// Write to Destination Register
|
||||||
// MOV
|
switch (op) {
|
||||||
cpu.r[dst_idx] = if (dst_idx == 0xF) src & 0xFFFF_FFFE else src;
|
0b01 => {}, // Test Instruction
|
||||||
},
|
|
||||||
0b11 => {
|
0b11 => {
|
||||||
// BX
|
// BX
|
||||||
cpu.cpsr.t.write(src & 1 == 1);
|
const is_thumb = op2 & 1 == 1;
|
||||||
cpu.r[15] = src & 0xFFFF_FFFE;
|
cpu.r[15] = op2 & ~@as(u32, 1);
|
||||||
|
|
||||||
|
cpu.cpsr.t.write(is_thumb);
|
||||||
|
cpu.pipe.reload(cpu);
|
||||||
},
|
},
|
||||||
|
else => {
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
if (rd == 0xF) {
|
||||||
|
cpu.r[15] &= ~@as(u32, 1);
|
||||||
|
cpu.pipe.reload(cpu);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write Flags
|
||||||
|
switch (op) {
|
||||||
|
0b01 => {
|
||||||
|
// CMP
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
cpu.cpsr.c.write(op2 <= op1);
|
||||||
|
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
||||||
|
},
|
||||||
|
0b00, 0b10, 0b11 => {}, // MOV and Branch Instruction
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
|
@ -90,21 +114,28 @@ pub fn fmt2(comptime I: bool, is_sub: bool, rn: u3) InstrFn {
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||||
const rs = opcode >> 3 & 0x7;
|
const rs = opcode >> 3 & 0x7;
|
||||||
const rd = @truncate(u3, opcode);
|
const rd = @truncate(u3, opcode);
|
||||||
|
const op1 = cpu.r[rs];
|
||||||
|
const op2: u32 = if (I) rn else cpu.r[rn];
|
||||||
|
|
||||||
if (is_sub) {
|
if (is_sub) {
|
||||||
// SUB
|
// SUB
|
||||||
cpu.r[rd] = if (I) blk: {
|
const result = op1 -% op2;
|
||||||
break :blk sub(true, cpu, cpu.r[rs], rn);
|
cpu.r[rd] = result;
|
||||||
} else blk: {
|
|
||||||
break :blk sub(true, cpu, cpu.r[rs], cpu.r[rn]);
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
};
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
cpu.cpsr.c.write(op2 <= op1);
|
||||||
|
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
||||||
} else {
|
} else {
|
||||||
// ADD
|
// ADD
|
||||||
cpu.r[rd] = if (I) blk: {
|
var overflow: bool = undefined;
|
||||||
break :blk add(true, cpu, cpu.r[rs], rn);
|
const result = add(&overflow, op1, op2);
|
||||||
} else blk: {
|
cpu.r[rd] = result;
|
||||||
break :blk add(true, cpu, cpu.r[rs], cpu.r[rn]);
|
|
||||||
};
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
cpu.cpsr.c.write(overflow);
|
||||||
|
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
|
@ -113,17 +144,36 @@ pub fn fmt2(comptime I: bool, is_sub: bool, rn: u3) InstrFn {
|
||||||
pub fn fmt3(comptime op: u2, comptime rd: u3) InstrFn {
|
pub fn fmt3(comptime op: u2, comptime rd: u3) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||||
const offset = @truncate(u8, opcode);
|
const op1 = cpu.r[rd];
|
||||||
|
const op2: u32 = opcode & 0xFF; // Offset
|
||||||
|
|
||||||
|
var overflow: bool = undefined;
|
||||||
|
const result: u32 = switch (op) {
|
||||||
|
0b00 => op2, // MOV
|
||||||
|
0b01 => op1 -% op2, // CMP
|
||||||
|
0b10 => add(&overflow, op1, op2), // ADD
|
||||||
|
0b11 => op1 -% op2, // SUB
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write to Register
|
||||||
|
if (op != 0b01) cpu.r[rd] = result;
|
||||||
|
|
||||||
|
// Write Flags
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
|
||||||
switch (op) {
|
switch (op) {
|
||||||
0b00 => {
|
0b00 => {}, // MOV | C set by Barrel Shifter, V is unaffected
|
||||||
// MOV
|
0b01, 0b11 => {
|
||||||
cpu.r[rd] = offset;
|
// SUB, CMP
|
||||||
setLogicOpFlags(true, cpu, offset);
|
cpu.cpsr.c.write(op2 <= op1);
|
||||||
|
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
||||||
|
},
|
||||||
|
0b10 => {
|
||||||
|
// ADD
|
||||||
|
cpu.cpsr.c.write(overflow);
|
||||||
|
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
|
||||||
},
|
},
|
||||||
0b01 => cmp(cpu, cpu.r[rd], offset), // CMP
|
|
||||||
0b10 => cpu.r[rd] = add(true, cpu, cpu.r[rd], offset), // ADD
|
|
||||||
0b11 => cpu.r[rd] = sub(true, cpu, cpu.r[rd], offset), // SUB
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
|
@ -133,10 +183,9 @@ pub fn fmt12(comptime isSP: bool, comptime rd: u3) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||||
// ADD
|
// ADD
|
||||||
const left = if (isSP) cpu.r[13] else (cpu.r[15] + 2) & 0xFFFF_FFFD;
|
const left = if (isSP) cpu.r[13] else cpu.r[15] & ~@as(u32, 2);
|
||||||
const right = (opcode & 0xFF) << 2;
|
const right = (opcode & 0xFF) << 2;
|
||||||
const result = left + right;
|
cpu.r[rd] = left + right;
|
||||||
cpu.r[rd] = result;
|
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,9 @@ pub fn fmt6(comptime rd: u3) InstrFn {
|
||||||
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
|
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
|
||||||
// LDR
|
// LDR
|
||||||
const offset = (opcode & 0xFF) << 2;
|
const offset = (opcode & 0xFF) << 2;
|
||||||
cpu.r[rd] = bus.read(u32, (cpu.r[15] + 2 & 0xFFFF_FFFD) + offset);
|
|
||||||
|
// Bit 1 of the PC intentionally ignored
|
||||||
|
cpu.r[rd] = bus.read(u32, (cpu.r[15] & ~@as(u32, 2)) + offset);
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ pub fn fmt17() InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, _: u16) void {
|
fn inner(cpu: *Arm7tdmi, _: *Bus, _: u16) void {
|
||||||
// Copy Values from Current Mode
|
// Copy Values from Current Mode
|
||||||
const r15 = cpu.r[15];
|
const ret_addr = cpu.r[15] - 2;
|
||||||
const cpsr = cpu.cpsr.raw;
|
const cpsr = cpu.cpsr.raw;
|
||||||
|
|
||||||
// Switch Mode
|
// Switch Mode
|
||||||
|
@ -14,9 +14,10 @@ pub fn fmt17() InstrFn {
|
||||||
cpu.cpsr.t.write(false); // Force ARM Mode
|
cpu.cpsr.t.write(false); // Force ARM Mode
|
||||||
cpu.cpsr.i.write(true); // Disable normal interrupts
|
cpu.cpsr.i.write(true); // Disable normal interrupts
|
||||||
|
|
||||||
cpu.r[14] = r15; // Resume Execution
|
cpu.r[14] = ret_addr; // Resume Execution
|
||||||
cpu.spsr.raw = cpsr; // Previous mode CPSR
|
cpu.spsr.raw = cpsr; // Previous mode CPSR
|
||||||
cpu.r[15] = 0x0000_0008;
|
cpu.r[15] = 0x0000_0008;
|
||||||
|
cpu.pipe.reload(cpu);
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
}
|
}
|
||||||
|
|
148
src/core/emu.zig
148
src/core/emu.zig
|
@ -1,5 +1,6 @@
|
||||||
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;
|
||||||
|
@ -12,14 +13,6 @@ 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
|
||||||
|
@ -40,18 +33,57 @@ const RunKind = enum {
|
||||||
UnlimitedFPS,
|
UnlimitedFPS,
|
||||||
Limited,
|
Limited,
|
||||||
LimitedFPS,
|
LimitedFPS,
|
||||||
LimitedBusy,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn run(quit: *Atomic(bool), fps: *FpsTracker, sched: *Scheduler, cpu: *Arm7tdmi) void {
|
pub fn run(quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: *FpsTracker) void {
|
||||||
if (sync_audio) log.info("Audio sync enabled", .{});
|
const audio_sync = config.config().guest.audio_sync;
|
||||||
|
if (audio_sync) log.info("Audio sync enabled", .{});
|
||||||
|
|
||||||
switch (sync_video) {
|
if (config.config().guest.video_sync) {
|
||||||
.Unlimited => runUnsynchronized(quit, sched, cpu, null),
|
inner(.LimitedFPS, audio_sync, quit, scheduler, cpu, tracker);
|
||||||
.Limited => runSynchronized(quit, sched, cpu, null),
|
} else {
|
||||||
.UnlimitedFPS => runUnsynchronized(quit, sched, cpu, fps),
|
inner(.UnlimitedFPS, audio_sync, quit, scheduler, cpu, tracker);
|
||||||
.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();
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +104,7 @@ pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn syncToAudio(stream: *SDL.SDL_AudioStream, is_buffer_full: *bool) void {
|
fn audioSync(audio_sync: bool, 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;
|
||||||
|
|
||||||
|
@ -85,90 +117,20 @@ fn syncToAudio(stream: *SDL.SDL_AudioStream, is_buffer_full: *bool) void {
|
||||||
|
|
||||||
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 (!sync_audio or !still_full) break;
|
if (!audio_sync or !still_full) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runUnsynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*FpsTracker) void {
|
fn videoSync(timer: *Timer, wake_time: u64) u64 {
|
||||||
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 maybe_recalc_wake_time = sleep(timer, wake_time);
|
const recalculated = 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 if (maybe_recalc_wake_time) |recalc| recalc else wake_time + frame_period;
|
return recalculated orelse 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();
|
||||||
|
|
|
@ -13,10 +13,7 @@ const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
||||||
const FrameBuffer = @import("../util.zig").FrameBuffer;
|
const FrameBuffer = @import("../util.zig").FrameBuffer;
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const log = std.log.scoped(.Ppu);
|
const log = std.log.scoped(.PPU);
|
||||||
|
|
||||||
/// This is used to generate byuu / Talurabi's Color Correction algorithm
|
|
||||||
const COLOUR_LUT = genColourLut();
|
|
||||||
|
|
||||||
pub const width = 240;
|
pub const width = 240;
|
||||||
pub const height = 160;
|
pub const height = 160;
|
||||||
|
@ -397,7 +394,7 @@ pub const Ppu = struct {
|
||||||
const maybe_btm = self.scanline.btm()[i];
|
const maybe_btm = self.scanline.btm()[i];
|
||||||
|
|
||||||
const bgr555 = self.getBgr555(maybe_top, maybe_btm);
|
const bgr555 = self.getBgr555(maybe_top, maybe_btm);
|
||||||
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
|
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset Current Scanline Pixel Buffer and list of fetched sprites
|
// Reset Current Scanline Pixel Buffer and list of fetched sprites
|
||||||
|
@ -424,7 +421,7 @@ pub const Ppu = struct {
|
||||||
const maybe_btm = self.scanline.btm()[i];
|
const maybe_btm = self.scanline.btm()[i];
|
||||||
|
|
||||||
const bgr555 = self.getBgr555(maybe_top, maybe_btm);
|
const bgr555 = self.getBgr555(maybe_top, maybe_btm);
|
||||||
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
|
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset Current Scanline Pixel Buffer and list of fetched sprites
|
// Reset Current Scanline Pixel Buffer and list of fetched sprites
|
||||||
|
@ -450,7 +447,7 @@ pub const Ppu = struct {
|
||||||
const maybe_btm = self.scanline.btm()[i];
|
const maybe_btm = self.scanline.btm()[i];
|
||||||
|
|
||||||
const bgr555 = self.getBgr555(maybe_top, maybe_btm);
|
const bgr555 = self.getBgr555(maybe_top, maybe_btm);
|
||||||
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
|
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset Current Scanline Pixel Buffer and list of fetched sprites
|
// Reset Current Scanline Pixel Buffer and list of fetched sprites
|
||||||
|
@ -465,7 +462,7 @@ pub const Ppu = struct {
|
||||||
var i: usize = 0;
|
var i: usize = 0;
|
||||||
while (i < width) : (i += 1) {
|
while (i < width) : (i += 1) {
|
||||||
const bgr555 = self.vram.read(u16, vram_base + i * @sizeOf(u16));
|
const bgr555 = self.vram.read(u16, vram_base + i * @sizeOf(u16));
|
||||||
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
|
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
0x4 => {
|
0x4 => {
|
||||||
|
@ -476,7 +473,7 @@ pub const Ppu = struct {
|
||||||
// Render Current Scanline
|
// Render Current Scanline
|
||||||
for (self.vram.buf[vram_base .. vram_base + width]) |byte, i| {
|
for (self.vram.buf[vram_base .. vram_base + width]) |byte, i| {
|
||||||
const bgr555 = self.palette.read(u16, @as(u16, byte) * @sizeOf(u16));
|
const bgr555 = self.palette.read(u16, @as(u16, byte) * @sizeOf(u16));
|
||||||
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
|
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
0x5 => {
|
0x5 => {
|
||||||
|
@ -493,7 +490,7 @@ pub const Ppu = struct {
|
||||||
const bgr555 =
|
const bgr555 =
|
||||||
if (scanline < m5_height and i < m5_width) self.vram.read(u16, vram_base + i * @sizeOf(u16)) else self.palette.backdrop();
|
if (scanline < m5_height and i < m5_width) self.vram.read(u16, vram_base + i * @sizeOf(u16)) else self.palette.backdrop();
|
||||||
|
|
||||||
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
|
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
else => std.debug.panic("[PPU] TODO: Implement BG Mode {}", .{bg_mode}),
|
else => std.debug.panic("[PPU] TODO: Implement BG Mode {}", .{bg_mode}),
|
||||||
|
@ -654,7 +651,7 @@ pub const Ppu = struct {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handleHDrawEnd(self: *Self, cpu: *Arm7tdmi, late: u64) void {
|
pub fn onHdrawEnd(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();
|
||||||
|
@ -670,7 +667,7 @@ pub const Ppu = struct {
|
||||||
self.sched.push(.HBlank, 68 * 4 -| late);
|
self.sched.push(.HBlank, 68 * 4 -| late);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handleHBlankEnd(self: *Self, cpu: *Arm7tdmi, late: u64) void {
|
pub fn onHblankEnd(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;
|
||||||
|
@ -767,18 +764,6 @@ 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 {
|
||||||
|
@ -1034,7 +1019,7 @@ fn spriteDimensions(shape: u2, size: u2) [2]u8 {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toRgba8888(bgr555: u16) u32 {
|
inline fn rgba888(bgr555: u16) u32 {
|
||||||
const b = @as(u32, bgr555 >> 10 & 0x1F);
|
const b = @as(u32, bgr555 >> 10 & 0x1F);
|
||||||
const g = @as(u32, bgr555 >> 5 & 0x1F);
|
const g = @as(u32, bgr555 >> 5 & 0x1F);
|
||||||
const r = @as(u32, bgr555 & 0x1F);
|
const r = @as(u32, bgr555 & 0x1F);
|
||||||
|
@ -1042,39 +1027,6 @@ fn toRgba8888(bgr555: u16) u32 {
|
||||||
return (r << 3 | r >> 2) << 24 | (g << 3 | g >> 2) << 16 | (b << 3 | b >> 2) << 8 | 0xFF;
|
return (r << 3 | r >> 2) << 24 | (g << 3 | g >> 2) << 16 | (b << 3 | b >> 2) << 8 | 0xFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn genColourLut() [0x8000]u32 {
|
|
||||||
return comptime {
|
|
||||||
@setEvalBranchQuota(0x10001);
|
|
||||||
|
|
||||||
var lut: [0x8000]u32 = undefined;
|
|
||||||
for (lut) |*px, i| px.* = toRgba8888(i);
|
|
||||||
return lut;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: The implementation is incorrect and using it in the LUT crashes the compiler (OOM)
|
|
||||||
/// Implementation courtesy of byuu and Talarubi at https://near.sh/articles/video/color-emulation
|
|
||||||
fn toRgba8888Talarubi(bgr555: u16) u32 {
|
|
||||||
@setRuntimeSafety(false);
|
|
||||||
|
|
||||||
const lcd_gamma: f64 = 4;
|
|
||||||
const out_gamma: f64 = 2.2;
|
|
||||||
|
|
||||||
const b = @as(u32, bgr555 >> 10 & 0x1F);
|
|
||||||
const g = @as(u32, bgr555 >> 5 & 0x1F);
|
|
||||||
const r = @as(u32, bgr555 & 0x1F);
|
|
||||||
|
|
||||||
const lb = std.math.pow(f64, @intToFloat(f64, b << 3 | b >> 2) / 31, lcd_gamma);
|
|
||||||
const lg = std.math.pow(f64, @intToFloat(f64, g << 3 | g >> 2) / 31, lcd_gamma);
|
|
||||||
const lr = std.math.pow(f64, @intToFloat(f64, r << 3 | r >> 2) / 31, lcd_gamma);
|
|
||||||
|
|
||||||
const out_b = std.math.pow(f64, (220 * lb + 10 * lg + 50 * lr) / 255, 1 / out_gamma);
|
|
||||||
const out_g = std.math.pow(f64, (30 * lb + 230 * lg + 10 * lr) / 255, 1 / out_gamma);
|
|
||||||
const out_r = std.math.pow(f64, (0 * lb + 50 * lg + 255 * lr) / 255, 1 / out_gamma);
|
|
||||||
|
|
||||||
return @floatToInt(u32, out_r) << 24 | @floatToInt(u32, out_g) << 16 | @floatToInt(u32, out_b) << 8 | 0xFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn alphaBlend(top: u16, btm: u16, bldalpha: io.BldAlpha) u16 {
|
fn alphaBlend(top: u16, btm: u16, bldalpha: io.BldAlpha) u16 {
|
||||||
const eva: u16 = bldalpha.eva.read();
|
const eva: u16 = bldalpha.eva.read();
|
||||||
const evb: u16 = bldalpha.evb.read();
|
const evb: u16 = bldalpha.evb.read();
|
||||||
|
|
|
@ -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.handleHDrawEnd(cpu, late);
|
cpu.bus.ppu.onHdrawEnd(cpu, late);
|
||||||
},
|
},
|
||||||
.TimerOverflow => |id| {
|
.TimerOverflow => |id| {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
0 => cpu.bus.tim[0].handleOverflow(cpu, late),
|
0 => cpu.bus.tim[0].onTimerExpire(cpu, late),
|
||||||
1 => cpu.bus.tim[1].handleOverflow(cpu, late),
|
1 => cpu.bus.tim[1].onTimerExpire(cpu, late),
|
||||||
2 => cpu.bus.tim[2].handleOverflow(cpu, late),
|
2 => cpu.bus.tim[2].onTimerExpire(cpu, late),
|
||||||
3 => cpu.bus.tim[3].handleOverflow(cpu, late),
|
3 => cpu.bus.tim[3].onTimerExpire(cpu, late),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.ApuChannel => |id| {
|
.ApuChannel => |id| {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
0 => cpu.bus.apu.ch1.channelTimerOverflow(late),
|
0 => cpu.bus.apu.ch1.onToneSweepEvent(late),
|
||||||
1 => cpu.bus.apu.ch2.channelTimerOverflow(late),
|
1 => cpu.bus.apu.ch2.onToneEvent(late),
|
||||||
2 => cpu.bus.apu.ch3.channelTimerOverflow(late),
|
2 => cpu.bus.apu.ch3.onWaveEvent(late),
|
||||||
3 => cpu.bus.apu.ch4.channelTimerOverflow(late),
|
3 => cpu.bus.apu.ch4.onNoiseEvent(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.updateTime(late);
|
clock.onClockUpdate(late);
|
||||||
},
|
},
|
||||||
.FrameSequencer => cpu.bus.apu.tickFrameSequencer(late),
|
.FrameSequencer => cpu.bus.apu.onSequencerTick(late),
|
||||||
.SampleAudio => cpu.bus.apu.sampleAudio(late),
|
.SampleAudio => cpu.bus.apu.sampleAudio(late),
|
||||||
.HBlank => cpu.bus.ppu.handleHBlankEnd(cpu, late), // The end of a HBlank
|
.HBlank => cpu.bus.ppu.onHblankEnd(cpu, late), // The end of a HBlank
|
||||||
.VBlank => cpu.bus.ppu.handleHDrawEnd(cpu, late), // The end of a VBlank
|
.VBlank => cpu.bus.ppu.onHdrawEnd(cpu, late), // The end of a VBlank
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
133
src/main.zig
133
src/main.zig
|
@ -1,9 +1,10 @@
|
||||||
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;
|
||||||
|
@ -14,16 +15,14 @@ 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.
|
||||||
|
\\-s, --skip Skip BIOS.
|
||||||
\\-b, --bios <str> Optional path to a GBA BIOS ROM.
|
\\-b, --bios <str> Optional path to a GBA BIOS ROM.
|
||||||
\\<str> Path to the GBA GamePak ROM
|
\\<str> Path to the GBA GamePak ROM.
|
||||||
\\
|
\\
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -31,16 +30,36 @@ pub fn main() anyerror!void {
|
||||||
// Main Allocator for ZBA
|
// Main Allocator for ZBA
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
defer std.debug.assert(!gpa.deinit());
|
defer std.debug.assert(!gpa.deinit());
|
||||||
|
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
// Handle CLI Input
|
// Determine the Data Directory (stores saves, config file, etc.)
|
||||||
const result = try clap.parse(clap.Help, ¶ms, clap.parsers.default, .{});
|
const data_path = blk: {
|
||||||
|
const result = known_folders.getPath(allocator, .data);
|
||||||
|
const option = result catch |e| exitln("interrupted while attempting to find a data directory: {}", .{e});
|
||||||
|
const path = option orelse exitln("no valid data directory could be found", .{});
|
||||||
|
ensureDirectoriesExist(path) catch |e| exitln("failed to create directories under \"{s}\": {}", .{ path, e });
|
||||||
|
|
||||||
|
break :blk path;
|
||||||
|
};
|
||||||
|
defer allocator.free(data_path);
|
||||||
|
|
||||||
|
// Parse CLI
|
||||||
|
const result = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{}) catch |e| exitln("failed to parse cli: {}", .{e});
|
||||||
defer result.deinit();
|
defer result.deinit();
|
||||||
|
|
||||||
const paths = try handleArguments(allocator, &result);
|
// TODO: Move config file to XDG Config directory?
|
||||||
|
const config_path = configFilePath(allocator, data_path) catch |e| exitln("failed to determine the config file path for ZBA: {}", .{e});
|
||||||
|
defer allocator.free(config_path);
|
||||||
|
|
||||||
|
config.load(allocator, config_path) catch |e| exitln("failed to read config file: {}", .{e});
|
||||||
|
|
||||||
|
const paths = handleArguments(allocator, data_path, &result) catch |e| exitln("failed to handle cli arguments: {}", .{e});
|
||||||
defer if (paths.save) |path| allocator.free(path);
|
defer if (paths.save) |path| allocator.free(path);
|
||||||
|
|
||||||
const log_file: ?std.fs.File = if (cpu_logging) try std.fs.cwd().createFile("zba.log", .{}) else null;
|
const log_file = if (config.config().debug.cpu_trace) blk: {
|
||||||
|
break :blk std.fs.cwd().createFile("zba.log", .{}) catch |e| exitln("failed to create trace log file: {}", .{e});
|
||||||
|
} 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
|
||||||
|
@ -49,54 +68,78 @@ pub fn main() anyerror!void {
|
||||||
|
|
||||||
var bus: Bus = undefined;
|
var bus: Bus = undefined;
|
||||||
var cpu = Arm7tdmi.init(&scheduler, &bus, log_file);
|
var cpu = Arm7tdmi.init(&scheduler, &bus, log_file);
|
||||||
if (paths.bios == null) cpu.fastBoot();
|
|
||||||
|
|
||||||
try bus.init(allocator, &scheduler, &cpu, paths);
|
bus.init(allocator, &scheduler, &cpu, paths) catch |e| exitln("failed to init zba bus: {}", .{e});
|
||||||
defer bus.deinit();
|
defer bus.deinit();
|
||||||
|
|
||||||
|
if (config.config().guest.skip_bios or result.args.skip or paths.bios == null) {
|
||||||
|
cpu.fastBoot();
|
||||||
|
}
|
||||||
|
|
||||||
var gui = Gui.init(&bus.pak.title, &bus.apu, width, height);
|
var gui = Gui.init(&bus.pak.title, &bus.apu, width, height);
|
||||||
defer gui.deinit();
|
defer gui.deinit();
|
||||||
|
|
||||||
try gui.run(&cpu, &scheduler);
|
gui.run(&cpu, &scheduler) catch |e| exitln("failed to run gui thread: {}", .{e});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getSavePath(allocator: Allocator) !?[]const u8 {
|
pub fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) !FilePaths {
|
||||||
const save_subpath = "zba" ++ [_]u8{std.fs.path.sep} ++ "save";
|
const rom_path = romPath(result);
|
||||||
|
|
||||||
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, ¶ms, 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, ¶ms, clap.parsers.default)) !FilePaths {
|
|
||||||
const rom_path = try getRomPath(result);
|
|
||||||
log.info("ROM path: {s}", .{rom_path});
|
log.info("ROM path: {s}", .{rom_path});
|
||||||
const bios_path = result.args.bios;
|
|
||||||
if (bios_path) |path| log.info("BIOS path: {s}", .{path}) else log.info("No BIOS provided", .{});
|
|
||||||
const save_path = try getSavePath(allocator);
|
|
||||||
if (save_path) |path| log.info("Save path: {s}", .{path});
|
|
||||||
|
|
||||||
return FilePaths{
|
const bios_path = result.args.bios;
|
||||||
|
if (bios_path) |path| log.info("BIOS path: {s}", .{path}) else log.warn("No BIOS provided", .{});
|
||||||
|
|
||||||
|
const save_path = try std.fs.path.join(allocator, &[_][]const u8{ data_path, "zba", "save" });
|
||||||
|
log.info("Save path: {s}", .{save_path});
|
||||||
|
|
||||||
|
return .{
|
||||||
.rom = rom_path,
|
.rom = rom_path,
|
||||||
.bios = bios_path,
|
.bios = bios_path,
|
||||||
.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" });
|
||||||
|
errdefer allocator.free(path);
|
||||||
|
|
||||||
|
// 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 |e| {
|
||||||
|
if (e != error.FileNotFound) return e;
|
||||||
|
|
||||||
|
const config_file = try std.fs.createFileAbsolute(path, .{});
|
||||||
|
defer config_file.close();
|
||||||
|
|
||||||
|
try config_file.writeAll(@embedFile("../example.toml"));
|
||||||
|
};
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensureDirectoriesExist(data_path: []const u8) !void {
|
||||||
|
var dir = try std.fs.openDirAbsolute(data_path, .{});
|
||||||
|
defer dir.close();
|
||||||
|
|
||||||
|
// We want to make sure: %APPDATA%/zba and %APPDATA%/zba/save exist
|
||||||
|
// (~/.local/share/zba/save for linux, ??? for macOS)
|
||||||
|
|
||||||
|
// Will recursively create directories
|
||||||
|
try dir.makePath("zba" ++ [_]u8{std.fs.path.sep} ++ "save");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn romPath(result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) []const u8 {
|
||||||
|
return switch (result.positionals.len) {
|
||||||
|
1 => result.positionals[0],
|
||||||
|
0 => exitln("ZBA requires a path to a GamePak ROM", .{}),
|
||||||
|
else => exitln("ZBA received too many positional arguments.", .{}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exitln(comptime format: []const u8, args: anytype) noreturn {
|
||||||
|
const stderr = std.io.getStdErr().writer();
|
||||||
|
stderr.print(format, args) catch {}; // Just exit already...
|
||||||
|
stderr.writeByte('\n') catch {};
|
||||||
|
std.os.exit(1);
|
||||||
|
}
|
||||||
|
|
166
src/platform.zig
166
src/platform.zig
|
@ -1,6 +1,8 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const SDL = @import("sdl2");
|
const SDL = @import("sdl2");
|
||||||
|
const gl = @import("gl");
|
||||||
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;
|
||||||
|
@ -10,61 +12,156 @@ 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 gba_width = @import("core/ppu.zig").width;
|
||||||
|
const gba_height = @import("core/ppu.zig").height;
|
||||||
|
|
||||||
const default_title: []const u8 = "ZBA";
|
const default_title: []const u8 = "ZBA";
|
||||||
|
|
||||||
pub const Gui = struct {
|
pub const Gui = struct {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
const SDL_GLContext = *anyopaque; // SDL.SDL_GLContext is a ?*anyopaque
|
||||||
const log = std.log.scoped(.Gui);
|
const log = std.log.scoped(.Gui);
|
||||||
|
|
||||||
|
// zig fmt: off
|
||||||
|
const vertices: [32]f32 = [_]f32{
|
||||||
|
// Positions // Colours // Texture Coords
|
||||||
|
1.0, -1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, // Top Right
|
||||||
|
1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, // Bottom Right
|
||||||
|
-1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, // Bottom Left
|
||||||
|
-1.0, -1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, // Top Left
|
||||||
|
};
|
||||||
|
|
||||||
|
const indices: [6]u32 = [_]u32{
|
||||||
|
0, 1, 3, // First Triangle
|
||||||
|
1, 2, 3, // Second Triangle
|
||||||
|
};
|
||||||
|
// zig fmt: on
|
||||||
|
|
||||||
window: *SDL.SDL_Window,
|
window: *SDL.SDL_Window,
|
||||||
|
ctx: SDL_GLContext,
|
||||||
title: []const u8,
|
title: []const u8,
|
||||||
renderer: *SDL.SDL_Renderer,
|
|
||||||
texture: *SDL.SDL_Texture,
|
|
||||||
audio: Audio,
|
audio: Audio,
|
||||||
|
|
||||||
|
program_id: gl.GLuint,
|
||||||
|
|
||||||
pub fn init(title: *const [12]u8, apu: *Apu, width: i32, height: i32) Self {
|
pub fn init(title: *const [12]u8, apu: *Apu, width: i32, height: i32) Self {
|
||||||
const ret = SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO | SDL.SDL_INIT_GAMECONTROLLER);
|
if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO) < 0) panic();
|
||||||
if (ret < 0) panic();
|
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_PROFILE_MASK, SDL.SDL_GL_CONTEXT_PROFILE_CORE) < 0) panic();
|
||||||
|
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic();
|
||||||
|
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 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 * scale),
|
@as(c_int, width * win_scale),
|
||||||
@as(c_int, height * scale),
|
@as(c_int, height * win_scale),
|
||||||
SDL.SDL_WINDOW_SHOWN,
|
SDL.SDL_WINDOW_OPENGL | SDL.SDL_WINDOW_SHOWN,
|
||||||
) orelse panic();
|
) orelse panic();
|
||||||
|
|
||||||
const renderer = SDL.SDL_CreateRenderer(window, -1, SDL.SDL_RENDERER_ACCELERATED | SDL.SDL_RENDERER_PRESENTVSYNC) orelse panic();
|
const ctx = SDL.SDL_GL_CreateContext(window) orelse panic();
|
||||||
|
if (SDL.SDL_GL_MakeCurrent(window, ctx) < 0) panic();
|
||||||
|
|
||||||
const texture = SDL.SDL_CreateTexture(
|
gl.load(ctx, Self.glGetProcAddress) catch @panic("gl.load failed");
|
||||||
renderer,
|
if (SDL.SDL_GL_SetSwapInterval(@boolToInt(config.config().host.vsync)) < 0) panic();
|
||||||
SDL.SDL_PIXELFORMAT_RGBA8888,
|
|
||||||
SDL.SDL_TEXTUREACCESS_STREAMING,
|
const program_id = compileShaders();
|
||||||
@as(c_int, width),
|
|
||||||
@as(c_int, height),
|
|
||||||
) orelse panic();
|
|
||||||
|
|
||||||
return Self{
|
return Self{
|
||||||
.window = window,
|
.window = window,
|
||||||
.title = span(title),
|
.title = span(title),
|
||||||
.renderer = renderer,
|
.ctx = ctx,
|
||||||
.texture = texture,
|
.program_id = program_id,
|
||||||
.audio = Audio.init(apu),
|
.audio = Audio.init(apu),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn compileShaders() gl.GLuint {
|
||||||
|
// TODO: Panic on Shader Compiler Failure + Error Message
|
||||||
|
const vert_shader = @embedFile("shader/pixelbuf.vert");
|
||||||
|
const frag_shader = @embedFile("shader/pixelbuf.frag");
|
||||||
|
|
||||||
|
const vs = gl.createShader(gl.VERTEX_SHADER);
|
||||||
|
defer gl.deleteShader(vs);
|
||||||
|
|
||||||
|
gl.shaderSource(vs, 1, &[_][*c]const u8{vert_shader}, 0);
|
||||||
|
gl.compileShader(vs);
|
||||||
|
|
||||||
|
const fs = gl.createShader(gl.FRAGMENT_SHADER);
|
||||||
|
defer gl.deleteShader(fs);
|
||||||
|
|
||||||
|
gl.shaderSource(fs, 1, &[_][*c]const u8{frag_shader}, 0);
|
||||||
|
gl.compileShader(fs);
|
||||||
|
|
||||||
|
const program = gl.createProgram();
|
||||||
|
gl.attachShader(program, vs);
|
||||||
|
gl.attachShader(program, fs);
|
||||||
|
gl.linkProgram(program);
|
||||||
|
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the VAO ID since it's used in run()
|
||||||
|
fn generateBuffers() [3]c_uint {
|
||||||
|
var vao_id: c_uint = undefined;
|
||||||
|
var vbo_id: c_uint = undefined;
|
||||||
|
var ebo_id: c_uint = undefined;
|
||||||
|
gl.genVertexArrays(1, &vao_id);
|
||||||
|
gl.genBuffers(1, &vbo_id);
|
||||||
|
gl.genBuffers(1, &ebo_id);
|
||||||
|
|
||||||
|
gl.bindVertexArray(vao_id);
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, vbo_id);
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, @sizeOf(@TypeOf(vertices)), &vertices, gl.STATIC_DRAW);
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo_id);
|
||||||
|
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, @sizeOf(@TypeOf(indices)), &indices, gl.STATIC_DRAW);
|
||||||
|
|
||||||
|
// Position
|
||||||
|
gl.vertexAttribPointer(0, 3, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, 0)); // lmao
|
||||||
|
gl.enableVertexAttribArray(0);
|
||||||
|
// Colour
|
||||||
|
gl.vertexAttribPointer(1, 3, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, (3 * @sizeOf(f32))));
|
||||||
|
gl.enableVertexAttribArray(1);
|
||||||
|
// Texture Coord
|
||||||
|
gl.vertexAttribPointer(2, 2, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, (6 * @sizeOf(f32))));
|
||||||
|
gl.enableVertexAttribArray(2);
|
||||||
|
|
||||||
|
return .{ vao_id, vbo_id, ebo_id };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generateTexture(buf: []const u8) c_uint {
|
||||||
|
var tex_id: c_uint = undefined;
|
||||||
|
gl.genTextures(1, &tex_id);
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, tex_id);
|
||||||
|
|
||||||
|
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||||
|
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||||
|
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||||
|
|
||||||
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gba_width, gba_height, 0, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, buf.ptr);
|
||||||
|
// gl.generateMipmap(gl.TEXTURE_2D); // TODO: Remove?
|
||||||
|
|
||||||
|
return tex_id;
|
||||||
|
}
|
||||||
|
|
||||||
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 frame_rate = FpsTracker.init();
|
var tracker = FpsTracker.init();
|
||||||
|
|
||||||
const thread = try std.Thread.spawn(.{}, emu.run, .{ &quit, &frame_rate, scheduler, cpu });
|
const thread = try std.Thread.spawn(.{}, emu.run, .{ &quit, scheduler, cpu, &tracker });
|
||||||
defer thread.join();
|
defer thread.join();
|
||||||
|
|
||||||
var title_buf: [0x100]u8 = [_]u8{0} ** 0x100;
|
var title_buf: [0x100]u8 = [_]u8{0} ** 0x100;
|
||||||
|
|
||||||
|
const vao_id = Self.generateBuffers()[0];
|
||||||
|
_ = Self.generateTexture(cpu.bus.ppu.framebuf.get(.Renderer));
|
||||||
|
|
||||||
emu_loop: while (true) {
|
emu_loop: while (true) {
|
||||||
var event: SDL.SDL_Event = undefined;
|
var event: SDL.SDL_Event = undefined;
|
||||||
while (SDL.SDL_PollEvent(&event) != 0) {
|
while (SDL.SDL_PollEvent(&event) != 0) {
|
||||||
|
@ -123,11 +220,14 @@ pub const Gui = struct {
|
||||||
|
|
||||||
// Emulator has an internal Double Buffer
|
// Emulator has an internal Double Buffer
|
||||||
const framebuf = cpu.bus.ppu.framebuf.get(.Renderer);
|
const framebuf = cpu.bus.ppu.framebuf.get(.Renderer);
|
||||||
_ = SDL.SDL_UpdateTexture(self.texture, null, framebuf.ptr, pitch);
|
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gba_width, gba_height, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, framebuf.ptr);
|
||||||
_ = SDL.SDL_RenderCopy(self.renderer, self.texture, null, null);
|
|
||||||
SDL.SDL_RenderPresent(self.renderer);
|
|
||||||
|
|
||||||
const dyn_title = std.fmt.bufPrint(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, frame_rate.value() }) catch unreachable;
|
gl.useProgram(self.program_id);
|
||||||
|
gl.bindVertexArray(vao_id);
|
||||||
|
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, null);
|
||||||
|
SDL.SDL_GL_SwapWindow(self.window);
|
||||||
|
|
||||||
|
const dyn_title = std.fmt.bufPrint(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, tracker.value() }) catch unreachable;
|
||||||
SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr);
|
SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,12 +236,18 @@ pub const Gui = struct {
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
pub fn deinit(self: *Self) void {
|
||||||
self.audio.deinit();
|
self.audio.deinit();
|
||||||
SDL.SDL_DestroyTexture(self.texture);
|
// TODO: Buffer deletions
|
||||||
SDL.SDL_DestroyRenderer(self.renderer);
|
gl.deleteProgram(self.program_id);
|
||||||
|
SDL.SDL_GL_DeleteContext(self.ctx);
|
||||||
SDL.SDL_DestroyWindow(self.window);
|
SDL.SDL_DestroyWindow(self.window);
|
||||||
SDL.SDL_Quit();
|
SDL.SDL_Quit();
|
||||||
self.* = undefined;
|
self.* = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn glGetProcAddress(ctx: SDL.SDL_GLContext, proc: [:0]const u8) ?*anyopaque {
|
||||||
|
_ = ctx;
|
||||||
|
return SDL.SDL_GL_GetProcAddress(proc.ptr);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Audio = struct {
|
const Audio = struct {
|
||||||
|
@ -176,10 +282,16 @@ const Audio = struct {
|
||||||
|
|
||||||
export fn callback(userdata: ?*anyopaque, stream: [*c]u8, len: c_int) void {
|
export fn callback(userdata: ?*anyopaque, stream: [*c]u8, len: c_int) void {
|
||||||
const apu = @ptrCast(*Apu, @alignCast(@alignOf(*Apu), userdata));
|
const apu = @ptrCast(*Apu, @alignCast(@alignOf(*Apu), userdata));
|
||||||
|
|
||||||
|
// TODO: Find a better way to mute this
|
||||||
|
if (!config.config().host.mute) {
|
||||||
_ = SDL.SDL_AudioStreamGet(apu.stream, stream, len);
|
_ = SDL.SDL_AudioStreamGet(apu.stream, stream, len);
|
||||||
|
} else {
|
||||||
|
// FIXME: I don't think this hack to remove DC Offset is acceptable :thinking:
|
||||||
|
std.mem.set(u8, stream[0..@intCast(usize, len)], 0x40);
|
||||||
|
}
|
||||||
|
|
||||||
// If we don't write anything, play silence otherwise garbage will be played
|
// If we don't write anything, play silence otherwise garbage will be played
|
||||||
// FIXME: I don't think this hack to remove DC Offset is acceptable :thinking:
|
|
||||||
// if (written == 0) std.mem.set(u8, stream[0..@intCast(usize, len)], 0x40);
|
// if (written == 0) std.mem.set(u8, stream[0..@intCast(usize, len)], 0x40);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
#version 330 core
|
||||||
|
out vec4 frag_color;
|
||||||
|
|
||||||
|
in vec3 color;
|
||||||
|
in vec2 uv;
|
||||||
|
|
||||||
|
uniform sampler2D screen;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// https://near.sh/video/color-emulation
|
||||||
|
// Thanks to Talarubi + Near for the Colour Correction
|
||||||
|
// Thanks to fleur + mattrb for the Shader Impl
|
||||||
|
|
||||||
|
vec4 color = texture(screen, uv);
|
||||||
|
color.rgb = pow(color.rgb, vec3(4.0)); // LCD Gamma
|
||||||
|
|
||||||
|
frag_color = vec4(
|
||||||
|
pow(vec3(
|
||||||
|
0 * color.b + 50 * color.g + 255 * color.r,
|
||||||
|
30 * color.b + 230 * color.g + 10 * color.r,
|
||||||
|
220 * color.b + 10 * color.g + 50 * color.r
|
||||||
|
) / 255, vec3(1.0 / 2.2)), // Out Gamma
|
||||||
|
1.0);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
#version 330 core
|
||||||
|
layout (location = 0) in vec3 pos;
|
||||||
|
layout (location = 1) in vec3 in_color;
|
||||||
|
layout (location = 2) in vec2 in_uv;
|
||||||
|
|
||||||
|
out vec3 color;
|
||||||
|
out vec2 uv;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
color = in_color;
|
||||||
|
uv = in_uv;
|
||||||
|
gl_Position = vec4(pos, 1.0);
|
||||||
|
}
|
106
src/util.zig
106
src/util.zig
|
@ -1,12 +1,12 @@
|
||||||
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 Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
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
|
||||||
|
@ -146,8 +146,10 @@ 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 !allow_unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{});
|
if (builtin.mode == .Debug and !unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{});
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -155,22 +157,13 @@ 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 !allow_unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{});
|
if (builtin.mode == .Debug and !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();
|
||||||
|
@ -186,6 +179,7 @@ pub const Logger = struct {
|
||||||
|
|
||||||
pub fn print(self: *Self, comptime format: []const u8, args: anytype) !void {
|
pub fn print(self: *Self, comptime format: []const u8, args: anytype) !void {
|
||||||
try self.buf.writer().print(format, args);
|
try self.buf.writer().print(format, args);
|
||||||
|
try self.buf.flush(); // FIXME: On panics, whatever is in the buffer isn't written to file
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mgbaLog(self: *Self, cpu: *const Arm7tdmi, opcode: u32) void {
|
pub fn mgbaLog(self: *Self, cpu: *const Arm7tdmi, opcode: u32) void {
|
||||||
|
@ -225,14 +219,92 @@ pub const Logger = struct {
|
||||||
cpu.r[12],
|
cpu.r[12],
|
||||||
cpu.r[13],
|
cpu.r[13],
|
||||||
cpu.r[14],
|
cpu.r[14],
|
||||||
cpu.r[15],
|
cpu.r[15] - if (cpu.cpsr.t.read()) 2 else @as(u32, 4),
|
||||||
cpu.cpsr.raw,
|
cpu.cpsr.raw,
|
||||||
opcode,
|
opcode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Double Buffering Implementation
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Double Buffering Implementation
|
||||||
pub const FrameBuffer = struct {
|
pub const FrameBuffer = struct {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue