7 Commits

Author SHA1 Message Date
f715585867 chore: dont allocate not-small ?Sprite array on stack
use memset like most other allocations in this emu
2022-10-23 04:46:07 -03:00
cab40efc59 chore: move FrameBuffer struct to util.zig 2022-10-23 04:46:07 -03:00
a17300a8e0 chore: move OAM, PALRAM and VRAM structs to separate files 2022-10-23 04:46:07 -03:00
2ebe1c0b0e fix: 8-bit writes to WIN PPU registers
Advance Wars depends on these registers similar to Mario Kart's 8-bit
writes to Affine Background registers:
2022-10-23 04:46:07 -03:00
6db70638fe chore: refactor window 2022-10-23 04:46:07 -03:00
bc5ab5810a chore: crude background window impl (no affine) 2022-10-23 04:46:07 -03:00
decf2a01c9 chore: rename function (misspelt until now somehow) 2022-10-23 04:46:07 -03:00
53 changed files with 3412 additions and 4528 deletions

View File

@@ -1,59 +0,0 @@
name: Nightly
on:
push:
paths:
- "**.zig"
branches:
- main
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
jobs:
build:
strategy:
matrix:
# os: [ubuntu-latest, windows-latest, macos-latest]
os: [ubuntu-latest, windows-latest]
runs-on: ${{matrix.os}}
steps:
- uses: goto-bus-stop/setup-zig@v2
with:
version: master
- name: prepare-linux
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install libsdl2-dev
- name: prepare-windows
if: runner.os == 'Windows'
run: |
vcpkg integrate install
vcpkg install sdl2:x64-windows
git config --global core.autocrlf false
- name: prepare-macos
if: runner.os == 'macOS'
run: |
brew install sdl2
- uses: actions/checkout@v3
with:
submodules: recursive
- name: build
run: zig build -Doptimize=ReleaseSafe -Dcpu=baseline
- name: upload
uses: actions/upload-artifact@v3
with:
name: zba-${{matrix.os}}
path: zig-out/bin
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: goto-bus-stop/setup-zig@v2
with:
version: master
- run: zig fmt src/**/*.zig

3
.gitmodules vendored
View File

@@ -13,6 +13,3 @@
[submodule "lib/zig-toml"]
path = lib/zig-toml
url = https://github.com/aeronavery/zig-toml
[submodule "lib/zba-gdbstub"]
path = lib/zba-gdbstub
url = https://git.musuka.dev/paoda/zba-gdbstub

8
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"recommendations": [
"augusterame.zls-vscode",
"usernamehw.errorlens",
"vadimcn.vscode-lldb",
"dan-c-underwood.arm"
]
}

138
README.md
View File

@@ -1,110 +1,80 @@
# ZBA (working title)
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).
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:
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
- [x] Affine Sprites
### TODO
- [ ] Affine Sprites
- [ ] Windowing (see [this branch](https://git.musuka.dev/paoda/zba/src/branch/window))
- [ ] Audio Resampler (Having issues with SDL2's)
- [x] Immediate Mode GUI (see [this branch](https://git.musuka.dev/paoda/zba/src/branch/imgui))
- [ ] Immediate Mode GUI
- [ ] Refactoring for easy-ish perf boosts
## Usage
As it currently exists, ZBA is run from the terminal. In your console of choice, type `./zba --help` to see what you can do.
I typically find myself typing `./zba -b ./bin/bios.bin ./bin/test/suite.gba` to see how badly my "cool new feature" broke everything else.
Need a BIOS? Why not try using the open-source [Cult-Of-GBA BIOS](https://github.com/Cult-of-GBA/BIOS) written by [fleroviux](https://github.com/fleroviux) and [DenSinH](https://github.com/DenSinH)?
Finally it's worth noting that ZBA uses a TOML config file it'll store in your OS's data directory. See `example.toml` to learn about the defaults and what exactly you can mess around with.
## Tests
GBA Tests | [jsmolka](https://github.com/jsmolka/)
--- | ---
`arm.gba`, `thumb.gba` | PASS
`memory.gba`, `bios.gba` | PASS
`flash64.gba`, `flash128.gba` | PASS
`sram.gba` | PASS
`none.gba` | PASS
`hello.gba`, `shades.gba`, `stripes.gba` | PASS
`nes.gba` | PASS
GBARoms | [DenSinH](https://github.com/DenSinH/)
--- | ---
`eeprom-test`, `flash-test` | PASS
`midikey2freq` | PASS
`swi-tests-random` | FAIL
gba_tests | [destoer](https://github.com/destoer/)
--- | ---
`cond_invalid.gba` | PASS
`dma_priority.gba` | PASS
`hello_world.gba` | PASS
`if_ack.gba` | PASS
`line_timing.gba` | FAIL
`lyc_midline.gba` | FAIL
`window_midframe.gba` | FAIL
GBA Test Collection | [ladystarbreeze](https://github.com/ladystarbreeze)
--- | ---
`retAddr.gba` | PASS
`helloWorld.gba` | PASS
`helloAudio.gba` | PASS
FuzzARM | [DenSinH](https://github.com/DenSinH/)
--- | ---
`main.gba` | PASS
arm7wrestler GBA Fixed | [destoer](https://github.com/destoer)
--- | ---
`armwrestler-gba-fixed.gba` | PASS
## Tests
- [x] [jsmolka's GBA Test Collection](https://github.com/jsmolka/gba-tests)
- [x] `arm.gba` and `thumb.gba`
- [x] `flash64.gba`, `flash128.gba`, `none.gba`, and `sram.gba`
- [x] `hello.gba`, `shades.gba`, and `stripes.gba`
- [x] `memory.gba`
- [x] `bios.gba`
- [x] `nes.gba`
- [ ] [DenSinH's GBA ROMs](https://github.com/DenSinH/GBARoms)
- [x] `eeprom-test` and `flash-test`
- [x] `midikey2freq`
- [ ] `swi-tests-random`
- [ ] [destoer's GBA Tests](https://github.com/destoer/gba_tests)
- [x] `cond_invalid.gba`
- [x] `dma_priority.gba`
- [x] `hello_world.gba`
- [x] `if_ack.gba`
- [ ] `line_timing.gba`
- [ ] `lyc_midline.gba`
- [ ] `window_midframe.gba`
- [x] [ladystarbreeze's GBA Test Collection](https://github.com/ladystarbreeze/GBA-Test-Collection)
- [x] `retAddr.gba`
- [x] `helloWorld.gba`
- [x] `helloAudio.gba`
- [x] [`armwrestler-gba-fixed.gba`](https://github.com/destoer/armwrestler-gba-fixed)
- [x] [FuzzARM](https://github.com/DenSinH/FuzzARM)
## Resources
- [GBATEK](https://problemkaputt.de/gbatek.htm)
- [TONC](https://coranac.com/tonc/text/toc.htm)
- [ARM Architecture Reference Manual](https://www.intel.com/content/dam/www/programmable/us/en/pdfs/literature/third-party/ddi0100e_arm_arm.pdf)
- [ARM7TDMI Data Sheet](https://www.dca.fee.unicamp.br/cursos/EA871/references/ARM/ARM7TDMIDataSheet.pdf)
* [GBATEK](https://problemkaputt.de/gbatek.htm)
* [TONC](https://coranac.com/tonc/text/toc.htm)
* [ARM Architecture Reference Manual](https://www.intel.com/content/dam/www/programmable/us/en/pdfs/literature/third-party/ddi0100e_arm_arm.pdf)
* [ARM7TDMI Data Sheet](https://www.dca.fee.unicamp.br/cursos/EA871/references/ARM/ARM7TDMIDataSheet.pdf)
## Compiling
Most recently built on Zig [v0.11.0-dev.1580+a5b34a61a](https://github.com/ziglang/zig/tree/a5b34a61a)
Most recently built on Zig [0.10.0-dev.4474+b41b35f57](https://github.com/ziglang/zig/tree/b41b35f57)
### Dependencies
* [SDL.zig](https://github.com/MasterQ32/SDL.zig)
* [SDL2](https://www.libsdl.org/download-2.0.php)
* [zig-clap](https://github.com/Hejsil/zig-clap)
* [known-folders](https://github.com/ziglibs/known-folders)
* [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)
Dependency | Source
--- | ---
SDL.zig | <https://github.com/MasterQ32/SDL.zig>
zig-clap | <https://github.com/Hejsil/zig-clap>
known-folders | <https://github.com/ziglibs/known-folders>
zig-toml | <https://github.com/aeronavery/zig-toml>
zig-datetime | <https://github.com/frmdstryr/zig-datetime>
`bitfields.zig` | [https://github.com/FlorenceOS/Florence](https://github.com/FlorenceOS/Florence/blob/aaa5a9e568/lib/util/bitfields.zig)
`gl.zig` | <https://github.com/MasterQ32/zig-opengl>
`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 relevant git submodules
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
* MacOS: ¯\\\_(ツ)_/¯
* Windows: [`vcpkg`](https://github.com/Microsoft/vcpkg) (install `sdl2:x64-windows`)
- Linux: Your distro's package manager
- macOS: ¯\\\_(ツ)_/¯ (try [this formula](https://formulae.brew.sh/formula/sdl2)?)
- Windows: [`vcpkg`](https://github.com/Microsoft/vcpkg) (install `sdl2:x64-windows`)
`SDL.zig` will provide a helpful compile error if the zig compiler is unable to find SDL2.
`SDL.zig` will provide a helpful compile error if the zig compiler is unable to find SDL2.
Once you've got all the dependencies, execute `zig build -Doptimize=ReleaseSafe`. The executable is located at `zig-out/bin/`.
Once you've got all the dependencies, execute `zig build -Drelease-fast`. The executable is located at `zig-out/bin/`.
## Controls
Key | Button
--- | ---
<kbd>X</kbd> | A

View File

@@ -1,55 +1,46 @@
const std = @import("std");
const builtin = @import("builtin");
const Sdk = @import("lib/SDL.zig/Sdk.zig");
const Gdbstub = @import("lib/zba-gdbstub/build.zig");
pub fn build(b: *std.build.Builder) void {
// Minimum Zig Version
const min_ver = std.SemanticVersion.parse("0.11.0-dev.1580+a5b34a61a") catch return; // https://github.com/ziglang/zig/commit/a5b34a61a
if (builtin.zig_version.order(min_ver).compare(.lt)) {
std.log.err("{s}", .{b.fmt("Zig v{} does not meet the minimum version requirement. (Zig v{})", .{ builtin.zig_version, min_ver })});
std.os.exit(1);
}
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
// for restricting supported target set are available.
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "zba",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
// Standard release options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
const mode = b.standardReleaseOptions();
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.)
exe.addAnonymousModule("known_folders", .{ .source_file = .{ .path = "lib/known-folders/known-folders.zig" } });
exe.addPackagePath("known_folders", "lib/known-folders/known-folders.zig");
// DateTime Library
exe.addAnonymousModule("datetime", .{ .source_file = .{ .path = "lib/zig-datetime/src/main.zig" } });
exe.addPackagePath("datetime", "lib/zig-datetime/src/main.zig");
// Bitfield type from FlorenceOS: https://github.com/FlorenceOS/
exe.addAnonymousModule("bitfield", .{ .source_file = .{ .path = "lib/util/bitfield.zig" } });
// exe.addPackage(.{ .name = "bitfield", .path = .{ .path = "lib/util/bitfield.zig" } });
exe.addPackagePath("bitfield", "lib/util/bitfield.zig");
// Argument Parsing Library
exe.addAnonymousModule("clap", .{ .source_file = .{ .path = "lib/zig-clap/clap.zig" } });
exe.addPackagePath("clap", "lib/zig-clap/clap.zig");
// TOML Library
exe.addAnonymousModule("toml", .{ .source_file = .{ .path = "lib/zig-toml/src/toml.zig" } });
exe.addPackagePath("toml", "lib/zig-toml/src/toml.zig");
// OpenGL 3.3 Bindings
exe.addAnonymousModule("gl", .{ .source_file = .{ .path = "lib/gl.zig" } });
// gdbstub
Gdbstub.link(exe);
exe.addPackagePath("gl", "lib/gl.zig");
// Zig SDL Bindings: https://github.com/MasterQ32/SDL.zig
const sdk = Sdk.init(b, null);
const sdk = Sdk.init(b);
sdk.link(exe, .dynamic);
exe.addModule("sdl2", sdk.getNativeModule());
exe.addPackage(sdk.getNativePackage("sdl2"));
exe.setBuildMode(mode);
exe.install();
const run_cmd = exe.run();
@@ -61,11 +52,9 @@ pub fn build(b: *std.build.Builder) void {
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
const exe_tests = b.addTest(.{
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
const exe_tests = b.addTest("src/main.zig");
exe_tests.setTarget(target);
exe_tests.setBuildMode(mode);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&exe_tests.step);

4163
lib/gl.zig

File diff suppressed because it is too large Load Diff

Submodule lib/zba-gdbstub deleted from acb59994fc

View File

@@ -49,19 +49,16 @@ pub fn config() *const Config {
}
/// Reads a config file and then loads it into the global state
pub fn load(allocator: Allocator, file_path: []const u8) !void {
var config_file = try std.fs.cwd().openFile(file_path, .{});
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}", .{file_path});
log.info("loaded from {s}", .{config_path});
const contents = try config_file.readToEndAlloc(allocator, try config_file.getEndPos());
defer allocator.free(contents);
var parser = try toml.parseFile(allocator, file_path);
defer parser.deinit();
const table = try parser.parse();
const table = try toml.parseContents(allocator, contents, null);
defer table.deinit();
// TODO: Report unknown config options

View File

@@ -1,5 +1,6 @@
const std = @import("std");
const AudioDeviceId = @import("sdl2").SDL_AudioDeviceID;
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
const Bios = @import("bus/Bios.zig");
const Ewram = @import("bus/Ewram.zig");
@@ -33,11 +34,6 @@ pub const fetch_timings: [2][0x10]u8 = [_][0x10]u8{
[_]u8{ 1, 1, 6, 1, 1, 2, 2, 1, 4, 4, 4, 4, 4, 4, 8, 8 }, // 32-bit
};
// Fastmem Related
const page_size = 1 * 0x400; // 1KiB
const address_space_size = 0x1000_0000;
const table_len = address_space_size / page_size;
const Self = @This();
pak: GamePak,
@@ -53,16 +49,7 @@ io: Io,
cpu: *Arm7tdmi,
sched: *Scheduler,
read_table: *const [table_len]?*const anyopaque,
write_tables: [2]*const [table_len]?*anyopaque,
allocator: Allocator,
pub fn init(self: *Self, allocator: Allocator, sched: *Scheduler, cpu: *Arm7tdmi, paths: FilePaths) !void {
const tables = try allocator.alloc(?*anyopaque, 3 * table_len); // Allocate all tables
const read_table = tables[0..table_len];
const write_tables = .{ tables[table_len .. 2 * table_len], tables[2 * table_len .. 3 * table_len] };
self.* = .{
.pak = try GamePak.init(allocator, cpu, paths.rom, paths.save),
.bios = try Bios.init(allocator, paths.bios),
@@ -75,17 +62,7 @@ pub fn init(self: *Self, allocator: Allocator, sched: *Scheduler, cpu: *Arm7tdmi
.io = Io.init(),
.cpu = cpu,
.sched = sched,
.read_table = read_table,
.write_tables = write_tables,
.allocator = allocator,
};
self.fillReadTable(read_table);
// Internal Display Memory behaves differently on 8-bit reads
self.fillWriteTable(u32, write_tables[0]);
self.fillWriteTable(u8, write_tables[1]);
}
pub fn deinit(self: *Self) void {
@@ -94,126 +71,58 @@ pub fn deinit(self: *Self) void {
self.pak.deinit();
self.bios.deinit();
self.ppu.deinit();
// This is so I can deallocate the original `allocator.alloc`. I have to re-make the type
// since I'm not keeping it around, This is very jank and bad though
// FIXME: please figure out another way
self.allocator.free(@ptrCast([*]const ?*anyopaque, self.read_table[0..])[0 .. 3 * table_len]);
self.* = undefined;
}
fn fillReadTable(self: *Self, table: *[table_len]?*const anyopaque) void {
const vramMirror = @import("ppu/Vram.zig").mirror;
pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
const page = @truncate(u8, address >> 24);
const aligned_addr = forceAlign(T, address);
for (table, 0..) |*ptr, i| {
const addr = @intCast(u32, page_size * i);
return switch (page) {
// General Internal Memory
0x00 => blk: {
if (address < Bios.size)
break :blk self.bios.dbgRead(T, self.cpu.r[15], aligned_addr);
ptr.* = switch (addr) {
// General Internal Memory
0x0000_0000...0x0000_3FFF => null, // BIOS has it's own checks
0x0200_0000...0x02FF_FFFF => &self.ewram.buf[addr & 0x3FFFF],
0x0300_0000...0x03FF_FFFF => &self.iwram.buf[addr & 0x7FFF],
0x0400_0000...0x0400_03FF => null, // I/O
break :blk self.openBus(T, address);
},
0x02 => self.ewram.read(T, aligned_addr),
0x03 => self.iwram.read(T, aligned_addr),
0x04 => self.readIo(T, address),
// Internal Display Memory
0x0500_0000...0x05FF_FFFF => &self.ppu.palette.buf[addr & 0x3FF],
0x0600_0000...0x06FF_FFFF => &self.ppu.vram.buf[vramMirror(addr)],
0x0700_0000...0x07FF_FFFF => &self.ppu.oam.buf[addr & 0x3FF],
// Internal Display Memory
0x05 => self.ppu.palette.read(T, aligned_addr),
0x06 => self.ppu.vram.read(T, aligned_addr),
0x07 => self.ppu.oam.read(T, aligned_addr),
// External Memory (Game Pak)
0x0800_0000...0x0DFF_FFFF => self.fillReadTableExternal(addr),
0x0E00_0000...0x0FFF_FFFF => null, // SRAM
else => null,
};
}
// External Memory (Game Pak)
0x08...0x0D => self.pak.dbgRead(T, aligned_addr),
0x0E...0x0F => blk: {
const value = self.pak.backup.read(address);
const multiplier = switch (T) {
u32 => 0x01010101,
u16 => 0x0101,
u8 => 1,
else => @compileError("Backup: Unsupported read width"),
};
break :blk @as(T, value) * multiplier;
},
else => self.openBus(T, address),
};
}
fn fillWriteTable(self: *Self, comptime T: type, table: *[table_len]?*const anyopaque) void {
comptime std.debug.assert(T == u32 or T == u16 or T == u8);
const vramMirror = @import("ppu/Vram.zig").mirror;
for (table, 0..) |*ptr, i| {
const addr = @intCast(u32, page_size * i);
ptr.* = switch (addr) {
// General Internal Memory
0x0000_0000...0x0000_3FFF => null, // BIOS has it's own checks
0x0200_0000...0x02FF_FFFF => &self.ewram.buf[addr & 0x3FFFF],
0x0300_0000...0x03FF_FFFF => &self.iwram.buf[addr & 0x7FFF],
0x0400_0000...0x0400_03FF => null, // I/O
// Internal Display Memory
0x0500_0000...0x05FF_FFFF => if (T != u8) &self.ppu.palette.buf[addr & 0x3FF] else null,
0x0600_0000...0x06FF_FFFF => if (T != u8) &self.ppu.vram.buf[vramMirror(addr)] else null,
0x0700_0000...0x07FF_FFFF => if (T != u8) &self.ppu.oam.buf[addr & 0x3FF] else null,
// External Memory (Game Pak)
0x0800_0000...0x0DFF_FFFF => null, // ROM
0x0E00_0000...0x0FFF_FFFF => null, // SRAM
else => null,
};
}
}
fn fillReadTableExternal(self: *Self, addr: u32) ?*anyopaque {
// see `GamePak.zig` for more information about what conditions need to be true
// so that a simple pointer dereference isn't possible
std.debug.assert(addr & @as(u32, page_size - 1) == 0); // addr is guaranteed to be page-aligned
const start_addr = addr;
const end_addr = start_addr + page_size;
{
const data = start_addr <= 0x0800_00C4 and 0x0800_00C4 < end_addr; // GPIO Data
const direction = start_addr <= 0x0800_00C6 and 0x0800_00C6 < end_addr; // GPIO Direction
const control = start_addr <= 0x0800_00C8 and 0x0800_00C8 < end_addr; // GPIO Control
const has_gpio = data or direction or control;
const gpio_kind = self.pak.gpio.device.kind;
// There is a GPIO Device, and the current page contains at least one memory-mapped GPIO register
if (gpio_kind != .None and has_gpio) return null;
}
if (self.pak.backup.kind == .Eeprom) {
if (self.pak.buf.len > 0x100_000) {
// We are using a "large" EEPROM which means that if the below check is true
// this page has an address that's reserved for the EEPROM and therefore must
// be handled in slowmem
if (addr & 0x1FF_FFFF > 0x1FF_FEFF) return null;
} else {
// We are using a "small" EEPROM which means that if the below check is true
// (that is, we're in the 0xD address page) then we must handle at least one
// address in this page in slowmem
if (@truncate(u4, addr >> 24) == 0xD) return null;
}
}
// Finally, the GamePak has some unique behaviour for reads past the end of the ROM,
// so those will be handled by slowmem as well
const masked_addr = addr & 0x1FF_FFFF;
if (masked_addr >= self.pak.buf.len) return null;
return &self.pak.buf[masked_addr];
}
fn readIo(self: *const Self, comptime T: type, address: u32) T {
return io.read(self, T, address) orelse self.openBus(T, address);
fn readIo(self: *const Self, comptime T: type, unaligned_address: u32) T {
const maybe_value = io.read(self, T, forceAlign(T, unaligned_address));
return if (maybe_value) |value| value else self.openBus(T, unaligned_address);
}
fn openBus(self: *const Self, comptime T: type, address: u32) T {
@setCold(true);
const r15 = self.cpu.r[15];
const word = blk: {
// If Arm, get the most recently fetched instruction (PC + 8)
//
// FIXME: This is most likely a faulty assumption.
// I think what *actually* happens is that the Bus has a latch for the most
// recently fetched piece of data, which is then returned during Open Bus (also DMA open bus?)
// I can "get away" with this because it's very statistically likely that the most recently latched value is
// the most recently fetched instruction by the pipeline
if (!self.cpu.cpsr.t.read()) break :blk self.cpu.pipe.stage[1].?;
const page = @truncate(u8, r15 >> 24);
@@ -263,237 +172,85 @@ fn openBus(self: *const Self, comptime T: type, address: u32) T {
return @truncate(T, word);
}
pub fn read(self: *Self, comptime T: type, unaligned_address: u32) T {
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits;
const page = unaligned_address >> bits;
const offset = unaligned_address & (page_size - 1);
pub fn read(self: *Self, comptime T: type, address: u32) T {
const page = @truncate(u8, address >> 24);
const aligned_addr = forceAlign(T, address);
// whether or not we do this in slowmem or fastmem, we should advance the scheduler
self.sched.tick += timings[@boolToInt(T == u32)][@truncate(u4, unaligned_address >> 24)];
// We're doing some serious out-of-bounds open-bus reads
if (page >= table_len) return self.openBus(T, unaligned_address);
if (self.read_table[page]) |some_ptr| {
// We have a pointer to a page, cast the pointer to it's underlying type
const Ptr = [*]const T;
const ptr = @ptrCast(Ptr, @alignCast(@alignOf(std.meta.Child(Ptr)), some_ptr));
// Note: We don't check array length, since we force align the
// lower bits of the address as the GBA would
return ptr[forceAlign(T, offset) / @sizeOf(T)];
}
return self.slowRead(T, unaligned_address);
}
pub fn dbgRead(self: *const Self, comptime T: type, unaligned_address: u32) T {
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits;
const page = unaligned_address >> bits;
const offset = unaligned_address & (page_size - 1);
// We're doing some serious out-of-bounds open-bus reads
if (page >= table_len) return self.openBus(T, unaligned_address);
if (self.read_table[page]) |some_ptr| {
// We have a pointer to a page, cast the pointer to it's underlying type
const Ptr = [*]const T;
const ptr = @ptrCast(Ptr, @alignCast(@alignOf(std.meta.Child(Ptr)), some_ptr));
// Note: We don't check array length, since we force align the
// lower bits of the address as the GBA would
return ptr[forceAlign(T, offset) / @sizeOf(T)];
}
return self.dbgSlowRead(T, unaligned_address);
}
fn slowRead(self: *Self, comptime T: type, unaligned_address: u32) T {
@setCold(true);
const page = @truncate(u8, unaligned_address >> 24);
const address = forceAlign(T, unaligned_address);
self.sched.tick += timings[@boolToInt(T == u32)][@truncate(u4, page)];
return switch (page) {
// General Internal Memory
0x00 => blk: {
if (address < Bios.size)
break :blk self.bios.read(T, self.cpu.r[15], unaligned_address);
break :blk self.bios.read(T, self.cpu.r[15], aligned_addr);
break :blk self.openBus(T, address);
},
0x02 => unreachable, // completely handled by fastmeme
0x03 => unreachable, // completely handled by fastmeme
0x02 => self.ewram.read(T, aligned_addr),
0x03 => self.iwram.read(T, aligned_addr),
0x04 => self.readIo(T, address),
// Internal Display Memory
0x05 => unreachable, // completely handled by fastmeme
0x06 => unreachable, // completely handled by fastmeme
0x07 => unreachable, // completely handled by fastmeme
0x05 => self.ppu.palette.read(T, aligned_addr),
0x06 => self.ppu.vram.read(T, aligned_addr),
0x07 => self.ppu.oam.read(T, aligned_addr),
// External Memory (Game Pak)
0x08...0x0D => self.pak.read(T, address),
0x0E...0x0F => self.readBackup(T, unaligned_address),
0x08...0x0D => self.pak.read(T, aligned_addr),
0x0E...0x0F => blk: {
const value = self.pak.backup.read(address);
const multiplier = switch (T) {
u32 => 0x01010101,
u16 => 0x0101,
u8 => 1,
else => @compileError("Backup: Unsupported read width"),
};
break :blk @as(T, value) * multiplier;
},
else => self.openBus(T, address),
};
}
fn dbgSlowRead(self: *const Self, comptime T: type, unaligned_address: u32) T {
const page = @truncate(u8, unaligned_address >> 24);
const address = forceAlign(T, unaligned_address);
pub fn write(self: *Self, comptime T: type, address: u32, value: T) void {
const page = @truncate(u8, address >> 24);
const aligned_addr = forceAlign(T, address);
return switch (page) {
self.sched.tick += timings[@boolToInt(T == u32)][@truncate(u4, page)];
switch (page) {
// General Internal Memory
0x00 => blk: {
if (address < Bios.size)
break :blk self.bios.dbgRead(T, self.cpu.r[15], unaligned_address);
0x00 => self.bios.write(T, aligned_addr, value),
0x02 => self.ewram.write(T, aligned_addr, value),
0x03 => self.iwram.write(T, aligned_addr, value),
0x04 => io.write(self, T, aligned_addr, value),
break :blk self.openBus(T, address);
// Internal Display Memory
0x05 => self.ppu.palette.write(T, aligned_addr, value),
0x06 => self.ppu.vram.write(T, self.ppu.dispcnt, aligned_addr, value),
0x07 => self.ppu.oam.write(T, aligned_addr, value),
// External Memory (Game Pak)
0x08...0x0D => self.pak.write(T, self.dma[3].word_count, aligned_addr, value),
0x0E...0x0F => {
const rotate_by = switch (T) {
u32 => address & 3,
u16 => address & 1,
u8 => 0,
else => @compileError("Backup: Unsupported write width"),
};
self.pak.backup.write(address, @truncate(u8, rotr(T, value, 8 * rotate_by)));
},
0x02 => unreachable, // handled by fastmem
0x03 => unreachable, // handled by fastmem
0x04 => self.readIo(T, address),
// Internal Display Memory
0x05 => unreachable, // handled by fastmem
0x06 => unreachable, // handled by fastmem
0x07 => unreachable, // handled by fastmem
// External Memory (Game Pak)
0x08...0x0D => self.pak.dbgRead(T, address),
0x0E...0x0F => self.readBackup(T, unaligned_address),
else => self.openBus(T, address),
};
}
fn readBackup(self: *const Self, comptime T: type, unaligned_address: u32) T {
const value = self.pak.backup.read(unaligned_address);
const multiplier = switch (T) {
u32 => 0x01010101,
u16 => 0x0101,
u8 => 1,
else => @compileError("Backup: Unsupported read width"),
};
return @as(T, value) * multiplier;
}
pub fn write(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits;
const page = unaligned_address >> bits;
const offset = unaligned_address & (page_size - 1);
// whether or not we do this in slowmem or fastmem, we should advance the scheduler
self.sched.tick += timings[@boolToInt(T == u32)][@truncate(u4, unaligned_address >> 24)];
// We're doing some serious out-of-bounds open-bus writes, they do nothing though
if (page >= table_len) return;
if (self.write_tables[@boolToInt(T == u8)][page]) |some_ptr| {
// We have a pointer to a page, cast the pointer to it's underlying type
const Ptr = [*]T;
const ptr = @ptrCast(Ptr, @alignCast(@alignOf(std.meta.Child(Ptr)), some_ptr));
// Note: We don't check array length, since we force align the
// lower bits of the address as the GBA would
ptr[forceAlign(T, offset) / @sizeOf(T)] = value;
} else {
// we can return early if this is an 8-bit OAM write
if (T == u8 and @truncate(u8, unaligned_address >> 24) == 0x07) return;
self.slowWrite(T, unaligned_address, value);
}
}
/// Mostly Identical to `Bus.write`, slowmeme is handled by `Bus.dbgSlowWrite`
pub fn dbgWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits;
const page = unaligned_address >> bits;
const offset = unaligned_address & (page_size - 1);
// We're doing some serious out-of-bounds open-bus writes, they do nothing though
if (page >= table_len) return;
if (self.write_tables[@boolToInt(T == u8)][page]) |some_ptr| {
// We have a pointer to a page, cast the pointer to it's underlying type
const Ptr = [*]T;
const ptr = @ptrCast(Ptr, @alignCast(@alignOf(std.meta.Child(Ptr)), some_ptr));
// Note: We don't check array length, since we force align the
// lower bits of the address as the GBA would
ptr[forceAlign(T, offset) / @sizeOf(T)] = value;
} else {
// we can return early if this is an 8-bit OAM write
if (T == u8 and @truncate(u8, unaligned_address >> 24) == 0x07) return;
self.dbgSlowWrite(T, unaligned_address, value);
}
}
fn slowWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
@setCold(true);
const page = @truncate(u8, unaligned_address >> 24);
const address = forceAlign(T, unaligned_address);
switch (page) {
// General Internal Memory
0x00 => self.bios.write(T, address, value),
0x02 => unreachable, // completely handled by fastmem
0x03 => unreachable, // completely handled by fastmem
0x04 => io.write(self, T, address, value),
// Internal Display Memory
0x05 => self.ppu.palette.write(T, address, value),
0x06 => self.ppu.vram.write(T, self.ppu.dispcnt, address, value),
0x07 => unreachable, // completely handled by fastmem
// External Memory (Game Pak)
0x08...0x0D => self.pak.write(T, self.dma[3].word_count, address, value),
0x0E...0x0F => self.pak.backup.write(unaligned_address, @truncate(u8, rotr(T, value, 8 * rotateBy(T, unaligned_address)))),
else => {},
}
}
fn dbgSlowWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
@setCold(true);
const page = @truncate(u8, unaligned_address >> 24);
const address = forceAlign(T, unaligned_address);
switch (page) {
// General Internal Memory
0x00 => self.bios.write(T, address, value),
0x02 => unreachable, // completely handled by fastmem
0x03 => unreachable, // completely handled by fastmem
0x04 => return, // FIXME: Let debug writes mess with I/O
// Internal Display Memory
0x05 => self.ppu.palette.write(T, address, value),
0x06 => self.ppu.vram.write(T, self.ppu.dispcnt, address, value),
0x07 => unreachable, // completely handled by fastmem
// External Memory (Game Pak)
0x08...0x0D => return, // FIXME: Debug Write to Backup/GPIO w/out messing with state
0x0E...0x0F => return, // FIXME: Debug Write to Backup w/out messing with state
else => {},
}
}
inline fn rotateBy(comptime T: type, address: u32) u32 {
fn forceAlign(comptime T: type, address: u32) u32 {
return switch (T) {
u32 => address & 3,
u16 => address & 1,
u8 => 0,
else => @compileError("Unsupported write width"),
};
}
pub inline fn forceAlign(comptime T: type, address: u32) u32 {
return switch (T) {
u32 => address & ~@as(u32, 3),
u16 => address & ~@as(u32, 1),
u32 => address & 0xFFFF_FFFC,
u16 => address & 0xFFFF_FFFE,
u8 => address,
else => @compileError("Bus: Invalid read/write type"),
};

View File

@@ -3,6 +3,8 @@ const SDL = @import("sdl2");
const io = @import("bus/io.zig");
const util = @import("../util.zig");
const AudioDeviceId = SDL.SDL_AudioDeviceID;
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
const Scheduler = @import("scheduler.zig").Scheduler;
const ToneSweep = @import("apu/ToneSweep.zig");
@@ -12,217 +14,133 @@ const Noise = @import("apu/Noise.zig");
const SoundFifo = std.fifo.LinearFifo(u8, .{ .Static = 0x20 });
const getHalf = util.getHalf;
const setHalf = util.setHalf;
const intToBytes = util.intToBytes;
const intToBytes = @import("../util.zig").intToBytes;
const setHi = @import("../util.zig").setHi;
const setLo = @import("../util.zig").setLo;
const log = std.log.scoped(.APU);
pub const host_rate = @import("../platform.zig").sample_rate;
pub const host_format = @import("../platform.zig").sample_format;
pub const host_sample_rate = 1 << 15;
pub fn read(comptime T: type, apu: *const Apu, addr: u32) ?T {
const byte_addr = @truncate(u8, addr);
const byte = @truncate(u8, addr);
return switch (T) {
u32 => switch (byte_addr) {
0x60 => @as(T, apu.ch1.sound1CntH()) << 16 | apu.ch1.sound1CntL(),
0x64 => apu.ch1.sound1CntX(),
0x68 => apu.ch2.sound2CntL(),
0x6C => apu.ch2.sound2CntH(),
0x70 => @as(T, apu.ch3.sound3CntH()) << 16 | apu.ch3.sound3CntL(),
0x74 => apu.ch3.sound3CntX(),
0x78 => apu.ch4.sound4CntL(),
0x7C => apu.ch4.sound4CntH(),
0x80 => @as(T, apu.dma_cnt.raw) << 16 | apu.psg_cnt.raw, // SOUNDCNT_H, SOUNDCNT_L
0x84 => apu.soundCntX(),
0x88 => apu.bias.raw, // SOUNDBIAS, high is unused
0x8C => null,
0x90, 0x94, 0x98, 0x9C => apu.ch3.wave_dev.read(T, apu.ch3.select, addr),
0xA0 => null, // FIFO_A
0xA4 => null, // FIFO_B
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
},
u16 => switch (byte_addr) {
u16 => switch (byte) {
0x60 => apu.ch1.sound1CntL(),
0x62 => apu.ch1.sound1CntH(),
0x64 => apu.ch1.sound1CntX(),
0x66 => 0x0000, // suite.gba expects 0x0000, not 0xDEAD
0x68 => apu.ch2.sound2CntL(),
0x6A => 0x0000,
0x6C => apu.ch2.sound2CntH(),
0x6E => 0x0000,
0x70 => apu.ch3.sound3CntL(),
0x70 => apu.ch3.select.raw & 0xE0, // SOUND3CNT_L
0x72 => apu.ch3.sound3CntH(),
0x74 => apu.ch3.sound3CntX(),
0x76 => 0x0000,
0x74 => apu.ch3.freq.raw & 0x4000, // SOUND3CNT_X
0x78 => apu.ch4.sound4CntL(),
0x7A => 0x0000,
0x7C => apu.ch4.sound4CntH(),
0x7E => 0x0000,
0x80 => apu.soundCntL(),
0x82 => apu.soundCntH(),
0x80 => apu.psg_cnt.raw & 0xFF77, // SOUNDCNT_L
0x82 => apu.dma_cnt.raw & 0x770F, // SOUNDCNT_H
0x84 => apu.soundCntX(),
0x86 => 0x0000,
0x88 => apu.bias.raw, // SOUNDBIAS
0x8A => 0x0000,
0x8C, 0x8E => null,
0x90, 0x92, 0x94, 0x96, 0x98, 0x9A, 0x9C, 0x9E => apu.ch3.wave_dev.read(T, apu.ch3.select, addr),
0xA0, 0xA2 => null, // FIFO_A
0xA4, 0xA6 => null, // FIFO_B
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
},
u8 => switch (byte_addr) {
0x60, 0x61 => @truncate(T, @as(u16, apu.ch1.sound1CntL()) >> getHalf(byte_addr)),
0x62, 0x63 => @truncate(T, apu.ch1.sound1CntH() >> getHalf(byte_addr)),
0x64, 0x65 => @truncate(T, apu.ch1.sound1CntX() >> getHalf(byte_addr)),
0x66, 0x67 => 0x00, // assuming behaviour is identical to that of 16-bit reads
0x68, 0x69 => @truncate(T, apu.ch2.sound2CntL() >> getHalf(byte_addr)),
0x6A, 0x6B => 0x00,
0x6C, 0x6D => @truncate(T, apu.ch2.sound2CntH() >> getHalf(byte_addr)),
0x6E, 0x6F => 0x00,
0x70, 0x71 => @truncate(T, @as(u16, apu.ch3.sound3CntL()) >> getHalf(byte_addr)), // SOUND3CNT_L
0x72, 0x73 => @truncate(T, apu.ch3.sound3CntH() >> getHalf(byte_addr)),
0x74, 0x75 => @truncate(T, apu.ch3.sound3CntX() >> getHalf(byte_addr)), // SOUND3CNT_L
0x76, 0x77 => 0x00,
0x78, 0x79 => @truncate(T, apu.ch4.sound4CntL() >> getHalf(byte_addr)),
0x7A, 0x7B => 0x00,
0x7C, 0x7D => @truncate(T, apu.ch4.sound4CntH() >> getHalf(byte_addr)),
0x7E, 0x7F => 0x00,
0x80, 0x81 => @truncate(T, apu.soundCntL() >> getHalf(byte_addr)), // SOUNDCNT_L
0x82, 0x83 => @truncate(T, apu.soundCntH() >> getHalf(byte_addr)), // SOUNDCNT_H
0x84, 0x85 => @truncate(T, @as(u16, apu.soundCntX()) >> getHalf(byte_addr)),
0x86, 0x87 => 0x00,
0x88, 0x89 => @truncate(T, apu.bias.raw >> getHalf(byte_addr)), // SOUNDBIAS
0x8A, 0x8B => 0x00,
0x8C...0x8F => null,
0x90...0x9F => apu.ch3.wave_dev.read(T, apu.ch3.select, addr),
0xA0, 0xA1, 0xA2, 0xA3 => null, // FIFO_A
0xA4, 0xA5, 0xA6, 0xA7 => null, // FIFO_B
else => util.io.read.err(T, log, "unexpected {} read from 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 }),
},
u8 => switch (byte) {
0x60 => apu.ch1.sound1CntL(), // NR10
0x62 => apu.ch1.duty.raw, // NR11
0x63 => apu.ch1.envelope.raw, // NR12
0x68 => apu.ch2.duty.raw, // NR21
0x69 => apu.ch2.envelope.raw, // NR22
0x73 => apu.ch3.vol.raw, // NR32
0x79 => apu.ch4.envelope.raw, // NR42
0x7C => apu.ch4.poly.raw, // NR43
0x81 => @truncate(u8, apu.psg_cnt.raw >> 8), // NR51
0x84 => apu.soundCntX(),
0x89 => @truncate(u8, apu.bias.raw >> 8), // SOUNDBIAS_H
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
},
u32 => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
else => @compileError("APU: Unsupported read width"),
};
}
pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void {
const byte_addr = @truncate(u8, addr);
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return;
const byte = @truncate(u8, addr);
switch (T) {
u32 => {
// 0x80 and 0x81 handled in setSoundCnt
if (byte_addr < 0x80 and !apu.cnt.apu_enable.read()) return;
u32 => switch (byte) {
0x60 => apu.ch1.setSound1Cnt(value),
0x64 => apu.ch1.setSound1CntX(&apu.fs, @truncate(u16, value)),
0x68 => apu.ch2.setSound2CntL(@truncate(u16, value)),
0x6C => apu.ch2.setSound2CntH(&apu.fs, @truncate(u16, value)),
0x70 => apu.ch3.setSound3Cnt(value),
0x74 => apu.ch3.setSound3CntX(&apu.fs, @truncate(u16, value)),
0x78 => apu.ch4.setSound4CntL(@truncate(u16, value)),
0x7C => apu.ch4.setSound4CntH(&apu.fs, @truncate(u16, value)),
switch (byte_addr) {
0x60 => apu.ch1.setSound1Cnt(value),
0x64 => apu.ch1.setSound1CntX(&apu.fs, @truncate(u16, value)),
0x68 => apu.ch2.setSound2CntL(@truncate(u16, value)),
0x6C => apu.ch2.setSound2CntH(&apu.fs, @truncate(u16, value)),
0x70 => apu.ch3.setSound3Cnt(value),
0x74 => apu.ch3.setSound3CntX(&apu.fs, @truncate(u16, value)),
0x78 => apu.ch4.setSound4CntL(@truncate(u16, value)),
0x7C => apu.ch4.setSound4CntH(&apu.fs, @truncate(u16, value)),
0x80 => apu.setSoundCnt(value),
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1),
0x88 => apu.bias.raw = @truncate(u16, value),
0x8C => {},
0x90, 0x94, 0x98, 0x9C => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
0xA0 => apu.chA.push(value), // FIFO_A
0xA4 => apu.chB.push(value), // FIFO_B
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
}
0x80 => apu.setSoundCnt(value),
// WAVE_RAM
0x90...0x9F => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
0xA0 => apu.chA.push(value), // FIFO_A
0xA4 => apu.chB.push(value), // FIFO_B
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
},
u16 => {
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return;
u16 => switch (byte) {
0x60 => apu.ch1.setSound1CntL(@truncate(u8, value)), // SOUND1CNT_L
0x62 => apu.ch1.setSound1CntH(value),
0x64 => apu.ch1.setSound1CntX(&apu.fs, value),
switch (byte_addr) {
0x60 => apu.ch1.setSound1CntL(@truncate(u8, value)), // SOUND1CNT_L
0x62 => apu.ch1.setSound1CntH(value),
0x64 => apu.ch1.setSound1CntX(&apu.fs, value),
0x66 => {},
0x68 => apu.ch2.setSound2CntL(value),
0x6C => apu.ch2.setSound2CntH(&apu.fs, value),
0x68 => apu.ch2.setSound2CntL(value),
0x6A => {},
0x6C => apu.ch2.setSound2CntH(&apu.fs, value),
0x6E => {},
0x70 => apu.ch3.setSound3CntL(@truncate(u8, value)),
0x72 => apu.ch3.setSound3CntH(value),
0x74 => apu.ch3.setSound3CntX(&apu.fs, value),
0x70 => apu.ch3.setSound3CntL(@truncate(u8, value)),
0x72 => apu.ch3.setSound3CntH(value),
0x74 => apu.ch3.setSound3CntX(&apu.fs, value),
0x76 => {},
0x78 => apu.ch4.setSound4CntL(value),
0x7C => apu.ch4.setSound4CntH(&apu.fs, value),
0x78 => apu.ch4.setSound4CntL(value),
0x7A => {},
0x7C => apu.ch4.setSound4CntH(&apu.fs, value),
0x7E => {},
0x80 => apu.setSoundCntL(value),
0x82 => apu.setSoundCntH(value),
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1),
0x86 => {},
0x88 => apu.bias.raw = value, // SOUNDBIAS
0x8A, 0x8C, 0x8E => {},
0x90, 0x92, 0x94, 0x96, 0x98, 0x9A, 0x9C, 0x9E => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
0xA0, 0xA2 => log.err("Tried to write 0x{X:0>4}{} to FIFO_A", .{ value, T }),
0xA4, 0xA6 => log.err("Tried to write 0x{X:0>4}{} to FIFO_B", .{ value, T }),
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
}
0x80 => apu.psg_cnt.raw = value, // SOUNDCNT_L
0x82 => apu.setSoundCntH(value),
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1),
0x88 => apu.bias.raw = value, // SOUNDBIAS
// WAVE_RAM
0x90...0x9F => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
},
u8 => {
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return;
u8 => switch (byte) {
0x60 => apu.ch1.setSound1CntL(value),
0x62 => apu.ch1.setNr11(value),
0x63 => apu.ch1.setNr12(value),
0x64 => apu.ch1.setNr13(value),
0x65 => apu.ch1.setNr14(&apu.fs, value),
switch (byte_addr) {
0x60 => apu.ch1.setSound1CntL(value),
0x61 => {},
0x62 => apu.ch1.setNr11(value),
0x63 => apu.ch1.setNr12(value),
0x64 => apu.ch1.setNr13(value),
0x65 => apu.ch1.setNr14(&apu.fs, value),
0x66, 0x67 => {},
0x68 => apu.ch2.setNr21(value),
0x69 => apu.ch2.setNr22(value),
0x6C => apu.ch2.setNr23(value),
0x6D => apu.ch2.setNr24(&apu.fs, value),
0x68 => apu.ch2.setNr21(value),
0x69 => apu.ch2.setNr22(value),
0x6A, 0x6B => {},
0x6C => apu.ch2.setNr23(value),
0x6D => apu.ch2.setNr24(&apu.fs, value),
0x6E, 0x6F => {},
0x70 => apu.ch3.setSound3CntL(value), // NR30
0x72 => apu.ch3.setNr31(value),
0x73 => apu.ch3.vol.raw = value, // NR32
0x74 => apu.ch3.setNr33(value),
0x75 => apu.ch3.setNr34(&apu.fs, value),
0x70 => apu.ch3.setSound3CntL(value), // NR30
0x71 => {},
0x72 => apu.ch3.setNr31(value),
0x73 => apu.ch3.vol.raw = value, // NR32
0x74 => apu.ch3.setNr33(value),
0x75 => apu.ch3.setNr34(&apu.fs, value),
0x76, 0x77 => {},
0x78 => apu.ch4.setNr41(value),
0x79 => apu.ch4.setNr42(value),
0x7C => apu.ch4.poly.raw = value, // NR 43
0x7D => apu.ch4.setNr44(&apu.fs, value),
0x78 => apu.ch4.setNr41(value),
0x79 => apu.ch4.setNr42(value),
0x7A, 0x7B => {},
0x7C => apu.ch4.poly.raw = value, // NR 43
0x7D => apu.ch4.setNr44(&apu.fs, value),
0x7E, 0x7F => {},
0x80, 0x81 => apu.setSoundCntL(setHalf(u16, apu.psg_cnt.raw, byte_addr, value)),
0x82, 0x83 => apu.setSoundCntH(setHalf(u16, apu.dma_cnt.raw, byte_addr, value)),
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1),
0x85 => {},
0x86, 0x87 => {},
0x88, 0x89 => apu.bias.raw = setHalf(u16, apu.bias.raw, byte_addr, value), // SOUNDBIAS
0x8A...0x8F => {},
0x90...0x9F => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
0xA0...0xA3 => log.err("Tried to write 0x{X:0>2}{} to FIFO_A", .{ value, T }),
0xA4...0xA7 => log.err("Tried to write 0x{X:0>2}{} to FIFO_B", .{ value, T }),
else => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
}
0x80 => apu.setNr50(value),
0x81 => apu.setNr51(value),
0x82 => apu.setSoundCntH(setLo(u16, apu.dma_cnt.raw, value)),
0x83 => apu.setSoundCntH(setHi(u16, apu.dma_cnt.raw, value)),
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1), // NR52
0x89 => apu.setSoundBiasH(value),
0x90...0x9F => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
else => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
},
else => @compileError("APU: Unsupported write width"),
}
@@ -271,7 +189,7 @@ pub const Apu = struct {
.bias = .{ .raw = 0x0200 },
.sampling_cycle = 0b00,
.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, 1 << 15, host_format, 2, host_rate).?,
.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, 1 << 15, SDL.AUDIO_U16, 2, host_sample_rate).?,
.sched = sched,
.capacitor = 0,
@@ -290,33 +208,18 @@ pub const Apu = struct {
}
fn reset(self: *Self) void {
// All PSG Registers between 0x0400_0060..0x0400_0081 are zeroed
// 0x0400_0082 and 0x0400_0088 retain their values
self.ch1.reset();
self.ch2.reset();
self.ch3.reset();
self.ch4.reset();
// GBATEK says 4000060h..4000081h I take this to mean inclusive
self.psg_cnt.raw = 0x0000;
}
/// SOUNDCNT
fn setSoundCnt(self: *Self, value: u32) void {
if (self.cnt.apu_enable.read()) self.setSoundCntL(@truncate(u16, value));
self.psg_cnt.raw = @truncate(u16, value);
self.setSoundCntH(@truncate(u16, value >> 16));
}
/// SOUNDCNT_L
pub fn soundCntL(self: *const Self) u16 {
return self.psg_cnt.raw & 0xFF77;
}
/// SOUNDCNT_L
pub fn setSoundCntL(self: *Self, value: u16) void {
self.psg_cnt.raw = value;
}
/// SOUNDCNT_H
pub fn setSoundCntH(self: *Self, value: u16) void {
const new: io.DmaSoundControl = .{ .raw = value };
@@ -329,11 +232,6 @@ pub const Apu = struct {
self.dma_cnt = new;
}
/// SOUNDCNT_H
pub fn soundCntH(self: *const Self) u16 {
return self.dma_cnt.raw & 0x770F;
}
/// NR52
pub fn setSoundCntX(self: *Self, value: bool) void {
self.cnt.apu_enable.write(value);
@@ -342,14 +240,11 @@ pub const Apu = struct {
self.fs.step = 0; // Reset Frame Sequencer
// Reset Square Wave Offsets
self.ch1.square.reset();
self.ch2.square.reset();
self.ch1.square.pos = 0;
self.ch2.square.pos = 0;
// Reset Wave
self.ch3.wave_dev.reset();
// Rest Noise
self.ch4.lfsr.reset();
// Reset Wave Device Offsets
self.ch3.wave_dev.offset = 0;
} else {
self.reset();
}
@@ -367,6 +262,20 @@ pub const Apu = struct {
return apu_enable << 7 | ch4_enable << 3 | ch3_enable << 2 | ch2_enable << 1 | ch1_enable;
}
/// NR50
pub fn setNr50(self: *Self, byte: u8) void {
self.psg_cnt.raw = (self.psg_cnt.raw & 0xFF00) | byte;
}
/// NR51
pub fn setNr51(self: *Self, byte: u8) void {
self.psg_cnt.raw = @as(u16, byte) << 8 | (self.psg_cnt.raw & 0xFF);
}
pub fn setSoundBiasH(self: *Self, byte: u8) void {
self.bias.raw = (@as(u16, byte) << 8) | (self.bias.raw & 0xFF);
}
pub fn sampleAudio(self: *Self, late: u64) void {
self.sched.push(.SampleAudio, self.interval() -| late);
@@ -418,8 +327,8 @@ pub const Apu = struct {
right += if (self.dma_cnt.chB_right.read()) chB_sample else 0;
// Add SOUNDBIAS
// FIXME: SOUNDBIAS is 10-bit but The waveform is centered around 0 if I treat it as 11-bit
const bias = @as(i16, self.bias.level.read()) << 2;
// FIXME: Is SOUNDBIAS 9-bit or 10-bit?
const bias = @as(i16, self.bias.level.read()) << 1;
left += bias;
right += bias;
@@ -430,6 +339,7 @@ pub const Apu = struct {
const ext_left = (clamped_left << 5) | (clamped_left >> 6);
const ext_right = (clamped_right << 5) | (clamped_right >> 6);
// FIXME: This rarely happens
if (self.sampling_cycle != self.bias.sampling_cycle.read()) self.replaceSDLResampler();
_ = SDL.SDL_AudioStreamPut(self.stream, &[2]u16{ ext_left, ext_right }, 2 * @sizeOf(u16));
@@ -446,7 +356,7 @@ pub const Apu = struct {
defer SDL.SDL_FreeAudioStream(old_stream);
self.sampling_cycle = self.bias.sampling_cycle.read();
self.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, @intCast(c_int, sample_rate), host_format, 2, host_rate).?;
self.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, @intCast(c_int, sample_rate), SDL.AUDIO_U16, 2, host_sample_rate).?;
}
fn interval(self: *const Self) u64 {
@@ -495,15 +405,11 @@ pub const Apu = struct {
if (!self.cnt.apu_enable.read()) return;
if (@boolToInt(self.dma_cnt.chA_timer.read()) == tim_id) {
if (!self.chA.enabled) return;
self.chA.updateSample();
if (self.chA.len() <= 15) cpu.bus.dma[1].requestAudio(0x0400_00A0);
}
if (@boolToInt(self.dma_cnt.chB_timer.read()) == tim_id) {
if (!self.chB.enabled) return;
self.chB.updateSample();
if (self.chB.len() <= 15) cpu.bus.dma[2].requestAudio(0x0400_00A4);
}
@@ -517,28 +423,19 @@ pub fn DmaSound(comptime kind: DmaSoundKind) type {
fifo: SoundFifo,
kind: DmaSoundKind,
sample: i8,
enabled: bool,
fn init() Self {
return .{
.fifo = SoundFifo.init(),
.kind = kind,
.sample = 0,
.enabled = false,
};
}
pub fn push(self: *Self, value: u32) void {
if (!self.enabled) self.enable();
self.fifo.write(&intToBytes(u32, value)) catch |e| log.err("{} Error: {}", .{ kind, e });
}
fn enable(self: *Self) void {
@setCold(true);
self.enabled = true;
}
pub fn len(self: *const Self) usize {
return self.fifo.readableLength();
}
@@ -559,8 +456,8 @@ const DmaSoundKind = enum {
};
pub const FrameSequencer = struct {
const interval = (1 << 24) / 512;
const Self = @This();
pub const interval = (1 << 24) / 512;
step: u3,

View File

@@ -49,13 +49,10 @@ pub fn init(sched: *Scheduler) Self {
}
pub fn reset(self: *Self) void {
self.len = 0; // NR41
self.envelope.raw = 0; // NR42
self.poly.raw = 0; // NR43
self.cnt.raw = 0; // NR44
self.len_dev.reset();
self.env_dev.reset();
self.len = 0;
self.envelope.raw = 0;
self.poly.raw = 0;
self.cnt.raw = 0;
self.sample = 0;
self.enabled = false;

View File

@@ -43,12 +43,9 @@ pub fn init(sched: *Scheduler) Self {
}
pub fn reset(self: *Self) void {
self.duty.raw = 0; // NR21
self.envelope.raw = 0; // NR22
self.freq.raw = 0; // NR32, NR24
self.len_dev.reset();
self.env_dev.reset();
self.duty.raw = 0;
self.envelope.raw = 0;
self.freq.raw = 0;
self.sample = 0;
self.enabled = false;

View File

@@ -50,14 +50,12 @@ pub fn init(sched: *Scheduler) Self {
}
pub fn reset(self: *Self) void {
self.sweep.raw = 0; // NR10
self.duty.raw = 0; // NR11
self.envelope.raw = 0; // NR12
self.freq.raw = 0; // NR13, NR14
self.sweep.raw = 0;
self.sweep_dev.calc_performed = false;
self.len_dev.reset();
self.sweep_dev.reset();
self.env_dev.reset();
self.duty.raw = 0;
self.envelope.raw = 0;
self.freq.raw = 0;
self.sample = 0;
self.enabled = false;
@@ -94,9 +92,10 @@ pub fn sound1CntL(self: *const Self) u8 {
pub fn setSound1CntL(self: *Self, value: u8) void {
const new = io.Sweep{ .raw = value };
if (!new.direction.read()) {
// If at least one (1) sweep calculation has been made with
// the negate bit set (since last trigger), disable the channel
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;
}

View File

@@ -42,13 +42,10 @@ pub fn init(sched: *Scheduler) Self {
}
pub fn reset(self: *Self) void {
self.select.raw = 0; // NR30
self.length = 0; // NR31
self.vol.raw = 0; // NR32
self.freq.raw = 0; // NR33, NR34
self.len_dev.reset();
self.wave_dev.reset();
self.select.raw = 0;
self.length = 0;
self.vol.raw = 0;
self.freq.raw = 0;
self.sample = 0;
self.enabled = false;
@@ -74,11 +71,6 @@ pub fn setSound3CntL(self: *Self, value: u8) void {
if (!self.select.enabled.read()) self.enabled = false;
}
/// NR30
pub fn sound3CntL(self: *const Self) u8 {
return self.select.raw & 0xE0;
}
/// NR31, NR32
pub fn sound3CntH(self: *const Self) u16 {
return @as(u16, self.length & 0xE0) << 8;
@@ -102,11 +94,6 @@ pub fn setSound3CntX(self: *Self, fs: *const FrameSequencer, value: u16) void {
self.setNr34(fs, @truncate(u8, value >> 8));
}
/// NR33, NR34
pub fn sound3CntX(self: *const Self) u16 {
return self.freq.raw & 0x4000;
}
/// NR33
pub fn setNr33(self: *Self, byte: u8) void {
self.freq.raw = (self.freq.raw & 0xFF00) | byte;

View File

@@ -11,11 +11,6 @@ pub fn create() Self {
return .{ .timer = 0, .vol = 0 };
}
pub fn reset(self: *Self) void {
self.timer = 0;
self.vol = 0;
}
pub fn tick(self: *Self, nrx2: io.Envelope) void {
if (nrx2.period.read() != 0) {
if (self.timer != 0) self.timer -= 1;

View File

@@ -6,10 +6,6 @@ pub fn create() Self {
return .{ .timer = 0 };
}
pub fn reset(self: *Self) void {
self.timer = 0;
}
pub fn tick(self: *Self, enabled: bool, ch_enable: *bool) void {
if (enabled) {
if (self.timer == 0) return;

View File

@@ -18,19 +18,13 @@ pub fn create() Self {
};
}
pub fn reset(self: *Self) void {
self.timer = 0;
self.enabled = false;
self.shadow = 0;
self.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);
@@ -51,10 +45,7 @@ pub fn calculate(self: *Self, sweep: io.Sweep, ch_enable: *bool) u12 {
const shadow_shifted = shadow >> sweep.shift.read();
const decrease = sweep.direction.read();
const freq = if (decrease) blk: {
self.calc_performed = true;
break :blk shadow - shadow_shifted;
} else shadow + shadow_shifted;
const freq = if (decrease) shadow - shadow_shifted else shadow + shadow_shifted;
if (freq > 0x7FF) ch_enable.* = false;
return freq;

View File

@@ -1,7 +1,9 @@
//! Linear Feedback Shift Register
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);
@@ -19,11 +21,6 @@ pub fn create(sched: *Scheduler) Self {
};
}
pub fn reset(self: *Self) void {
self.shift = 0;
self.timer = 0;
}
pub fn sample(self: *const Self) i8 {
return if ((~self.shift & 1) == 1) 1 else -1;
}
@@ -38,7 +35,7 @@ pub fn reload(self: *Self, poly: io.PolyCounter) void {
}
/// Scheduler Event Handler for LFSR Timer Expire
/// FIXME: This gets called a lot, slowing down the scheduler
/// 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."

View File

@@ -2,6 +2,7 @@ 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");
@@ -20,11 +21,6 @@ pub fn init(sched: *Scheduler) Self {
};
}
pub fn reset(self: *Self) void {
self.timer = 0;
self.pos = 0;
}
/// 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);

View File

@@ -2,6 +2,8 @@ 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);
@@ -38,13 +40,6 @@ pub fn init(sched: *Scheduler) Self {
};
}
pub fn reset(self: *Self) void {
self.timer = 0;
self.offset = 0;
// sample buffer isn't reset because it's outside of the range of what NR52{7}'s effects
}
/// Reload internal Wave Timer
pub fn reload(self: *Self, value: u11) void {
self.sched.removeScheduledEvent(.{ .ApuChannel = 2 });

View File

@@ -3,9 +3,6 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const log = std.log.scoped(.Bios);
const rotr = @import("../../util.zig").rotr;
const forceAlign = @import("../Bus.zig").forceAlign;
/// Size of the BIOS in bytes
pub const size = 0x4000;
const Self = @This();
@@ -13,37 +10,21 @@ const Self = @This();
buf: ?[]u8,
allocator: Allocator,
addr_latch: u32 = 0,
addr_latch: u32,
// https://github.com/ITotalJustice/notorious_beeg/issues/106
pub fn read(self: *Self, comptime T: type, r15: u32, address: u32) T {
pub fn read(self: *Self, comptime T: type, r15: u32, addr: u32) T {
if (r15 < Self.size) {
const addr = forceAlign(T, address);
self.addr_latch = addr;
return self._read(T, addr);
}
log.warn("Open Bus! Read from 0x{X:0>8}, but PC was 0x{X:0>8}", .{ address, r15 });
const value = self._read(u32, self.addr_latch);
return @truncate(T, rotr(u32, value, 8 * rotateBy(T, address)));
log.debug("Rejected read since r15=0x{X:0>8}", .{r15});
return @truncate(T, self._read(T, self.addr_latch));
}
fn rotateBy(comptime T: type, address: u32) u32 {
return switch (T) {
u8 => address & 3,
u16 => address & 2,
u32 => 0,
else => @compileError("bios: unsupported read width"),
};
}
pub fn dbgRead(self: *const Self, comptime T: type, r15: u32, address: u32) T {
if (r15 < Self.size) return self._read(T, forceAlign(T, address));
const value = self._read(u32, self.addr_latch);
return @truncate(T, rotr(u32, value, 8 * rotateBy(T, address)));
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
@@ -62,19 +43,18 @@ pub fn write(_: *Self, comptime T: type, addr: u32, value: T) void {
}
pub fn init(allocator: Allocator, maybe_path: ?[]const u8) !Self {
if (maybe_path == null) return .{ .buf = null, .allocator = allocator };
const path = maybe_path.?;
const buf: ?[]u8 = if (maybe_path) |path| blk: {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
const buf = try allocator.alloc(u8, Self.size);
errdefer allocator.free(buf);
break :blk try file.readToEndAlloc(allocator, try file.getEndPos());
} else null;
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
const file_len = try file.readAll(buf);
if (file_len != Self.size) log.err("Expected BIOS to be {}B, was {}B", .{ Self.size, file_len });
return Self{ .buf = buf, .allocator = allocator };
return Self{
.buf = buf,
.allocator = allocator,
.addr_latch = 0,
};
}
pub fn deinit(self: *Self) void {

View File

@@ -1,6 +1,10 @@
const std = @import("std");
const config = @import("../../config.zig");
const Bit = @import("bitfield").Bit;
const Bitfield = @import("bitfield").Bitfield;
const DateTime = @import("datetime").datetime.Datetime;
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
const Backup = @import("backup.zig").Backup;
const Gpio = @import("gpio.zig").Gpio;
@@ -105,13 +109,14 @@ pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
switch (T) {
u32 => switch (address) {
// FIXME: Do I even need to implement these?
// 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),
@@ -213,7 +218,6 @@ 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
// TODO: Use new for loop syntax?
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;

View File

@@ -6,6 +6,7 @@ const Eeprom = @import("backup/eeprom.zig").Eeprom;
const Flash = @import("backup/Flash.zig");
const escape = @import("../../util.zig").escape;
const span = @import("../../util.zig").span;
const Needle = struct { str: []const u8, kind: Backup.Kind };
const backup_kinds = [6]Needle{
@@ -137,7 +138,6 @@ pub const Backup = struct {
for (backup_kinds) |needle| {
const needle_len = needle.str.len;
// TODO: Use new for loop syntax?
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;
@@ -151,8 +151,8 @@ pub const Backup = struct {
const file_path = try self.savePath(allocator, path);
defer allocator.free(file_path);
const expected = "untitled.sav";
if (std.mem.eql(u8, file_path[file_path.len - expected.len .. file_path.len], expected)) {
// 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", .{});
}
@@ -195,7 +195,7 @@ pub const Backup = struct {
}
fn saveName(self: *const Self, allocator: Allocator) ![]const u8 {
const title_str = std.mem.sliceTo(&escape(self.title), 0);
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" });

View File

@@ -63,7 +63,7 @@ pub const Eeprom = struct {
}
if (self.state == .RequestEnd) {
// if (bit != 0) log.debug("EEPROM Request did not end in 0u1. TODO: is this ok?", .{});
if (bit != 0) log.debug("EEPROM Request did not end in 0u1. TODO: is this ok?", .{});
self.state = .Ready;
return;
}

View File

@@ -5,12 +5,11 @@ const DmaControl = @import("io.zig").DmaControl;
const Bus = @import("../Bus.zig");
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
pub const DmaTuple = struct { 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 getHalf = util.getHalf;
const setHalf = util.setHalf;
const setQuart = util.setQuart;
const setHi = util.setHi;
const setLo = util.setLo;
const rotr = @import("../../util.zig").rotr;
@@ -19,126 +18,78 @@ pub fn create() DmaTuple {
}
pub fn read(comptime T: type, dma: *const DmaTuple, addr: u32) ?T {
const byte_addr = @truncate(u8, addr);
const byte = @truncate(u8, addr);
return switch (T) {
u32 => switch (byte_addr) {
0xB0, 0xB4 => null, // DMA0SAD, DMA0DAD,
0xB8 => @as(T, dma.*[0].dmacntH()) << 16, // DMA0CNT_L is write-only
0xBC, 0xC0 => null, // DMA1SAD, DMA1DAD
0xC4 => @as(T, dma.*[1].dmacntH()) << 16, // DMA1CNT_L is write-only
0xC8, 0xCC => null, // DMA2SAD, DMA2DAD
0xD0 => @as(T, dma.*[2].dmacntH()) << 16, // DMA2CNT_L is write-only
0xD4, 0xD8 => null, // DMA3SAD, DMA3DAD
0xDC => @as(T, dma.*[3].dmacntH()) << 16, // DMA3CNT_L is write-only
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
u32 => switch (byte) {
0xB8 => @as(T, dma.*[0].cnt.raw) << 16,
0xC4 => @as(T, dma.*[1].cnt.raw) << 16,
0xD0 => @as(T, dma.*[2].cnt.raw) << 16,
0xDC => @as(T, dma.*[3].cnt.raw) << 16,
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
},
u16 => switch (byte_addr) {
0xB0, 0xB2, 0xB4, 0xB6 => null, // DMA0SAD, DMA0DAD
0xB8 => 0x0000, // DMA0CNT_L, suite.gba expects 0x0000 instead of 0xDEAD
0xBA => dma.*[0].dmacntH(),
0xBC, 0xBE, 0xC0, 0xC2 => null, // DMA1SAD, DMA1DAD
0xC4 => 0x0000, // DMA1CNT_L
0xC6 => dma.*[1].dmacntH(),
0xC8, 0xCA, 0xCC, 0xCE => null, // DMA2SAD, DMA2DAD
0xD0 => 0x0000, // DMA2CNT_L
0xD2 => dma.*[2].dmacntH(),
0xD4, 0xD6, 0xD8, 0xDA => null, // DMA3SAD, DMA3DAD
0xDC => 0x0000, // DMA3CNT_L
0xDE => dma.*[3].dmacntH(),
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
},
u8 => switch (byte_addr) {
0xB0...0xB7 => null, // DMA0SAD, DMA0DAD
0xB8, 0xB9 => 0x00, // DMA0CNT_L
0xBA, 0xBB => @truncate(T, dma.*[0].dmacntH() >> getHalf(byte_addr)),
0xBC...0xC3 => null, // DMA1SAD, DMA1DAD
0xC4, 0xC5 => 0x00, // DMA1CNT_L
0xC6, 0xC7 => @truncate(T, dma.*[1].dmacntH() >> getHalf(byte_addr)),
0xC8...0xCF => null, // DMA2SAD, DMA2DAD
0xD0, 0xD1 => 0x00, // DMA2CNT_L
0xD2, 0xD3 => @truncate(T, dma.*[2].dmacntH() >> getHalf(byte_addr)),
0xD4...0xDB => null, // DMA3SAD, DMA3DAD
0xDC, 0xDD => 0x00, // DMA3CNT_L
0xDE, 0xDF => @truncate(T, dma.*[3].dmacntH() >> getHalf(byte_addr)),
else => util.io.read.err(T, log, "unexpected {} read from 0x{X:0>8}", .{ T, addr }),
u16 => switch (byte) {
0xBA => dma.*[0].cnt.raw,
0xC6 => dma.*[1].cnt.raw,
0xD2 => dma.*[2].cnt.raw,
0xDE => dma.*[3].cnt.raw,
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
},
u8 => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
else => @compileError("DMA: Unsupported read width"),
};
}
pub fn write(comptime T: type, dma: *DmaTuple, addr: u32, value: T) void {
const byte_addr = @truncate(u8, addr);
const byte = @truncate(u8, addr);
switch (T) {
u32 => switch (byte_addr) {
u32 => switch (byte) {
0xB0 => dma.*[0].setDmasad(value),
0xB4 => dma.*[0].setDmadad(value),
0xB8 => dma.*[0].setDmacnt(value),
0xBC => dma.*[1].setDmasad(value),
0xC0 => dma.*[1].setDmadad(value),
0xC4 => dma.*[1].setDmacnt(value),
0xC8 => dma.*[2].setDmasad(value),
0xCC => dma.*[2].setDmadad(value),
0xD0 => dma.*[2].setDmacnt(value),
0xD4 => dma.*[3].setDmasad(value),
0xD8 => dma.*[3].setDmadad(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 }),
},
u16 => switch (byte_addr) {
0xB0, 0xB2 => dma.*[0].setDmasad(setHalf(u32, dma.*[0].sad, byte_addr, value)),
0xB4, 0xB6 => dma.*[0].setDmadad(setHalf(u32, dma.*[0].dad, byte_addr, value)),
u16 => switch (byte) {
0xB0 => dma.*[0].setDmasad(setLo(u32, dma.*[0].sad, value)),
0xB2 => dma.*[0].setDmasad(setHi(u32, dma.*[0].sad, value)),
0xB4 => dma.*[0].setDmadad(setLo(u32, dma.*[0].dad, value)),
0xB6 => dma.*[0].setDmadad(setHi(u32, dma.*[0].dad, value)),
0xB8 => dma.*[0].setDmacntL(value),
0xBA => dma.*[0].setDmacntH(value),
0xBC, 0xBE => dma.*[1].setDmasad(setHalf(u32, dma.*[1].sad, byte_addr, value)),
0xC0, 0xC2 => dma.*[1].setDmadad(setHalf(u32, dma.*[1].dad, byte_addr, value)),
0xBC => dma.*[1].setDmasad(setLo(u32, dma.*[1].sad, value)),
0xBE => dma.*[1].setDmasad(setHi(u32, dma.*[1].sad, value)),
0xC0 => dma.*[1].setDmadad(setLo(u32, dma.*[1].dad, value)),
0xC2 => dma.*[1].setDmadad(setHi(u32, dma.*[1].dad, value)),
0xC4 => dma.*[1].setDmacntL(value),
0xC6 => dma.*[1].setDmacntH(value),
0xC8, 0xCA => dma.*[2].setDmasad(setHalf(u32, dma.*[2].sad, byte_addr, value)),
0xCC, 0xCE => dma.*[2].setDmadad(setHalf(u32, dma.*[2].dad, byte_addr, value)),
0xC8 => dma.*[2].setDmasad(setLo(u32, dma.*[2].sad, value)),
0xCA => dma.*[2].setDmasad(setHi(u32, dma.*[2].sad, value)),
0xCC => dma.*[2].setDmadad(setLo(u32, dma.*[2].dad, value)),
0xCE => dma.*[2].setDmadad(setHi(u32, dma.*[2].dad, value)),
0xD0 => dma.*[2].setDmacntL(value),
0xD2 => dma.*[2].setDmacntH(value),
0xD4, 0xD6 => dma.*[3].setDmasad(setHalf(u32, dma.*[3].sad, byte_addr, value)),
0xD8, 0xDA => dma.*[3].setDmadad(setHalf(u32, dma.*[3].dad, byte_addr, value)),
0xD4 => dma.*[3].setDmasad(setLo(u32, dma.*[3].sad, value)),
0xD6 => dma.*[3].setDmasad(setHi(u32, dma.*[3].sad, value)),
0xD8 => dma.*[3].setDmadad(setLo(u32, dma.*[3].dad, value)),
0xDA => dma.*[3].setDmadad(setHi(u32, dma.*[3].dad, value)),
0xDC => dma.*[3].setDmacntL(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 }),
},
u8 => switch (byte_addr) {
0xB0, 0xB1, 0xB2, 0xB3 => dma.*[0].setDmasad(setQuart(dma.*[0].sad, byte_addr, value)),
0xB4, 0xB5, 0xB6, 0xB7 => dma.*[0].setDmadad(setQuart(dma.*[0].dad, byte_addr, value)),
0xB8, 0xB9 => dma.*[0].setDmacntL(setHalf(u16, dma.*[0].word_count, byte_addr, value)),
0xBA, 0xBB => dma.*[0].setDmacntH(setHalf(u16, dma.*[0].cnt.raw, byte_addr, value)),
0xBC, 0xBD, 0xBE, 0xBF => dma.*[1].setDmasad(setQuart(dma.*[1].sad, byte_addr, value)),
0xC0, 0xC1, 0xC2, 0xC3 => dma.*[1].setDmadad(setQuart(dma.*[1].dad, byte_addr, value)),
0xC4, 0xC5 => dma.*[1].setDmacntL(setHalf(u16, dma.*[1].word_count, byte_addr, value)),
0xC6, 0xC7 => dma.*[1].setDmacntH(setHalf(u16, dma.*[1].cnt.raw, byte_addr, value)),
0xC8, 0xC9, 0xCA, 0xCB => dma.*[2].setDmasad(setQuart(dma.*[2].sad, byte_addr, value)),
0xCC, 0xCD, 0xCE, 0xCF => dma.*[2].setDmadad(setQuart(dma.*[2].dad, byte_addr, value)),
0xD0, 0xD1 => dma.*[2].setDmacntL(setHalf(u16, dma.*[2].word_count, byte_addr, value)),
0xD2, 0xD3 => dma.*[2].setDmacntH(setHalf(u16, dma.*[2].cnt.raw, byte_addr, value)),
0xD4, 0xD5, 0xD6, 0xD7 => dma.*[3].setDmasad(setQuart(dma.*[3].sad, byte_addr, value)),
0xD8, 0xD9, 0xDA, 0xDB => dma.*[3].setDmadad(setQuart(dma.*[3].dad, byte_addr, value)),
0xDC, 0xDD => dma.*[3].setDmacntL(setHalf(u16, dma.*[3].word_count, byte_addr, value)),
0xDE, 0xDF => dma.*[3].setDmacntH(setHalf(u16, dma.*[3].cnt.raw, byte_addr, value)),
else => 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 }),
else => @compileError("DMA: Unsupported write width"),
}
}
@@ -207,10 +158,6 @@ fn DmaController(comptime id: u2) type {
self.word_count = @truncate(@TypeOf(self.word_count), halfword);
}
pub fn dmacntH(self: *const Self) u16 {
return self.cnt.raw & if (id == 3) 0xFFE0 else 0xF7E0;
}
pub fn setDmacntH(self: *Self, halfword: u16) void {
const new = DmaControl{ .raw = halfword };
@@ -256,16 +203,12 @@ fn DmaController(comptime id: u2) type {
cpu.bus.write(u16, dad_addr, @truncate(u16, rotr(u32, self.data_latch, 8 * (dad_addr & 3))));
}
switch (@truncate(u8, sad_addr >> 24)) {
// according to fleroviux, DMAs with a source address in ROM misbehave
// the resultant behaviour is that the source address will increment despite what DMAXCNT says
0x08...0x0D => self.sad_latch +%= offset, // obscure behaviour
else => switch (sad_adj) {
.Increment => self.sad_latch +%= offset,
.Decrement => self.sad_latch -%= offset,
.IncrementReload => log.err("{} is a prohibited adjustment on SAD", .{sad_adj}),
.Fixed => {},
},
switch (sad_adj) {
.Increment => self.sad_latch +%= offset,
.Decrement => self.sad_latch -%= offset,
// FIXME: Is just ignoring this ok?
.IncrementReload => log.err("{} is a prohibited adjustment on SAD", .{sad_adj}),
.Fixed => {},
}
switch (dad_adj) {
@@ -339,7 +282,10 @@ fn DmaController(comptime id: u2) type {
}
pub fn onBlanking(bus: *Bus, comptime kind: DmaKind) void {
inline for (0..4) |i| bus.dma[i].poll(kind);
bus.dma[0].poll(kind);
bus.dma[1].poll(kind);
bus.dma[2].poll(kind);
bus.dma[3].poll(kind);
}
const Adjustment = enum(u2) {

View File

@@ -1,5 +1,6 @@
const std = @import("std");
const Bit = @import("bitfield").Bit;
const Bitfield = @import("bitfield").Bitfield;
const DateTime = @import("datetime").datetime.Datetime;
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
@@ -71,7 +72,7 @@ pub const Gpio = struct {
self.* = .{
.data = 0b0000,
.direction = 0b1111, // TODO: What is GPIO Direction set to by default?
.direction = 0b1111, // TODO: What is GPIO DIrection set to by default?
.cnt = 0b0,
.device = switch (kind) {
@@ -293,13 +294,13 @@ pub const Clock = struct {
self.cpu.sched.push(.RealTimeClock, (1 << 24) -| late); // Reschedule
const now = DateTime.now();
self.year = bcd(@intCast(u8, now.date.year - 2000));
self.month = @truncate(u5, bcd(now.date.month));
self.day = @truncate(u6, bcd(now.date.day));
self.weekday = @truncate(u3, bcd((now.date.weekday() + 1) % 7)); // API is Monday = 0, Sunday = 6. We want Sunday = 0, Saturday = 6
self.hour = @truncate(u6, bcd(now.time.hour));
self.minute = @truncate(u7, bcd(now.time.minute));
self.second = @truncate(u7, bcd(now.time.second));
self.year = bcd(u8, @intCast(u8, now.date.year - 2000));
self.month = bcd(u5, now.date.month);
self.day = bcd(u6, now.date.day);
self.weekday = bcd(u3, (now.date.weekday() + 1) % 7); // API is Monday = 0, Sunday = 6. We want Sunday = 0, Saturday = 6
self.hour = bcd(u6, now.time.hour);
self.minute = bcd(u7, now.time.minute);
self.second = bcd(u7, now.time.second);
}
fn step(self: *Self, value: Data) u4 {
@@ -449,8 +450,16 @@ pub const Clock = struct {
}
};
/// Converts an 8-bit unsigned integer to its BCD representation.
/// Note: Algorithm only works for values between 0 and 99 inclusive.
fn bcd(value: u8) u8 {
return ((value / 10) << 4) + (value % 10);
fn bcd(comptime T: type, value: u8) T {
var input = value;
var ret: u8 = 0;
var shift: u3 = 0;
while (input > 0) {
ret |= (input % 10) << (shift << 2);
shift += 1;
input /= 10;
}
return @truncate(T, ret);
}

View File

@@ -1,16 +1,18 @@
const std = @import("std");
const builtin = @import("builtin");
const timer = @import("timer.zig");
const dma = @import("dma.zig");
const apu = @import("../apu.zig");
const ppu = @import("../ppu.zig");
const util = @import("../../util.zig");
const Bit = @import("bitfield").Bit;
const Bitfield = @import("bitfield").Bitfield;
const Bus = @import("../Bus.zig");
const DmaController = @import("dma.zig").DmaController;
const Scheduler = @import("../scheduler.zig").Scheduler;
const getHalf = util.getHalf;
const setHalf = util.setHalf;
const setHi = util.setHi;
const setLo = util.setLo;
const log = std.log.scoped(.@"I/O");
@@ -22,17 +24,15 @@ pub const Io = struct {
ie: InterruptEnable,
irq: InterruptRequest,
postflg: PostFlag,
waitcnt: WaitControl,
haltcnt: HaltControl,
keyinput: AtomicKeyInput,
keyinput: KeyInput,
pub fn init() Self {
return .{
.ime = false,
.ie = .{ .raw = 0x0000 },
.irq = .{ .raw = 0x0000 },
.keyinput = AtomicKeyInput.init(.{ .raw = 0x03FF }),
.waitcnt = .{ .raw = 0x0000_0000 }, // Bit 15 == 0 for GBA
.keyinput = .{ .raw = 0x03FF },
.postflg = .FirstBoot,
.haltcnt = .Execute,
};
@@ -48,10 +48,9 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) ?T {
return switch (T) {
u32 => switch (address) {
// Display
0x0400_0000...0x0400_0054 => ppu.read(T, &bus.ppu, address),
// Sound
0x0400_0060...0x0400_00A4 => apu.read(T, &bus.apu, address),
0x0400_0000 => bus.ppu.dispcnt.raw,
0x0400_0004 => @as(T, bus.ppu.vcount.raw) << 16 | bus.ppu.dispstat.raw,
0x0400_0006 => @as(T, bus.ppu.bg[0].cnt.raw) << 16 | bus.ppu.vcount.raw,
// DMA Transfers
0x0400_00B0...0x0400_00DC => dma.read(T, &bus.dma, address),
@@ -69,18 +68,26 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) ?T {
0x0400_0150 => util.io.read.todo(log, "Read {} from JOY_RECV", .{T}),
// Interrupts
0x0400_0200 => @as(u32, bus.io.irq.raw) << 16 | bus.io.ie.raw,
0x0400_0204 => bus.io.waitcnt.raw,
0x0400_0200 => @as(T, bus.io.irq.raw) << 16 | bus.io.ie.raw,
0x0400_0208 => @boolToInt(bus.io.ime),
0x0400_0300 => @enumToInt(bus.io.postflg),
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
},
u16 => switch (address) {
// Display
0x0400_0000...0x0400_0054 => ppu.read(T, &bus.ppu, address),
0x0400_0000 => bus.ppu.dispcnt.raw,
0x0400_0004 => bus.ppu.dispstat.raw,
0x0400_0006 => bus.ppu.vcount.raw,
0x0400_0008 => bus.ppu.bg[0].cnt.raw,
0x0400_000A => bus.ppu.bg[1].cnt.raw,
0x0400_000C => bus.ppu.bg[2].cnt.raw,
0x0400_000E => bus.ppu.bg[3].cnt.raw,
0x0400_004C => util.io.read.todo(log, "Read {} from MOSAIC", .{T}),
0x0400_0050 => bus.ppu.bldcnt.raw,
0x0400_0052 => bus.ppu.bldalpha.raw,
0x0400_0054 => bus.ppu.bldy.raw,
// Sound
0x0400_0060...0x0400_00A6 => apu.read(T, &bus.apu, address),
0x0400_0060...0x0400_009E => apu.read(T, &bus.apu, address),
// DMA Transfers
0x0400_00B0...0x0400_00DE => dma.read(T, &bus.dma, address),
@@ -92,38 +99,32 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) ?T {
0x0400_0128 => util.io.read.todo(log, "Read {} from SIOCNT", .{T}),
// Keypad Input
0x0400_0130 => bus.io.keyinput.load(.Monotonic).raw,
0x0400_0130 => bus.io.keyinput.raw,
// Serial Communication 2
0x0400_0134 => util.io.read.todo(log, "Read {} from RCNT", .{T}),
0x0400_0136 => 0x0000,
0x0400_0142 => 0x0000,
0x0400_015A => 0x0000,
// Interrupts
0x0400_0200 => bus.io.ie.raw,
0x0400_0202 => bus.io.irq.raw,
0x0400_0204 => bus.io.waitcnt.raw,
0x0400_0206 => 0x0000,
0x0400_0204 => util.io.read.todo(log, "Read {} from WAITCNT", .{T}),
0x0400_0208 => @boolToInt(bus.io.ime),
0x0400_020A => 0x0000,
0x0400_0300 => @enumToInt(bus.io.postflg),
0x0400_0302 => 0x0000,
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
},
u8 => return switch (address) {
// Display
0x0400_0000...0x0400_0055 => ppu.read(T, &bus.ppu, address),
0x0400_0000 => @truncate(T, bus.ppu.dispcnt.raw),
0x0400_0004 => @truncate(T, bus.ppu.dispstat.raw),
0x0400_0005 => @truncate(T, bus.ppu.dispcnt.raw >> 8),
0x0400_0006 => @truncate(T, bus.ppu.vcount.raw),
0x0400_0008 => @truncate(T, bus.ppu.bg[0].cnt.raw),
0x0400_0009 => @truncate(T, bus.ppu.bg[0].cnt.raw >> 8),
0x0400_000A => @truncate(T, bus.ppu.bg[1].cnt.raw),
0x0400_000B => @truncate(T, bus.ppu.bg[1].cnt.raw >> 8),
// Sound
0x0400_0060...0x0400_00A7 => apu.read(T, &bus.apu, address),
// DMA Transfers
0x0400_00B0...0x0400_00DF => dma.read(T, &bus.dma, address),
// Timers
0x0400_0100...0x0400_010F => timer.read(T, &bus.tim, address),
// Serial Communication 1
0x0400_0128 => util.io.read.todo(log, "Read {} from SIOCNT_L", .{T}),
@@ -132,20 +133,10 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) ?T {
// Serial Communication 2
0x0400_0135 => util.io.read.todo(log, "Read {} from RCNT_H", .{T}),
0x0400_0136, 0x0400_0137 => 0x00,
0x0400_0142, 0x0400_0143 => 0x00,
0x0400_015A, 0x0400_015B => 0x00,
// Interrupts
0x0400_0200, 0x0400_0201 => @truncate(T, bus.io.ie.raw >> getHalf(@truncate(u8, address))),
0x0400_0202, 0x0400_0203 => @truncate(T, bus.io.irq.raw >> getHalf(@truncate(u8, address))),
0x0400_0204, 0x0400_0205 => @truncate(T, bus.io.waitcnt.raw >> getHalf(@truncate(u8, address))),
0x0400_0206, 0x0400_0207 => 0x00,
0x0400_0208, 0x0400_0209 => @truncate(T, @as(u16, @boolToInt(bus.io.ime)) >> getHalf(@truncate(u8, address))),
0x0400_020A, 0x0400_020B => 0x00,
0x0400_0200 => @truncate(T, bus.io.ie.raw),
0x0400_0300 => @enumToInt(bus.io.postflg),
0x0400_0301 => null,
0x0400_0302, 0x0400_0303 => 0x00,
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
},
else => @compileError("I/O: Unsupported read width"),
@@ -156,7 +147,34 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
return switch (T) {
u32 => switch (address) {
// Display
0x0400_0000...0x0400_0054 => ppu.write(T, &bus.ppu, address, value),
0x0400_0000 => bus.ppu.dispcnt.raw = @truncate(u16, value),
0x0400_0004 => {
bus.ppu.dispstat.raw = @truncate(u16, value);
bus.ppu.vcount.raw = @truncate(u16, value >> 16);
},
0x0400_0008 => bus.ppu.setAdjCnts(0, value),
0x0400_000C => bus.ppu.setAdjCnts(2, value),
0x0400_0010 => bus.ppu.setBgOffsets(0, value),
0x0400_0014 => bus.ppu.setBgOffsets(1, value),
0x0400_0018 => bus.ppu.setBgOffsets(2, value),
0x0400_001C => bus.ppu.setBgOffsets(3, value),
0x0400_0020 => bus.ppu.aff_bg[0].writePaPb(value),
0x0400_0024 => bus.ppu.aff_bg[0].writePcPd(value),
0x0400_0028 => bus.ppu.aff_bg[0].setX(bus.ppu.dispstat.vblank.read(), value),
0x0400_002C => bus.ppu.aff_bg[0].setY(bus.ppu.dispstat.vblank.read(), value),
0x0400_0030 => bus.ppu.aff_bg[1].writePaPb(value),
0x0400_0034 => bus.ppu.aff_bg[1].writePcPd(value),
0x0400_0038 => bus.ppu.aff_bg[1].setX(bus.ppu.dispstat.vblank.read(), value),
0x0400_003C => bus.ppu.aff_bg[1].setY(bus.ppu.dispstat.vblank.read(), value),
0x0400_0040 => bus.ppu.win.setH(value),
0x0400_0044 => bus.ppu.win.setV(value),
0x0400_0048 => bus.ppu.win.setIo(value),
0x0400_004C => log.debug("Wrote 0x{X:0>8} to MOSAIC", .{value}),
0x0400_0050 => {
bus.ppu.bldcnt.raw = @truncate(u16, value);
bus.ppu.bldalpha.raw = @truncate(u16, value >> 16);
},
0x0400_0054 => bus.ppu.bldy.raw = @truncate(u16, value),
0x0400_0058...0x0400_005C => {}, // Unused
// Sound
@@ -192,28 +210,65 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
// Interrupts
0x0400_0200 => bus.io.setIrqs(value),
0x0400_0204 => bus.io.waitcnt.set(@truncate(u16, value)),
0x0400_0204 => log.debug("Wrote 0x{X:0>8} to WAITCNT", .{value}),
0x0400_0208 => bus.io.ime = value & 1 == 1,
0x0400_0300 => {
bus.io.postflg = @intToEnum(PostFlag, value & 1);
bus.io.haltcnt = if (value >> 15 & 1 == 0) .Halt else @panic("TODO: Implement STOP");
},
0x0400_020C...0x0400_021C => {}, // Unused
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, address }),
},
u16 => switch (address) {
// Display
0x0400_0000...0x0400_0054 => ppu.write(T, &bus.ppu, address, value),
0x0400_0056 => {}, // Not used
0x0400_0000 => bus.ppu.dispcnt.raw = value,
0x0400_0004 => bus.ppu.dispstat.raw = value,
0x0400_0006 => {}, // vcount is read-only
0x0400_0008 => bus.ppu.bg[0].cnt.raw = value,
0x0400_000A => bus.ppu.bg[1].cnt.raw = value,
0x0400_000C => bus.ppu.bg[2].cnt.raw = value,
0x0400_000E => bus.ppu.bg[3].cnt.raw = value,
0x0400_0010 => bus.ppu.bg[0].hofs.raw = value, // TODO: Don't write out every HOFS / VOFS?
0x0400_0012 => bus.ppu.bg[0].vofs.raw = value,
0x0400_0014 => bus.ppu.bg[1].hofs.raw = value,
0x0400_0016 => bus.ppu.bg[1].vofs.raw = value,
0x0400_0018 => bus.ppu.bg[2].hofs.raw = value,
0x0400_001A => bus.ppu.bg[2].vofs.raw = value,
0x0400_001C => bus.ppu.bg[3].hofs.raw = value,
0x0400_001E => bus.ppu.bg[3].vofs.raw = value,
0x0400_0020 => bus.ppu.aff_bg[0].pa = @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_0026 => bus.ppu.aff_bg[0].pd = @bitCast(i16, value),
0x0400_0028 => bus.ppu.aff_bg[0].x = @bitCast(i32, setLo(u32, @bitCast(u32, bus.ppu.aff_bg[0].x), value)),
0x0400_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, setLo(u32, @bitCast(u32, bus.ppu.aff_bg[0].y), value)),
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_0032 => bus.ppu.aff_bg[1].pb = @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_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, setHi(u32, @bitCast(u32, bus.ppu.aff_bg[1].x), 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, setHi(u32, @bitCast(u32, bus.ppu.aff_bg[1].y), value)),
0x0400_0040 => bus.ppu.win.h[0].raw = value,
0x0400_0042 => bus.ppu.win.h[1].raw = value,
0x0400_0044 => bus.ppu.win.v[0].raw = value,
0x0400_0046 => bus.ppu.win.v[1].raw = value,
0x0400_0048 => bus.ppu.win.in.raw = value,
0x0400_004A => bus.ppu.win.out.raw = value,
0x0400_004C => log.debug("Wrote 0x{X:0>4} to MOSAIC", .{value}),
0x0400_0050 => bus.ppu.bldcnt.raw = value,
0x0400_0052 => bus.ppu.bldalpha.raw = value,
0x0400_0054 => bus.ppu.bldy.raw = value,
0x0400_004E, 0x0400_0056 => {}, // Not used
// Sound
0x0400_0060...0x0400_00A6 => apu.write(T, &bus.apu, address, value),
0x0400_0060...0x0400_009E => apu.write(T, &bus.apu, address, value),
// Dma Transfers
0x0400_00B0...0x0400_00DE => dma.write(T, &bus.dma, address, value),
// Timers
0x0400_0100...0x0400_010E => timer.write(T, &bus.tim, address, value),
0x0400_0114 => {},
0x0400_0114 => {}, // TODO: Gyakuten Saiban writes 0x8000 to 0x0400_0114
0x0400_0110 => {}, // Not Used,
// Serial Communication 1
@@ -237,29 +292,35 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
// Interrupts
0x0400_0200 => bus.io.ie.raw = value,
0x0400_0202 => bus.io.irq.raw &= ~value,
0x0400_0204 => bus.io.waitcnt.set(value),
0x0400_0206 => {},
0x0400_0204 => log.debug("Wrote 0x{X:0>4} to WAITCNT", .{value}),
0x0400_0208 => bus.io.ime = value & 1 == 1,
0x0400_020A => {},
0x0400_0300 => {
bus.io.postflg = @intToEnum(PostFlag, value & 1);
bus.io.haltcnt = if (value >> 15 & 1 == 0) .Halt else @panic("TODO: Implement STOP");
},
0x0400_0206, 0x0400_020A => {}, // Not Used
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, address }),
},
u8 => switch (address) {
// Display
0x0400_0000...0x0400_0055 => ppu.write(T, &bus.ppu, address, value),
0x0400_0004 => bus.ppu.dispstat.raw = setLo(u16, bus.ppu.dispstat.raw, value),
0x0400_0005 => bus.ppu.dispstat.raw = setHi(u16, bus.ppu.dispstat.raw, 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 = setHi(u16, bus.ppu.bg[0].cnt.raw, 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 = setHi(u16, bus.ppu.bg[1].cnt.raw, 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].raw = setHi(u16, bus.ppu.win.h[0].raw, 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].raw = setHi(u16, bus.ppu.win.h[1].raw, 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].raw = setHi(u16, bus.ppu.win.v[0].raw, 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].raw = setHi(u16, bus.ppu.win.v[1].raw, value),
0x0400_0048 => bus.ppu.win.in.raw = setLo(u16, bus.ppu.win.in.raw, value),
0x0400_0049 => bus.ppu.win.in.raw = setHi(u16, bus.ppu.win.in.raw, value),
0x0400_004A => bus.ppu.win.out.raw = setLo(u16, bus.ppu.win.out.raw, value),
0x0400_0054 => bus.ppu.bldy.raw = setLo(u16, bus.ppu.bldy.raw, value),
// Sound
0x0400_0060...0x0400_00A7 => apu.write(T, &bus.apu, address, value),
// Dma Transfers
0x0400_00B0...0x0400_00DF => dma.write(T, &bus.dma, address, value),
// Timers
0x0400_0100...0x0400_010F => timer.write(T, &bus.tim, address, value),
// Serial Communication 1
0x0400_0120 => log.debug("Wrote 0x{X:0>2} to SIODATA32_L_L", .{value}),
0x0400_0128 => log.debug("Wrote 0x{X:0>2} to SIOCNT_L", .{value}),
@@ -269,16 +330,9 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
0x0400_0140 => log.debug("Wrote 0x{X:0>2} to JOYCNT_L", .{value}),
// Interrupts
0x0400_0200, 0x0400_0201 => bus.io.ie.raw = setHalf(u16, bus.io.ie.raw, @truncate(u8, address), value),
0x0400_0202 => bus.io.irq.raw &= ~@as(u16, value),
0x0400_0203 => bus.io.irq.raw &= ~@as(u16, value) << 8, // TODO: Is this good?
0x0400_0204, 0x0400_0205 => bus.io.waitcnt.set(setHalf(u16, @truncate(u16, bus.io.waitcnt.raw), @truncate(u8, address), value)),
0x0400_0206, 0x0400_0207 => {},
0x0400_0208 => bus.io.ime = value & 1 == 1,
0x0400_0209 => {},
0x0400_020A, 0x0400_020B => {},
0x0400_0300 => bus.io.postflg = @intToEnum(PostFlag, value & 1),
0x0400_0300 => bus.io.postflg = std.meta.intToEnum(PostFlag, value & 1) catch unreachable,
0x0400_0301 => bus.io.haltcnt = if (value >> 7 & 1 == 0) .Halt else std.debug.panic("TODO: Implement STOP", .{}),
0x0400_0410 => log.debug("Wrote 0x{X:0>2} to the common yet undocumented 0x{X:0>8}", .{ value, address }),
@@ -317,22 +371,14 @@ pub const DisplayControl = extern union {
/// Read / Write
pub const DisplayStatus = extern union {
/// read-only
vblank: Bit(u16, 0),
/// read-only
hblank: Bit(u16, 1),
// read-only
coincidence: Bit(u16, 2),
vblank_irq: Bit(u16, 3),
hblank_irq: Bit(u16, 4),
vcount_irq: Bit(u16, 5),
vcount_trigger: Bitfield(u16, 8, 8),
raw: u16,
pub fn set(self: *DisplayStatus, value: u16) void {
const mask: u16 = 0x00C7; // set bits are read-only
self.raw = (self.raw & mask) | (value & ~mask);
}
};
/// Read Only
@@ -376,31 +422,6 @@ const KeyInput = extern union {
raw: u16,
};
const AtomicKeyInput = struct {
const Self = @This();
const Ordering = std.atomic.Ordering;
inner: KeyInput,
pub fn init(value: KeyInput) Self {
return .{ .inner = value };
}
pub inline fn load(self: *const Self, comptime ordering: Ordering) KeyInput {
return .{ .raw = switch (ordering) {
.AcqRel, .Release => @compileError("not supported for atomic loads"),
else => @atomicLoad(u16, &self.inner.raw, ordering),
} };
}
pub inline fn store(self: *Self, value: u16, comptime ordering: Ordering) void {
switch (ordering) {
.AcqRel, .Acquire => @compileError("not supported for atomic stores"),
else => @atomicStore(u16, &self.inner.raw, value, ordering),
}
}
};
// Read / Write
pub const BackgroundControl = extern union {
priority: Bitfield(u16, 0, 2),
@@ -453,6 +474,8 @@ const u8WriteKind = enum { Hi, Lo };
/// Write-only
pub const WinH = extern union {
const Self = @This();
x2: Bitfield(u16, 0, 8),
x1: Bitfield(u16, 8, 8),
raw: u16,
@@ -465,6 +488,13 @@ pub const WinV = extern union {
y2: Bitfield(u16, 0, 8),
y1: Bitfield(u16, 8, 8),
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,
};
}
};
pub const WinIn = extern union {
@@ -652,24 +682,3 @@ pub const SoundBias = extern union {
sampling_cycle: Bitfield(u16, 14, 2),
raw: u16,
};
/// Read / Write
pub const WaitControl = extern union {
sram_cnt: Bitfield(u16, 0, 2),
s0_first: Bitfield(u16, 2, 2),
s0_second: Bit(u16, 4),
s1_first: Bitfield(u16, 5, 2),
s1_second: Bit(u16, 7),
s2_first: Bitfield(u16, 8, 2),
s2_second: Bit(u16, 10),
phi_out: Bitfield(u16, 11, 2),
prefetch_enable: Bit(u16, 14),
pak_kind: Bit(u16, 15),
raw: u16,
pub fn set(self: *WaitControl, value: u16) void {
const mask: u16 = 0x8000; // set bits are read-only
self.raw = (self.raw & mask) | (value & ~mask);
}
};

View File

@@ -2,99 +2,68 @@ const std = @import("std");
const util = @import("../../util.zig");
const TimerControl = @import("io.zig").TimerControl;
const Io = @import("io.zig").Io;
const Scheduler = @import("../scheduler.zig").Scheduler;
const Event = @import("../scheduler.zig").Event;
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
pub const TimerTuple = struct { Timer(0), Timer(1), Timer(2), Timer(3) };
pub const TimerTuple = std.meta.Tuple(&[_]type{ Timer(0), Timer(1), Timer(2), Timer(3) });
const log = std.log.scoped(.Timer);
const getHalf = util.getHalf;
const setHalf = util.setHalf;
pub fn create(sched: *Scheduler) TimerTuple {
return .{ Timer(0).init(sched), Timer(1).init(sched), Timer(2).init(sched), Timer(3).init(sched) };
}
pub fn read(comptime T: type, tim: *const TimerTuple, addr: u32) ?T {
const nybble_addr = @truncate(u4, addr);
const nybble = @truncate(u4, addr);
return switch (T) {
u32 => switch (nybble_addr) {
u32 => switch (nybble) {
0x0 => @as(T, tim.*[0].cnt.raw) << 16 | tim.*[0].timcntL(),
0x4 => @as(T, tim.*[1].cnt.raw) << 16 | tim.*[1].timcntL(),
0x8 => @as(T, tim.*[2].cnt.raw) << 16 | tim.*[2].timcntL(),
0xC => @as(T, tim.*[3].cnt.raw) << 16 | tim.*[3].timcntL(),
else => util.io.read.err(T, log, "unaligned {} read from 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_addr) {
u16 => switch (nybble) {
0x0 => tim.*[0].timcntL(),
0x2 => tim.*[0].cnt.raw,
0x4 => tim.*[1].timcntL(),
0x6 => tim.*[1].cnt.raw,
0x8 => tim.*[2].timcntL(),
0xA => tim.*[2].cnt.raw,
0xC => tim.*[3].timcntL(),
0xE => tim.*[3].cnt.raw,
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
},
u8 => switch (nybble_addr) {
0x0, 0x1 => @truncate(T, tim.*[0].timcntL() >> getHalf(nybble_addr)),
0x2, 0x3 => @truncate(T, tim.*[0].cnt.raw >> getHalf(nybble_addr)),
0x4, 0x5 => @truncate(T, tim.*[1].timcntL() >> getHalf(nybble_addr)),
0x6, 0x7 => @truncate(T, tim.*[1].cnt.raw >> getHalf(nybble_addr)),
0x8, 0x9 => @truncate(T, tim.*[2].timcntL() >> getHalf(nybble_addr)),
0xA, 0xB => @truncate(T, tim.*[2].cnt.raw >> getHalf(nybble_addr)),
0xC, 0xD => @truncate(T, tim.*[3].timcntL() >> getHalf(nybble_addr)),
0xE, 0xF => @truncate(T, tim.*[3].cnt.raw >> getHalf(nybble_addr)),
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
},
u8 => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
else => @compileError("TIM: Unsupported read width"),
};
}
pub fn write(comptime T: type, tim: *TimerTuple, addr: u32, value: T) void {
const nybble_addr = @truncate(u4, addr);
const nybble = @truncate(u4, addr);
return switch (T) {
u32 => switch (nybble_addr) {
u32 => switch (nybble) {
0x0 => tim.*[0].setTimcnt(value),
0x4 => tim.*[1].setTimcnt(value),
0x8 => tim.*[2].setTimcnt(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 }),
},
u16 => switch (nybble_addr) {
u16 => switch (nybble) {
0x0 => tim.*[0].setTimcntL(value),
0x2 => tim.*[0].setTimcntH(value),
0x4 => tim.*[1].setTimcntL(value),
0x6 => tim.*[1].setTimcntH(value),
0x8 => tim.*[2].setTimcntL(value),
0xA => tim.*[2].setTimcntH(value),
0xC => tim.*[3].setTimcntL(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 }),
},
u8 => switch (nybble_addr) {
0x0, 0x1 => tim.*[0].setTimcntL(setHalf(u16, tim.*[0]._reload, nybble_addr, value)),
0x2, 0x3 => tim.*[0].setTimcntH(setHalf(u16, tim.*[0].cnt.raw, nybble_addr, value)),
0x4, 0x5 => tim.*[1].setTimcntL(setHalf(u16, tim.*[1]._reload, nybble_addr, value)),
0x6, 0x7 => tim.*[1].setTimcntH(setHalf(u16, tim.*[1].cnt.raw, nybble_addr, value)),
0x8, 0x9 => tim.*[2].setTimcntL(setHalf(u16, tim.*[2]._reload, nybble_addr, value)),
0xA, 0xB => tim.*[2].setTimcntH(setHalf(u16, tim.*[2].cnt.raw, nybble_addr, value)),
0xC, 0xD => tim.*[3].setTimcntL(setHalf(u16, tim.*[3]._reload, nybble_addr, value)),
0xE, 0xF => tim.*[3].setTimcntH(setHalf(u16, tim.*[3].cnt.raw, nybble_addr, value)),
},
u8 => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
else => @compileError("TIM: Unsupported write width"),
};
}
@@ -150,36 +119,21 @@ fn Timer(comptime id: u2) type {
pub fn setTimcntH(self: *Self, halfword: u16) void {
const new = TimerControl{ .raw = halfword };
if (self.cnt.enabled.read()) {
// timer was already enabled
// If Timer happens to be enabled, It will either be resheduled or disabled
self.sched.removeScheduledEvent(.{ .TimerOverflow = id });
// If enabled falling edge or cascade falling edge, timer is paused
if (!new.enabled.read() or (!self.cnt.cascade.read() and new.cascade.read())) {
self.sched.removeScheduledEvent(.{ .TimerOverflow = id });
// Counter should hold the value it stopped at meaning we have to calculate it now
self._counter +%= @truncate(u16, (self.sched.now() - self._start_timestamp) / self.frequency());
}
// the timer has always been enabled, but the cascade bit which was blocking the timer has been unset
if (new.enabled.read() and (self.cnt.cascade.read() and !new.cascade.read())) {
// we want to reschedule the timer event, however we won't reload the counter.
// the invariant here is that self._counter holds the already calculated paused value
self.rescheduleTimerExpire(0);
}
} else {
// the timer was previously disabeld
if (new.enabled.read()) {
// timer should start counting (with a reloaded counter value)
self._counter = self._reload;
// if cascade happens to be set, the timer doesn't actually do anything though
if (!new.cascade.read()) self.rescheduleTimerExpire(0);
}
if (self.cnt.enabled.read() and (new.cascade.read() or !new.enabled.read())) {
// Either through the cascade bit or the enable bit, the timer has effectively been disabled
// The Counter should hold whatever value it should have been at when it was disabled
self._counter +%= @truncate(u16, (self.sched.now() - self._start_timestamp) / self.frequency());
}
// The counter is only reloaded on the rising edge of the enable bit
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 (new.enabled.read() and !new.cascade.read()) self.rescheduleTimerExpire(0);
self.cnt.raw = halfword;
}
@@ -205,20 +159,23 @@ fn Timer(comptime id: u2) type {
// Perform Cascade Behaviour
switch (id) {
inline 0, 1, 2 => |idx| {
const next = idx + 1;
if (cpu.bus.tim[next].cnt.cascade.read()) {
cpu.bus.tim[next]._counter +%= 1;
if (cpu.bus.tim[next]._counter == 0) cpu.bus.tim[next].onTimerExpire(cpu, late);
}
0 => if (cpu.bus.tim[1].cnt.cascade.read()) {
cpu.bus.tim[1]._counter +%= 1;
if (cpu.bus.tim[1]._counter == 0) cpu.bus.tim[1].onTimerExpire(cpu, late);
},
3 => {}, // THere is no timer for TIM3 to cascade to
1 => if (cpu.bus.tim[2].cnt.cascade.read()) {
cpu.bus.tim[2]._counter +%= 1;
if (cpu.bus.tim[2]._counter == 0) cpu.bus.tim[2].onTimerExpire(cpu, late);
},
2 => if (cpu.bus.tim[3].cnt.cascade.read()) {
cpu.bus.tim[3]._counter +%= 1;
if (cpu.bus.tim[3]._counter == 0) cpu.bus.tim[3].onTimerExpire(cpu, late);
},
3 => {}, // There is no Timer for TIM3 to "cascade" to,
}
// Reschedule Timer if we're not cascading
// TIM0 cascade value is N/A
if (id == 0 or !self.cnt.cascade.read()) {
if (!self.cnt.cascade.read()) {
self._counter = self._reload;
self.rescheduleTimerExpire(late);
}

View File

@@ -1,13 +1,14 @@
const std = @import("std");
const util = @import("../util.zig");
const Bus = @import("Bus.zig");
const Bit = @import("bitfield").Bit;
const Bitfield = @import("bitfield").Bitfield;
const Scheduler = @import("scheduler.zig").Scheduler;
const FilePaths = @import("../util.zig").FilePaths;
const Logger = @import("../util.zig").Logger;
const File = std.fs.File;
const log = std.log.scoped(.Arm7Tdmi);
// ARM Instructions
pub const arm = struct {
@@ -39,12 +40,13 @@ pub const arm = struct {
}
fn populate() [0x1000]InstrFn {
comptime {
return comptime {
@setEvalBranchQuota(0xE000);
var table = [_]InstrFn{und} ** 0x1000;
var ret = [_]InstrFn{und} ** 0x1000;
for (&table, 0..) |*handler, i| {
handler.* = switch (@as(u2, i >> 10)) {
var i: usize = 0;
while (i < ret.len) : (i += 1) {
ret[i] = switch (@as(u2, i >> 10)) {
0b00 => if (i == 0x121) blk: {
break :blk branchExchange;
} else if (i & 0xFCF == 0x009) blk: {
@@ -106,8 +108,8 @@ pub const arm = struct {
};
}
return table;
}
return ret;
};
}
};
@@ -135,12 +137,13 @@ pub const thumb = struct {
}
fn populate() [0x400]InstrFn {
comptime {
return comptime {
@setEvalBranchQuota(5025); // This is exact
var table = [_]InstrFn{und} ** 0x400;
var ret = [_]InstrFn{und} ** 0x400;
for (&table, 0..) |*handler, i| {
handler.* = switch (@as(u3, i >> 7 & 0x7)) {
var i: usize = 0;
while (i < ret.len) : (i += 1) {
ret[i] = switch (@as(u3, i >> 7 & 0x7)) {
0b000 => if (i >> 5 & 0x3 == 0b11) blk: {
const I = i >> 4 & 1 == 1;
const is_sub = i >> 3 & 1 == 1;
@@ -228,11 +231,13 @@ pub const thumb = struct {
};
}
return table;
}
return ret;
};
}
};
const log = std.log.scoped(.Arm7Tdmi);
pub const Arm7tdmi = struct {
const Self = @This();
@@ -243,64 +248,18 @@ pub const Arm7tdmi = struct {
cpsr: PSR,
spsr: PSR,
bank: Bank,
/// Storage for R8_fiq -> R12_fiq and their normal counterparts
/// e.g [r[0 + 8], fiq_r[0 + 8], r[1 + 8], fiq_r[1 + 8]...]
banked_fiq: [2 * 5]u32,
/// Storage for r13_<mode>, r14_<mode>
/// e.g. [r13, r14, r13_svc, r14_svc]
banked_r: [2 * 6]u32,
banked_spsr: [5]PSR,
logger: ?Logger,
/// Bank of Registers from other CPU Modes
const Bank = struct {
/// Storage for r13_<mode>, r14_<mode>
/// e.g. [r13, r14, r13_svc, r14_svc]
r: [2 * 6]u32,
/// Storage for R8_fiq -> R12_fiq and their normal counterparts
/// e.g [r[0 + 8], fiq_r[0 + 8], r[1 + 8], fiq_r[1 + 8]...]
fiq: [2 * 5]u32,
spsr: [5]PSR,
const Kind = enum(u1) {
R13 = 0,
R14,
};
pub fn create() Bank {
return .{
.r = [_]u32{0x00} ** 12,
.fiq = [_]u32{0x00} ** 10,
.spsr = [_]PSR{.{ .raw = 0x0000_0000 }} ** 5,
};
}
inline fn regIdx(mode: Mode, kind: Kind) usize {
const idx: usize = switch (mode) {
.User, .System => 0,
.Supervisor => 1,
.Abort => 2,
.Undefined => 3,
.Irq => 4,
.Fiq => 5,
};
return (idx * 2) + if (kind == .R14) @as(usize, 1) else 0;
}
inline fn spsrIdx(mode: Mode) usize {
return switch (mode) {
.Supervisor => 0,
.Abort => 1,
.Undefined => 2,
.Irq => 3,
.Fiq => 4,
else => std.debug.panic("[CPU/Mode] {} does not have a SPSR Register", .{mode}),
};
}
inline fn fiqIdx(i: usize, mode: Mode) usize {
return (i * 2) + if (mode == .Fiq) @as(usize, 1) else 0;
}
};
pub fn init(sched: *Scheduler, bus: *Bus, log_file: ?std.fs.File) Self {
return Self{
.r = [_]u32{0x00} ** 16,
@@ -309,11 +268,41 @@ pub const Arm7tdmi = struct {
.bus = bus,
.cpsr = .{ .raw = 0x0000_001F },
.spsr = .{ .raw = 0x0000_0000 },
.bank = Bank.create(),
.banked_fiq = [_]u32{0x00} ** 10,
.banked_r = [_]u32{0x00} ** 12,
.banked_spsr = [_]PSR{.{ .raw = 0x0000_0000 }} ** 5,
.logger = if (log_file) |file| Logger.init(file) else null,
};
}
inline fn bankedIdx(mode: Mode, kind: BankedKind) usize {
const idx: usize = switch (mode) {
.User, .System => 0,
.Supervisor => 1,
.Abort => 2,
.Undefined => 3,
.Irq => 4,
.Fiq => 5,
};
return (idx * 2) + if (kind == .R14) @as(usize, 1) else 0;
}
inline fn bankedSpsrIndex(mode: Mode) usize {
return switch (mode) {
.Supervisor => 0,
.Abort => 1,
.Undefined => 2,
.Irq => 3,
.Fiq => 4,
else => std.debug.panic("[CPU/Mode] {} does not have a SPSR Register", .{mode}),
};
}
inline fn bankedFiqIdx(i: usize, mode: Mode) usize {
return (i * 2) + if (mode == .Fiq) @as(usize, 1) else 0;
}
pub inline fn hasSPSR(self: *const Self) bool {
const mode = getModeChecked(self, self.cpsr.mode.read());
return switch (mode) {
@@ -349,14 +338,14 @@ pub const Arm7tdmi = struct {
switch (idx) {
8...12 => {
if (current == .Fiq) {
self.bank.fiq[Bank.fiqIdx(idx - 8, .User)] = value;
self.banked_fiq[bankedFiqIdx(idx - 8, .User)] = value;
} else self.r[idx] = value;
},
13, 14 => switch (current) {
.User, .System => self.r[idx] = value,
else => {
const kind = std.meta.intToEnum(Bank.Kind, idx - 13) catch unreachable;
self.bank.r[Bank.regIdx(.User, kind)] = value;
const kind = std.meta.intToEnum(BankedKind, idx - 13) catch unreachable;
self.banked_r[bankedIdx(.User, kind)] = value;
},
},
else => self.r[idx] = value, // R0 -> R7 and R15
@@ -367,12 +356,12 @@ pub const Arm7tdmi = struct {
const current = getModeChecked(self, self.cpsr.mode.read());
return switch (idx) {
8...12 => if (current == .Fiq) self.bank.fiq[Bank.fiqIdx(idx - 8, .User)] else self.r[idx],
8...12 => if (current == .Fiq) self.banked_fiq[bankedFiqIdx(idx - 8, .User)] else self.r[idx],
13, 14 => switch (current) {
.User, .System => self.r[idx],
else => blk: {
const kind = std.meta.intToEnum(Bank.Kind, idx - 13) catch unreachable;
break :blk self.bank.r[Bank.regIdx(.User, kind)];
const kind = std.meta.intToEnum(BankedKind, idx - 13) catch unreachable;
break :blk self.banked_r[bankedIdx(.User, kind)];
},
},
else => self.r[idx], // R0 -> R7 and R15
@@ -383,38 +372,40 @@ pub const Arm7tdmi = struct {
const now = getModeChecked(self, self.cpsr.mode.read());
// Bank R8 -> r12
for (0..5) |i| {
self.bank.fiq[Bank.fiqIdx(i, now)] = self.r[8 + i];
var i: usize = 0;
while (i < 5) : (i += 1) {
self.banked_fiq[bankedFiqIdx(i, now)] = self.r[8 + i];
}
// Bank r13, r14, SPSR
switch (now) {
.User, .System => {
self.bank.r[Bank.regIdx(now, .R13)] = self.r[13];
self.bank.r[Bank.regIdx(now, .R14)] = self.r[14];
self.banked_r[bankedIdx(now, .R13)] = self.r[13];
self.banked_r[bankedIdx(now, .R14)] = self.r[14];
},
else => {
self.bank.r[Bank.regIdx(now, .R13)] = self.r[13];
self.bank.r[Bank.regIdx(now, .R14)] = self.r[14];
self.bank.spsr[Bank.spsrIdx(now)] = self.spsr;
self.banked_r[bankedIdx(now, .R13)] = self.r[13];
self.banked_r[bankedIdx(now, .R14)] = self.r[14];
self.banked_spsr[bankedSpsrIndex(now)] = self.spsr;
},
}
// Grab R8 -> R12
for (0..5) |i| {
self.r[8 + i] = self.bank.fiq[Bank.fiqIdx(i, next)];
i = 0;
while (i < 5) : (i += 1) {
self.r[8 + i] = self.banked_fiq[bankedFiqIdx(i, next)];
}
// Grab r13, r14, SPSR
switch (next) {
.User, .System => {
self.r[13] = self.bank.r[Bank.regIdx(next, .R13)];
self.r[14] = self.bank.r[Bank.regIdx(next, .R14)];
self.r[13] = self.banked_r[bankedIdx(next, .R13)];
self.r[14] = self.banked_r[bankedIdx(next, .R14)];
},
else => {
self.r[13] = self.bank.r[Bank.regIdx(next, .R13)];
self.r[14] = self.bank.r[Bank.regIdx(next, .R14)];
self.spsr = self.bank.spsr[Bank.spsrIdx(next)];
self.r[13] = self.banked_r[bankedIdx(next, .R13)];
self.r[14] = self.banked_r[bankedIdx(next, .R14)];
self.spsr = self.banked_spsr[bankedSpsrIndex(next)];
},
}
@@ -435,8 +426,8 @@ pub const Arm7tdmi = struct {
self.r[13] = 0x0300_7F00;
self.r[15] = 0x0800_0000;
self.bank.r[Bank.regIdx(.Irq, .R13)] = 0x0300_7FA0;
self.bank.r[Bank.regIdx(.Supervisor, .R13)] = 0x0300_7FE0;
self.banked_r[bankedIdx(.Irq, .R13)] = 0x0300_7FA0;
self.banked_r[bankedIdx(.Supervisor, .R13)] = 0x0300_7FE0;
// self.cpsr.raw = 0x6000001F;
self.cpsr.raw = 0x0000_001F;
@@ -466,11 +457,29 @@ pub const Arm7tdmi = struct {
}
pub fn stepDmaTransfer(self: *Self) bool {
inline for (0..4) |i| {
if (self.bus.dma[i].in_progress) {
self.bus.dma[i].step(self);
return true;
}
const dma0 = &self.bus.dma[0];
const dma1 = &self.bus.dma[1];
const dma2 = &self.bus.dma[2];
const dma3 = &self.bus.dma[3];
if (dma0.in_progress) {
dma0.step(self);
return true;
}
if (dma1.in_progress) {
dma1.step(self);
return true;
}
if (dma2.in_progress) {
dma2.step(self);
return true;
}
if (dma3.in_progress) {
dma3.step(self);
return true;
}
return false;
@@ -525,10 +534,10 @@ pub const Arm7tdmi = struct {
std.debug.print("R{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\n", .{ i, self.r[i], i_1, self.r[i_1], i_2, self.r[i_2], i_3, self.r[i_3] });
}
std.debug.print("cpsr: 0x{X:0>8} ", .{self.cpsr.raw});
self.cpsr.toString();
prettyPrintPsr(&self.cpsr);
std.debug.print("spsr: 0x{X:0>8} ", .{self.spsr.raw});
self.spsr.toString();
prettyPrintPsr(&self.spsr);
std.debug.print("pipeline: {??X:0>8}\n", .{self.pipe.stage});
@@ -546,31 +555,97 @@ pub const Arm7tdmi = struct {
std.debug.panic(format, args);
}
fn prettyPrintPsr(psr: *const PSR) void {
std.debug.print("[", .{});
if (psr.n.read()) std.debug.print("N", .{}) else std.debug.print("-", .{});
if (psr.z.read()) std.debug.print("Z", .{}) else std.debug.print("-", .{});
if (psr.c.read()) std.debug.print("C", .{}) else std.debug.print("-", .{});
if (psr.v.read()) std.debug.print("V", .{}) else std.debug.print("-", .{});
if (psr.i.read()) std.debug.print("I", .{}) else std.debug.print("-", .{});
if (psr.f.read()) std.debug.print("F", .{}) else std.debug.print("-", .{});
if (psr.t.read()) std.debug.print("T", .{}) else std.debug.print("-", .{});
std.debug.print("|", .{});
if (getMode(psr.mode.read())) |mode| std.debug.print("{s}", .{modeString(mode)}) else std.debug.print("---", .{});
std.debug.print("]\n", .{});
}
fn modeString(mode: Mode) []const u8 {
return switch (mode) {
.User => "usr",
.Fiq => "fiq",
.Irq => "irq",
.Supervisor => "svc",
.Abort => "abt",
.Undefined => "und",
.System => "sys",
};
}
fn mgbaLog(self: *const Self, file: *const File, opcode: u32) !void {
const thumb_fmt = "{X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} cpsr: {X:0>8} | {X:0>4}:\n";
const arm_fmt = "{X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} cpsr: {X:0>8} | {X:0>8}:\n";
var buf: [0x100]u8 = [_]u8{0x00} ** 0x100; // this is larger than it needs to be
const r0 = self.r[0];
const r1 = self.r[1];
const r2 = self.r[2];
const r3 = self.r[3];
const r4 = self.r[4];
const r5 = self.r[5];
const r6 = self.r[6];
const r7 = self.r[7];
const r8 = self.r[8];
const r9 = self.r[9];
const r10 = self.r[10];
const r11 = self.r[11];
const r12 = self.r[12];
const r13 = self.r[13];
const r14 = self.r[14];
const r15 = self.r[15] -| if (self.cpsr.t.read()) 2 else @as(u32, 4);
const c_psr = self.cpsr.raw;
var log_str: []u8 = undefined;
if (self.cpsr.t.read()) {
if (opcode >> 11 == 0x1E) {
// Instruction 1 of a BL Opcode, print in ARM mode
const other_half = self.bus.debugRead(u16, self.r[15] - 2);
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 });
} else {
log_str = try std.fmt.bufPrint(&buf, thumb_fmt, .{ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, c_psr, opcode });
}
} else {
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, opcode });
}
_ = try file.writeAll(log_str);
}
};
const condition_lut = [_]u16{
0xF0F0, // EQ - Equal
0x0F0F, // NE - Not Equal
0xCCCC, // CS - Unsigned higher or same
0x3333, // CC - Unsigned lower
0xFF00, // MI - Negative
0x00FF, // PL - Positive or Zero
0xAAAA, // VS - Overflow
0x5555, // VC - No Overflow
0x0C0C, // HI - unsigned hierh
0xF3F3, // LS - unsigned lower or same
0xAA55, // GE - greater or equal
0x55AA, // LT - less than
0x0A05, // GT - greater than
0xF5FA, // LE - less than or equal
0xFFFF, // AL - always
0x0000, // NV - never
};
pub inline fn checkCond(cpsr: PSR, cond: u4) bool {
const flags = @truncate(u4, cpsr.raw >> 28);
return condition_lut[cond] & (@as(u16, 1) << flags) != 0;
pub fn checkCond(cpsr: PSR, cond: u4) bool {
return switch (cond) {
0x0 => cpsr.z.read(), // EQ - Equal
0x1 => !cpsr.z.read(), // NE - Not equal
0x2 => cpsr.c.read(), // CS - Unsigned higher or same
0x3 => !cpsr.c.read(), // CC - Unsigned lower
0x4 => cpsr.n.read(), // MI - Negative
0x5 => !cpsr.n.read(), // PL - Positive or zero
0x6 => cpsr.v.read(), // VS - Overflow
0x7 => !cpsr.v.read(), // VC - No overflow
0x8 => cpsr.c.read() and !cpsr.z.read(), // HI - unsigned higher
0x9 => !cpsr.c.read() or cpsr.z.read(), // LS - unsigned lower or same
0xA => cpsr.n.read() == cpsr.v.read(), // GE - Greater or equal
0xB => cpsr.n.read() != cpsr.v.read(), // LT - Less than
0xC => !cpsr.z.read() and (cpsr.n.read() == cpsr.v.read()), // GT - Greater than
0xD => cpsr.z.read() or (cpsr.n.read() != cpsr.v.read()), // LE - Less than or equal
0xE => true, // AL - Always
0xF => false, // NV - Never (reserved in ARMv3 and up, but seems to have not changed?)
};
}
const Pipeline = struct {
@@ -592,7 +667,9 @@ const Pipeline = struct {
pub fn step(self: *Self, cpu: *Arm7tdmi, comptime T: type) ?u32 {
comptime std.debug.assert(T == u32 or T == u16);
const opcode = self.stage[0];
// 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]);
@@ -624,22 +701,6 @@ pub const PSR = extern union {
z: Bit(u32, 30),
n: Bit(u32, 31),
raw: u32,
fn toString(self: PSR) void {
std.debug.print("[", .{});
if (self.n.read()) std.debug.print("N", .{}) else std.debug.print("-", .{});
if (self.z.read()) std.debug.print("Z", .{}) else std.debug.print("-", .{});
if (self.c.read()) std.debug.print("C", .{}) else std.debug.print("-", .{});
if (self.v.read()) std.debug.print("V", .{}) else std.debug.print("-", .{});
if (self.i.read()) std.debug.print("I", .{}) else std.debug.print("-", .{});
if (self.f.read()) std.debug.print("F", .{}) else std.debug.print("-", .{});
if (self.t.read()) std.debug.print("T", .{}) else std.debug.print("-", .{});
std.debug.print("|", .{});
if (getMode(self.mode.read())) |m| std.debug.print("{s}", .{m.toString()}) else std.debug.print("---", .{});
std.debug.print("]\n", .{});
}
};
const Mode = enum(u5) {
@@ -650,18 +711,11 @@ const Mode = enum(u5) {
Abort = 0b10111,
Undefined = 0b11011,
System = 0b11111,
};
fn toString(self: Mode) []const u8 {
return switch (self) {
.User => "usr",
.Fiq => "fiq",
.Irq => "irq",
.Supervisor => "svc",
.Abort => "abt",
.Undefined => "und",
.System => "sys",
};
}
const BankedKind = enum(u1) {
R13 = 0,
R14,
};
fn getMode(bits: u5) ?Mode {

View File

@@ -57,6 +57,7 @@ pub fn blockDataTransfer(comptime P: bool, comptime U: bool, comptime S: bool, c
cpu.r[15] = bus.read(u32, und_addr);
cpu.pipe.reload(cpu);
} else {
// FIXME: Should r15 on write be +12 ahead?
bus.write(u32, und_addr, cpu.r[15] + 4);
}

View File

@@ -1,3 +1,5 @@
const std = @import("std");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn;

View File

@@ -24,7 +24,7 @@ pub fn dataProcessing(comptime I: bool, comptime S: bool, comptime kind: u4) Ins
if (!I and opcode >> 4 & 1 == 1) cpu.r[15] -= 4;
var result: u32 = undefined;
var overflow: u1 = undefined;
var overflow: bool = undefined;
// Perform Data Processing Logic
switch (kind) {
@@ -62,9 +62,7 @@ pub fn dataProcessing(comptime I: bool, comptime S: bool, comptime kind: u4) Ins
if (rd == 0xF)
return undefinedTestBehaviour(cpu);
const tmp = @addWithOverflow(op1, op2);
result = tmp[0];
overflow = tmp[1];
overflow = @addWithOverflow(u32, op1, op2, &result);
},
0xC => result = op1 | op2, // ORR
0xD => result = op2, // MOV
@@ -112,7 +110,7 @@ pub fn dataProcessing(comptime I: bool, comptime S: bool, comptime kind: u4) Ins
// ADD, ADC Flags
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
cpu.cpsr.c.write(overflow == 0b1);
cpu.cpsr.c.write(overflow);
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
},
0x6, 0x7 => if (S and rd != 0xF) {
@@ -143,7 +141,7 @@ pub fn dataProcessing(comptime I: bool, comptime S: bool, comptime kind: u4) Ins
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
} else if (kind == 0xB) {
// CMN specific
cpu.cpsr.c.write(overflow == 0b1);
cpu.cpsr.c.write(overflow);
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
} else {
// TST, TEQ specific
@@ -164,19 +162,19 @@ pub fn sbc(left: u32, right: u32, old_carry: u1) u32 {
return ret;
}
pub fn add(overflow: *u1, left: u32, right: u32) u32 {
const ret = @addWithOverflow(left, right);
overflow.* = ret[1];
return ret[0];
pub fn add(overflow: *bool, left: u32, right: u32) u32 {
var ret: u32 = undefined;
overflow.* = @addWithOverflow(u32, left, right, &ret);
return ret;
}
pub fn adc(overflow: *u1, left: u32, right: u32, old_carry: u1) u32 {
const tmp = @addWithOverflow(left, right);
const ret = @addWithOverflow(tmp[0], old_carry);
overflow.* = tmp[1] | ret[1];
pub fn adc(overflow: *bool, left: u32, right: u32, old_carry: u1) u32 {
var ret: u32 = undefined;
const first = @addWithOverflow(u32, left, right, &ret);
const second = @addWithOverflow(u32, ret, old_carry, &ret);
return ret[0];
overflow.* = first or second;
return ret;
}
fn undefinedTestBehaviour(cpu: *Arm7tdmi) void {

View File

@@ -1,3 +1,5 @@
const std = @import("std");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn;

View File

@@ -1,3 +1,5 @@
const std = @import("std");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn;

View File

@@ -1,3 +1,6 @@
const std = @import("std");
const util = @import("../../../util.zig");
const shifter = @import("../barrel_shifter.zig");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
@@ -11,7 +14,9 @@ pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool,
const rn = opcode >> 16 & 0xF;
const rd = opcode >> 12 & 0xF;
const base = cpu.r[rn];
// rn is r15 and L is not set, the PC is 12 ahead
const base = cpu.r[rn] + if (!L and rn == 0xF) 4 else @as(u32, 0);
const offset = if (I) shifter.immediate(false, cpu, opcode) else opcode & 0xFFF;
const modified_base = if (U) base +% offset else base -% offset;

View File

@@ -1,3 +1,5 @@
const std = @import("std");
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
const CPSR = @import("../cpu.zig").PSR;

View File

@@ -21,8 +21,7 @@ pub fn fmt4(comptime op: u4) InstrFn {
const op2 = cpu.r[rs];
var result: u32 = undefined;
var overflow: u1 = undefined;
var overflow: bool = undefined;
switch (op) {
0x0 => result = op1 & op2, // AND
0x1 => result = op1 ^ op2, // EOR
@@ -35,12 +34,7 @@ pub fn fmt4(comptime op: u4) InstrFn {
0x8 => result = op1 & op2, // TST
0x9 => result = 0 -% op2, // NEG
0xA => result = op1 -% op2, // CMP
0xB => {
// CMN
const tmp = @addWithOverflow(op1, op2);
result = tmp[0];
overflow = tmp[1];
},
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,
@@ -77,7 +71,7 @@ pub fn fmt4(comptime op: u4) InstrFn {
// ADC, CMN
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
cpu.cpsr.c.write(overflow == 0b1);
cpu.cpsr.c.write(overflow);
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
},
0x6 => {

View File

@@ -92,7 +92,8 @@ pub fn fmt15(comptime L: bool, comptime rb: u3) InstrFn {
inline fn countRlist(opcode: u16) u32 {
var count: u32 = 0;
inline for (0..8) |i| {
comptime var i: u4 = 0;
inline while (i < 8) : (i += 1) {
if (opcode >> (7 - i) & 1 == 1) count += 1;
}

View File

@@ -1,3 +1,5 @@
const std = @import("std");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
@@ -64,7 +66,7 @@ pub fn fmt5(comptime op: u2, comptime h1: u1, comptime h2: u1) InstrFn {
const op2 = cpu.r[rs];
var result: u32 = undefined;
var overflow: u1 = undefined;
var overflow: bool = undefined;
switch (op) {
0b00 => result = add(&overflow, op1, op2), // ADD
0b01 => result = op1 -% op2, // CMP
@@ -126,13 +128,13 @@ pub fn fmt2(comptime I: bool, is_sub: bool, rn: u3) InstrFn {
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
} else {
// ADD
var overflow: u1 = undefined;
var overflow: bool = undefined;
const result = add(&overflow, op1, op2);
cpu.r[rd] = result;
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
cpu.cpsr.c.write(overflow == 0b1);
cpu.cpsr.c.write(overflow);
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
}
}
@@ -145,7 +147,7 @@ pub fn fmt3(comptime op: u2, comptime rd: u3) InstrFn {
const op1 = cpu.r[rd];
const op2: u32 = opcode & 0xFF; // Offset
var overflow: u1 = undefined;
var overflow: bool = undefined;
const result: u32 = switch (op) {
0b00 => op2, // MOV
0b01 => op1 -% op2, // CMP
@@ -169,7 +171,7 @@ pub fn fmt3(comptime op: u2, comptime rd: u3) InstrFn {
},
0b10 => {
// ADD
cpu.cpsr.c.write(overflow == 0b1);
cpu.cpsr.c.write(overflow);
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
},
}

View File

@@ -1,3 +1,5 @@
const std = @import("std");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;

View File

@@ -2,29 +2,29 @@ const std = @import("std");
const SDL = @import("sdl2");
const config = @import("../config.zig");
const Bus = @import("Bus.zig");
const Scheduler = @import("scheduler.zig").Scheduler;
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
const FpsTracker = @import("../util.zig").FpsTracker;
const FilePaths = @import("../util.zig").FilePaths;
const Timer = std.time.Timer;
const Thread = std.Thread;
const Atomic = std.atomic.Atomic;
const Allocator = std.mem.Allocator;
/// 4 Cycles in 1 dot
const cycles_per_dot = 4;
// 228 Lines which consist of 308 dots (which are 4 cycles long)
const cycles_per_frame: u64 = 228 * (308 * 4); //280896
const clock_rate: u64 = 1 << 24; // 16.78MHz
/// The GBA draws 228 Horizontal which each consist 308 dots
/// (note: not all lines are visible)
const cycles_per_frame = 228 * (308 * cycles_per_dot); //280896
// TODO: Don't truncate this, be more accurate w/ timing
// 59.6046447754ns (truncated to just 59ns)
const clock_period: u64 = std.time.ns_per_s / clock_rate;
const frame_period = (clock_period * cycles_per_frame);
/// The GBA ARM7TDMI runs at 2^24 Hz
const clock_rate = 1 << 24; // 16.78MHz
/// The # of nanoseconds a frame should take
const frame_period = (std.time.ns_per_s * cycles_per_frame) / clock_rate;
/// Exact Value: 59.7275005696Hz
/// The inverse of the frame period
pub const frame_rate: f64 = @intToFloat(f64, clock_rate) / cycles_per_frame;
// 59.7275005696Hz
pub const frame_rate = @intToFloat(f64, std.time.ns_per_s) /
((@intToFloat(f64, std.time.ns_per_s) / @intToFloat(f64, clock_rate)) * @intToFloat(f64, cycles_per_frame));
const log = std.log.scoped(.Emulation);
@@ -36,7 +36,7 @@ const RunKind = enum {
};
pub fn run(quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: *FpsTracker) void {
const audio_sync = config.config().guest.audio_sync and !config.config().host.mute;
const audio_sync = config.config().guest.audio_sync;
if (audio_sync) log.info("Audio sync enabled", .{});
if (config.config().guest.video_sync) {
@@ -56,7 +56,7 @@ fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), schedule
.Unlimited, .UnlimitedFPS => {
log.info("Emulation w/out video sync", .{});
while (!quit.load(.Monotonic)) {
while (!quit.load(.SeqCst)) {
runFrame(scheduler, cpu);
audioSync(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
@@ -68,7 +68,7 @@ fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), schedule
var timer = Timer.start() catch @panic("failed to initalize std.timer.Timer");
var wake_time: u64 = frame_period;
while (!quit.load(.Monotonic)) {
while (!quit.load(.SeqCst)) {
runFrame(scheduler, cpu);
const new_wake_time = videoSync(&timer, wake_time);
@@ -94,7 +94,7 @@ pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
if (!cpu.stepDmaTransfer()) {
if (cpu.isHalted()) {
// Fast-forward to next Event
sched.tick = sched.nextTimestamp();
sched.tick = sched.queue.peek().?.tick;
} else {
cpu.step();
}
@@ -105,7 +105,6 @@ pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
}
fn audioSync(audio_sync: bool, stream: *SDL.SDL_AudioStream, is_buffer_full: *bool) void {
comptime std.debug.assert(@import("../platform.zig").sample_format == SDL.AUDIO_U16);
const sample_size = 2 * @sizeOf(u16);
const max_buf_size: c_int = 0x400;
@@ -133,10 +132,11 @@ fn videoSync(timer: *Timer, wake_time: u64) u64 {
// TODO: Better sleep impl?
fn sleep(timer: *Timer, wake_time: u64) ?u64 {
// const step = std.time.ns_per_ms * 10; // 10ms
const timestamp = timer.read();
// ns_late is non zero if we are late.
var ns_late = timestamp -| wake_time;
const ns_late = timestamp -| wake_time;
// If we're more than a frame late, skip the rest of this loop
// Recalculate what our new wake time should be so that we can
@@ -144,17 +144,15 @@ fn sleep(timer: *Timer, wake_time: u64) ?u64 {
if (ns_late > frame_period) return timestamp + frame_period;
const sleep_for = frame_period - ns_late;
const step = 2 * std.time.ns_per_ms; // Granularity of 2ms
const times = sleep_for / step;
// // Employ several sleep calls in periods of 10ms
// // By doing this the behaviour should average out to be
// // more consistent
// const loop_count = sleep_for / step; // How many groups of 10ms
for (0..times) |_| {
std.time.sleep(step);
// var i: usize = 0;
// while (i < loop_count) : (i += 1) std.time.sleep(step);
// Upon wakeup, check to see if this particular sleep was longer than expected
// if so we should exit early, but probably not skip a whole frame period
ns_late = timer.read() -| wake_time;
if (ns_late > frame_period) return null;
}
std.time.sleep(sleep_for);
return null;
}
@@ -162,59 +160,3 @@ fn sleep(timer: *Timer, wake_time: u64) ?u64 {
fn spinLoop(timer: *Timer, wake_time: u64) void {
while (true) if (timer.read() > wake_time) break;
}
pub const EmuThing = struct {
const Self = @This();
const Interface = @import("gdbstub").Emulator;
const Allocator = std.mem.Allocator;
cpu: *Arm7tdmi,
scheduler: *Scheduler,
pub fn init(cpu: *Arm7tdmi, scheduler: *Scheduler) Self {
return .{ .cpu = cpu, .scheduler = scheduler };
}
pub fn interface(self: *Self, allocator: Allocator) Interface {
return Interface.init(allocator, self);
}
pub fn read(self: *const Self, addr: u32) u8 {
return self.cpu.bus.dbgRead(u8, addr);
}
pub fn write(self: *Self, addr: u32, value: u8) void {
self.cpu.bus.dbgWrite(u8, addr, value);
}
pub fn registers(self: *const Self) *[16]u32 {
return &self.cpu.r;
}
pub fn cpsr(self: *const Self) u32 {
return self.cpu.cpsr.raw;
}
pub fn step(self: *Self) void {
const cpu = self.cpu;
const sched = self.scheduler;
// Is true when we have executed one (1) instruction
var did_step: bool = false;
// TODO: How can I make it easier to keep this in lock-step with runFrame?
while (!did_step) {
if (!cpu.stepDmaTransfer()) {
if (cpu.isHalted()) {
// Fast-forward to next Event
sched.tick = sched.queue.peek().?.tick;
} else {
cpu.step();
did_step = true;
}
}
if (sched.tick >= sched.nextTimestamp()) sched.handleEvent(cpu);
}
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -42,6 +42,6 @@ pub fn deinit(self: *Self) void {
self.* = undefined;
}
pub inline fn backdrop(self: *const Self) u16 {
return std.mem.readIntNative(u16, self.buf[0..2]);
pub fn backdrop(self: *const Self) u16 {
return self.read(u16, 0);
}

View File

@@ -50,7 +50,7 @@ pub fn deinit(self: *Self) void {
self.* = undefined;
}
pub fn mirror(address: usize) usize {
fn mirror(address: usize) usize {
// Mirrored in steps of 128K (64K + 32K + 32K) (abcc)
const addr = address & 0x1FFFF;

View File

@@ -1,5 +1,6 @@
const std = @import("std");
const Bus = @import("Bus.zig");
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
const Clock = @import("bus/gpio.zig").Clock;
@@ -31,55 +32,64 @@ pub const Scheduler = struct {
}
pub fn handleEvent(self: *Self, cpu: *Arm7tdmi) void {
const event = self.queue.remove();
const late = self.tick - event.tick;
if (self.queue.removeOrNull()) |event| {
const late = self.tick - event.tick;
switch (event.kind) {
.HeatDeath => {
log.err("u64 overflow. This *actually* should never happen.", .{});
unreachable;
},
.Draw => {
// The end of a VDraw
cpu.bus.ppu.drawScanline();
cpu.bus.ppu.onHdrawEnd(cpu, late);
},
.TimerOverflow => |id| {
switch (id) {
inline 0...3 => |idx| cpu.bus.tim[idx].onTimerExpire(cpu, late),
}
},
.ApuChannel => |id| {
switch (id) {
0 => cpu.bus.apu.ch1.onToneSweepEvent(late),
1 => cpu.bus.apu.ch2.onToneEvent(late),
2 => cpu.bus.apu.ch3.onWaveEvent(late),
3 => cpu.bus.apu.ch4.onNoiseEvent(late),
}
},
.RealTimeClock => {
const device = &cpu.bus.pak.gpio.device;
if (device.kind != .Rtc or device.ptr == null) return;
switch (event.kind) {
.HeatDeath => {
log.err("u64 overflow. This *actually* should never happen.", .{});
unreachable;
},
.Draw => {
// The end of a VDraw
cpu.bus.ppu.drawScanline();
cpu.bus.ppu.onHdrawEnd(cpu, late);
},
.TimerOverflow => |id| {
switch (id) {
0 => cpu.bus.tim[0].onTimerExpire(cpu, late),
1 => cpu.bus.tim[1].onTimerExpire(cpu, late),
2 => cpu.bus.tim[2].onTimerExpire(cpu, late),
3 => cpu.bus.tim[3].onTimerExpire(cpu, late),
}
},
.ApuChannel => |id| {
switch (id) {
0 => cpu.bus.apu.ch1.onToneSweepEvent(late),
1 => cpu.bus.apu.ch2.onToneEvent(late),
2 => cpu.bus.apu.ch3.onWaveEvent(late),
3 => cpu.bus.apu.ch4.onNoiseEvent(late),
}
},
.RealTimeClock => {
const device = &cpu.bus.pak.gpio.device;
if (device.kind != .Rtc or device.ptr == null) return;
const clock = @ptrCast(*Clock, @alignCast(@alignOf(*Clock), device.ptr.?));
clock.onClockUpdate(late);
},
.FrameSequencer => cpu.bus.apu.onSequencerTick(late),
.SampleAudio => cpu.bus.apu.sampleAudio(late),
.HBlank => cpu.bus.ppu.onHblankEnd(cpu, late), // The end of a HBlank
.VBlank => cpu.bus.ppu.onHdrawEnd(cpu, late), // The end of a VBlank
const clock = @ptrCast(*Clock, @alignCast(@alignOf(*Clock), device.ptr.?));
clock.onClockUpdate(late);
},
.FrameSequencer => cpu.bus.apu.onSequencerTick(late),
.SampleAudio => cpu.bus.apu.sampleAudio(late),
.HBlank => cpu.bus.ppu.onHblankEnd(cpu, late), // The end of a HBlank
.VBlank => cpu.bus.ppu.onHdrawEnd(cpu, late), // The end of a VBlank
}
}
}
/// Removes the **first** scheduled event of type `needle`
pub fn removeScheduledEvent(self: *Self, needle: EventKind) void {
for (self.queue.items, 0..) |event, i| {
var it = self.queue.iterator();
var i: usize = 0;
while (it.next()) |event| : (i += 1) {
if (std.meta.eql(event.kind, needle)) {
// invalidates the slice we're iterating over
// This invalidates the iterator
_ = self.queue.removeIndex(i);
log.debug("Removed {?}@{}", .{ event.kind, event.tick });
// Since removing something from the PQ invalidates the iterator,
// this implementation can safely only remove the first instance of
// a Scheduled Event. Exit Early
break;
}
}

View File

@@ -4,17 +4,14 @@ const known_folders = @import("known_folders");
const clap = @import("clap");
const config = @import("config.zig");
const emu = @import("core/emu.zig");
const Gui = @import("platform.zig").Gui;
const Bus = @import("core/Bus.zig");
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
const Scheduler = @import("core/scheduler.zig").Scheduler;
const FilePaths = @import("util.zig").FilePaths;
const FpsTracker = @import("util.zig").FpsTracker;
const Allocator = std.mem.Allocator;
const Atomic = std.atomic.Atomic;
const Allocator = std.mem.Allocator;
const log = std.log.scoped(.Cli);
const width = @import("core/ppu.zig").width;
const height = @import("core/ppu.zig").height;
@@ -25,49 +22,37 @@ const params = clap.parseParamsComptime(
\\-h, --help Display this help and exit.
\\-s, --skip Skip BIOS.
\\-b, --bios <str> Optional path to a GBA BIOS ROM.
\\ --gdb Run ZBA from the context of a GDB Server
\\<str> Path to the GBA GamePak ROM.
\\
);
pub fn main() void {
pub fn main() anyerror!void {
// Main Allocator for ZBA
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer std.debug.assert(!gpa.deinit());
const allocator = gpa.allocator();
// Determine the Data Directory (stores saves)
// Determine the Data Directory (stores saves, config file, etc.)
const data_path = blk: {
const result = known_folders.getPath(allocator, .data);
const option = result catch |e| exitln("interrupted while determining the data folder: {}", .{e});
const path = option orelse exitln("no valid data folder found", .{});
ensureDataDirsExist(path) catch |e| exitln("failed to create folders under \"{s}\": {}", .{ path, e });
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);
// Determine the Config Directory
const config_path = blk: {
const result = known_folders.getPath(allocator, .roaming_configuration);
const option = result catch |e| exitln("interreupted while determining the config folder: {}", .{e});
const path = option orelse exitln("no valid config folder found", .{});
ensureConfigDirExists(path) catch |e| exitln("failed to create required folder \"{s}\": {}", .{ path, e });
break :blk path;
};
defer allocator.free(config_path);
// Parse CLI
const result = clap.parse(clap.Help, &params, clap.parsers.default, .{}) catch |e| exitln("failed to parse cli: {}", .{e});
defer result.deinit();
// TODO: Move config file to XDG Config directory?
const cfg_file_path = configFilePath(allocator, config_path) catch |e| exitln("failed to ready config file for access: {}", .{e});
defer allocator.free(cfg_file_path);
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, cfg_file_path) catch |e| exitln("failed to load config file: {}", .{e});
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);
@@ -91,49 +76,13 @@ pub fn main() void {
cpu.fastBoot();
}
var quit = Atomic(bool).init(false);
var gui = Gui.init(&bus.pak.title, &bus.apu, width, height) catch |e| exitln("failed to init gui: {}", .{e});
var gui = Gui.init(&bus.pak.title, &bus.apu, width, height);
defer gui.deinit();
if (result.args.gdb) {
const Server = @import("gdbstub").Server;
const EmuThing = @import("core/emu.zig").EmuThing;
var wrapper = EmuThing.init(&cpu, &scheduler);
var emulator = wrapper.interface(allocator);
defer emulator.deinit();
log.info("Ready to connect", .{});
var server = Server.init(emulator) catch |e| exitln("failed to init gdb server: {}", .{e});
defer server.deinit(allocator);
log.info("Starting GDB Server Thread", .{});
const thread = std.Thread.spawn(.{}, Server.run, .{ &server, allocator, &quit }) catch |e| exitln("gdb server thread crashed: {}", .{e});
defer thread.join();
gui.run(.{
.cpu = &cpu,
.scheduler = &scheduler,
.quit = &quit,
}) catch |e| exitln("main thread panicked: {}", .{e});
} else {
var tracker = FpsTracker.init();
const thread = std.Thread.spawn(.{}, emu.run, .{ &quit, &scheduler, &cpu, &tracker }) catch |e| exitln("emu thread panicked: {}", .{e});
defer thread.join();
gui.run(.{
.cpu = &cpu,
.scheduler = &scheduler,
.tracker = &tracker,
.quit = &quit,
}) catch |e| exitln("main thread panicked: {}", .{e});
}
gui.run(&cpu, &scheduler) catch |e| exitln("failed to run gui thread: {}", .{e});
}
fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const clap.Result(clap.Help, &params, clap.parsers.default)) !FilePaths {
pub fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const clap.Result(clap.Help, &params, clap.parsers.default)) !FilePaths {
const rom_path = romPath(result);
log.info("ROM path: {s}", .{rom_path});
@@ -150,8 +99,8 @@ fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const c
};
}
fn configFilePath(allocator: Allocator, config_path: []const u8) ![]const u8 {
const path = try std.fs.path.join(allocator, &[_][]const u8{ config_path, "zba", "config.toml" });
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.
@@ -160,7 +109,7 @@ fn configFilePath(allocator: Allocator, config_path: []const u8) ![]const u8 {
std.fs.accessAbsolute(path, .{}) catch |e| {
if (e != error.FileNotFound) return e;
const config_file = std.fs.createFileAbsolute(path, .{}) catch |err| exitln("failed to create \"{s}\": {}", .{ path, err });
const config_file = try std.fs.createFileAbsolute(path, .{});
defer config_file.close();
try config_file.writeAll(@embedFile("../example.toml"));
@@ -169,19 +118,15 @@ fn configFilePath(allocator: Allocator, config_path: []const u8) ![]const u8 {
return path;
}
fn ensureDataDirsExist(data_path: []const u8) !void {
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" ++ std.fs.path.sep_str ++ "save");
}
fn ensureConfigDirExists(config_path: []const u8) !void {
var dir = try std.fs.openDirAbsolute(config_path, .{});
defer dir.close();
try dir.makePath("zba");
try dir.makePath("zba" ++ [_]u8{std.fs.path.sep} ++ "save");
}
fn romPath(result: *const clap.Result(clap.Help, &params, clap.parsers.default)) []const u8 {

View File

@@ -9,13 +9,13 @@ const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
const Scheduler = @import("core/scheduler.zig").Scheduler;
const FpsTracker = @import("util.zig").FpsTracker;
const span = @import("util.zig").span;
const pitch = @import("core/ppu.zig").framebuf_pitch;
const gba_width = @import("core/ppu.zig").width;
const gba_height = @import("core/ppu.zig").height;
pub const sample_rate = 1 << 15;
pub const sample_format = SDL.AUDIO_U16;
const default_title = "ZBA";
const default_title: []const u8 = "ZBA";
pub const Gui = struct {
const Self = @This();
@@ -44,7 +44,7 @@ pub const Gui = struct {
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 {
if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO) < 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();
@@ -53,7 +53,7 @@ pub const Gui = struct {
const win_scale = @intCast(c_int, config.config().host.win_scale);
const window = SDL.SDL_CreateWindow(
default_title,
default_title.ptr,
SDL.SDL_WINDOWPOS_CENTERED,
SDL.SDL_WINDOWPOS_CENTERED,
@as(c_int, width * win_scale),
@@ -64,21 +64,21 @@ pub const Gui = struct {
const ctx = SDL.SDL_GL_CreateContext(window) orelse panic();
if (SDL.SDL_GL_MakeCurrent(window, ctx) < 0) panic();
gl.load(ctx, Self.glGetProcAddress) catch {};
gl.load(ctx, Self.glGetProcAddress) catch @panic("gl.load failed");
if (SDL.SDL_GL_SetSwapInterval(@boolToInt(config.config().host.vsync)) < 0) panic();
const program_id = try compileShaders();
const program_id = compileShaders();
return Self{
.window = window,
.title = std.mem.sliceTo(title, 0),
.title = span(title),
.ctx = ctx,
.program_id = program_id,
.audio = Audio.init(apu),
};
}
fn compileShaders() !gl.GLuint {
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");
@@ -89,16 +89,12 @@ pub const Gui = struct {
gl.shaderSource(vs, 1, &[_][*c]const u8{vert_shader}, 0);
gl.compileShader(vs);
if (!shader.didCompile(vs)) return error.VertexCompileError;
const fs = gl.createShader(gl.FRAGMENT_SHADER);
defer gl.deleteShader(fs);
gl.shaderSource(fs, 1, &[_][*c]const u8{frag_shader}, 0);
gl.compileShader(fs);
if (!shader.didCompile(fs)) return error.FragmentCompileError;
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
@@ -108,7 +104,7 @@ pub const Gui = struct {
}
// Returns the VAO ID since it's used in run()
fn generateBuffers() struct { c_uint, c_uint, c_uint } {
fn generateBuffers() [3]c_uint {
var vao_id: c_uint = undefined;
var vbo_id: c_uint = undefined;
var ebo_id: c_uint = undefined;
@@ -154,86 +150,69 @@ pub const Gui = struct {
return tex_id;
}
const RunOptions = struct {
quit: *std.atomic.Atomic(bool),
tracker: ?*FpsTracker = null,
cpu: *Arm7tdmi,
scheduler: *Scheduler,
};
pub fn run(self: *Self, cpu: *Arm7tdmi, scheduler: *Scheduler) !void {
var quit = std.atomic.Atomic(bool).init(false);
var tracker = FpsTracker.init();
pub fn run(self: *Self, opt: RunOptions) !void {
const cpu = opt.cpu;
const tracker = opt.tracker;
const quit = opt.quit;
const thread = try std.Thread.spawn(.{}, emu.run, .{ &quit, scheduler, cpu, &tracker });
defer thread.join();
var buffer_ids = Self.generateBuffers();
defer {
gl.deleteBuffers(1, &buffer_ids[2]); // EBO
gl.deleteBuffers(1, &buffer_ids[1]); // VBO
gl.deleteVertexArrays(1, &buffer_ids[0]); // VAO
}
const vao_id = buffer_ids[0];
var title_buf: [0x100]u8 = [_]u8{0} ** 0x100;
const tex_id = Self.generateTexture(cpu.bus.ppu.framebuf.get(.Renderer));
defer gl.deleteTextures(1, &tex_id);
var title_buf: [0x100]u8 = undefined;
const vao_id = Self.generateBuffers()[0];
_ = Self.generateTexture(cpu.bus.ppu.framebuf.get(.Renderer));
emu_loop: while (true) {
var event: SDL.SDL_Event = undefined;
// This might be true if the emu is running via a gdbstub server
// and the gdb stub exits first
if (quit.load(.Monotonic)) break :emu_loop;
while (SDL.SDL_PollEvent(&event) != 0) {
switch (event.type) {
SDL.SDL_QUIT => break :emu_loop,
SDL.SDL_KEYDOWN => {
const io = &cpu.bus.io;
const key_code = event.key.keysym.sym;
var keyinput = cpu.bus.io.keyinput.load(.Monotonic);
switch (key_code) {
SDL.SDLK_UP => keyinput.up.unset(),
SDL.SDLK_DOWN => keyinput.down.unset(),
SDL.SDLK_LEFT => keyinput.left.unset(),
SDL.SDLK_RIGHT => keyinput.right.unset(),
SDL.SDLK_x => keyinput.a.unset(),
SDL.SDLK_z => keyinput.b.unset(),
SDL.SDLK_a => keyinput.shoulder_l.unset(),
SDL.SDLK_s => keyinput.shoulder_r.unset(),
SDL.SDLK_RETURN => keyinput.start.unset(),
SDL.SDLK_RSHIFT => keyinput.select.unset(),
SDL.SDLK_UP => io.keyinput.up.unset(),
SDL.SDLK_DOWN => io.keyinput.down.unset(),
SDL.SDLK_LEFT => io.keyinput.left.unset(),
SDL.SDLK_RIGHT => io.keyinput.right.unset(),
SDL.SDLK_x => io.keyinput.a.unset(),
SDL.SDLK_z => io.keyinput.b.unset(),
SDL.SDLK_a => io.keyinput.shoulder_l.unset(),
SDL.SDLK_s => io.keyinput.shoulder_r.unset(),
SDL.SDLK_RETURN => io.keyinput.start.unset(),
SDL.SDLK_RSHIFT => io.keyinput.select.unset(),
else => {},
}
cpu.bus.io.keyinput.store(keyinput.raw, .Monotonic);
},
SDL.SDL_KEYUP => {
const io = &cpu.bus.io;
const key_code = event.key.keysym.sym;
var keyinput = cpu.bus.io.keyinput.load(.Monotonic);
switch (key_code) {
SDL.SDLK_UP => keyinput.up.set(),
SDL.SDLK_DOWN => keyinput.down.set(),
SDL.SDLK_LEFT => keyinput.left.set(),
SDL.SDLK_RIGHT => keyinput.right.set(),
SDL.SDLK_x => keyinput.a.set(),
SDL.SDLK_z => keyinput.b.set(),
SDL.SDLK_a => keyinput.shoulder_l.set(),
SDL.SDLK_s => keyinput.shoulder_r.set(),
SDL.SDLK_RETURN => keyinput.start.set(),
SDL.SDLK_RSHIFT => keyinput.select.set(),
SDL.SDLK_i => {
comptime std.debug.assert(sample_format == SDL.AUDIO_U16);
log.err("Sample Count: {}", .{@intCast(u32, SDL.SDL_AudioStreamAvailable(cpu.bus.apu.stream)) / (2 * @sizeOf(u16))});
SDL.SDLK_UP => io.keyinput.up.set(),
SDL.SDLK_DOWN => io.keyinput.down.set(),
SDL.SDLK_LEFT => io.keyinput.left.set(),
SDL.SDLK_RIGHT => io.keyinput.right.set(),
SDL.SDLK_x => io.keyinput.a.set(),
SDL.SDLK_z => io.keyinput.b.set(),
SDL.SDLK_a => io.keyinput.shoulder_l.set(),
SDL.SDLK_s => io.keyinput.shoulder_r.set(),
SDL.SDLK_RETURN => io.keyinput.start.set(),
SDL.SDLK_RSHIFT => io.keyinput.select.set(),
SDL.SDLK_i => log.err("Sample Count: {}", .{@intCast(u32, SDL.SDL_AudioStreamAvailable(cpu.bus.apu.stream)) / (2 * @sizeOf(u16))}),
SDL.SDLK_j => log.err("Scheduler Capacity: {} | Scheduler Event Count: {}", .{ scheduler.queue.capacity(), scheduler.queue.count() }),
SDL.SDLK_k => {
// Dump IWRAM to file
log.info("PC: 0x{X:0>8}", .{cpu.r[15]});
log.info("LR: 0x{X:0>8}", .{cpu.r[14]});
// const iwram_file = try std.fs.cwd().createFile("iwram.bin", .{});
// defer iwram_file.close();
// try iwram_file.writeAll(cpu.bus.iwram.buf);
},
// SDL.SDLK_j => log.err("Scheduler Capacity: {} | Scheduler Event Count: {}", .{ scheduler.queue.capacity(), scheduler.queue.count() }),
SDL.SDLK_k => {},
else => {},
}
cpu.bus.io.keyinput.store(keyinput.raw, .Monotonic);
},
else => {},
}
@@ -248,17 +227,16 @@ pub const Gui = struct {
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, null);
SDL.SDL_GL_SwapWindow(self.window);
if (tracker) |t| {
const dyn_title = std.fmt.bufPrintZ(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, t.value() }) catch unreachable;
SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr);
}
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);
}
quit.store(true, .Monotonic); // Terminate Emulator Thread
quit.store(true, .SeqCst); // Terminate Emulator Thread
}
pub fn deinit(self: *Self) void {
self.audio.deinit();
// TODO: Buffer deletions
gl.deleteProgram(self.program_id);
SDL.SDL_GL_DeleteContext(self.ctx);
SDL.SDL_DestroyWindow(self.window);
@@ -275,6 +253,7 @@ pub const Gui = struct {
const Audio = struct {
const Self = @This();
const log = std.log.scoped(.PlatformAudio);
const sample_rate = @import("core/apu.zig").host_sample_rate;
device: SDL.SDL_AudioDeviceID,
@@ -282,22 +261,16 @@ const Audio = struct {
var have: SDL.SDL_AudioSpec = undefined;
var want: SDL.SDL_AudioSpec = std.mem.zeroes(SDL.SDL_AudioSpec);
want.freq = sample_rate;
want.format = sample_format;
want.format = SDL.AUDIO_U16;
want.channels = 2;
want.samples = 0x100;
want.callback = Self.callback;
want.userdata = apu;
std.debug.assert(sample_format == SDL.AUDIO_U16);
log.info("Host Sample Rate: {}Hz, Host Format: SDL.AUDIO_U16", .{sample_rate});
const device = SDL.SDL_OpenAudioDevice(null, 0, &want, &have, 0);
if (device == 0) panic();
if (!config.config().host.mute) {
SDL.SDL_PauseAudioDevice(device, 0); // Unpause Audio
log.info("Unpaused Device", .{});
}
SDL.SDL_PauseAudioDevice(device, 0); // Unpause Audio
return .{ .device = device };
}
@@ -308,32 +281,18 @@ const Audio = struct {
}
export fn callback(userdata: ?*anyopaque, stream: [*c]u8, len: c_int) void {
const T = *Apu;
const apu = @ptrCast(T, @alignCast(@alignOf(T), userdata));
const apu = @ptrCast(*Apu, @alignCast(@alignOf(*Apu), userdata));
_ = SDL.SDL_AudioStreamGet(apu.stream, stream, len);
}
};
// TODO: Find a better way to mute this
if (!config.config().host.mute) {
_ = 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);
}
const shader = struct {
const Kind = enum { vertex, fragment };
const log = std.log.scoped(.Shader);
fn didCompile(id: gl.GLuint) bool {
var success: gl.GLint = undefined;
gl.getShaderiv(id, gl.COMPILE_STATUS, &success);
if (success == 0) err(id);
return success == 1;
}
fn err(id: gl.GLuint) void {
const buf_len = 512;
var error_msg: [buf_len]u8 = undefined;
gl.getShaderInfoLog(id, buf_len, 0, &error_msg);
log.err("{s}", .{std.mem.sliceTo(&error_msg, 0)});
// If we don't write anything, play silence otherwise garbage will be played
// if (written == 0) std.mem.set(u8, stream[0..@intCast(usize, len)], 0x40);
}
};

View File

@@ -14,9 +14,9 @@ pub fn sext(comptime T: type, comptime U: type, value: T) T {
const iT = std.meta.Int(.signed, @typeInfo(T).Int.bits);
const ExtU = if (@typeInfo(U).Int.signedness == .unsigned) T else iT;
const shift_amt = @intCast(Log2Int(T), @typeInfo(T).Int.bits - @typeInfo(U).Int.bits);
const shift = @intCast(Log2Int(T), @typeInfo(T).Int.bits - @typeInfo(U).Int.bits);
return @bitCast(T, @bitCast(iT, @as(ExtU, @truncate(U, value)) << shift_amt) >> shift_amt);
return @bitCast(T, @bitCast(iT, @as(ExtU, @truncate(U, value)) << shift) >> shift);
}
/// See https://godbolt.org/z/W3en9Eche
@@ -49,7 +49,7 @@ pub const FpsTracker = struct {
pub fn value(self: *Self) u32 {
if (self.timer.read() >= std.time.ns_per_s) {
self.fps = self.count.swap(0, .Monotonic);
self.fps = self.count.swap(0, .SeqCst);
self.timer.reset();
}
@@ -68,6 +68,57 @@ pub fn intToBytes(comptime T: type, value: anytype) [@sizeOf(T)]u8 {
return result;
}
/// The Title from the GBA Cartridge is an Uppercase ASCII string which is
/// null-padded to 12 bytes
///
/// This function returns a slice of the ASCII string without the null terminator(s)
/// (essentially, a proper Zig/Rust/Any modern language String)
pub fn span(title: *const [12]u8) []const u8 {
const end = std.mem.indexOfScalar(u8, title, '\x00');
return title[0 .. end orelse title.len];
}
test "span" {
var example: *const [12]u8 = "POKEMON_EMER";
try std.testing.expectEqualSlices(u8, "POKEMON_EMER", span(example));
example = "POKEMON_EME\x00";
try std.testing.expectEqualSlices(u8, "POKEMON_EME", span(example));
example = "POKEMON_EM\x00\x00";
try std.testing.expectEqualSlices(u8, "POKEMON_EM", span(example));
example = "POKEMON_E\x00\x00\x00";
try std.testing.expectEqualSlices(u8, "POKEMON_E", span(example));
example = "POKEMON_\x00\x00\x00\x00";
try std.testing.expectEqualSlices(u8, "POKEMON_", span(example));
example = "POKEMON\x00\x00\x00\x00\x00";
try std.testing.expectEqualSlices(u8, "POKEMON", span(example));
example = "POKEMO\x00\x00\x00\x00\x00\x00";
try std.testing.expectEqualSlices(u8, "POKEMO", span(example));
example = "POKEM\x00\x00\x00\x00\x00\x00\x00";
try std.testing.expectEqualSlices(u8, "POKEM", span(example));
example = "POKE\x00\x00\x00\x00\x00\x00\x00\x00";
try std.testing.expectEqualSlices(u8, "POKE", span(example));
example = "POK\x00\x00\x00\x00\x00\x00\x00\x00\x00";
try std.testing.expectEqualSlices(u8, "POK", span(example));
example = "PO\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
try std.testing.expectEqualSlices(u8, "PO", span(example));
example = "P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
try std.testing.expectEqualSlices(u8, "P", span(example));
example = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
try std.testing.expectEqualSlices(u8, "", span(example));
}
/// Creates a copy of a title with all Filesystem-invalid characters replaced
///
/// e.g. POKEPIN R/S to POKEPIN R_S
@@ -94,9 +145,7 @@ pub const io = struct {
return 0;
}
pub fn undef(comptime T: type, comptime log: anytype, comptime format: []const u8, args: anytype) ?T {
@setCold(true);
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);
@@ -104,13 +153,6 @@ pub const io = struct {
return null;
}
pub fn err(comptime T: type, comptime log: anytype, comptime format: []const u8, args: anytype) ?T {
@setCold(true);
log.err(format, args);
return null;
}
};
pub const write = struct {
@@ -148,7 +190,7 @@ pub const Logger = struct {
if (cpu.cpsr.t.read()) {
if (opcode >> 11 == 0x1E) {
// Instruction 1 of a BL Opcode, print in ARM mode
const low = cpu.bus.dbgRead(u16, cpu.r[15] - 2);
const low = cpu.bus.dbgRead(u16, cpu.r[15]);
const bl_opcode = @as(u32, opcode) << 16 | low;
self.print(arm_fmt, Self.fmtArgs(cpu, bl_opcode)) catch @panic("failed to write to log file");
@@ -233,37 +275,22 @@ pub const audio = struct {
};
};
/// Sets a quarter (8) of the bits of the u32 `left` to the value of u8 `right`
pub inline fn setQuart(left: u32, addr: u8, right: u8) u32 {
const offset = @truncate(u2, addr);
return switch (offset) {
0b00 => (left & 0xFFFF_FF00) | right,
0b01 => (left & 0xFFFF_00FF) | @as(u32, right) << 8,
0b10 => (left & 0xFF00_FFFF) | @as(u32, right) << 16,
0b11 => (left & 0x00FF_FFFF) | @as(u32, right) << 24,
/// Sets the high 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 & 0xFFFF_0000) | right,
u16 => (left & 0xFF00) | right,
u8 => (left & 0xF0) | right,
else => @compileError("unsupported type"),
};
}
/// Calculates the correct shift offset for an aligned/unaligned u8 read
///
/// TODO: Support u16 reads of u32 values?
pub inline fn getHalf(byte: u8) u4 {
return @truncate(u4, byte & 1) << 3;
}
pub inline fn setHalf(comptime T: type, left: T, addr: u8, right: HalfInt(T)) T {
const offset = @truncate(u1, addr >> if (T == u32) 1 else 0);
/// sets the low bits of an integer to a value
pub inline fn setHi(comptime T: type, left: T, right: HalfInt(T)) T {
return switch (T) {
u32 => switch (offset) {
0b0 => (left & 0xFFFF_0000) | right,
0b1 => (left & 0x0000_FFFF) | @as(u32, right) << 16,
},
u16 => switch (offset) {
0b0 => (left & 0xFF00) | right,
0b1 => (left & 0x00FF) | @as(u16, right) << 8,
},
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"),
};
}