4 Commits

Author SHA1 Message Date
4d99c8c139 chore(emu): refactor code 2022-10-12 22:18:07 -03:00
0772259db5 feat(config): add support for (and read from) TOML config file 2022-10-12 22:05:35 -03:00
7e4684b629 feat: parse config.toml in data folder
Also took the chance to rework parts of the logic that determines
ZBA's save path
2022-09-25 19:05:51 -03:00
85e33e9661 chore: add zig-toml dependency 2022-09-25 19:05:35 -03:00
72 changed files with 5524 additions and 6447 deletions

View File

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

11
.gitignore vendored
View File

@@ -1,8 +1,7 @@
/.vscode
/bin
**/zig-cache
**/.zig-cache
**/zig-out
/zig-cache
/zig-out
/docs
**/*.log
**/*.bin
@@ -12,8 +11,4 @@
/lib/SDL2
# Any Custom Scripts for Debugging purposes
*.sh
# Dear ImGui
**/imgui.ini
*.sh

15
.gitmodules vendored
View File

@@ -0,0 +1,15 @@
[submodule "lib/SDL.zig"]
path = lib/SDL.zig
url = https://github.com/MasterQ32/SDL.zig
[submodule "lib/zig-clap"]
path = lib/zig-clap
url = https://github.com/Hejsil/zig-clap
[submodule "lib/known-folders"]
path = lib/known-folders
url = https://github.com/ziglibs/known-folders
[submodule "lib/zig-datetime"]
path = lib/zig-datetime
url = https://github.com/frmdstryr/zig-datetime
[submodule "lib/zig-toml"]
path = lib/zig-toml
url = https://github.com/aeronavery/zig-toml

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"
]
}

155
README.md
View File

@@ -1,96 +1,69 @@
# ZBA (working title)
An in-progress Game Boy Advance Emulator written in Zig ⚡!
A Game Boy Advance Emulator written in Zig ⚡!
![ZBA running リズム天国](assets/screenshot.png)
## Scope
I'm hardly the first to write a Game Boy Advance Emulator nor will I be the last. This project isn't going to compete with the GOATs like [mGBA](https://github.com/mgba-emu) or [NanoBoyAdvance](https://github.com/nba-emu/NanoBoyAdvance). There aren't any interesting ideas either like in [DSHBA](https://github.com/DenSinH/DSHBA).
This is a simple (read: incomplete) for-fun long-term project. I hope to get "mostly there", which to me means that I'm not missing any major hardware features and the set of possible improvements would be in memory timing or in UI/UX. With respect to that goal, here's what's outstanding:
### TODO
- [x] Affine Sprites
- [ ] Windowing (see [this branch](https://git.musuka.dev/paoda/zba/src/branch/window))
- [ ] Audio Resampler (Having issues with SDL2's)
- [ ] Refactoring for easy-ish perf boosts
## Usage
ZBA supports both a CLI and a GUI. If running from the terminal, try using `zba --help` to see what you can do. If you want to use the GUI, feel free to just run `zba` without any arguments.
ZBA does not feature any BIOS HLE, so providing one will be necessary if a ROM makes use of it. Need one? 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.
## Compiling
Most recently built on Zig [v0.11.0](https://github.com/ziglang/zig/tree/0.11.0)
### Dependencies
Dependency | Source
--- | ---
known-folders | <https://github.com/ziglibs/known-folders>
nfd-zig | <https://github.com/fabioarnold/nfd-zig>
SDL.zig | <https://github.com/MasterQ32/SDL.zig>
tomlz | <https://github.com/mattyhall/tomlz>
zba-gdbstub | <https://github.com/paoda/zba-gdbstub>
zba-util | <https://git.musuka.dev/paoda/zba-util>
zgui | <https://github.com/michal-z/zig-gamedev/tree/main/libs/zgui>
zig-clap | <https://github.com/Hejsil/zig-clap>
zig-datetime | <https://github.com/frmdstryr/zig-datetime>
`bitfield.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>
Use `git submodule update --init` from the project root to pull the git relevant git submodules
Be sure to provide SDL2 using:
- 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.
Once you've got all the dependencies, execute `zig build -Doptimize=ReleaseSafe`. The executable will be under `zig-out/bin` and the shared libraries (if enabled) under `zig-out/lib`. If working with shared libraries on windows, be sure to add all artifacts to the same directory. On Unix, you'll want to make use of `LD_PRELOAD`.
## Controls
Key | Button | | Key | Button
--- | --- | --- | --- | ---
<kbd>A</kbd> | L | | <kbd>S</kbd> | R
<kbd>X</kbd> | A | | <kbd>Z</kbd> | B
<kbd>Return</kbd> | Start | | <kbd>RShift</kbd> | Select
Arrow Keys | D-Pad
## Tests
GBA Tests | [jsmolka](https://github.com/jsmolka/) | gba_tests | [destoer](https://github.com/destoer/)
--- | --- | --- | ---
`arm.gba`, `thumb.gba` | PASS | `cond_invalid.gba` | PASS
`memory.gba`, `bios.gba` | PASS | `dma_priority.gba` | PASS
`flash64.gba`, `flash128.gba` | PASS | `hello_world.gba` | PASS
`sram.gba` | PASS | `if_ack.gba` | PASS
`none.gba` | PASS | `line_timing.gba` | FAIL
`hello.gba`, `shades.gba`, `stripes.gba` | PASS | `lyc_midline.gba` | FAIL
`nes.gba` | PASS | `window_midframe.gba` | FAIL
GBARoms | [DenSinH](https://github.com/DenSinH/) | GBA Test Collection | [ladystarbreeze](https://github.com/ladystarbreeze)
--- | --- | --- | ---
`eeprom-test`, `flash-test` | PASS | `retAddr.gba` | PASS
`midikey2freq` | PASS | `helloWorld.gba` | PASS
`swi-tests-random` | FAIL | `helloAudio.gba` | PASS
FuzzARM | [DenSinH](https://github.com/DenSinH/) | arm7wrestler GBA Fixed | [destoer](https://github.com/destoer)
--- | --- | --- | ---
`main.gba` | PASS | `armwrestler-gba-fixed.gba` | PASS
## Tests
- [ ] [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`
- [ ] `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 [0.10.0-dev.3900+ab4b26d8a](https://github.com/ziglang/zig/tree/ab4b26d8a)
### 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)
* [`bitfields.zig`](https://github.com/FlorenceOS/Florence/blob/aaa5a9e568197ad24780ec9adb421217530d4466/lib/util/bitfields.zig)
`bitfields.zig` from [FlorenceOS](https://github.com/FlorenceOS) is included under `lib/util/bitfield.zig`.
Use `git submodule update --init` from the project root to pull the git submodules `SDL.zig`, `zig-clap`, and `known-folders`
Be sure to provide SDL2 using:
* Linux: Your distro's package manager
* MacOS: ¯\\\_(ツ)_/¯
* 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.
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
<kbd>Z</kbd> | B
<kbd>A</kbd> | L
<kbd>S</kbd> | R
<kbd>Return</kbd> | Start
<kbd>RShift</kbd> | Select
Arrow Keys | D-Pad

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

163
build.zig
View File

@@ -1,154 +1,57 @@
const std = @import("std");
const Sdk = @import("lib/SDL.zig/Sdk.zig");
// Although this function looks imperative, it does not perform the build
// directly and instead it mutates the build graph (`b`) that will be then
// executed by an external runner. The functions in `std.Build` implement a DSL
// for defining build steps and express dependencies between them, allowing the
// build runner to parallelize the build automatically (and the cache system to
// know when a step doesn't need to be re-run).
pub fn build(b: *std.Build) void {
// Standard target options allow the person running `zig build` to choose
pub fn build(b: *std.build.Builder) void {
// 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(.{});
// Standard optimization options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
// set a preferred release mode, allowing the user to decide how to optimize.
const optimize = b.standardOptimizeOption(.{});
// It's also possible to define more custom flags to toggle optional features
// of this build script using `b.option()`. All defined flags (including
// target and optimize options) will be listed when running `zig build --help`
// in this directory.
// This creates a module, which represents a collection of source files alongside
// some compilation options, such as optimization mode and linked system libraries.
// Zig modules are the preferred way of making Zig code available to consumers.
// addModule defines a module that we intend to make available for importing
// to our consumers. We must give it a name because a Zig package can expose
// multiple modules and consumers will need to be able to specify which
// module they want to access.
const exe_mod = b.addModule("zba", .{
// The root source file is the "entry point" of this module. Users of
// this module will only be able to access public declarations contained
// in this file, which means that if you have declarations that you
// intend to expose to consumers that were defined in other files part
// of this module, you will have to make sure to re-export them from
// the root file.
.root_source_file = b.path("src/main.zig"),
// Later on we'll use this module as the root module of a test executable
// which requires us to specify a target.
.target = target,
.link_libc = true,
});
// Standard release options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
const mode = b.standardReleaseOptions();
// Here we define an executable. An executable needs to have a root module
// which needs to expose a `main` function. While we could add a main function
// to the module defined above, it's sometimes preferable to split business
// business logic and the CLI into two separate modules.
//
// If your goal is to create a Zig library for others to use, consider if
// it might benefit from also exposing a CLI tool. A parser library for a
// data serialization format could also bundle a CLI syntax checker, for example.
//
// If instead your goal is to create an executable, consider if users might
// be interested in also being able to embed the core functionality of your
// program in their own executable in order to avoid the overhead involved in
// subprocessing your CLI tool.
//
// If neither case applies to you, feel free to delete the declaration you
// don't need and to put everything under a single module.
const exe = b.addExecutable(.{ .name = "zba", .root_module = exe_mod });
const exe = b.addExecutable("zba", "src/main.zig");
exe.setTarget(target);
const zgui = b.dependency("zgui", .{ .shared = false, .with_implot = true, .backend = .sdl3_opengl3 });
const sdl = b.dependency("sdl", .{ .target = target, .optimize = optimize, .preferred_linkage = .static });
const gl = @import("zigglgen").generateBindingsModule(b, .{ .api = .gl, .version = .@"3.3", .profile = .core });
// Known Folders (%APPDATA%, XDG, etc.)
exe.addPackagePath("known_folders", "lib/known-folders/known-folders.zig");
const sdl_lib = sdl.artifact("SDL3");
const zgui_lib = zgui.artifact("imgui");
// DateTime Library
exe.addPackagePath("datetime", "lib/zig-datetime/src/main.zig");
exe_mod.linkLibrary(sdl_lib);
exe_mod.linkLibrary(zgui_lib);
// Bitfield type from FlorenceOS: https://github.com/FlorenceOS/
// exe.addPackage(.{ .name = "bitfield", .path = .{ .path = "lib/util/bitfield.zig" } });
exe.addPackagePath("bitfield", "lib/util/bitfield.zig");
exe_mod.addImport("gl", gl);
exe_mod.addImport("known_folders", b.dependency("known_folders", .{}).module("known-folders"));
exe_mod.addImport("datetime", b.dependency("datetime", .{}).module("datetime"));
exe_mod.addImport("clap", b.dependency("clap", .{}).module("clap"));
exe_mod.addImport("zba_util", b.dependency("zba_util", .{}).module("zba_util"));
exe_mod.addImport("arm32", b.dependency("arm32", .{}).module("arm32"));
exe_mod.addImport("gdbstub", b.dependency("zba_gdbstub", .{}).module("zba_gdbstub"));
exe_mod.addImport("nfd", b.dependency("nfdzig", .{}).module("nfd"));
exe_mod.addImport("zgui", zgui.module("root"));
exe_mod.addImport("bitjuggle", b.dependency("bitjuggle", .{}).module("bitjuggle"));
// Argument Parsing Library
exe.addPackagePath("clap", "lib/zig-clap/clap.zig");
// This declares intent for the executable to be installed into the
// install prefix when running `zig build` (i.e. when executing the default
// step). By default the install prefix is `zig-out/` but can be overridden
// by passing `--prefix` or `-p`.
b.installArtifact(exe);
// TOML Library
exe.addPackagePath("toml", "lib/zig-toml/src/toml.zig");
// This creates a top level step. Top level steps have a name and can be
// invoked by name when running `zig build` (e.g. `zig build run`).
// This will evaluate the `run` step rather than the default step.
// For a top level step to actually do something, it must depend on other
// steps (e.g. a Run step, as we will see in a moment).
const run_step = b.step("run", "Run the app");
// Zig SDL Bindings: https://github.com/MasterQ32/SDL.zig
const sdk = Sdk.init(b);
sdk.link(exe, .dynamic);
exe.addPackage(sdk.getNativePackage("sdl2"));
// This creates a RunArtifact step in the build graph. A RunArtifact step
// invokes an executable compiled by Zig. Steps will only be executed by the
// runner if invoked directly by the user (in the case of top level steps)
// or if another step depends on it, so it's up to you to define when and
// how this Run step will be executed. In our case we want to run it when
// the user runs `zig build run`, so we create a dependency link.
const run_cmd = b.addRunArtifact(exe);
run_step.dependOn(&run_cmd.step);
exe.setBuildMode(mode);
exe.install();
// By making the run step depend on the default step, it will be run from the
// installation directory rather than directly from within the cache directory.
const run_cmd = exe.run();
run_cmd.step.dependOn(b.getInstallStep());
// This allows the user to pass arguments to the application in the build
// command itself, like this: `zig build run -- arg1 arg2 etc`
if (b.args) |args| {
run_cmd.addArgs(args);
}
// Creates an executable that will run `test` blocks from the provided module.
// Here `mod` needs to define a target, which is why earlier we made sure to
// set the releative field.
const mod_tests = b.addTest(.{
.root_module = exe_mod,
});
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
// A run step that will run the test executable.
const run_mod_tests = b.addRunArtifact(mod_tests);
const exe_tests = b.addTest("src/main.zig");
exe_tests.setTarget(target);
exe_tests.setBuildMode(mode);
// Creates an executable that will run `test` blocks from the executable's
// root module. Note that test executables only test one module at a time,
// hence why we have to create two separate ones.
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});
// A run step that will run the second test executable.
const run_exe_tests = b.addRunArtifact(exe_tests);
// A top level step for running all tests. dependOn can be called multiple
// times and since the two run steps do not depend on one another, this will
// make the two of them run in parallel.
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_mod_tests.step);
test_step.dependOn(&run_exe_tests.step);
// Just like flags, top level steps are also listed in the `--help` menu.
//
// The Zig build system is entirely implemented in userland, which means
// that it cannot hook into private compiler APIs. All compilation work
// orchestrated by the build system will result in other Zig compiler
// subcommands being invoked with the right flags defined. You can observe
// these invocations when one fails (or you pass a flag to increase
// verbosity) to validate assumptions and diagnose problems.
//
// Lastly, the Zig build system is relatively simple and self-contained,
// and reading its source code will allow you to master it.
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&exe_tests.step);
}

View File

@@ -1,55 +0,0 @@
.{
.name = .zba,
.version = "0.1.0",
.paths = .{
"build.zig",
"build.zig.zon",
"lib/bitfield.zig",
"lib/gl.zig",
"src",
},
.minimum_zig_version = "0.15.1",
.fingerprint = 0xcb596c7fbdb20efc,
.dependencies = .{
// .tomlz = .{
// .url = "git+https://github.com/paoda/tomlz#9a16dd53927ef2012478b6494bafb4475e44f4c9",
// .hash = "12204f922cab84980e36b5c058d354ec0ee169bda401c8e0e80a463580349b476569",
// },
.zba_util = .{ .path = "../zba-util" },
.arm32 = .{ .path = "../arm32" },
.zba_gdbstub = .{ .path = "../zba-gdbstub" },
.known_folders = .{
.url = "git+https://github.com/ziglibs/known-folders.git#ab5cf5feb936fa3b72c95d3ad0c0c67791937ba1",
.hash = "known_folders-0.0.0-Fy-PJtTTAADUOhGKM0sxzG4eMkNQxRvx9e5dfHVyaeA3",
},
.nfdzig = .{
.url = "git+https://github.com/paoda/nfd-zig#0ad2a0c092ffba0c98613d619b82100c991f5ad6",
.hash = "nfdzig-0.1.0-11fxvN6IBgD5rvvfjrw1wPqibMsbUJ-h2ZcGR6FOEvrm",
},
.datetime = .{
.url = "git+https://github.com/frmdstryr/zig-datetime#3a39a21e6e34dcb0ade0ff828d0914d40ba535f3",
.hash = "datetime-0.8.0-cJNXzP_YAQBxQ5hkNNP6ScnG5XsqciJmeP5RVV4xwCBA",
},
.clap = .{
.url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.11.0.tar.gz",
.hash = "clap-0.11.0-oBajB-HnAQDPCKYzwF7rO3qDFwRcD39Q0DALlTSz5H7e",
},
.sdl = .{
.url = "git+https://github.com/castholm/SDL.git#b1913e7c31ad72ecfd3ab04aeac387027754cfaf",
.hash = "sdl-0.3.0+3.2.22-7uIn9Pg3fwGG2IyIOPxxOSVe-75nUng9clt7tXGFLzMr",
},
.zigglgen = .{
.url = "git+https://github.com/castholm/zigglgen.git#a1e969b3e35818785fab0373124f50463793b28a",
.hash = "zigglgen-0.4.0-bmyqLQGMLwA0EPVmSm-Nc6Olb84zBzvwMjguxwKYLf1S",
},
.bitjuggle = .{
.url = "git+https://github.com/leecannon/zig-bitjuggle#80111f4f8c672aaea94a8a189ae2a7c8bbaf883f",
.hash = "bitjuggle-2.0.0-SJdU76dvAAARompHEhqKDiwZ4FE4FZ8eHvPvmz5JUOS0",
},
.zgui = .{
.url = "git+https://github.com/zig-gamedev/zgui#7fa8081c208885b85e3fdfc043cd9d9cb9559123",
.hash = "zgui-0.6.0-dev--L6sZL7tbQAPRLYrcQAVx0V49tPHAXNxclZ-v8IP4wLr",
},
},
}

View File

@@ -1,36 +0,0 @@
$SDL2Version = "2.30.0"
$ArchiveFile = ".\SDL2-devel-mingw.zip"
$Json = @"
{
"x86_64-windows-gnu": {
"include": ".build_config\\SDL2\\include",
"libs": ".build_config\\SDL2\\lib",
"bin": ".build_config\\SDL2\\bin"
}
}
"@
New-Item -Force -ItemType Directory -Path .\.build_config
Set-Location -Path .build_config -PassThru
if (!(Test-Path -PathType Leaf $ArchiveFile)) {
Invoke-WebRequest "https://github.com/libsdl-org/SDL/releases/download/release-$SDL2Version/SDL2-devel-$SDL2Version-mingw.zip" -OutFile $ArchiveFile
}
Expand-Archive $ArchiveFile
if (Test-Path -PathType Container .\SDL2) {
Remove-Item -Recurse .\SDL2
}
New-Item -Force -ItemType Directory -Path .\SDL2
Get-ChildItem -Path ".\SDL2-devel-mingw\SDL2-$SDL2Version\x86_64-w64-mingw32" | Move-Item -Destination .\SDL2
# #include <SDL.h>
Move-Item -Force -Path .\SDL2\include\SDL2\* -Destination .\SDL2\include
Remove-Item -Force .\SDL2\include\SDL2
New-Item -Force .\sdl.json -Value $Json
Remove-Item -Recurse .\SDL2-devel-mingw
Set-Location -Path .. -PassThru

View File

@@ -1,25 +0,0 @@
[host]
# Using nearest-neighbour scaling, how many times the native resolution
# of the game bow should the screen be?
win_scale = 3
# Enable VSYNC on the UI thread
vsync = true
# Mute ZBA
mute = false
[guest]
# Sync Emulation to Audio
audio_sync = true
# Sync Emulation to Video
video_sync = true
# Force RTC support
force_rtc = false
# Skip BIOS
skip_bios = false
[debug]
# Enable detailed CPU logs
cpu_trace = false
# When false and builtin.mode == .Debug, ZBA will panic
# on unknown I/O reads
unhandled_io = true

1
lib/SDL.zig Submodule

Submodule lib/SDL.zig added at 6a9e37687a

1
lib/known-folders Submodule

Submodule lib/known-folders added at 24845b0103

146
lib/util/bitfield.zig Normal file
View File

@@ -0,0 +1,146 @@
const std = @import("std");
fn PtrCastPreserveCV(comptime T: type, comptime PtrToT: type, comptime NewT: type) type {
return switch (PtrToT) {
*T => *NewT,
*const T => *const NewT,
*volatile T => *volatile NewT,
*const volatile T => *const volatile NewT,
else => @compileError("wtf you doing"),
};
}
fn BitType(comptime FieldType: type, comptime ValueType: type, comptime shamt: usize) type {
const self_bit: FieldType = (1 << shamt);
return extern struct {
bits: Bitfield(FieldType, shamt, 1),
pub fn set(self: anytype) void {
self.bits.field().* |= self_bit;
}
pub fn unset(self: anytype) void {
self.bits.field().* &= ~self_bit;
}
pub fn read(self: anytype) ValueType {
return @bitCast(ValueType, @truncate(u1, self.bits.field().* >> shamt));
}
// Since these are mostly used with MMIO, I want to avoid
// reading the memory just to write it again, also races
pub fn write(self: anytype, val: ValueType) void {
if (@bitCast(bool, val)) {
self.set();
} else {
self.unset();
}
}
};
}
// Original Bit Constructor
// pub fn Bit(comptime FieldType: type, comptime shamt: usize) type {
// return BitType(FieldType, u1, shamt);
// }
pub fn Bit(comptime FieldType: type, comptime shamt: usize) type {
return BitType(FieldType, bool, shamt);
}
fn Boolean(comptime FieldType: type, comptime shamt: usize) type {
return BitType(FieldType, bool, shamt);
}
pub fn Bitfield(comptime FieldType: type, comptime shamt: usize, comptime num_bits: usize) type {
if (shamt + num_bits > @bitSizeOf(FieldType)) {
@compileError("bitfield doesn't fit");
}
const self_mask: FieldType = ((1 << num_bits) - 1) << shamt;
const ValueType = std.meta.Int(.unsigned, num_bits);
return extern struct {
dummy: FieldType,
fn field(self: anytype) PtrCastPreserveCV(@This(), @TypeOf(self), FieldType) {
return @ptrCast(PtrCastPreserveCV(@This(), @TypeOf(self), FieldType), self);
}
pub fn write(self: anytype, val: ValueType) void {
self.field().* &= ~self_mask;
self.field().* |= @intCast(FieldType, val) << shamt;
}
pub fn read(self: anytype) ValueType {
const val: FieldType = self.field().*;
return @intCast(ValueType, (val & self_mask) >> shamt);
}
};
}
test "bit" {
const S = extern union {
low: Bit(u32, 0),
high: Bit(u32, 1),
val: u32,
};
std.testing.expect(@sizeOf(S) == 4);
std.testing.expect(@bitSizeOf(S) == 32);
var s: S = .{ .val = 1 };
std.testing.expect(s.low.read() == 1);
std.testing.expect(s.high.read() == 0);
s.low.write(0);
s.high.write(1);
std.testing.expect(s.val == 2);
}
test "boolean" {
const S = extern union {
low: Boolean(u32, 0),
high: Boolean(u32, 1),
val: u32,
};
std.testing.expect(@sizeOf(S) == 4);
std.testing.expect(@bitSizeOf(S) == 32);
var s: S = .{ .val = 2 };
std.testing.expect(s.low.read() == false);
std.testing.expect(s.high.read() == true);
s.low.write(true);
s.high.write(false);
std.testing.expect(s.val == 1);
}
test "bitfield" {
const S = extern union {
low: Bitfield(u32, 0, 16),
high: Bitfield(u32, 16, 16),
val: u32,
};
std.testing.expect(@sizeOf(S) == 4);
std.testing.expect(@bitSizeOf(S) == 32);
var s: S = .{ .val = 0x13376969 };
std.testing.expect(s.low.read() == 0x6969);
std.testing.expect(s.high.read() == 0x1337);
s.low.write(0x1337);
s.high.write(0x6969);
std.testing.expect(s.val == 0x69691337);
}

1
lib/zig-clap Submodule

Submodule lib/zig-clap added at 4f4196fc3b

1
lib/zig-datetime Submodule

Submodule lib/zig-datetime added at 5ec1c36cf3

1
lib/zig-toml Submodule

Submodule lib/zig-toml added at 5dfa919e03

View File

@@ -1,13 +1,11 @@
const std = @import("std");
// const tomlz = @import("tomlz");
const toml = @import("toml");
const Allocator = std.mem.Allocator;
const log = std.log.scoped(.Config);
var state: Config = .{};
const Config = struct {
// FIXME: tomlz expects these to be case sensitive
host: Host = .{},
guest: Guest = .{},
debug: Debug = .{},
@@ -20,8 +18,6 @@ const Config = struct {
///
/// Note: This does not affect whether Emulation is synced to 59Hz
vsync: bool = true,
/// Mute ZBA
mute: bool = false,
};
// Settings realted to the emulation itself
@@ -32,15 +28,13 @@ const Config = struct {
video_sync: bool = true,
/// Whether RTC I/O should always be enabled
force_rtc: bool = false,
/// Skip BIOS
skip_bios: bool = false,
};
/// Settings related to debugging ZBA
const Debug = struct {
/// Enable CPU Trace logs
cpu_trace: bool = false,
/// If false and ZBA is built in debug mode, ZBA will panic on unhandled I/O
/// If false and ZBA is build in debug mode, ZBA will panic on unhandled I/O
unhandled_io: bool = true,
};
};
@@ -50,15 +44,31 @@ 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});
const contents = try config_file.readToEndAlloc(allocator, try config_file.getEndPos());
defer allocator.free(contents);
// FIXME(2025-09-22): re-enable
// state = try tomlz.parser.decode(Config, allocator, contents);
const table = try toml.parseContents(allocator, contents, null);
defer table.deinit();
// TODO: Report unknown config options
if (table.keys.get("Host")) |host| {
if (host.Table.keys.get("win_scale")) |scale| state.host.win_scale = scale.Integer;
if (host.Table.keys.get("vsync")) |vsync| state.host.vsync = vsync.Boolean;
}
if (table.keys.get("Guest")) |guest| {
if (guest.Table.keys.get("audio_sync")) |sync| state.guest.audio_sync = sync.Boolean;
if (guest.Table.keys.get("video_sync")) |sync| state.guest.video_sync = sync.Boolean;
if (guest.Table.keys.get("force_rtc")) |forced| state.guest.force_rtc = forced.Boolean;
}
if (table.keys.get("Debug")) |debug| {
if (debug.Table.keys.get("cpu_trace")) |trace| state.debug.cpu_trace = trace.Boolean;
if (debug.Table.keys.get("unhandled_io")) |unhandled| state.debug.unhandled_io = unhandled.Boolean;
}
}

View File

@@ -1,6 +1,7 @@
const std = @import("std");
const Arm7tdmi = @import("arm32").Arm7tdmi;
const AudioDeviceId = @import("sdl2").SDL_AudioDeviceID;
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
const Bios = @import("bus/Bios.zig");
const Ewram = @import("bus/Ewram.zig");
const GamePak = @import("bus/GamePak.zig");
@@ -13,13 +14,13 @@ const TimerTuple = @import("bus/timer.zig").TimerTuple;
const Scheduler = @import("scheduler.zig").Scheduler;
const FilePaths = @import("../util.zig").FilePaths;
const _io = @import("bus/io.zig");
const io = @import("bus/io.zig");
const Allocator = std.mem.Allocator;
const log = std.log.scoped(.Bus);
const createDmaTuple = @import("bus/dma.zig").create;
const createTimerTuple = @import("bus/timer.zig").create;
const rotr = @import("zba_util").rotr;
const rotr = @import("../util.zig").rotr;
const timings: [2][0x10]u8 = [_][0x10]u8{
// BIOS, Unused, EWRAM, IWRAM, I/0, PALRAM, VRAM, OAM, ROM0, ROM0, ROM1, ROM1, ROM2, ROM2, SRAM, Unused
@@ -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,
@@ -50,19 +46,10 @@ iwram: Iwram,
ewram: Ewram,
io: Io,
cpu: *Arm7tdmi,
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(@ptrCast(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,441 +71,171 @@ 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(@as([*]const ?*anyopaque, @ptrCast(self.read_table[0..]))[0 .. 3 * table_len]);
self.* = undefined;
}
pub fn reset(self: *Self) void {
self.bios.reset();
self.ppu.reset();
self.apu.reset();
self.iwram.reset();
self.ewram.reset();
pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
const page = @truncate(u8, address >> 24);
const aligned_addr = forceAlign(T, address);
// https://github.com/ziglang/zig/issues/14705
{
comptime var i: usize = 0;
inline while (i < self.dma.len) : (i += 1) {
self.dma[0].reset();
}
}
return switch (page) {
// General Internal Memory
0x00 => blk: {
if (address < Bios.size)
break :blk self.bios.dbgRead(T, self.cpu.?.r[15], aligned_addr);
// https://github.com/ziglang/zig/issues/14705
{
comptime var i: usize = 0;
inline while (i < self.tim.len) : (i += 1) {
self.tim[0].reset();
}
}
break :blk self.readOpenBus(T, address);
},
0x02 => self.ewram.read(T, aligned_addr),
0x03 => self.iwram.read(T, aligned_addr),
0x04 => self.readIo(T, address),
self.io.reset();
// 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)
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.readOpenBus(T, address),
};
}
pub fn replaceGamepak(self: *Self, file_path: []const u8) !void {
// Note: `save_path` isn't owned by `Backup`
const save_path = self.pak.backup.save_path;
self.pak.deinit();
self.pak = try GamePak.init(self.allocator, self.cpu, file_path, save_path);
// SAFETY: TODO: why do we know this is safe?
self.fillReadTable(@ptrCast(@constCast(self.read_table)));
self.fillWriteTable(u32, @constCast(self.write_tables[0]));
self.fillWriteTable(u8, @constCast(self.write_tables[1]));
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.readOpenBus(T, unaligned_address);
}
fn fillReadTable(self: *Self, table: *[table_len]?*const anyopaque) void {
const vramMirror = @import("ppu/Vram.zig").mirror;
for (table, 0..) |*ptr, i| {
const addr: u32 = @intCast(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 => &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],
// External Memory (Game Pak)
0x0800_0000...0x0DFF_FFFF => self.fillReadTableExternal(addr),
0x0E00_0000...0x0FFF_FFFF => null, // SRAM
else => null,
};
}
}
fn fillWriteTable(self: *Self, comptime T: type, table: *[table_len]?*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: u32 = @intCast(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 (@as(u4, @truncate(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 openBus(self: *const Self, comptime T: type, address: u32) T {
@branchHint(.cold);
const r15 = self.cpu.r[15];
fn readOpenBus(self: *const Self, comptime T: type, address: u32) T {
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: u8 = @truncate(r15 >> 24);
// PC + 2 = stage[0]
// PC + 4 = stage[1]
// PC + 6 = Need a Debug Read for this?
// If u32 Open Bus, read recently fetched opcode (PC + 8)
if (!self.cpu.?.cpsr.t.read()) break :blk self.dbgRead(u32, r15 + 4);
const page = @truncate(u8, r15 >> 24);
switch (page) {
// EWRAM, PALRAM, VRAM, and Game ROM (16-bit)
0x02, 0x05, 0x06, 0x08...0x0D => {
const halfword: u32 = @as(u16, @truncate(self.cpu.pipe.stage[1].?));
break :blk halfword << 16 | halfword;
},
// (PC + 4)
const halfword = self.dbgRead(u16, r15 + 2);
break :blk @as(u32, halfword) << 16 | halfword;
},
// BIOS or OAM (32-bit)
0x00, 0x07 => {
// Aligned: (PC + 6) | (PC + 4)
// Unaligned: (PC + 4) | (PC + 2)
const aligned = address & 3 == 0b00;
const offset: u32 = if (address & 3 == 0b00) 2 else 0;
// TODO: What to do on PC + 6?
const high: u32 = if (aligned) self.dbgRead(u16, r15 + 4) else @as(u16, @truncate(self.cpu.pipe.stage[1].?));
const low: u32 = @as(u16, @truncate(self.cpu.pipe.stage[@intFromBool(aligned)].?));
break :blk high << 16 | low;
break :blk @as(u32, self.dbgRead(u16, r15 + 2 + offset)) << 16 | self.dbgRead(u16, r15 + offset);
},
// IWRAM (16-bit but special)
0x03 => {
// Aligned: (PC + 2) | (PC + 4)
// Unaligned: (PC + 4) | (PC + 2)
const aligned = address & 3 == 0b00;
const offset: u32 = if (address & 3 == 0b00) 2 else 0;
const high: u32 = @as(u16, @truncate(self.cpu.pipe.stage[1 - @intFromBool(aligned)].?));
const low: u32 = @as(u16, @truncate(self.cpu.pipe.stage[@intFromBool(aligned)].?));
break :blk high << 16 | low;
},
else => {
log.err("THUMB open bus read from 0x{X:0>2} page @0x{X:0>8}", .{ page, address });
@panic("invariant most-likely broken");
break :blk @as(u32, self.dbgRead(u16, r15 + 2 - offset)) << 16 | self.dbgRead(u16, r15 + offset);
},
else => unreachable,
}
};
return @truncate(word);
return @truncate(T, rotr(u32, word, 8 * (address & 3)));
}
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[@intFromBool(T == u32)][@as(u4, @truncate(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 = @ptrCast(@alignCast(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 = @ptrCast(@alignCast(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 {
@branchHint(.cold);
const page: u8 = @truncate(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);
break :blk self.readOpenBus(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),
else => self.openBus(T, address),
};
}
0x08...0x0D => self.pak.read(T, aligned_addr),
0x0E...0x0F => blk: {
const value = self.pak.backup.read(address);
fn dbgSlowRead(self: *const Self, comptime T: type, unaligned_address: u32) T {
const page: u8 = @truncate(unaligned_address >> 24);
const address = forceAlign(T, unaligned_address);
const multiplier = switch (T) {
u32 => 0x01010101,
u16 => 0x0101,
u8 => 1,
else => @compileError("Backup: Unsupported read width"),
};
return switch (page) {
// General Internal Memory
0x00 => blk: {
if (address < Bios.size)
break :blk self.bios.dbgRead(T, self.cpu.r[15], unaligned_address);
break :blk self.openBus(T, address);
break :blk @as(T, value) * multiplier;
},
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),
else => self.readOpenBus(T, address),
};
}
fn readBackup(self: *const Self, comptime T: type, unaligned_address: u32) T {
const value = self.pak.backup.read(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);
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[@intFromBool(T == u32)][@as(u4, @truncate(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[@intFromBool(T == u8)][page]) |some_ptr| {
// We have a pointer to a page, cast the pointer to it's underlying type
const ptr: [*]T = @ptrCast(@alignCast(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 @as(u8, @truncate(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[@intFromBool(T == u8)][page]) |some_ptr| {
// We have a pointer to a page, cast the pointer to it's underlying type
const ptr: [*]T = @ptrCast(@alignCast(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 @as(u8, @truncate(unaligned_address >> 24)) == 0x07) return;
self.dbgSlowWrite(T, unaligned_address, value);
}
}
fn slowWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
@branchHint(.cold);
const page: u8 = @truncate(unaligned_address >> 24);
const address = forceAlign(T, unaligned_address);
self.sched.tick += timings[@boolToInt(T == u32)][@truncate(u4, page)];
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),
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),
// 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
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, address, value),
0x0E...0x0F => self.pak.backup.write(unaligned_address, @truncate(rotr(T, value, 8 * rotateBy(T, unaligned_address)))),
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)));
},
else => {},
}
}
fn dbgSlowWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
@branchHint(.cold);
const page: u8 = @truncate(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"),
};

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,9 +3,6 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const log = std.log.scoped(.Bios);
const rotr = @import("zba_util").rotr;
const forceAlign = @import("../Bus.zig").forceAlign;
/// Size of the BIOS in bytes
pub const size = 0x4000;
const Self = @This();
@@ -13,80 +10,55 @@ const Self = @This();
buf: ?[]u8,
allocator: Allocator,
addr_latch: u32 = 0,
// https://github.com/ITotalJustice/notorious_beeg/issues/106
pub fn read(self: *Self, comptime T: type, r15: u32, address: 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(rotr(u32, value, 8 * rotateBy(T, address)));
}
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(rotr(u32, value, 8 * rotateBy(T, address)));
}
/// Read without the GBA safety checks
fn _read(self: *const Self, comptime T: type, addr: u32) T {
const buf = self.buf orelse std.debug.panic("[BIOS] ZBA tried to read {} from 0x{X:0>8} but not BIOS was present", .{ T, addr });
return switch (T) {
u32, u16, u8 => std.mem.readInt(T, buf[addr..][0..@sizeOf(T)], .little),
else => @compileError("BIOS: Unsupported read width"),
};
}
pub fn write(_: *Self, comptime T: type, addr: u32, value: T) void {
@branchHint(.cold);
log.debug("Tried to write {} 0x{X:} to 0x{X:0>8} ", .{ T, value, addr });
}
addr_latch: u32,
pub fn init(allocator: Allocator, maybe_path: ?[]const u8) !Self {
if (maybe_path == null) return .{ .buf = null, .allocator = allocator };
const file_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;
var self: Self = .{ .buf = buf, .allocator = allocator };
try self.load(file_path);
return self;
}
pub fn load(self: *Self, file_path: []const u8) !void {
const file = try std.fs.cwd().openFile(file_path, .{});
defer file.close();
const len = try file.readAll(self.buf orelse return error.UnallocatedBuffer);
if (len != Self.size) log.err("Expected BIOS to be {}B, was {}B", .{ Self.size, len });
}
pub fn reset(self: *Self) void {
self.addr_latch = 0;
return Self{
.buf = buf,
.allocator = allocator,
.addr_latch = 0,
};
}
pub fn deinit(self: *Self) void {
if (self.buf) |buf| self.allocator.free(buf);
self.* = undefined;
}
pub fn read(self: *Self, comptime T: type, r15: u32, addr: u32) T {
if (r15 < Self.size) {
self.addr_latch = addr;
return self.uncheckedRead(T, addr);
}
log.debug("Rejected read since r15=0x{X:0>8}", .{r15});
return @truncate(T, self.uncheckedRead(T, self.addr_latch + 8));
}
pub fn dbgRead(self: *const Self, comptime T: type, r15: u32, addr: u32) T {
if (r15 < Self.size) return self.uncheckedRead(T, addr);
return @truncate(T, self.uncheckedRead(T, self.addr_latch + 8));
}
fn uncheckedRead(self: *const Self, comptime T: type, addr: u32) T {
if (self.buf) |buf| {
return switch (T) {
u32, u16, u8 => std.mem.readIntSliceLittle(T, buf[addr..][0..@sizeOf(T)]),
else => @compileError("BIOS: Unsupported read width"),
};
}
std.debug.panic("[BIOS] ZBA tried to read {} from 0x{X:0>8} but not BIOS was present", .{ T, addr });
}
pub fn write(_: *Self, comptime T: type, addr: u32, value: T) void {
@setCold(true);
log.debug("Tried to write {} 0x{X:} to 0x{X:0>8} ", .{ T, value, addr });
}

View File

@@ -7,11 +7,26 @@ const Self = @This();
buf: []u8,
allocator: Allocator,
pub fn init(allocator: Allocator) !Self {
const buf = try allocator.alloc(u8, ewram_size);
std.mem.set(u8, buf, 0);
return Self{
.buf = buf,
.allocator = allocator,
};
}
pub fn deinit(self: *Self) void {
self.allocator.free(self.buf);
self.* = undefined;
}
pub fn read(self: *const Self, comptime T: type, address: usize) T {
const addr = address & 0x3FFFF;
return switch (T) {
u32, u16, u8 => std.mem.readInt(T, self.buf[addr..][0..@sizeOf(T)], .little),
u32, u16, u8 => std.mem.readIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)]),
else => @compileError("EWRAM: Unsupported read width"),
};
}
@@ -20,26 +35,7 @@ pub fn write(self: *const Self, comptime T: type, address: usize, value: T) void
const addr = address & 0x3FFFF;
return switch (T) {
u32, u16, u8 => std.mem.writeInt(T, self.buf[addr..][0..@sizeOf(T)], value, .little),
u32, u16, u8 => std.mem.writeIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)], value),
else => @compileError("EWRAM: Unsupported write width"),
};
}
pub fn init(allocator: Allocator) !Self {
const buf = try allocator.alloc(u8, ewram_size);
@memset(buf, 0);
return Self{
.buf = buf,
.allocator = allocator,
};
}
pub fn reset(self: *Self) void {
@memset(self.buf, 0);
}
pub fn deinit(self: *Self) void {
self.allocator.free(self.buf);
self.* = undefined;
}

View File

@@ -1,7 +1,11 @@
const std = @import("std");
const config = @import("../../config.zig");
const Arm7tdmi = @import("arm32").Arm7tdmi;
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;
const Allocator = std.mem.Allocator;
@@ -16,11 +20,78 @@ allocator: Allocator,
backup: Backup,
gpio: *Gpio,
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, rom_path: []const u8, save_path: ?[]const u8) !Self {
const file = try std.fs.cwd().openFile(rom_path, .{});
defer file.close();
const file_buf = try file.readToEndAlloc(allocator, try file.getEndPos());
const title = file_buf[0xA0..0xAC].*;
const kind = Backup.guessKind(file_buf);
const device = if (config.config().guest.force_rtc) .Rtc else guessDevice(file_buf);
logHeader(file_buf, &title);
return .{
.buf = file_buf,
.allocator = allocator,
.title = title,
.backup = try Backup.init(allocator, kind, title, save_path),
.gpio = try Gpio.init(allocator, cpu, device),
};
}
/// Searches the ROM to see if it can determine whether the ROM it's searching uses
/// any GPIO device, like a RTC for example.
fn guessDevice(buf: []const u8) Gpio.Device.Kind {
// Try to Guess if ROM uses RTC
const needle = "RTC_V"; // I was told SIIRTC_V, though Pokemen Firered (USA) is a false negative
var i: usize = 0;
while ((i + needle.len) < buf.len) : (i += 1) {
if (std.mem.eql(u8, needle, buf[i..(i + needle.len)])) return .Rtc;
}
// TODO: Detect other GPIO devices
return .None;
}
fn logHeader(buf: []const u8, title: *const [12]u8) void {
const code = buf[0xAC..0xB0];
const maker = buf[0xB0..0xB2];
const version = buf[0xBC];
log.info("Title: {s}", .{title});
if (version != 0) log.info("Version: {}", .{version});
log.info("Game Code: {s}", .{code});
if (lookupMaker(maker)) |c| log.info("Maker: {s}", .{c}) else log.info("Maker Code: {s}", .{maker});
}
fn lookupMaker(slice: *const [2]u8) ?[]const u8 {
const id = @as(u16, slice[1]) << 8 | @as(u16, slice[0]);
return switch (id) {
0x3130 => "Nintendo",
else => null,
};
}
inline fn isLarge(self: *const Self) bool {
return self.buf.len > 0x100_0000;
}
pub fn deinit(self: *Self) void {
self.backup.deinit();
self.gpio.deinit(self.allocator);
self.allocator.destroy(self.gpio);
self.allocator.free(self.buf);
self.* = undefined;
}
pub fn read(self: *Self, comptime T: type, address: u32) T {
const addr = address & 0x1FF_FFFF;
if (self.backup.kind == .Eeprom) {
if (self.buf.len > 0x100_0000) { // Large
if (self.isLarge()) {
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
// * Backup type is EEPROM
// * Large ROM (Size is greater than 16MB)
@@ -30,7 +101,7 @@ pub fn read(self: *Self, comptime T: type, address: u32) T {
// Addresses 0x0D00_0000 to 0x0DFF_FFFF are reserved for EEPROM accesses if
// * Backup type is EEPROM
// * Small ROM (less than 16MB)
if (@as(u8, @truncate(address >> 24)) == 0x0D)
if (@truncate(u8, address >> 24) == 0x0D)
return self.backup.eeprom.read();
}
}
@@ -72,19 +143,11 @@ pub fn read(self: *Self, comptime T: type, address: u32) T {
};
}
inline fn get(self: *const Self, i: u32) u8 {
@setRuntimeSafety(false);
if (i < self.buf.len) return self.buf[i];
const lhs = i >> 1 & 0xFFFF;
return @truncate(lhs >> 8 * @as(u5, @truncate(i & 1)));
}
pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
const addr = address & 0x1FF_FFFF;
if (self.backup.kind == .Eeprom) {
if (self.buf.len > 0x100_0000) { // Large
if (self.isLarge()) {
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
// * Backup type is EEPROM
// * Large ROM (Size is greater than 16MB)
@@ -94,39 +157,11 @@ pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
// Addresses 0x0D00_0000 to 0x0DFF_FFFF are reserved for EEPROM accesses if
// * Backup type is EEPROM
// * Small ROM (less than 16MB)
if (@as(u8, @truncate(address >> 24)) == 0x0D)
if (@truncate(u8, address >> 24) == 0x0D)
return self.backup.eeprom.dbgRead();
}
}
if (self.gpio.cnt == 1) {
// GPIO Can be read from
// We assume that this will only be true when a ROM actually does want something from GPIO
switch (T) {
u32 => switch (address) {
// FIXME: 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) {
0x0800_00C4 => return self.gpio.read(.Data),
0x0800_00C6 => return self.gpio.read(.Direction),
0x0800_00C8 => return self.gpio.read(.Control),
else => {},
},
u8 => switch (address) {
0x0800_00C4 => return self.gpio.read(.Data),
0x0800_00C6 => return self.gpio.read(.Direction),
0x0800_00C8 => return self.gpio.read(.Control),
else => {},
},
else => @compileError("GamePak[GPIO]: Unsupported read width"),
}
}
return switch (T) {
u32 => (@as(T, self.get(addr + 3)) << 24) | (@as(T, self.get(addr + 2)) << 16) | (@as(T, self.get(addr + 1)) << 8) | (@as(T, self.get(addr))),
u16 => (@as(T, self.get(addr + 1)) << 8) | @as(T, self.get(addr)),
@@ -139,9 +174,9 @@ pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value
const addr = address & 0x1FF_FFFF;
if (self.backup.kind == .Eeprom) {
const bit: u1 = @truncate(value);
const bit = @truncate(u1, value);
if (self.buf.len > 0x100_0000) { // Large
if (self.isLarge()) {
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
// * Backup type is EEPROM
// * Large ROM (Size is greater than 16MB)
@@ -151,7 +186,7 @@ pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value
// Addresses 0x0D00_0000 to 0x0DFF_FFFF are reserved for EEPROM accesses if
// * Backup type is EEPROM
// * Small ROM (less than 16MB)
if (@as(u8, @truncate(address >> 24)) == 0x0D)
if (@truncate(u8, address >> 24) == 0x0D)
return self.backup.eeprom.write(word_count, &self.backup.buf, bit);
}
}
@@ -159,19 +194,19 @@ pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value
switch (T) {
u32 => switch (address) {
0x0800_00C4 => {
self.gpio.write(.Data, @as(u4, @truncate(value)));
self.gpio.write(.Direction, @as(u4, @truncate(value >> 16)));
self.gpio.write(.Data, @truncate(u4, value));
self.gpio.write(.Direction, @truncate(u4, value >> 16));
},
0x0800_00C6 => {
self.gpio.write(.Direction, @as(u4, @truncate(value)));
self.gpio.write(.Control, @as(u1, @truncate(value >> 16)));
self.gpio.write(.Direction, @truncate(u4, value));
self.gpio.write(.Control, @truncate(u1, value >> 16));
},
else => log.err("Wrote {} 0x{X:0>8} to 0x{X:0>8}, Unhandled", .{ T, value, address }),
},
u16 => switch (address) {
0x0800_00C4 => self.gpio.write(.Data, @as(u4, @truncate(value))),
0x0800_00C6 => self.gpio.write(.Direction, @as(u4, @truncate(value))),
0x0800_00C8 => self.gpio.write(.Control, @as(u1, @truncate(value))),
0x0800_00C4 => self.gpio.write(.Data, @truncate(u4, value)),
0x0800_00C6 => self.gpio.write(.Direction, @truncate(u4, value)),
0x0800_00C8 => self.gpio.write(.Control, @truncate(u1, value)),
else => log.err("Wrote {} 0x{X:0>4} to 0x{X:0>8}, Unhandled", .{ T, value, address }),
},
u8 => log.debug("Wrote {} 0x{X:0>2} to 0x{X:0>8}, Ignored.", .{ T, value, address }),
@@ -179,65 +214,12 @@ pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value
}
}
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, maybe_rom: ?[]const u8, maybe_save: ?[]const u8) !Self {
const Device = Gpio.Device;
fn get(self: *const Self, i: u32) u8 {
@setRuntimeSafety(false);
if (i < self.buf.len) return self.buf[i];
const items: struct { []u8, [12]u8, Backup.Kind, Device.Kind } = if (maybe_rom) |file_path| blk: {
const file = try std.fs.cwd().openFile(file_path, .{});
defer file.close();
const buffer = try file.readToEndAlloc(allocator, try file.getEndPos());
const title = buffer[0xA0..0xAC];
logHeader(buffer, title);
const device_kind = if (config.config().guest.force_rtc) .Rtc else guessDevice(buffer);
break :blk .{ buffer, title.*, Backup.guess(buffer), device_kind };
} else .{ try allocator.alloc(u8, 0), [_]u8{0} ** 12, .None, .None };
const title = items[1];
return .{
.buf = items[0],
.allocator = allocator,
.title = title,
.backup = try Backup.init(allocator, items[2], title, maybe_save),
.gpio = try Gpio.init(allocator, cpu, items[3]),
};
}
pub fn deinit(self: *Self) void {
self.backup.deinit();
self.gpio.deinit(self.allocator);
self.allocator.destroy(self.gpio);
self.allocator.free(self.buf);
self.* = undefined;
}
/// Searches the ROM to see if it can determine whether the ROM it's searching uses
/// any GPIO device, like a RTC for example.
fn guessDevice(buf: []const u8) Gpio.Device.Kind {
// Try to Guess if ROM uses RTC
const needle = "RTC_V"; // I was told SIIRTC_V, though Pokemen Firered (USA) is a false negative
// 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;
}
// TODO: Detect other GPIO devices
return .None;
}
fn logHeader(buf: []const u8, title: *const [12]u8) void {
const version = buf[0xBC];
log.info("Title: {s}", .{title});
if (version != 0) log.info("Version: {}", .{version});
log.info("Game Code: {s}", .{buf[0xAC..0xB0]});
log.info("Maker Code: {s}", .{buf[0xB0..0xB2]});
const lhs = i >> 1 & 0xFFFF;
return @truncate(u8, lhs >> 8 * @truncate(u5, i & 1));
}
test "OOB Access" {

View File

@@ -7,11 +7,26 @@ const Self = @This();
buf: []u8,
allocator: Allocator,
pub fn init(allocator: Allocator) !Self {
const buf = try allocator.alloc(u8, iwram_size);
std.mem.set(u8, buf, 0);
return Self{
.buf = buf,
.allocator = allocator,
};
}
pub fn deinit(self: *Self) void {
self.allocator.free(self.buf);
self.* = undefined;
}
pub fn read(self: *const Self, comptime T: type, address: usize) T {
const addr = address & 0x7FFF;
return switch (T) {
u32, u16, u8 => std.mem.readInt(T, self.buf[addr..][0..@sizeOf(T)], .little),
u32, u16, u8 => std.mem.readIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)]),
else => @compileError("IWRAM: Unsupported read width"),
};
}
@@ -20,26 +35,7 @@ pub fn write(self: *const Self, comptime T: type, address: usize, value: T) void
const addr = address & 0x7FFF;
return switch (T) {
u32, u16, u8 => std.mem.writeInt(T, self.buf[addr..][0..@sizeOf(T)], value, .little),
u32, u16, u8 => std.mem.writeIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)], value),
else => @compileError("IWRAM: Unsupported write width"),
};
}
pub fn init(allocator: Allocator) !Self {
const buf = try allocator.alloc(u8, iwram_size);
@memset(buf, 0);
return Self{
.buf = buf,
.allocator = allocator,
};
}
pub fn reset(self: *Self) void {
@memset(self.buf, 0);
}
pub fn deinit(self: *Self) void {
self.allocator.free(self.buf);
self.* = undefined;
}

View File

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

View File

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

View File

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

View File

@@ -3,143 +3,88 @@ const util = @import("../../util.zig");
const DmaControl = @import("io.zig").DmaControl;
const Bus = @import("../Bus.zig");
const Arm7tdmi = @import("arm32").Arm7tdmi;
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 handleInterrupt = @import("../cpu_util.zig").handleInterrupt;
const rotr = @import("zba_util").rotr;
pub fn create() DmaTuple {
return .{ DmaController(0).init(), DmaController(1).init(), DmaController(2).init(), DmaController(3).init() };
}
pub fn read(comptime T: type, dma: *const DmaTuple, addr: u32) ?T {
const byte_addr: u8 = @truncate(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(dma.*[0].dmacntH() >> getHalf(byte_addr)),
0xBC...0xC3 => null, // DMA1SAD, DMA1DAD
0xC4, 0xC5 => 0x00, // DMA1CNT_L
0xC6, 0xC7 => @truncate(dma.*[1].dmacntH() >> getHalf(byte_addr)),
0xC8...0xCF => null, // DMA2SAD, DMA2DAD
0xD0, 0xD1 => 0x00, // DMA2CNT_L
0xD2, 0xD3 => @truncate(dma.*[2].dmacntH() >> getHalf(byte_addr)),
0xD4...0xDB => null, // DMA3SAD, DMA3DAD
0xDC, 0xDD => 0x00, // DMA3CNT_L
0xDE, 0xDF => @truncate(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: u8 = @truncate(addr);
const byte = @truncate(u8, addr);
switch (T) {
u32 => switch (byte_addr) {
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),
u32 => switch (byte) {
0xB0 => dma.*[0].setSad(value),
0xB4 => dma.*[0].setDad(value),
0xB8 => dma.*[0].setCnt(value),
0xBC => dma.*[1].setSad(value),
0xC0 => dma.*[1].setDad(value),
0xC4 => dma.*[1].setCnt(value),
0xC8 => dma.*[2].setSad(value),
0xCC => dma.*[2].setDad(value),
0xD0 => dma.*[2].setCnt(value),
0xD4 => dma.*[3].setSad(value),
0xD8 => dma.*[3].setDad(value),
0xDC => dma.*[3].setCnt(value),
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
},
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)),
0xB8 => dma.*[0].setDmacntL(value),
0xBA => dma.*[0].setDmacntH(value),
u16 => switch (byte) {
0xB0 => dma.*[0].setSad(setU32L(dma.*[0].sad, value)),
0xB2 => dma.*[0].setSad(setU32H(dma.*[0].sad, value)),
0xB4 => dma.*[0].setDad(setU32L(dma.*[0].dad, value)),
0xB6 => dma.*[0].setDad(setU32H(dma.*[0].dad, value)),
0xB8 => dma.*[0].setCntL(value),
0xBA => dma.*[0].setCntH(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)),
0xC4 => dma.*[1].setDmacntL(value),
0xC6 => dma.*[1].setDmacntH(value),
0xBC => dma.*[1].setSad(setU32L(dma.*[1].sad, value)),
0xBE => dma.*[1].setSad(setU32H(dma.*[1].sad, value)),
0xC0 => dma.*[1].setDad(setU32L(dma.*[1].dad, value)),
0xC2 => dma.*[1].setDad(setU32H(dma.*[1].dad, value)),
0xC4 => dma.*[1].setCntL(value),
0xC6 => dma.*[1].setCntH(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)),
0xD0 => dma.*[2].setDmacntL(value),
0xD2 => dma.*[2].setDmacntH(value),
0xC8 => dma.*[2].setSad(setU32L(dma.*[2].sad, value)),
0xCA => dma.*[2].setSad(setU32H(dma.*[2].sad, value)),
0xCC => dma.*[2].setDad(setU32L(dma.*[2].dad, value)),
0xCE => dma.*[2].setDad(setU32H(dma.*[2].dad, value)),
0xD0 => dma.*[2].setCntL(value),
0xD2 => dma.*[2].setCntH(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)),
0xDC => dma.*[3].setDmacntL(value),
0xDE => dma.*[3].setDmacntH(value),
0xD4 => dma.*[3].setSad(setU32L(dma.*[3].sad, value)),
0xD6 => dma.*[3].setSad(setU32H(dma.*[3].sad, value)),
0xD8 => dma.*[3].setDad(setU32L(dma.*[3].dad, value)),
0xDA => dma.*[3].setDad(setU32H(dma.*[3].dad, value)),
0xDC => dma.*[3].setCntL(value),
0xDE => dma.*[3].setCntH(value),
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
},
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"),
}
}
@@ -151,7 +96,6 @@ fn DmaController(comptime id: u2) type {
const sad_mask: u32 = if (id == 0) 0x07FF_FFFF else 0x0FFF_FFFF;
const dad_mask: u32 = if (id != 3) 0x07FF_FFFF else 0x0FFF_FFFF;
const WordCount = if (id == 3) u16 else u14;
/// Write-only. The first address in a DMA transfer. (DMASAD)
/// Note: use writeSrc instead of manipulating src_addr directly
@@ -160,19 +104,20 @@ fn DmaController(comptime id: u2) type {
/// Note: Use writeDst instead of manipulatig dst_addr directly
dad: u32,
/// Write-only. The Word Count for the DMA Transfer (DMACNT_L)
word_count: WordCount,
word_count: if (id == 3) u16 else u14,
/// Read / Write. DMACNT_H
/// Note: Use writeControl instead of manipulating cnt directly.
cnt: DmaControl,
/// Internal. The last successfully read value
data_latch: u32,
/// Internal. Currrent Source Address
sad_latch: u32,
_sad: u32,
/// Internal. Current Destination Address
dad_latch: u32,
_dad: u32,
/// Internal. Word Count
_word_count: WordCount,
_word_count: if (id == 3) u16 else u14,
// Internal. FIFO Word Count
_fifo_word_count: u8,
/// Some DMA Transfers are enabled during Hblank / VBlank and / or
/// have delays. Thefore bit 15 of DMACNT isn't actually something
@@ -187,43 +132,34 @@ fn DmaController(comptime id: u2) type {
.cnt = .{ .raw = 0x000 },
// Internals
.sad_latch = 0,
.dad_latch = 0,
.data_latch = 0,
._sad = 0,
._dad = 0,
._word_count = 0,
._fifo_word_count = 4,
.in_progress = false,
};
}
pub fn reset(self: *Self) void {
self.* = Self.init();
}
pub fn setDmasad(self: *Self, addr: u32) void {
pub fn setSad(self: *Self, addr: u32) void {
self.sad = addr & sad_mask;
}
pub fn setDmadad(self: *Self, addr: u32) void {
pub fn setDad(self: *Self, addr: u32) void {
self.dad = addr & dad_mask;
}
pub fn setDmacntL(self: *Self, halfword: u16) void {
self.word_count = @truncate(halfword);
pub fn setCntL(self: *Self, halfword: u16) void {
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 {
pub fn setCntH(self: *Self, halfword: u16) void {
const new = DmaControl{ .raw = halfword };
if (!self.cnt.enabled.read() and new.enabled.read()) {
// Reload Internals on Rising Edge.
self.sad_latch = self.sad;
self.dad_latch = self.dad;
self._word_count = if (self.word_count == 0) std.math.maxInt(WordCount) else self.word_count;
self._sad = self.sad;
self._dad = self.dad;
self._word_count = if (self.word_count == 0) std.math.maxInt(@TypeOf(self._word_count)) else self.word_count;
// Only a Start Timing of 00 has a DMA Transfer immediately begin
self.in_progress = new.start_timing.read() == 0b00;
@@ -232,52 +168,38 @@ fn DmaController(comptime id: u2) type {
self.cnt.raw = halfword;
}
pub fn setDmacnt(self: *Self, word: u32) void {
self.setDmacntL(@truncate(word));
self.setDmacntH(@truncate(word >> 16));
pub fn setCnt(self: *Self, word: u32) void {
self.setCntL(@truncate(u16, word));
self.setCntH(@truncate(u16, word >> 16));
}
pub fn step(self: *Self, cpu: *Arm7tdmi) void {
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
const is_fifo = (id == 1 or id == 2) and self.cnt.start_timing.read() == 0b11;
const sad_adj: Adjustment = @enumFromInt(self.cnt.sad_adj.read());
const dad_adj: Adjustment = if (is_fifo) .Fixed else @enumFromInt(self.cnt.dad_adj.read());
const sad_adj = Self.adjustment(self.cnt.sad_adj.read());
const dad_adj = if (is_fifo) .Fixed else Self.adjustment(self.cnt.dad_adj.read());
const transfer_type = is_fifo or self.cnt.transfer_type.read();
const offset: u32 = if (transfer_type) @sizeOf(u32) else @sizeOf(u16);
const mask = if (transfer_type) ~@as(u32, 3) else ~@as(u32, 1);
const sad_addr = self.sad_latch & mask;
const dad_addr = self.dad_latch & mask;
if (transfer_type) {
if (sad_addr >= 0x0200_0000) self.data_latch = cpu.bus.read(u32, sad_addr);
cpu.bus.write(u32, dad_addr, self.data_latch);
cpu.bus.write(u32, self._dad & mask, cpu.bus.read(u32, self._sad & mask));
} else {
if (sad_addr >= 0x0200_0000) {
const value: u32 = cpu.bus.read(u16, sad_addr);
self.data_latch = value << 16 | value;
}
cpu.bus.write(u16, dad_addr, @as(u16, @truncate(rotr(u32, self.data_latch, 8 * (dad_addr & 3)))));
cpu.bus.write(u16, self._dad & mask, cpu.bus.read(u16, self._sad & mask));
}
switch (@as(u8, @truncate(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 +%= offset,
.Decrement => self._sad -%= offset,
// TODO: Is just ignoring this ok?
.IncrementReload => log.err("{} is a prohibited adjustment on SAD", .{sad_adj}),
.Fixed => {},
}
switch (dad_adj) {
.Increment, .IncrementReload => self.dad_latch +%= offset,
.Decrement => self.dad_latch -%= offset,
.Increment, .IncrementReload => self._dad +%= offset,
.Decrement => self._dad -%= offset,
.Fixed => {},
}
@@ -286,17 +208,17 @@ fn DmaController(comptime id: u2) type {
if (self._word_count == 0) {
if (self.cnt.irq.read()) {
switch (id) {
0 => bus_ptr.io.irq.dma0.write(true),
1 => bus_ptr.io.irq.dma1.write(true),
2 => bus_ptr.io.irq.dma2.write(true),
3 => bus_ptr.io.irq.dma3.write(true),
0 => cpu.bus.io.irq.dma0.set(),
1 => cpu.bus.io.irq.dma1.set(),
2 => cpu.bus.io.irq.dma2.set(),
3 => cpu.bus.io.irq.dma3.set(),
}
handleInterrupt(cpu);
cpu.handleInterrupt();
}
// If we're not repeating, Fire the IRQs and disable the DMA
if (!self.cnt.repeat.read()) self.cnt.enabled.write(false);
if (!self.cnt.repeat.read()) self.cnt.enabled.unset();
// We want to disable our internal enabled flag regardless of repeat
// because we only want to step A DMA that repeats during it's specific
@@ -305,7 +227,7 @@ fn DmaController(comptime id: u2) type {
}
}
fn poll(self: *Self, comptime kind: DmaKind) void {
pub fn pollBlankingDma(self: *Self, comptime kind: DmaKind) void {
if (self.in_progress) return; // If there's an ongoing DMA Transfer, exit early
// No ongoing DMA Transfer, We want to check if we should repeat an existing one
@@ -321,11 +243,11 @@ fn DmaController(comptime id: u2) type {
// Reload internal DAD latch if we are in IncrementRelaod
if (self.in_progress) {
self._word_count = if (self.word_count == 0) std.math.maxInt(@TypeOf(self._word_count)) else self.word_count;
if (@as(Adjustment, @enumFromInt(self.cnt.dad_adj.read())) == .IncrementReload) self.dad_latch = self.dad;
if (Self.adjustment(self.cnt.dad_adj.read()) == .IncrementReload) self._dad = self.dad;
}
}
pub fn requestAudio(self: *Self, _: u32) void {
pub fn requestSoundDma(self: *Self, _: u32) void {
comptime std.debug.assert(id == 1 or id == 2);
if (self.in_progress) return; // APU must wait their turn
@@ -337,16 +259,23 @@ fn DmaController(comptime id: u2) type {
// We Assume DMACNT_L is set to 4
// FIXME: Safe to just assume whatever DAD is set to is the FIFO Address?
// self.dad_latch = fifo_addr;
self.cnt.repeat.write(true);
// self._dad = fifo_addr;
self.cnt.repeat.set();
self._word_count = 4;
self.in_progress = true;
}
fn adjustment(idx: u2) Adjustment {
return std.meta.intToEnum(Adjustment, idx) catch unreachable;
}
};
}
pub fn onBlanking(bus: *Bus, comptime kind: DmaKind) void {
inline for (0..4) |i| bus.dma[i].poll(kind);
pub fn pollBlankingDma(bus: *Bus, comptime kind: DmaKind) void {
bus.dma[0].pollBlankingDma(kind);
bus.dma[1].pollBlankingDma(kind);
bus.dma[2].pollBlankingDma(kind);
bus.dma[3].pollBlankingDma(kind);
}
const Adjustment = enum(u2) {
@@ -362,3 +291,11 @@ const DmaKind = enum(u2) {
VBlank,
Special,
};
fn setU32L(left: u32, right: u16) u32 {
return (left & 0xFFFF_0000) | right;
}
fn setU32H(left: u32, right: u16) u32 {
return (left & 0x0000_FFFF) | (@as(u32, right) << 16);
}

View File

@@ -1,14 +1,11 @@
const std = @import("std");
const Bit = @import("bitjuggle").Boolean;
const Bit = @import("bitfield").Bit;
const Bitfield = @import("bitfield").Bitfield;
const DateTime = @import("datetime").datetime.Datetime;
const Arm7tdmi = @import("arm32").Arm7tdmi;
const Bus = @import("../Bus.zig");
const Scheduler = @import("../scheduler.zig").Scheduler;
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
const Allocator = std.mem.Allocator;
const handleInterrupt = @import("../cpu_util.zig").handleInterrupt;
/// GPIO Register Implementation
pub const Gpio = struct {
const Self = @This();
@@ -31,8 +28,8 @@ pub const Gpio = struct {
fn step(self: *Device, value: u4) u4 {
return switch (self.kind) {
.Rtc => blk: {
const clock: *Clock = @ptrCast(@alignCast(self.ptr.?));
break :blk clock.step(.{ .raw = value });
const clock = @ptrCast(*Clock, @alignCast(@alignOf(*Clock), self.ptr.?));
break :blk clock.step(Clock.Data{ .raw = value });
},
.None => value,
};
@@ -71,11 +68,9 @@ pub const Gpio = struct {
log.info("Device: {}", .{kind});
const self = try allocator.create(Self);
errdefer allocator.destroy(self);
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) {
@@ -94,7 +89,7 @@ pub const Gpio = struct {
pub fn deinit(self: *Self, allocator: Allocator) void {
switch (self.device.kind) {
.Rtc => allocator.destroy(@as(*Clock, @ptrCast(@alignCast(self.device.ptr.?)))),
.Rtc => allocator.destroy(@ptrCast(*Clock, @alignCast(@alignOf(*Clock), self.device.ptr.?))),
.None => {},
}
@@ -146,16 +141,16 @@ pub const Clock = struct {
/// 2. A `count`, which keeps track of which byte is currently being read
/// 3. An index, which keeps track of which bit of the byte determined by `count` is being read
fn read(self: *Reader, clock: *const Clock, register: Register) u1 {
const idx: u3 = @intCast(self.i);
const idx = @intCast(u3, self.i);
defer self.i += 1;
// FIXME: What do I do about the unused bits?
return switch (register) {
.Control => @truncate(switch (self.count) {
.Control => @truncate(u1, switch (self.count) {
0 => clock.cnt.raw >> idx,
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 1 byte)", .{ self.count, register }),
}),
.DateTime => @truncate(switch (self.count) {
.DateTime => @truncate(u1, switch (self.count) {
// Date
0 => clock.year >> idx,
1 => @as(u8, clock.month) >> idx,
@@ -168,7 +163,7 @@ pub const Clock = struct {
6 => @as(u8, clock.second) >> idx,
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 7 bytes)", .{ self.count, register }),
}),
.Time => @truncate(switch (self.count) {
.Time => @truncate(u1, switch (self.count) {
0 => @as(u8, clock.hour) >> idx,
1 => @as(u8, clock.minute) >> idx,
2 => @as(u8, clock.second) >> idx,
@@ -207,7 +202,7 @@ pub const Clock = struct {
/// Append a bit to the internal bit buffer (aka an integer)
fn push(self: *Writer, value: u1) void {
const idx: u3 = @intCast(self.i);
const idx = @intCast(u3, self.i);
self.buf = (self.buf & ~(@as(u8, 1) << idx)) | @as(u8, value) << idx;
self.i += 1;
}
@@ -290,22 +285,20 @@ pub const Clock = struct {
.gpio = gpio, // Can't use Arm7tdmi ptr b/c not initialized yet
};
const sched_ptr: *Scheduler = @ptrCast(@alignCast(cpu.sched.ptr));
sched_ptr.push(.RealTimeClock, 1 << 24); // Every Second
cpu.sched.push(.RealTimeClock, 1 << 24); // Every Second
}
pub fn onClockUpdate(self: *Self, late: u64) void {
const sched_ptr: *Scheduler = @ptrCast(@alignCast(self.cpu.sched.ptr));
sched_ptr.push(.RealTimeClock, (1 << 24) -| late); // Reschedule
pub fn updateTime(self: *Self, late: u64) void {
self.cpu.sched.push(.RealTimeClock, (1 << 24) -| late); // Reschedule
const now = DateTime.now();
self.year = bcd(@intCast(now.date.year - 2000));
self.month = @truncate(bcd(now.date.month));
self.day = @truncate(bcd(now.date.day));
self.weekday = @truncate(bcd((now.date.weekday() + 1) % 7)); // API is Monday = 0, Sunday = 6. We want Sunday = 0, Saturday = 6
self.hour = @truncate(bcd(now.time.hour));
self.minute = @truncate(bcd(now.time.minute));
self.second = @truncate(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 {
@@ -321,14 +314,14 @@ pub const Clock = struct {
}
}
break :blk @truncate(value.raw);
break :blk @truncate(u4, value.raw);
},
.Command => blk: {
if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state});
// If SCK rises, sample SIO
if (!cache.sck.read() and value.sck.read()) {
self.writer.push(@intFromBool(value.sio.read()));
self.writer.push(@boolToInt(value.sio.read()));
if (self.writer.finished()) {
self.state = self.processCommand(self.writer.buf);
@@ -338,14 +331,14 @@ pub const Clock = struct {
}
}
break :blk @truncate(value.raw);
break :blk @truncate(u4, value.raw);
},
.Write => |register| blk: {
if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state});
// If SCK rises, sample SIO
if (!cache.sck.read() and value.sck.read()) {
self.writer.push(@intFromBool(value.sio.read()));
self.writer.push(@boolToInt(value.sio.read()));
const register_width: u32 = switch (register) {
.Control => 1,
@@ -364,7 +357,7 @@ pub const Clock = struct {
}
}
break :blk @truncate(value.raw);
break :blk @truncate(u4, value.raw);
},
.Read => |register| blk: {
if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state});
@@ -390,7 +383,7 @@ pub const Clock = struct {
}
}
break :blk @truncate(ret.raw);
break :blk @truncate(u4, ret.raw);
},
};
}
@@ -403,13 +396,11 @@ pub const Clock = struct {
}
fn irq(self: *Self) void {
const bus_ptr: *Bus = @ptrCast(@alignCast(self.cpu.bus.ptr));
// TODO: Confirm that this is the right behaviour
log.debug("Force GamePak IRQ", .{});
bus_ptr.io.irq.game_pak.write(true);
handleInterrupt(self.cpu);
self.cpu.bus.io.irq.game_pak.set();
self.cpu.handleInterrupt();
}
fn processCommand(self: *Self, raw_command: u8) State {
@@ -429,7 +420,7 @@ pub const Clock = struct {
log.debug("Handling Command 0x{X:0>2} [0b{b:0>8}]", .{ command, command });
const is_write = command & 1 == 0;
const rtc_register: u3 = @truncate(command >> 1 & 0x7);
const rtc_register = @truncate(u3, command >> 1 & 0x7);
if (is_write) {
return switch (rtc_register) {
@@ -457,8 +448,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,15 @@
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("bitjuggle").Boolean;
const Bitfield = @import("bitjuggle").Bitfield;
const Bit = @import("bitfield").Bit;
const Bitfield = @import("bitfield").Bitfield;
const Bus = @import("../Bus.zig");
const getHalf = util.getHalf;
const setHalf = util.setHalf;
const DmaController = @import("dma.zig").DmaController;
const Scheduler = @import("../scheduler.zig").Scheduler;
const log = std.log.scoped(.@"I/O");
@@ -22,29 +21,23 @@ 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,
};
}
pub fn reset(self: *Self) void {
self.* = Self.init();
}
fn setIrqs(self: *Io, word: u32) void {
self.ie.raw = @truncate(word);
self.irq.raw &= ~@as(u16, @truncate(word >> 16));
self.ie.raw = @truncate(u16, word);
self.irq.raw &= ~@truncate(u16, word >> 16);
}
};
@@ -52,10 +45,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),
@@ -73,18 +65,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_0208 => @intFromBool(bus.io.ime),
0x0400_0300 => @intFromEnum(bus.io.postflg),
0x0400_0200 => @as(T, bus.io.irq.raw) << 16 | bus.io.ie.raw,
0x0400_0208 => @boolToInt(bus.io.ime),
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),
@@ -96,38 +96,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),
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_0208 => @intFromBool(bus.io.ime),
0x0400_020A => 0x0000,
0x0400_0300 => @intFromEnum(bus.io.postflg),
0x0400_0302 => 0x0000,
0x0400_0204 => util.io.read.todo(log, "Read {} from WAITCNT", .{T}),
0x0400_0208 => @boolToInt(bus.io.ime),
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}),
@@ -136,20 +130,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(bus.io.ie.raw >> getHalf(@truncate(address))),
0x0400_0202, 0x0400_0203 => @truncate(bus.io.irq.raw >> getHalf(@truncate(address))),
0x0400_0204, 0x0400_0205 => @truncate(bus.io.waitcnt.raw >> getHalf(@truncate(address))),
0x0400_0206, 0x0400_0207 => 0x00,
0x0400_0208, 0x0400_0209 => @truncate(@as(u16, @intFromBool(bus.io.ime)) >> getHalf(@truncate(address))),
0x0400_020A, 0x0400_020B => 0x00,
0x0400_0300 => @intFromEnum(bus.io.postflg),
0x0400_0301 => null,
0x0400_0302, 0x0400_0303 => 0x00,
0x0400_0200 => @truncate(T, bus.io.ie.raw),
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 }),
},
else => @compileError("I/O: Unsupported read width"),
@@ -160,7 +144,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
@@ -196,28 +207,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(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 = @enumFromInt(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, @bitCast(u32, bus.ppu.aff_bg[0].x) & 0xFFFF_0000 | value),
0x0400_002A => bus.ppu.aff_bg[0].x = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[0].x) & 0x0000_FFFF | (@as(u32, value) << 16)),
0x0400_002C => bus.ppu.aff_bg[0].y = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[0].y) & 0xFFFF_0000 | value),
0x0400_002E => bus.ppu.aff_bg[0].y = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[0].y) & 0x0000_FFFF | (@as(u32, value) << 16)),
0x0400_0030 => bus.ppu.aff_bg[1].pa = @bitCast(i16, value),
0x0400_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, @bitCast(u32, bus.ppu.aff_bg[1].x) & 0xFFFF_0000 | value),
0x0400_003A => bus.ppu.aff_bg[1].x = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[1].x) & 0x0000_FFFF | (@as(u32, value) << 16)),
0x0400_003C => bus.ppu.aff_bg[1].y = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[1].y) & 0xFFFF_0000 | value),
0x0400_003E => bus.ppu.aff_bg[1].y = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[1].y) & 0x0000_FFFF | (@as(u32, value) << 16)),
0x0400_0040 => bus.ppu.win.h[0].raw = value,
0x0400_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
@@ -241,29 +289,27 @@ 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 = @enumFromInt(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 = (bus.ppu.dispstat.raw & 0xFF00) | value,
0x0400_0005 => bus.ppu.dispstat.raw = (@as(u16, value) << 8) | (bus.ppu.dispstat.raw & 0xFF),
0x0400_0008 => bus.ppu.bg[0].cnt.raw = (bus.ppu.bg[0].cnt.raw & 0xFF00) | value,
0x0400_0009 => bus.ppu.bg[0].cnt.raw = (@as(u16, value) << 8) | (bus.ppu.bg[0].cnt.raw & 0xFF),
0x0400_000A => bus.ppu.bg[1].cnt.raw = (bus.ppu.bg[1].cnt.raw & 0xFF00) | value,
0x0400_000B => bus.ppu.bg[1].cnt.raw = (@as(u16, value) << 8) | (bus.ppu.bg[1].cnt.raw & 0xFF),
0x0400_0048 => bus.ppu.win.setInL(value),
0x0400_0049 => bus.ppu.win.setInH(value),
0x0400_004A => bus.ppu.win.setOutL(value),
0x0400_0054 => bus.ppu.bldy.raw = (bus.ppu.bldy.raw & 0xFF00) | 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}),
@@ -273,16 +319,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(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, bus.io.waitcnt.raw, @truncate(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 = @enumFromInt(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 }),
@@ -321,22 +360,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
@@ -350,10 +381,10 @@ const InterruptEnable = extern union {
vblank: Bit(u16, 0),
hblank: Bit(u16, 1),
coincidence: Bit(u16, 2),
tim0: Bit(u16, 3),
tim1: Bit(u16, 4),
tim2: Bit(u16, 5),
tim3: Bit(u16, 6),
tm0_overflow: Bit(u16, 3),
tm1_overflow: Bit(u16, 4),
tm2_overflow: Bit(u16, 5),
tm3_overflow: Bit(u16, 6),
serial: Bit(u16, 7),
dma0: Bit(u16, 8),
dma1: Bit(u16, 9),
@@ -366,7 +397,7 @@ const InterruptEnable = extern union {
/// Read Only
/// 0 = Pressed, 1 = Released
pub const KeyInput = extern union {
const KeyInput = extern union {
a: Bit(u16, 0),
b: Bit(u16, 1),
select: Bit(u16, 2),
@@ -380,32 +411,6 @@ pub const KeyInput = extern union {
raw: u16,
};
const AtomicKeyInput = struct {
const Self = @This();
const AtomicOrder = std.builtin.AtomicOrder;
inner: KeyInput,
pub fn init(value: KeyInput) Self {
return .{ .inner = value };
}
pub inline fn load(self: *const Self, comptime ordering: AtomicOrder) u16 {
return switch (ordering) {
.acq_rel, .release => @compileError("not supported for atomic loads"),
else => @atomicLoad(u16, &self.inner.raw, ordering),
};
}
pub inline fn fetchOr(self: *Self, value: u16, comptime ordering: AtomicOrder) void {
_ = @atomicRmw(u16, &self.inner.raw, .Or, value, ordering);
}
pub inline fn fetchAnd(self: *Self, value: u16, comptime ordering: AtomicOrder) void {
_ = @atomicRmw(u16, &self.inner.raw, .And, value, ordering);
}
};
// Read / Write
pub const BackgroundControl = extern union {
priority: Bitfield(u16, 0, 2),
@@ -454,8 +459,6 @@ pub const BldY = extern union {
raw: u16,
};
const u8WriteKind = enum { Hi, Lo };
/// Write-only
pub const WinH = extern union {
x2: Bitfield(u16, 0, 8),
@@ -465,8 +468,6 @@ pub const WinH = extern union {
/// Write-only
pub const WinV = extern union {
const Self = @This();
y2: Bitfield(u16, 0, 8),
y1: Bitfield(u16, 8, 8),
raw: u16,
@@ -475,20 +476,20 @@ pub const WinV = extern union {
pub const WinIn = extern union {
w0_bg: Bitfield(u16, 0, 4),
w0_obj: Bit(u16, 4),
w0_bld: Bit(u16, 5),
w0_colour: Bit(u16, 5),
w1_bg: Bitfield(u16, 8, 4),
w1_obj: Bit(u16, 12),
w1_bld: Bit(u16, 13),
w1_colour: Bit(u16, 13),
raw: u16,
};
pub const WinOut = extern union {
out_bg: Bitfield(u16, 0, 4),
out_obj: Bit(u16, 4),
out_bld: Bit(u16, 5),
out_colour: Bit(u16, 5),
obj_bg: Bitfield(u16, 8, 4),
obj_obj: Bit(u16, 12),
obj_bld: Bit(u16, 13),
obj_colour: Bit(u16, 13),
raw: u16,
};
@@ -657,24 +658,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,102 +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 Arm7tdmi = @import("arm32").Arm7tdmi;
const Bus = @import("../Bus.zig");
const Event = @import("../scheduler.zig").Event;
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
const handleInterrupt = @import("../cpu_util.zig").handleInterrupt;
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: u4 = @truncate(addr);
const nybble = @truncate(u4, addr);
return switch (T) {
u32 => switch (nybble_addr) {
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 }),
u32 => switch (nybble) {
0x0 => @as(T, tim.*[0].cnt.raw) << 16 | tim.*[0].getCntL(),
0x4 => @as(T, tim.*[1].cnt.raw) << 16 | tim.*[1].getCntL(),
0x8 => @as(T, tim.*[2].cnt.raw) << 16 | tim.*[2].getCntL(),
0xC => @as(T, tim.*[3].cnt.raw) << 16 | tim.*[3].getCntL(),
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
},
u16 => switch (nybble_addr) {
0x0 => tim.*[0].timcntL(),
u16 => switch (nybble) {
0x0 => tim.*[0].getCntL(),
0x2 => tim.*[0].cnt.raw,
0x4 => tim.*[1].timcntL(),
0x4 => tim.*[1].getCntL(),
0x6 => tim.*[1].cnt.raw,
0x8 => tim.*[2].timcntL(),
0x8 => tim.*[2].getCntL(),
0xA => tim.*[2].cnt.raw,
0xC => tim.*[3].timcntL(),
0xC => tim.*[3].getCntL(),
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(tim.*[0].timcntL() >> getHalf(nybble_addr)),
0x2, 0x3 => @truncate(tim.*[0].cnt.raw >> getHalf(nybble_addr)),
0x4, 0x5 => @truncate(tim.*[1].timcntL() >> getHalf(nybble_addr)),
0x6, 0x7 => @truncate(tim.*[1].cnt.raw >> getHalf(nybble_addr)),
0x8, 0x9 => @truncate(tim.*[2].timcntL() >> getHalf(nybble_addr)),
0xA, 0xB => @truncate(tim.*[2].cnt.raw >> getHalf(nybble_addr)),
0xC, 0xD => @truncate(tim.*[3].timcntL() >> getHalf(nybble_addr)),
0xE, 0xF => @truncate(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: u4 = @truncate(addr);
const nybble = @truncate(u4, addr);
return switch (T) {
u32 => switch (nybble_addr) {
0x0 => tim.*[0].setTimcnt(value),
0x4 => tim.*[1].setTimcnt(value),
0x8 => tim.*[2].setTimcnt(value),
0xC => tim.*[3].setTimcnt(value),
u32 => switch (nybble) {
0x0 => tim.*[0].setCnt(value),
0x4 => tim.*[1].setCnt(value),
0x8 => tim.*[2].setCnt(value),
0xC => tim.*[3].setCnt(value),
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
},
u16 => switch (nybble_addr) {
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),
u16 => switch (nybble) {
0x0 => tim.*[0].setCntL(value),
0x2 => tim.*[0].setCntH(value),
0x4 => tim.*[1].setCntL(value),
0x6 => tim.*[1].setCntH(value),
0x8 => tim.*[2].setCntL(value),
0xA => tim.*[2].setCntH(value),
0xC => tim.*[3].setCntL(value),
0xE => tim.*[3].setCntH(value),
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
},
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"),
};
}
@@ -106,13 +72,13 @@ fn Timer(comptime id: u2) type {
return struct {
const Self = @This();
/// Read Only, Internal. Please use self.timcntL()
/// Read Only, Internal. Please use self.getCntL()
_counter: u16,
/// Write Only, Internal. Please use self.setTimcntL()
/// Write Only, Internal. Please use self.setCntL()
_reload: u16,
/// Write Only, Internal. Please use self.setTimcntH()
/// Write Only, Internal. Please use self.setCntH()
cnt: TimerControl,
/// Internal.
@@ -131,111 +97,91 @@ fn Timer(comptime id: u2) type {
};
}
pub fn reset(self: *Self) void {
const scheduler = self.sched;
self.* = Self.init(scheduler);
}
/// TIMCNT_L Getter
pub fn timcntL(self: *const Self) u16 {
/// TIMCNT_L
pub fn getCntL(self: *const Self) u16 {
if (self.cnt.cascade.read() or !self.cnt.enabled.read()) return self._counter;
return self._counter +% @as(u16, @truncate((self.sched.now() - self._start_timestamp) / self.frequency()));
return self._counter +% @truncate(u16, (self.sched.now() - self._start_timestamp) / self.frequency());
}
/// TIMCNT_L Setter
pub fn setTimcntL(self: *Self, halfword: u16) void {
/// TIMCNT_L
pub fn setCntL(self: *Self, halfword: u16) void {
self._reload = halfword;
}
/// TIMCNT_L & TIMCNT_H
pub fn setTimcnt(self: *Self, word: u32) void {
self.setTimcntL(@truncate(word));
self.setTimcntH(@truncate(word >> 16));
pub fn setCnt(self: *Self, word: u32) void {
self.setCntL(@truncate(u16, word));
self.setCntH(@truncate(u16, word >> 16));
}
/// TIMCNT_H
pub fn setTimcntH(self: *Self, halfword: u16) void {
pub fn setCntH(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((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.scheduleOverflow(0);
self.cnt.raw = halfword;
}
pub fn onTimerExpire(self: *Self, cpu: *Arm7tdmi, late: u64) void {
pub fn handleOverflow(self: *Self, cpu: *Arm7tdmi, late: u64) void {
// Fire IRQ if enabled
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
const io = &bus_ptr.io;
const io = &cpu.bus.io;
if (self.cnt.irq.read()) {
switch (id) {
0 => io.irq.tim0.write(true),
1 => io.irq.tim1.write(true),
2 => io.irq.tim2.write(true),
3 => io.irq.tim3.write(true),
0 => io.irq.tim0.set(),
1 => io.irq.tim1.set(),
2 => io.irq.tim2.set(),
3 => io.irq.tim3.set(),
}
handleInterrupt(cpu);
cpu.handleInterrupt();
}
// DMA Sound Things
if (id == 0 or id == 1) {
bus_ptr.apu.onDmaAudioSampleRequest(cpu, id);
cpu.bus.apu.handleTimerOverflow(cpu, id);
}
// Perform Cascade Behaviour
switch (id) {
inline 0, 1, 2 => |idx| {
const next = idx + 1;
if (bus_ptr.tim[next].cnt.cascade.read()) {
bus_ptr.tim[next]._counter +%= 1;
if (bus_ptr.tim[next]._counter == 0) bus_ptr.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].handleOverflow(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].handleOverflow(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].handleOverflow(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);
self.scheduleOverflow(late);
}
}
fn rescheduleTimerExpire(self: *Self, late: u64) void {
fn scheduleOverflow(self: *Self, late: u64) void {
const when = (@as(u64, 0x10000) - self._counter) * self.frequency();
self._start_timestamp = self.sched.now();

667
src/core/cpu.zig Normal file
View File

@@ -0,0 +1,667 @@
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;
// ARM Instructions
pub const arm = struct {
pub const InstrFn = *const fn (*Arm7tdmi, *Bus, u32) void;
const lut: [0x1000]InstrFn = populate();
const processing = @import("cpu/arm/data_processing.zig").dataProcessing;
const psrTransfer = @import("cpu/arm/psr_transfer.zig").psrTransfer;
const transfer = @import("cpu/arm/single_data_transfer.zig").singleDataTransfer;
const halfSignedTransfer = @import("cpu/arm/half_signed_data_transfer.zig").halfAndSignedDataTransfer;
const blockTransfer = @import("cpu/arm/block_data_transfer.zig").blockDataTransfer;
const branch = @import("cpu/arm/branch.zig").branch;
const branchExchange = @import("cpu/arm/branch.zig").branchAndExchange;
const swi = @import("cpu/arm/software_interrupt.zig").armSoftwareInterrupt;
const swap = @import("cpu/arm/single_data_swap.zig").singleDataSwap;
const multiply = @import("cpu/arm/multiply.zig").multiply;
const multiplyLong = @import("cpu/arm/multiply.zig").multiplyLong;
/// Determine index into ARM InstrFn LUT
fn idx(opcode: u32) u12 {
return @truncate(u12, opcode >> 20 & 0xFF) << 4 | @truncate(u12, opcode >> 4 & 0xF);
}
// Undefined ARM Instruction handler
fn und(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
const id = idx(opcode);
cpu.panic("[CPU/Decode] ID: 0x{X:0>3} 0x{X:0>8} is an illegal opcode", .{ id, opcode });
}
fn populate() [0x1000]InstrFn {
return comptime {
@setEvalBranchQuota(0xE000);
var ret = [_]InstrFn{und} ** 0x1000;
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: {
const A = i >> 5 & 1 == 1;
const S = i >> 4 & 1 == 1;
break :blk multiply(A, S);
} else if (i & 0xFBF == 0x109) blk: {
const B = i >> 6 & 1 == 1;
break :blk swap(B);
} else if (i & 0xF8F == 0x089) blk: {
const U = i >> 6 & 1 == 1;
const A = i >> 5 & 1 == 1;
const S = i >> 4 & 1 == 1;
break :blk multiplyLong(U, A, S);
} else if (i & 0xE49 == 0x009 or i & 0xE49 == 0x049) blk: {
const P = i >> 8 & 1 == 1;
const U = i >> 7 & 1 == 1;
const I = i >> 6 & 1 == 1;
const W = i >> 5 & 1 == 1;
const L = i >> 4 & 1 == 1;
break :blk halfSignedTransfer(P, U, I, W, L);
} else if (i & 0xD90 == 0x100) blk: {
const I = i >> 9 & 1 == 1;
const R = i >> 6 & 1 == 1;
const kind = i >> 4 & 0x3;
break :blk psrTransfer(I, R, kind);
} else blk: {
const I = i >> 9 & 1 == 1;
const S = i >> 4 & 1 == 1;
const instrKind = i >> 5 & 0xF;
break :blk processing(I, S, instrKind);
},
0b01 => if (i >> 9 & 1 == 1 and i & 1 == 1) und else blk: {
const I = i >> 9 & 1 == 1;
const P = i >> 8 & 1 == 1;
const U = i >> 7 & 1 == 1;
const B = i >> 6 & 1 == 1;
const W = i >> 5 & 1 == 1;
const L = i >> 4 & 1 == 1;
break :blk transfer(I, P, U, B, W, L);
},
else => switch (@as(u2, i >> 9 & 0x3)) {
// MSB is guaranteed to be 1
0b00 => blk: {
const P = i >> 8 & 1 == 1;
const U = i >> 7 & 1 == 1;
const S = i >> 6 & 1 == 1;
const W = i >> 5 & 1 == 1;
const L = i >> 4 & 1 == 1;
break :blk blockTransfer(P, U, S, W, L);
},
0b01 => blk: {
const L = i >> 8 & 1 == 1;
break :blk branch(L);
},
0b10 => und, // COP Data Transfer
0b11 => if (i >> 8 & 1 == 1) swi() else und, // COP Data Operation + Register Transfer
},
};
}
return ret;
};
}
};
// THUMB Instructions
pub const thumb = struct {
pub const InstrFn = *const fn (*Arm7tdmi, *Bus, u16) void;
const lut: [0x400]InstrFn = populate();
const processing = @import("cpu/thumb/data_processing.zig");
const alu = @import("cpu/thumb/alu.zig").fmt4;
const transfer = @import("cpu/thumb/data_transfer.zig");
const block_transfer = @import("cpu/thumb/block_data_transfer.zig");
const swi = @import("cpu/thumb/software_interrupt.zig").fmt17;
const branch = @import("cpu/thumb/branch.zig");
/// Determine index into THUMB InstrFn LUT
fn idx(opcode: u16) u10 {
return @truncate(u10, opcode >> 6);
}
/// Undefined THUMB Instruction Handler
fn und(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
const id = idx(opcode);
cpu.panic("[CPU/Decode] ID: 0b{b:0>10} 0x{X:0>2} is an illegal opcode", .{ id, opcode });
}
fn populate() [0x400]InstrFn {
return comptime {
@setEvalBranchQuota(5025); // This is exact
var ret = [_]InstrFn{und} ** 0x400;
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;
const rn = i & 0x7;
break :blk processing.fmt2(I, is_sub, rn);
} else blk: {
const op = i >> 5 & 0x3;
const offset = i & 0x1F;
break :blk processing.fmt1(op, offset);
},
0b001 => blk: {
const op = i >> 5 & 0x3;
const rd = i >> 2 & 0x7;
break :blk processing.fmt3(op, rd);
},
0b010 => switch (@as(u2, i >> 5 & 0x3)) {
0b00 => if (i >> 4 & 1 == 1) blk: {
const op = i >> 2 & 0x3;
const h1 = i >> 1 & 1;
const h2 = i & 1;
break :blk processing.fmt5(op, h1, h2);
} else blk: {
const op = i & 0xF;
break :blk alu(op);
},
0b01 => blk: {
const rd = i >> 2 & 0x7;
break :blk transfer.fmt6(rd);
},
else => blk: {
const op = i >> 4 & 0x3;
const T = i >> 3 & 1 == 1;
break :blk transfer.fmt78(op, T);
},
},
0b011 => blk: {
const B = i >> 6 & 1 == 1;
const L = i >> 5 & 1 == 1;
const offset = i & 0x1F;
break :blk transfer.fmt9(B, L, offset);
},
else => switch (@as(u3, i >> 6 & 0x7)) {
// MSB is guaranteed to be 1
0b000 => blk: {
const L = i >> 5 & 1 == 1;
const offset = i & 0x1F;
break :blk transfer.fmt10(L, offset);
},
0b001 => blk: {
const L = i >> 5 & 1 == 1;
const rd = i >> 2 & 0x7;
break :blk transfer.fmt11(L, rd);
},
0b010 => blk: {
const isSP = i >> 5 & 1 == 1;
const rd = i >> 2 & 0x7;
break :blk processing.fmt12(isSP, rd);
},
0b011 => if (i >> 4 & 1 == 1) blk: {
const L = i >> 5 & 1 == 1;
const R = i >> 2 & 1 == 1;
break :blk block_transfer.fmt14(L, R);
} else blk: {
const S = i >> 1 & 1 == 1;
break :blk processing.fmt13(S);
},
0b100 => blk: {
const L = i >> 5 & 1 == 1;
const rb = i >> 2 & 0x7;
break :blk block_transfer.fmt15(L, rb);
},
0b101 => if (i >> 2 & 0xF == 0b1111) blk: {
break :blk thumb.swi();
} else blk: {
const cond = i >> 2 & 0xF;
break :blk branch.fmt16(cond);
},
0b110 => branch.fmt18(),
0b111 => blk: {
const is_low = i >> 5 & 1 == 1;
break :blk branch.fmt19(is_low);
},
},
};
}
return ret;
};
}
};
const log = std.log.scoped(.Arm7Tdmi);
pub const Arm7tdmi = struct {
const Self = @This();
r: [16]u32,
sched: *Scheduler,
bus: *Bus,
cpsr: PSR,
spsr: PSR,
/// 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,
pub fn init(sched: *Scheduler, bus: *Bus, log_file: ?std.fs.File) Self {
return Self{
.r = [_]u32{0x00} ** 16,
.sched = sched,
.bus = bus,
.cpsr = .{ .raw = 0x0000_001F },
.spsr = .{ .raw = 0x0000_0000 },
.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) {
.System, .User => false,
else => true,
};
}
pub inline fn isPrivileged(self: *const Self) bool {
const mode = getModeChecked(self, self.cpsr.mode.read());
return switch (mode) {
.User => false,
else => true,
};
}
pub inline fn isHalted(self: *const Self) bool {
return self.bus.io.haltcnt == .Halt;
}
pub fn setCpsr(self: *Self, value: u32) void {
if (value & 0x1F != self.cpsr.raw & 0x1F) self.changeModeFromIdx(@truncate(u5, value & 0x1F));
self.cpsr.raw = value;
}
fn changeModeFromIdx(self: *Self, next: u5) void {
self.changeMode(getModeChecked(self, next));
}
pub fn setUserModeRegister(self: *Self, idx: usize, value: u32) void {
const current = getModeChecked(self, self.cpsr.mode.read());
switch (idx) {
8...12 => {
if (current == .Fiq) {
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(BankedKind, idx - 13) catch unreachable;
self.banked_r[bankedIdx(.User, kind)] = value;
},
},
else => self.r[idx] = value, // R0 -> R7 and R15
}
}
pub fn getUserModeRegister(self: *Self, idx: usize) u32 {
const current = getModeChecked(self, self.cpsr.mode.read());
return switch (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(BankedKind, idx - 13) catch unreachable;
break :blk self.banked_r[bankedIdx(.User, kind)];
},
},
else => self.r[idx], // R0 -> R7 and R15
};
}
pub fn changeMode(self: *Self, next: Mode) void {
const now = getModeChecked(self, self.cpsr.mode.read());
// Bank R8 -> r12
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.banked_r[bankedIdx(now, .R13)] = self.r[13];
self.banked_r[bankedIdx(now, .R14)] = self.r[14];
},
else => {
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
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.banked_r[bankedIdx(next, .R13)];
self.r[14] = self.banked_r[bankedIdx(next, .R14)];
},
else => {
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)];
},
}
self.cpsr.mode.write(@enumToInt(next));
}
pub fn fastBoot(self: *Self) void {
self.r = std.mem.zeroes([16]u32);
self.r[0] = 0x08000000;
self.r[1] = 0x000000EA;
self.r[13] = 0x0300_7F00;
self.r[15] = 0x0800_0000;
self.banked_r[bankedIdx(.Irq, .R13)] = 0x0300_7FA0;
self.banked_r[bankedIdx(.Supervisor, .R13)] = 0x0300_7FE0;
self.cpsr.raw = 0x6000001F;
}
pub fn step(self: *Self) void {
if (self.cpsr.t.read()) {
const opcode = self.fetch(u16);
if (self.logger) |*trace| trace.mgbaLog(self, opcode);
thumb.lut[thumb.idx(opcode)](self, self.bus, opcode);
} else {
const opcode = self.fetch(u32);
if (self.logger) |*trace| trace.mgbaLog(self, opcode);
if (checkCond(self.cpsr, @truncate(u4, opcode >> 28))) {
arm.lut[arm.idx(opcode)](self, self.bus, opcode);
}
}
}
pub fn stepDmaTransfer(self: *Self) bool {
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;
}
pub fn handleInterrupt(self: *Self) void {
const should_handle = self.bus.io.ie.raw & self.bus.io.irq.raw;
if (should_handle != 0) {
self.bus.io.haltcnt = .Execute;
// log.debug("An Interrupt was Fired!", .{});
// Either IME is not true or I in CPSR is true
// Don't handle interrupts
if (!self.bus.io.ime or self.cpsr.i.read()) return;
// log.debug("An interrupt was Handled!", .{});
// retAddr.gba says r15 on it's own is off by -04h in both ARM and THUMB mode
const r15 = self.r[15] + 4;
const cpsr = self.cpsr.raw;
self.changeMode(.Irq);
self.cpsr.t.write(false);
self.cpsr.i.write(true);
self.r[14] = r15;
self.spsr.raw = cpsr;
self.r[15] = 0x000_0018;
}
}
inline fn fetch(self: *Self, comptime T: type) T {
comptime std.debug.assert(T == u32 or T == u16); // Opcode may be 32-bit (ARM) or 16-bit (THUMB)
defer self.r[15] += if (T == u32) 4 else 2;
// FIXME: You better hope this is optimized out
const tick_cache = self.sched.tick;
defer self.sched.tick = tick_cache + Bus.fetch_timings[@boolToInt(T == u32)][@truncate(u4, self.r[15] >> 24)];
return self.bus.read(T, self.r[15]);
}
pub fn fakePC(self: *const Self) u32 {
return self.r[15] + 4;
}
pub fn panic(self: *const Self, comptime format: []const u8, args: anytype) noreturn {
var i: usize = 0;
while (i < 16) : (i += 4) {
const i_1 = i + 1;
const i_2 = i + 2;
const i_3 = i + 3;
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});
prettyPrintPsr(&self.cpsr);
std.debug.print("spsr: 0x{X:0>8} ", .{self.spsr.raw});
prettyPrintPsr(&self.spsr);
if (self.cpsr.t.read()) {
const opcode = self.bus.dbgRead(u16, self.r[15] - 4);
const id = thumb.idx(opcode);
std.debug.print("opcode: ID: 0x{b:0>10} 0x{X:0>4}\n", .{ id, opcode });
} else {
const opcode = self.bus.dbgRead(u32, self.r[15] - 4);
const id = arm.idx(opcode);
std.debug.print("opcode: ID: 0x{X:0>3} 0x{X:0>8}\n", .{ id, opcode });
}
std.debug.print("tick: {}\n\n", .{self.sched.tick});
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];
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.dbgRead(u16, self.r[15]);
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);
}
};
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?)
};
}
pub const PSR = extern union {
mode: Bitfield(u32, 0, 5),
t: Bit(u32, 5),
f: Bit(u32, 6),
i: Bit(u32, 7),
v: Bit(u32, 28),
c: Bit(u32, 29),
z: Bit(u32, 30),
n: Bit(u32, 31),
raw: u32,
};
const Mode = enum(u5) {
User = 0b10000,
Fiq = 0b10001,
Irq = 0b10010,
Supervisor = 0b10011,
Abort = 0b10111,
Undefined = 0b11011,
System = 0b11111,
};
const BankedKind = enum(u1) {
R13 = 0,
R14,
};
fn getMode(bits: u5) ?Mode {
return std.meta.intToEnum(Mode, bits) catch null;
}
fn getModeChecked(cpu: *const Arm7tdmi, bits: u5) Mode {
return getMode(bits) orelse cpu.panic("[CPU/CPSR] 0b{b:0>5} is an invalid CPU mode", .{bits});
}

View File

@@ -0,0 +1,104 @@
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
pub fn blockDataTransfer(comptime P: bool, comptime U: bool, comptime S: bool, comptime W: bool, comptime L: bool) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u32) void {
const rn = @truncate(u4, opcode >> 16 & 0xF);
const rlist = opcode & 0xFFFF;
const r15 = rlist >> 15 & 1 == 1;
var count: u32 = 0;
var i: u5 = 0;
var first: u4 = 0;
var write_to_base = true;
while (i < 16) : (i += 1) {
const r = @truncate(u4, 15 - i);
if (rlist >> r & 1 == 1) {
first = r;
count += 1;
}
}
var start = cpu.r[rn];
if (U) {
start += if (P) 4 else 0;
} else {
start = start - (4 * count) + if (!P) 4 else 0;
}
var end = cpu.r[rn];
if (U) {
end = end + (4 * count) - if (!P) 4 else 0;
} else {
end -= if (P) 4 else 0;
}
var new_base = cpu.r[rn];
if (U) {
new_base += 4 * count;
} else {
new_base -= 4 * count;
}
var address = start;
if (rlist == 0) {
var und_addr = cpu.r[rn];
if (U) {
und_addr += if (P) 4 else 0;
} else {
und_addr -= 0x40 - if (!P) 4 else 0;
}
if (L) {
cpu.r[15] = bus.read(u32, und_addr);
} else {
bus.write(u32, und_addr, cpu.r[15] + 8);
}
cpu.r[rn] = if (U) cpu.r[rn] + 0x40 else cpu.r[rn] - 0x40;
return;
}
i = first;
while (i < 16) : (i += 1) {
if (rlist >> i & 1 == 1) {
transfer(cpu, bus, r15, i, address);
address += 4;
if (W and !L and write_to_base) {
cpu.r[rn] = new_base;
write_to_base = false;
}
}
}
if (W and L and rlist >> rn & 1 == 0) cpu.r[rn] = new_base;
}
fn transfer(cpu: *Arm7tdmi, bus: *Bus, r15_present: bool, i: u5, address: u32) void {
if (L) {
if (S and !r15_present) {
// Always Transfer User mode Registers
cpu.setUserModeRegister(i, bus.read(u32, address));
} else {
const value = bus.read(u32, address);
cpu.r[i] = if (i == 0xF) value & 0xFFFF_FFFC else value;
if (S and i == 0xF) cpu.setCpsr(cpu.spsr.raw);
}
} else {
if (S) {
// Always Transfer User mode Registers
// This happens regardless if r15 is in the list
const value = cpu.getUserModeRegister(i);
bus.write(u32, address, value + if (i == 0xF) 8 else @as(u32, 0)); // PC is already 4 ahead to make 12
} else {
bus.write(u32, address, cpu.r[i] + if (i == 0xF) 8 else @as(u32, 0));
}
}
}
}.inner;
}

View File

@@ -0,0 +1,22 @@
const std = @import("std");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
const sext = @import("../../../util.zig").sext;
pub fn branch(comptime L: bool) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
if (L) cpu.r[14] = cpu.r[15];
cpu.r[15] = cpu.fakePC() +% (sext(u32, u24, opcode) << 2);
}
}.inner;
}
pub fn branchAndExchange(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
const rn = opcode & 0xF;
cpu.cpsr.t.write(cpu.r[rn] & 1 == 1);
cpu.r[15] = cpu.r[rn] & 0xFFFF_FFFE;
}

View File

@@ -0,0 +1,284 @@
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
const rotateRight = @import("../barrel_shifter.zig").rotateRight;
const execute = @import("../barrel_shifter.zig").execute;
pub fn dataProcessing(comptime I: bool, comptime S: bool, comptime instrKind: u4) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
const rd = @truncate(u4, opcode >> 12 & 0xF);
const rn = opcode >> 16 & 0xF;
const old_carry = @boolToInt(cpu.cpsr.c.read());
// If certain conditions are met, PC is 12 ahead instead of 8
if (!I and opcode >> 4 & 1 == 1) cpu.r[15] += 4;
const op1 = if (rn == 0xF) cpu.fakePC() else cpu.r[rn];
var op2: u32 = undefined;
if (I) {
const amount = @truncate(u8, (opcode >> 8 & 0xF) << 1);
op2 = rotateRight(S, &cpu.cpsr, opcode & 0xFF, amount);
} else {
op2 = execute(S, cpu, opcode);
}
// Undo special condition from above
if (!I and opcode >> 4 & 1 == 1) cpu.r[15] -= 4;
switch (instrKind) {
0x0 => {
// AND
const result = op1 & op2;
cpu.r[rd] = result;
setArmLogicOpFlags(S, cpu, rd, result);
},
0x1 => {
// EOR
const result = op1 ^ op2;
cpu.r[rd] = result;
setArmLogicOpFlags(S, cpu, rd, result);
},
0x2 => {
// SUB
cpu.r[rd] = armSub(S, cpu, rd, op1, op2);
},
0x3 => {
// RSB
cpu.r[rd] = armSub(S, cpu, rd, op2, op1);
},
0x4 => {
// ADD
cpu.r[rd] = armAdd(S, cpu, rd, op1, op2);
},
0x5 => {
// ADC
cpu.r[rd] = armAdc(S, cpu, rd, op1, op2, old_carry);
},
0x6 => {
// SBC
cpu.r[rd] = armSbc(S, cpu, rd, op1, op2, old_carry);
},
0x7 => {
// RSC
cpu.r[rd] = armSbc(S, cpu, rd, op2, op1, old_carry);
},
0x8 => {
// TST
if (rd == 0xF) {
undefinedTestBehaviour(cpu);
return;
}
const result = op1 & op2;
setTestOpFlags(S, cpu, opcode, result);
},
0x9 => {
// TEQ
if (rd == 0xF) {
undefinedTestBehaviour(cpu);
return;
}
const result = op1 ^ op2;
setTestOpFlags(S, cpu, opcode, result);
},
0xA => {
// CMP
if (rd == 0xF) {
undefinedTestBehaviour(cpu);
return;
}
cmp(cpu, op1, op2);
},
0xB => {
// CMN
if (rd == 0xF) {
undefinedTestBehaviour(cpu);
return;
}
cmn(cpu, op1, op2);
},
0xC => {
// ORR
const result = op1 | op2;
cpu.r[rd] = result;
setArmLogicOpFlags(S, cpu, rd, result);
},
0xD => {
// MOV
cpu.r[rd] = op2;
setArmLogicOpFlags(S, cpu, rd, op2);
},
0xE => {
// BIC
const result = op1 & ~op2;
cpu.r[rd] = result;
setArmLogicOpFlags(S, cpu, rd, result);
},
0xF => {
// MVN
const result = ~op2;
cpu.r[rd] = result;
setArmLogicOpFlags(S, cpu, rd, result);
},
}
}
}.inner;
}
fn armSbc(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32, old_carry: u1) u32 {
var result: u32 = undefined;
if (S and rd == 0xF) {
result = sbc(false, cpu, left, right, old_carry);
cpu.setCpsr(cpu.spsr.raw);
} else {
result = sbc(S, cpu, left, right, old_carry);
}
return result;
}
pub fn sbc(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32, old_carry: u1) u32 {
// TODO: Make your own version (thanks peach.bot)
const subtrahend = @as(u64, right) -% old_carry +% 1;
const result = @truncate(u32, left -% subtrahend);
if (S) {
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
cpu.cpsr.c.write(subtrahend <= left);
cpu.cpsr.v.write(((left ^ result) & (~right ^ result)) >> 31 & 1 == 1);
}
return result;
}
fn armSub(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32) u32 {
var result: u32 = undefined;
if (S and rd == 0xF) {
result = sub(false, cpu, left, right);
cpu.setCpsr(cpu.spsr.raw);
} else {
result = sub(S, cpu, left, right);
}
return result;
}
pub fn sub(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32) u32 {
const result = left -% right;
if (S) {
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
cpu.cpsr.c.write(right <= left);
cpu.cpsr.v.write(((left ^ result) & (~right ^ result)) >> 31 & 1 == 1);
}
return result;
}
fn armAdd(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32) u32 {
var result: u32 = undefined;
if (S and rd == 0xF) {
result = add(false, cpu, left, right);
cpu.setCpsr(cpu.spsr.raw);
} else {
result = add(S, cpu, left, right);
}
return result;
}
pub fn add(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32) u32 {
var result: u32 = undefined;
const didOverflow = @addWithOverflow(u32, left, right, &result);
if (S) {
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
cpu.cpsr.c.write(didOverflow);
cpu.cpsr.v.write(((left ^ result) & (right ^ result)) >> 31 & 1 == 1);
}
return result;
}
fn armAdc(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32, old_carry: u1) u32 {
var result: u32 = undefined;
if (S and rd == 0xF) {
result = adc(false, cpu, left, right, old_carry);
cpu.setCpsr(cpu.spsr.raw);
} else {
result = adc(S, cpu, left, right, old_carry);
}
return result;
}
pub fn adc(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32, old_carry: u1) u32 {
var result: u32 = undefined;
const did = @addWithOverflow(u32, left, right, &result);
const overflow = @addWithOverflow(u32, result, old_carry, &result);
if (S) {
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
cpu.cpsr.c.write(did or overflow);
cpu.cpsr.v.write(((left ^ result) & (right ^ result)) >> 31 & 1 == 1);
}
return result;
}
pub fn cmp(cpu: *Arm7tdmi, left: u32, right: u32) void {
const result = left -% right;
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
cpu.cpsr.c.write(right <= left);
cpu.cpsr.v.write(((left ^ result) & (~right ^ result)) >> 31 & 1 == 1);
}
pub fn cmn(cpu: *Arm7tdmi, left: u32, right: u32) void {
var result: u32 = undefined;
const didOverflow = @addWithOverflow(u32, left, right, &result);
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
cpu.cpsr.c.write(didOverflow);
cpu.cpsr.v.write(((left ^ result) & (right ^ result)) >> 31 & 1 == 1);
}
fn setArmLogicOpFlags(comptime S: bool, cpu: *Arm7tdmi, rd: u4, result: u32) void {
if (S and rd == 0xF) {
cpu.setCpsr(cpu.spsr.raw);
} else {
setLogicOpFlags(S, cpu, result);
}
}
pub fn setLogicOpFlags(comptime S: bool, cpu: *Arm7tdmi, result: u32) void {
if (S) {
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
// C set by Barrel Shifter, V is unaffected
}
}
fn setTestOpFlags(comptime S: bool, cpu: *Arm7tdmi, opcode: u32, result: u32) void {
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
// Barrel Shifter should always calc CPSR C in TST
if (!S) _ = execute(true, cpu, opcode);
}
fn undefinedTestBehaviour(cpu: *Arm7tdmi) void {
@setCold(true);
cpu.setCpsr(cpu.spsr.raw);
}

View File

@@ -0,0 +1,70 @@
const std = @import("std");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
const sext = @import("../../../util.zig").sext;
const rotr = @import("../../../util.zig").rotr;
pub fn halfAndSignedDataTransfer(comptime P: bool, comptime U: bool, comptime I: bool, comptime W: bool, comptime L: bool) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u32) void {
const rn = opcode >> 16 & 0xF;
const rd = opcode >> 12 & 0xF;
const rm = opcode & 0xF;
const imm_offset_high = opcode >> 8 & 0xF;
var base: u32 = undefined;
if (rn == 0xF) {
base = cpu.fakePC();
if (!L) base += 4;
} else {
base = cpu.r[rn];
}
var offset: u32 = undefined;
if (I) {
offset = imm_offset_high << 4 | rm;
} else {
offset = cpu.r[rm];
}
const modified_base = if (U) base +% offset else base -% offset;
var address = if (P) modified_base else base;
var result: u32 = undefined;
if (L) {
switch (@truncate(u2, opcode >> 5)) {
0b01 => {
// LDRH
const value = bus.read(u16, address);
result = rotr(u32, value, 8 * (address & 1));
},
0b10 => {
// LDRSB
result = sext(u32, u8, bus.read(u8, address));
},
0b11 => {
// LDRSH
result = if (address & 1 == 1) blk: {
break :blk sext(u32, u8, bus.read(u8, address));
} else blk: {
break :blk sext(u32, u16, bus.read(u16, address));
};
},
0b00 => unreachable, // SWP
}
} else {
if (opcode >> 5 & 0x01 == 0x01) {
// STRH
bus.write(u16, address, @truncate(u16, cpu.r[rd]));
} else unreachable; // SWP
}
address = modified_base;
if (W and P or !P) cpu.r[rn] = address;
if (L) cpu.r[rd] = result; // // This emulates the LDR rd == rn behaviour
}
}.inner;
}

View File

@@ -0,0 +1,57 @@
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
pub fn multiply(comptime A: bool, comptime S: bool) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
const rd = opcode >> 16 & 0xF;
const rn = opcode >> 12 & 0xF;
const rs = opcode >> 8 & 0xF;
const rm = opcode & 0xF;
const temp: u64 = @as(u64, cpu.r[rm]) * @as(u64, cpu.r[rs]) + if (A) cpu.r[rn] else 0;
const result = @truncate(u32, temp);
cpu.r[rd] = result;
if (S) {
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
// V is unaffected, C is *actually* undefined in ARMv4
}
}
}.inner;
}
pub fn multiplyLong(comptime U: bool, comptime A: bool, comptime S: bool) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
const rd_hi = opcode >> 16 & 0xF;
const rd_lo = opcode >> 12 & 0xF;
const rs = opcode >> 8 & 0xF;
const rm = opcode & 0xF;
if (U) {
// Signed (WHY IS IT U THEN?)
var result: i64 = @as(i64, @bitCast(i32, cpu.r[rm])) * @as(i64, @bitCast(i32, cpu.r[rs]));
if (A) result +%= @bitCast(i64, @as(u64, cpu.r[rd_hi]) << 32 | @as(u64, cpu.r[rd_lo]));
cpu.r[rd_hi] = @bitCast(u32, @truncate(i32, result >> 32));
cpu.r[rd_lo] = @bitCast(u32, @truncate(i32, result));
} else {
// Unsigned
var result: u64 = @as(u64, cpu.r[rm]) * @as(u64, cpu.r[rs]);
if (A) result +%= @as(u64, cpu.r[rd_hi]) << 32 | @as(u64, cpu.r[rd_lo]);
cpu.r[rd_hi] = @truncate(u32, result >> 32);
cpu.r[rd_lo] = @truncate(u32, result);
}
if (S) {
cpu.cpsr.z.write(cpu.r[rd_hi] == 0 and cpu.r[rd_lo] == 0);
cpu.cpsr.n.write(cpu.r[rd_hi] >> 31 & 1 == 1);
// C and V are set to meaningless values
}
}
}.inner;
}

View File

@@ -0,0 +1,59 @@
const std = @import("std");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
const PSR = @import("../../cpu.zig").PSR;
const log = std.log.scoped(.PsrTransfer);
const rotr = @import("../../../util.zig").rotr;
pub fn psrTransfer(comptime I: bool, comptime R: bool, comptime kind: u2) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
switch (kind) {
0b00 => {
// MRS
const rd = opcode >> 12 & 0xF;
if (R and !cpu.hasSPSR()) log.err("Tried to read SPSR from User/System Mode", .{});
cpu.r[rd] = if (R) cpu.spsr.raw else cpu.cpsr.raw;
},
0b10 => {
// MSR
const field_mask = @truncate(u4, opcode >> 16 & 0xF);
const rm_idx = opcode & 0xF;
const right = if (I) rotr(u32, opcode & 0xFF, (opcode >> 8 & 0xF) * 2) else cpu.r[rm_idx];
if (R and !cpu.hasSPSR()) log.err("Tried to write to SPSR in User/System Mode", .{});
if (R) {
// arm.gba seems to expect the SPSR to do somethign in SYS mode,
// so we just assume that despite writing to the SPSR in USR or SYS mode
// being UNPREDICTABLE, it just magically has a working SPSR somehow
cpu.spsr.raw = fieldMask(&cpu.spsr, field_mask, right);
} else {
if (cpu.isPrivileged()) cpu.setCpsr(fieldMask(&cpu.cpsr, field_mask, right));
}
},
else => cpu.panic("[CPU/PSR Transfer] Bits 21:220 of {X:0>8} are undefined", .{opcode}),
}
}
}.inner;
}
fn fieldMask(psr: *const PSR, field_mask: u4, right: u32) u32 {
// This bitwise ORs bits 3 and 0 of the field mask into a u2
// We do this because we only care about bits 7:0 and 31:28 of the CPSR
const bits = @truncate(u2, (field_mask >> 2 & 0x2) | (field_mask & 1));
const mask: u32 = switch (bits) {
0b00 => 0x0000_0000,
0b01 => 0x0000_00FF,
0b10 => 0xF000_0000,
0b11 => 0xF000_00FF,
};
return (psr.raw & ~mask) | (right & mask);
}

View File

@@ -0,0 +1,31 @@
const std = @import("std");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
const rotr = @import("../../../util.zig").rotr;
pub fn singleDataSwap(comptime B: bool) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u32) void {
const rn = opcode >> 16 & 0xF;
const rd = opcode >> 12 & 0xF;
const rm = opcode & 0xF;
const address = cpu.r[rn];
if (B) {
// SWPB
const value = bus.read(u8, address);
bus.write(u8, address, @truncate(u8, cpu.r[rm]));
cpu.r[rd] = value;
} else {
// SWP
const value = rotr(u32, bus.read(u32, address), 8 * (address & 0x3));
bus.write(u32, address, cpu.r[rm]);
cpu.r[rd] = value;
}
}
}.inner;
}

View File

@@ -0,0 +1,57 @@
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;
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
const rotr = @import("../../../util.zig").rotr;
pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool, comptime B: bool, comptime W: bool, comptime L: bool) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u32) void {
const rn = opcode >> 16 & 0xF;
const rd = opcode >> 12 & 0xF;
var base: u32 = undefined;
if (rn == 0xF) {
base = cpu.fakePC();
if (!L) base += 4; // Offset of 12
} else {
base = cpu.r[rn];
}
const offset = if (I) shifter.immShift(false, cpu, opcode) else opcode & 0xFFF;
const modified_base = if (U) base +% offset else base -% offset;
var address = if (P) modified_base else base;
var result: u32 = undefined;
if (L) {
if (B) {
// LDRB
result = bus.read(u8, address);
} else {
// LDR
const value = bus.read(u32, address);
result = rotr(u32, value, 8 * (address & 0x3));
}
} else {
if (B) {
// STRB
const value = if (rd == 0xF) cpu.r[rd] + 8 else cpu.r[rd];
bus.write(u8, address, @truncate(u8, value));
} else {
// STR
const value = if (rd == 0xF) cpu.r[rd] + 8 else cpu.r[rd];
bus.write(u32, address, value);
}
}
address = modified_base;
if (W and P or !P) cpu.r[rn] = address;
if (L) cpu.r[rd] = result; // This emulates the LDR rd == rn behaviour
}
}.inner;
}

View File

@@ -0,0 +1,22 @@
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
pub fn armSoftwareInterrupt() InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, _: u32) void {
// Copy Values from Current Mode
const r15 = cpu.r[15];
const cpsr = cpu.cpsr.raw;
// Switch Mode
cpu.changeMode(.Supervisor);
cpu.cpsr.t.write(false); // Force ARM Mode
cpu.cpsr.i.write(true); // Disable normal interrupts
cpu.r[14] = r15; // Resume Execution
cpu.spsr.raw = cpsr; // Previous mode CPSR
cpu.r[15] = 0x0000_0008;
}
}.inner;
}

View File

@@ -0,0 +1,153 @@
const std = @import("std");
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
const CPSR = @import("../cpu.zig").PSR;
const rotr = @import("../../util.zig").rotr;
pub fn execute(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
var result: u32 = undefined;
if (opcode >> 4 & 1 == 1) {
result = registerShift(S, cpu, opcode);
} else {
result = immShift(S, cpu, opcode);
}
return result;
}
fn registerShift(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
const rs_idx = opcode >> 8 & 0xF;
const rs = @truncate(u8, cpu.r[rs_idx]);
const rm_idx = opcode & 0xF;
const rm = if (rm_idx == 0xF) cpu.fakePC() else cpu.r[rm_idx];
return switch (@truncate(u2, opcode >> 5)) {
0b00 => logicalLeft(S, &cpu.cpsr, rm, rs),
0b01 => logicalRight(S, &cpu.cpsr, rm, rs),
0b10 => arithmeticRight(S, &cpu.cpsr, rm, rs),
0b11 => rotateRight(S, &cpu.cpsr, rm, rs),
};
}
pub fn immShift(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
const amount = @truncate(u8, opcode >> 7 & 0x1F);
const rm_idx = opcode & 0xF;
const rm = if (rm_idx == 0xF) cpu.fakePC() else cpu.r[rm_idx];
var result: u32 = undefined;
if (amount == 0) {
switch (@truncate(u2, opcode >> 5)) {
0b00 => {
// LSL #0
result = rm;
},
0b01 => {
// LSR #0 aka LSR #32
if (S) cpu.cpsr.c.write(rm >> 31 & 1 == 1);
result = 0x0000_0000;
},
0b10 => {
// ASR #0 aka ASR #32
result = @bitCast(u32, @bitCast(i32, rm) >> 31);
if (S) cpu.cpsr.c.write(result >> 31 & 1 == 1);
},
0b11 => {
// ROR #0 aka RRX
const carry: u32 = @boolToInt(cpu.cpsr.c.read());
if (S) cpu.cpsr.c.write(rm & 1 == 1);
result = (carry << 31) | (rm >> 1);
},
}
} else {
switch (@truncate(u2, opcode >> 5)) {
0b00 => result = logicalLeft(S, &cpu.cpsr, rm, amount),
0b01 => result = logicalRight(S, &cpu.cpsr, rm, amount),
0b10 => result = arithmeticRight(S, &cpu.cpsr, rm, amount),
0b11 => result = rotateRight(S, &cpu.cpsr, rm, amount),
}
}
return result;
}
pub fn logicalLeft(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
const amount = @truncate(u5, total_amount);
const bit_count: u8 = @typeInfo(u32).Int.bits;
var result: u32 = 0x0000_0000;
if (total_amount < bit_count) {
// We can perform a well-defined shift here
result = rm << amount;
if (S and total_amount != 0) {
const carry_bit = @truncate(u5, bit_count - amount);
cpsr.c.write(rm >> carry_bit & 1 == 1);
}
} else {
if (S) {
if (total_amount == bit_count) {
// Shifted all bits out, carry bit is bit 0 of rm
cpsr.c.write(rm & 1 == 1);
} else {
cpsr.c.write(false);
}
}
}
return result;
}
pub fn logicalRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u32) u32 {
const amount = @truncate(u5, total_amount);
const bit_count: u8 = @typeInfo(u32).Int.bits;
var result: u32 = 0x0000_0000;
if (total_amount < bit_count) {
// We can perform a well-defined shift
result = rm >> amount;
if (S and total_amount != 0) cpsr.c.write(rm >> (amount - 1) & 1 == 1);
} else {
if (S) {
if (total_amount == bit_count) {
// LSR #32
cpsr.c.write(rm >> 31 & 1 == 1);
} else {
// All bits have been shifted out, including carry bit
cpsr.c.write(false);
}
}
}
return result;
}
pub fn arithmeticRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
const amount = @truncate(u5, total_amount);
const bit_count: u8 = @typeInfo(u32).Int.bits;
var result: u32 = 0x0000_0000;
if (total_amount < bit_count) {
result = @bitCast(u32, @bitCast(i32, rm) >> amount);
if (S and total_amount != 0) cpsr.c.write(rm >> (amount - 1) & 1 == 1);
} else {
// ASR #32 and ASR #>32 have the same result
result = @bitCast(u32, @bitCast(i32, rm) >> 31);
if (S) cpsr.c.write(result >> 31 & 1 == 1);
}
return result;
}
pub fn rotateRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
const result = rotr(u32, rm, total_amount);
if (S and total_amount != 0) {
cpsr.c.write(result >> 31 & 1 == 1);
}
return result;
}

118
src/core/cpu/thumb/alu.zig Normal file
View File

@@ -0,0 +1,118 @@
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
const adc = @import("../arm/data_processing.zig").adc;
const sbc = @import("../arm/data_processing.zig").sbc;
const sub = @import("../arm/data_processing.zig").sub;
const cmp = @import("../arm/data_processing.zig").cmp;
const cmn = @import("../arm/data_processing.zig").cmn;
const setTestOpFlags = @import("../arm/data_processing.zig").setTestOpFlags;
const setLogicOpFlags = @import("../arm/data_processing.zig").setLogicOpFlags;
const logicalLeft = @import("../barrel_shifter.zig").logicalLeft;
const logicalRight = @import("../barrel_shifter.zig").logicalRight;
const arithmeticRight = @import("../barrel_shifter.zig").arithmeticRight;
const rotateRight = @import("../barrel_shifter.zig").rotateRight;
pub fn fmt4(comptime op: u4) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
const rs = opcode >> 3 & 0x7;
const rd = opcode & 0x7;
const carry = @boolToInt(cpu.cpsr.c.read());
switch (op) {
0x0 => {
// AND
const result = cpu.r[rd] & cpu.r[rs];
cpu.r[rd] = result;
setLogicOpFlags(true, cpu, result);
},
0x1 => {
// EOR
const result = cpu.r[rd] ^ cpu.r[rs];
cpu.r[rd] = result;
setLogicOpFlags(true, cpu, result);
},
0x2 => {
// LSL
const result = logicalLeft(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
cpu.r[rd] = result;
setLogicOpFlags(true, cpu, result);
},
0x3 => {
// LSR
const result = logicalRight(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
cpu.r[rd] = result;
setLogicOpFlags(true, cpu, result);
},
0x4 => {
// ASR
const result = arithmeticRight(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
cpu.r[rd] = result;
setLogicOpFlags(true, cpu, result);
},
0x5 => {
// ADC
cpu.r[rd] = adc(true, cpu, cpu.r[rd], cpu.r[rs], carry);
},
0x6 => {
// SBC
cpu.r[rd] = sbc(true, cpu, cpu.r[rd], cpu.r[rs], carry);
},
0x7 => {
// ROR
const result = rotateRight(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
cpu.r[rd] = result;
setLogicOpFlags(true, cpu, result);
},
0x8 => {
// TST
const result = cpu.r[rd] & cpu.r[rs];
setLogicOpFlags(true, cpu, result);
},
0x9 => {
// NEG
cpu.r[rd] = sub(true, cpu, 0, cpu.r[rs]);
},
0xA => {
// CMP
cmp(cpu, cpu.r[rd], cpu.r[rs]);
},
0xB => {
// CMN
cmn(cpu, cpu.r[rd], cpu.r[rs]);
},
0xC => {
// ORR
const result = cpu.r[rd] | cpu.r[rs];
cpu.r[rd] = result;
setLogicOpFlags(true, cpu, result);
},
0xD => {
// MUL
const temp = @as(u64, cpu.r[rs]) * @as(u64, cpu.r[rd]);
const result = @truncate(u32, temp);
cpu.r[rd] = result;
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
// V is unaffected, assuming similar behaviour to ARMv4 MUL C is undefined
},
0xE => {
// BIC
const result = cpu.r[rd] & ~cpu.r[rs];
cpu.r[rd] = result;
setLogicOpFlags(true, cpu, result);
},
0xF => {
// MVN
const result = ~cpu.r[rs];
cpu.r[rd] = result;
setLogicOpFlags(true, cpu, result);
},
}
}
}.inner;
}

View File

@@ -0,0 +1,94 @@
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
pub fn fmt14(comptime L: bool, comptime R: bool) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
const count = @boolToInt(R) + countRlist(opcode);
const start = cpu.r[13] - if (!L) count * 4 else 0;
var end = cpu.r[13];
if (L) {
end += count * 4;
} else {
end -= 4;
}
var address = start;
var i: u4 = 0;
while (i < 8) : (i += 1) {
if (opcode >> i & 1 == 1) {
if (L) {
cpu.r[i] = bus.read(u32, address);
} else {
bus.write(u32, address, cpu.r[i]);
}
address += 4;
}
}
if (R) {
if (L) {
const value = bus.read(u32, address);
cpu.r[15] = value & 0xFFFF_FFFE;
} else {
bus.write(u32, address, cpu.r[14]);
}
address += 4;
}
cpu.r[13] = if (L) end else start;
}
}.inner;
}
pub fn fmt15(comptime L: bool, comptime rb: u3) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
var address = cpu.r[rb];
const end_address = cpu.r[rb] + 4 * countRlist(opcode);
if (opcode & 0xFF == 0) {
if (L) cpu.r[15] = bus.read(u32, address) else bus.write(u32, address, cpu.r[15] + 4);
cpu.r[rb] += 0x40;
return;
}
var i: u4 = 0;
var first_write = true;
while (i < 8) : (i += 1) {
if (opcode >> i & 1 == 1) {
if (L) {
cpu.r[i] = bus.read(u32, address);
} else {
bus.write(u32, address, cpu.r[i]);
}
if (!L and first_write) {
cpu.r[rb] = end_address;
first_write = false;
}
address += 4;
}
}
if (L and opcode >> rb & 1 != 1) cpu.r[rb] = address;
}
}.inner;
}
inline fn countRlist(opcode: u16) u32 {
var count: u32 = 0;
comptime var i: u4 = 0;
inline while (i < 8) : (i += 1) {
if (opcode >> (7 - i) & 1 == 1) count += 1;
}
return count;
}

View File

@@ -0,0 +1,54 @@
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
const checkCond = @import("../../cpu.zig").checkCond;
const sext = @import("../../../util.zig").sext;
pub fn fmt16(comptime cond: u4) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
// B
const offset = sext(u32, u8, opcode & 0xFF) << 1;
const should_execute = switch (cond) {
0xE, 0xF => cpu.panic("[CPU/THUMB.16] Undefined conditional branch with condition {}", .{cond}),
else => checkCond(cpu.cpsr, cond),
};
if (should_execute) {
cpu.r[15] = (cpu.r[15] + 2) +% offset;
}
}
}.inner;
}
pub fn fmt18() InstrFn {
return struct {
// B but conditional
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
const offset = sext(u32, u11, opcode & 0x7FF) << 1;
cpu.r[15] = (cpu.r[15] + 2) +% offset;
}
}.inner;
}
pub fn fmt19(comptime is_low: bool) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
// BL
const offset = opcode & 0x7FF;
if (is_low) {
// Instruction 2
const old_pc = cpu.r[15];
cpu.r[15] = cpu.r[14] +% (offset << 1);
cpu.r[14] = old_pc | 1;
} else {
// Instruction 1
cpu.r[14] = (cpu.r[15] + 2) +% (sext(u32, u11, offset) << 12);
}
}
}.inner;
}

View File

@@ -0,0 +1,152 @@
const std = @import("std");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
const shifter = @import("../barrel_shifter.zig");
const add = @import("../arm/data_processing.zig").add;
const sub = @import("../arm/data_processing.zig").sub;
const cmp = @import("../arm/data_processing.zig").cmp;
const setLogicOpFlags = @import("../arm/data_processing.zig").setLogicOpFlags;
const log = std.log.scoped(.Thumb1);
pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
const rs = opcode >> 3 & 0x7;
const rd = opcode & 0x7;
const result = switch (op) {
0b00 => blk: {
// LSL
if (offset == 0) {
break :blk cpu.r[rs];
} else {
break :blk shifter.logicalLeft(true, &cpu.cpsr, cpu.r[rs], offset);
}
},
0b01 => blk: {
// LSR
if (offset == 0) {
cpu.cpsr.c.write(cpu.r[rs] >> 31 & 1 == 1);
break :blk @as(u32, 0);
} else {
break :blk shifter.logicalRight(true, &cpu.cpsr, cpu.r[rs], offset);
}
},
0b10 => blk: {
// ASR
if (offset == 0) {
cpu.cpsr.c.write(cpu.r[rs] >> 31 & 1 == 1);
break :blk @bitCast(u32, @bitCast(i32, cpu.r[rs]) >> 31);
} else {
break :blk shifter.arithmeticRight(true, &cpu.cpsr, cpu.r[rs], offset);
}
},
else => cpu.panic("[CPU/THUMB.1] 0b{b:0>2} is not a valid op", .{op}),
};
// Equivalent to an ARM MOVS
cpu.r[rd] = result;
setLogicOpFlags(true, cpu, result);
}
}.inner;
}
pub fn fmt5(comptime op: u2, comptime h1: u1, comptime h2: u1) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
const src_idx = @as(u4, h2) << 3 | (opcode >> 3 & 0x7);
const dst_idx = @as(u4, h1) << 3 | (opcode & 0x7);
const src = if (src_idx == 0xF) (cpu.r[src_idx] + 2) & 0xFFFF_FFFE else cpu.r[src_idx];
const dst = if (dst_idx == 0xF) (cpu.r[dst_idx] + 2) & 0xFFFF_FFFE else cpu.r[dst_idx];
switch (op) {
0b00 => {
// ADD
const sum = add(false, cpu, dst, src);
cpu.r[dst_idx] = if (dst_idx == 0xF) sum & 0xFFFF_FFFE else sum;
},
0b01 => cmp(cpu, dst, src), // CMP
0b10 => {
// MOV
cpu.r[dst_idx] = if (dst_idx == 0xF) src & 0xFFFF_FFFE else src;
},
0b11 => {
// BX
cpu.cpsr.t.write(src & 1 == 1);
cpu.r[15] = src & 0xFFFF_FFFE;
},
}
}
}.inner;
}
pub fn fmt2(comptime I: bool, is_sub: bool, rn: u3) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
const rs = opcode >> 3 & 0x7;
const rd = @truncate(u3, opcode);
if (is_sub) {
// SUB
cpu.r[rd] = if (I) blk: {
break :blk sub(true, cpu, cpu.r[rs], rn);
} else blk: {
break :blk sub(true, cpu, cpu.r[rs], cpu.r[rn]);
};
} else {
// ADD
cpu.r[rd] = if (I) blk: {
break :blk add(true, cpu, cpu.r[rs], rn);
} else blk: {
break :blk add(true, cpu, cpu.r[rs], cpu.r[rn]);
};
}
}
}.inner;
}
pub fn fmt3(comptime op: u2, comptime rd: u3) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
const offset = @truncate(u8, opcode);
switch (op) {
0b00 => {
// MOV
cpu.r[rd] = offset;
setLogicOpFlags(true, cpu, offset);
},
0b01 => cmp(cpu, cpu.r[rd], offset), // CMP
0b10 => cpu.r[rd] = add(true, cpu, cpu.r[rd], offset), // ADD
0b11 => cpu.r[rd] = sub(true, cpu, cpu.r[rd], offset), // SUB
}
}
}.inner;
}
pub fn fmt12(comptime isSP: bool, comptime rd: u3) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
// ADD
const left = if (isSP) cpu.r[13] else (cpu.r[15] + 2) & 0xFFFF_FFFD;
const right = (opcode & 0xFF) << 2;
const result = left + right;
cpu.r[rd] = result;
}
}.inner;
}
pub fn fmt13(comptime S: bool) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
// ADD
const offset = (opcode & 0x7F) << 2;
cpu.r[13] = if (S) cpu.r[13] - offset else cpu.r[13] + offset;
}
}.inner;
}

View File

@@ -0,0 +1,148 @@
const std = @import("std");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
const rotr = @import("../../../util.zig").rotr;
const sext = @import("../../../util.zig").sext;
pub fn fmt6(comptime rd: u3) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
// LDR
const offset = (opcode & 0xFF) << 2;
cpu.r[rd] = bus.read(u32, (cpu.r[15] + 2 & 0xFFFF_FFFD) + offset);
}
}.inner;
}
pub fn fmt78(comptime op: u2, comptime T: bool) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
const ro = opcode >> 6 & 0x7;
const rb = opcode >> 3 & 0x7;
const rd = opcode & 0x7;
const address = cpu.r[rb] +% cpu.r[ro];
if (T) {
// Format 8
switch (op) {
0b00 => {
// STRH
bus.write(u16, address, @truncate(u16, cpu.r[rd]));
},
0b01 => {
// LDSB
cpu.r[rd] = sext(u32, u8, bus.read(u8, address));
},
0b10 => {
// LDRH
const value = bus.read(u16, address);
cpu.r[rd] = rotr(u32, value, 8 * (address & 1));
},
0b11 => {
// LDRSH
cpu.r[rd] = if (address & 1 == 1) blk: {
break :blk sext(u32, u8, bus.read(u8, address));
} else blk: {
break :blk sext(u32, u16, bus.read(u16, address));
};
},
}
} else {
// Format 7
switch (op) {
0b00 => {
// STR
bus.write(u32, address, cpu.r[rd]);
},
0b01 => {
// STRB
bus.write(u8, address, @truncate(u8, cpu.r[rd]));
},
0b10 => {
// LDR
const value = bus.read(u32, address);
cpu.r[rd] = rotr(u32, value, 8 * (address & 0x3));
},
0b11 => {
// LDRB
cpu.r[rd] = bus.read(u8, address);
},
}
}
}
}.inner;
}
pub fn fmt9(comptime B: bool, comptime L: bool, comptime offset: u5) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
const rb = opcode >> 3 & 0x7;
const rd = opcode & 0x7;
if (L) {
if (B) {
// LDRB
const address = cpu.r[rb] + offset;
cpu.r[rd] = bus.read(u8, address);
} else {
// LDR
const address = cpu.r[rb] + (@as(u32, offset) << 2);
const value = bus.read(u32, address);
cpu.r[rd] = rotr(u32, value, 8 * (address & 0x3));
}
} else {
if (B) {
// STRB
const address = cpu.r[rb] + offset;
bus.write(u8, address, @truncate(u8, cpu.r[rd]));
} else {
// STR
const address = cpu.r[rb] + (@as(u32, offset) << 2);
bus.write(u32, address, cpu.r[rd]);
}
}
}
}.inner;
}
pub fn fmt10(comptime L: bool, comptime offset: u5) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
const rb = opcode >> 3 & 0x7;
const rd = opcode & 0x7;
const address = cpu.r[rb] + (@as(u6, offset) << 1);
if (L) {
// LDRH
const value = bus.read(u16, address);
cpu.r[rd] = rotr(u32, value, 8 * (address & 1));
} else {
// STRH
bus.write(u16, address, @truncate(u16, cpu.r[rd]));
}
}
}.inner;
}
pub fn fmt11(comptime L: bool, comptime rd: u3) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
const offset = (opcode & 0xFF) << 2;
const address = cpu.r[13] + offset;
if (L) {
// LDR
const value = bus.read(u32, address);
cpu.r[rd] = rotr(u32, value, 8 * (address & 0x3));
} else {
// STR
bus.write(u32, address, cpu.r[rd]);
}
}
}.inner;
}

View File

@@ -0,0 +1,22 @@
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
pub fn fmt17() InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, _: u16) void {
// Copy Values from Current Mode
const r15 = cpu.r[15];
const cpsr = cpu.cpsr.raw;
// Switch Mode
cpu.changeMode(.Supervisor);
cpu.cpsr.t.write(false); // Force ARM Mode
cpu.cpsr.i.write(true); // Disable normal interrupts
cpu.r[14] = r15; // Resume Execution
cpu.spsr.raw = cpsr; // Previous mode CPSR
cpu.r[15] = 0x0000_0008;
}
}.inner;
}

View File

@@ -1,75 +0,0 @@
const std = @import("std");
const Arm7tdmi = @import("arm32").Arm7tdmi;
const Bank = @import("arm32").Arm7tdmi.Bank;
const Bus = @import("Bus.zig");
pub inline fn isHalted(cpu: *const Arm7tdmi) bool {
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
return bus_ptr.io.haltcnt == .Halt;
}
pub fn stepDmaTransfer(cpu: *Arm7tdmi) bool {
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
inline for (0..4) |i| {
if (bus_ptr.dma[i].in_progress) {
bus_ptr.dma[i].step(cpu);
return true;
}
}
return false;
}
pub fn handleInterrupt(cpu: *Arm7tdmi) void {
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
const should_handle = bus_ptr.io.ie.raw & bus_ptr.io.irq.raw;
// Return if IME is disabled, CPSR I is set or there is nothing to handle
if (!bus_ptr.io.ime or cpu.cpsr.i.read() or should_handle == 0) return;
// If Pipeline isn't full, we have a bug
std.debug.assert(cpu.pipe.isFull());
// log.debug("Handling Interrupt!", .{});
bus_ptr.io.haltcnt = .Execute;
// FIXME: This seems weird, but retAddr.gba suggests I need to make these changes
const ret_addr = cpu.r[15] - if (cpu.cpsr.t.read()) 0 else @as(u32, 4);
const new_spsr = cpu.cpsr.raw;
cpu.changeMode(.Irq);
cpu.cpsr.t.write(false);
cpu.cpsr.i.write(true);
cpu.r[14] = ret_addr;
cpu.spsr.raw = new_spsr;
cpu.r[15] = 0x0000_0018;
cpu.pipe.reload(cpu);
}
/// Advances state so that the BIOS is skipped
///
/// Note: This accesses the CPU's bus ptr so it only may be called
/// once the Bus has been properly initialized
///
/// TODO: Make above notice impossible to do in code
pub fn fastBoot(cpu: *Arm7tdmi) void {
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
cpu.r = std.mem.zeroes([16]u32);
// cpu.r[0] = 0x08000000;
// cpu.r[1] = 0x000000EA;
cpu.r[13] = 0x0300_7F00;
cpu.r[15] = 0x0800_0000;
cpu.bank.r[Bank.regIdx(.Irq, .R13)] = 0x0300_7FA0;
cpu.bank.r[Bank.regIdx(.Supervisor, .R13)] = 0x0300_7FE0;
// cpu.cpsr.raw = 0x6000001F;
cpu.cpsr.raw = 0x0000_001F;
bus_ptr.bios.addr_latch = 0x0000_00DC + 8;
}

View File

@@ -1,60 +1,30 @@
const std = @import("std");
const SDL = @import("sdl2");
const config = @import("../config.zig");
const c = @import("../lib.zig").c;
const Scheduler = @import("scheduler.zig").Scheduler;
const Arm7tdmi = @import("arm32").Arm7tdmi;
const Bus = @import("Bus.zig");
const Tracker = @import("../util.zig").FpsTracker;
const Channel = @import("../util.zig").Queue;
const stepDmaTransfer = @import("cpu_util.zig").stepDmaTransfer;
const isHalted = @import("cpu_util.zig").isHalted;
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;
pub const Synchro = struct {
const AtomicBool = std.atomic.Value(bool);
// 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
// FIXME: This Enum ends up being really LARGE!!!
pub const Message = union(enum) {
rom_path: [std.fs.max_path_bytes]u8,
bios_path: [std.fs.max_path_bytes]u8,
restart: void,
};
// 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);
paused: AtomicBool = AtomicBool.init(true), // FIXME: can ui_busy and paused be the same?
should_quit: AtomicBool = AtomicBool.init(false),
ch: Channel(Message),
pub fn init(allocator: std.mem.Allocator) !@This() {
const msg_buf = try allocator.alloc(Message, 1);
return .{ .ch = Channel(Message).init(msg_buf) };
}
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
allocator.free(self.ch.inner.buf);
self.* = undefined;
}
};
/// 4 Cycles in 1 dot
const cycles_per_dot = 4;
/// 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
/// 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 = @as(f64, @floatFromInt(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);
@@ -65,37 +35,30 @@ const RunKind = enum {
LimitedFPS,
};
pub fn run(cpu: *Arm7tdmi, scheduler: *Scheduler, tracker: *Tracker, sync: *Synchro) void {
const audio_sync = config.config().guest.audio_sync and !config.config().host.mute;
pub fn run(quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: *FpsTracker) void {
const audio_sync = config.config().guest.audio_sync;
if (audio_sync) log.info("Audio sync enabled", .{});
if (config.config().guest.video_sync) {
inner(.LimitedFPS, audio_sync, cpu, scheduler, tracker, sync);
inner(.LimitedFPS, audio_sync, quit, scheduler, cpu, tracker);
} else {
inner(.UnlimitedFPS, audio_sync, cpu, scheduler, tracker, sync);
inner(.UnlimitedFPS, audio_sync, quit, scheduler, cpu, tracker);
}
}
fn inner(comptime kind: RunKind, audio_sync: bool, cpu: *Arm7tdmi, scheduler: *Scheduler, tracker: ?*Tracker, sync: *Synchro) void {
fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: ?*FpsTracker) void {
if (kind == .UnlimitedFPS or kind == .LimitedFPS) {
std.debug.assert(tracker != null);
log.info("FPS tracking enabled", .{});
}
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
// FIXME: audioSync accesses emulator state without any guarantees
switch (kind) {
.Unlimited, .UnlimitedFPS => {
log.info("Emulation w/out video sync", .{});
while (!sync.should_quit.load(.monotonic)) {
handleChannel(cpu, &sync.ch);
if (sync.paused.load(.monotonic)) continue;
while (!quit.load(.SeqCst)) {
runFrame(scheduler, cpu);
audioSync(audio_sync, bus_ptr.apu.stream, &bus_ptr.apu.is_buffer_full);
audioSync(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
if (kind == .UnlimitedFPS) tracker.?.tick();
}
@@ -105,10 +68,7 @@ fn inner(comptime kind: RunKind, audio_sync: bool, cpu: *Arm7tdmi, scheduler: *S
var timer = Timer.start() catch @panic("failed to initalize std.timer.Timer");
var wake_time: u64 = frame_period;
while (!sync.should_quit.load(.monotonic)) {
handleChannel(cpu, &sync.ch);
if (sync.paused.load(.monotonic)) continue;
while (!quit.load(.SeqCst)) {
runFrame(scheduler, cpu);
const new_wake_time = videoSync(&timer, wake_time);
@@ -117,7 +77,7 @@ fn inner(comptime kind: RunKind, audio_sync: bool, cpu: *Arm7tdmi, scheduler: *S
// the amount of time needed for audio to catch up rather than
// our expected wake-up time
audioSync(audio_sync, bus_ptr.apu.stream, &bus_ptr.apu.is_buffer_full);
audioSync(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
if (!audio_sync) spinLoop(&timer, wake_time);
wake_time = new_wake_time;
@@ -127,30 +87,14 @@ fn inner(comptime kind: RunKind, audio_sync: bool, cpu: *Arm7tdmi, scheduler: *S
}
}
inline fn handleChannel(cpu: *Arm7tdmi, channel: *Channel(Synchro.Message)) void {
const message = channel.pop() orelse return;
switch (message) {
.rom_path => |path_buf| {
const path = std.mem.sliceTo(&path_buf, 0);
replaceGamepak(cpu, path) catch |e| log.err("failed to replace GamePak: {}", .{e});
},
.bios_path => |path_buf| {
const path = std.mem.sliceTo(&path_buf, 0);
replaceBios(cpu, path) catch |e| log.err("failed to replace BIOS: {}", .{e});
},
.restart => reset(cpu),
}
}
pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
const frame_end = sched.tick + cycles_per_frame;
while (sched.tick < frame_end) {
if (!stepDmaTransfer(cpu)) {
if (isHalted(cpu)) {
if (!cpu.stepDmaTransfer()) {
if (cpu.isHalted()) {
// Fast-forward to next Event
sched.tick = sched.nextTimestamp();
sched.tick = sched.queue.peek().?.tick;
} else {
cpu.step();
}
@@ -160,35 +104,21 @@ pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
}
}
fn audioSync(audio_sync: bool, stream: *c.SDL_AudioStream, is_buffer_full: *bool) void {
// comptime std.debug.assert(@import("../platform.zig").sample_format == SDL.AUDIO_U16);
fn audioSync(audio_sync: bool, stream: *SDL.SDL_AudioStream, is_buffer_full: *bool) void {
const sample_size = 2 * @sizeOf(u16);
const max_buf_size: c_int = 0x400;
_ = audio_sync;
_ = stream;
_ = is_buffer_full;
// Determine whether the APU is busy right at this moment
var still_full: bool = SDL.SDL_AudioStreamAvailable(stream) > sample_size * if (is_buffer_full.*) max_buf_size >> 1 else max_buf_size;
defer is_buffer_full.* = still_full; // Update APU Busy status right before exiting scope
_ = sample_size;
_ = max_buf_size;
// If Busy is false, there's no need to sync here
if (!still_full) return;
// TODO(paoda): re-enable
// // Determine whether the APU is busy right at this moment
// var still_full: bool = SDL.SDL_AudioStreamAvailable(stream) > sample_size * if (is_buffer_full.*) max_buf_size >> 1 else max_buf_size;
// defer is_buffer_full.* = still_full; // Update APU Busy status right before exiting scope
// // If Busy is false, there's no need to sync here
// if (!still_full) return;
// // TODO: Refactor!!!!
// // while (SDL.SDL_AudioStreamAvailable(stream) > sample_size * max_buf_size >> 1)
// // std.atomic.spinLoopHint();
// while (true) {
// still_full = SDL.SDL_AudioStreamAvailable(stream) > sample_size * max_buf_size >> 1;
// if (!audio_sync or !still_full) break;
// }
while (true) {
still_full = SDL.SDL_AudioStreamAvailable(stream) > sample_size * max_buf_size >> 1;
if (!audio_sync or !still_full) break;
}
}
fn videoSync(timer: *Timer, wake_time: u64) u64 {
@@ -202,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
@@ -213,145 +144,19 @@ 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.Thread.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;
}
fn spinLoop(timer: *Timer, wake_time: u64) void {
while (timer.read() < wake_time)
std.atomic.spinLoopHint();
}
pub const EmuThing = struct {
const Self = @This();
const Interface = @import("gdbstub").Emulator;
const Allocator = std.mem.Allocator;
pub const target =
\\<target version="1.0">
\\ <architecture>armv4t</architecture>
\\ <feature name="org.gnu.gdb.arm.core">
\\ <reg name="r0" bitsize="32" type="uint32"/>
\\ <reg name="r1" bitsize="32" type="uint32"/>
\\ <reg name="r2" bitsize="32" type="uint32"/>
\\ <reg name="r3" bitsize="32" type="uint32"/>
\\ <reg name="r4" bitsize="32" type="uint32"/>
\\ <reg name="r5" bitsize="32" type="uint32"/>
\\ <reg name="r6" bitsize="32" type="uint32"/>
\\ <reg name="r7" bitsize="32" type="uint32"/>
\\ <reg name="r8" bitsize="32" type="uint32"/>
\\ <reg name="r9" bitsize="32" type="uint32"/>
\\ <reg name="r10" bitsize="32" type="uint32"/>
\\ <reg name="r11" bitsize="32" type="uint32"/>
\\ <reg name="r12" bitsize="32" type="uint32"/>
\\ <reg name="sp" bitsize="32" type="data_ptr"/>
\\ <reg name="lr" bitsize="32"/>
\\ <reg name="pc" bitsize="32" type="code_ptr"/>
\\
\\ <reg name="cpsr" bitsize="32" regnum="25"/>
\\ </feature>
\\</target>
;
// Game Pak SRAM isn't included
// TODO: Can i be more specific here?
pub const map =
\\ <memory-map version="1.0">
\\ <memory type="rom" start="0x00000000" length="0x00004000"/>
\\ <memory type="ram" start="0x02000000" length="0x00040000"/>
\\ <memory type="ram" start="0x03000000" length="0x00008000"/>
\\ <memory type="ram" start="0x04000000" length="0x00000400"/>
\\ <memory type="ram" start="0x05000000" length="0x00000400"/>
\\ <memory type="ram" start="0x06000000" length="0x00018000"/>
\\ <memory type="ram" start="0x07000000" length="0x00000400"/>
\\ <memory type="rom" start="0x08000000" length="0x02000000"/>
\\ <memory type="rom" start="0x0A000000" length="0x02000000"/>
\\ <memory type="rom" start="0x0C000000" length="0x02000000"/>
\\ </memory-map>
;
cpu: *Arm7tdmi,
scheduler: *Scheduler,
pub fn init(cpu: *Arm7tdmi, scheduler: *Scheduler) Self {
return .{ .cpu = cpu, .scheduler = scheduler };
}
pub fn interface(self: *Self) Interface {
return Interface.init(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 (!stepDmaTransfer(cpu)) {
if (isHalted(cpu)) {
// 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);
}
}
};
fn reset(cpu: *Arm7tdmi) void {
// @breakpoint();
cpu.sched.reset(); // Yes this is order sensitive, see the PPU reset for why
cpu.bus.reset();
cpu.reset();
}
fn replaceGamepak(cpu: *Arm7tdmi, file_path: []const u8) !void {
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
try bus_ptr.replaceGamepak(file_path);
reset(cpu);
}
fn replaceBios(cpu: *Arm7tdmi, file_path: []const u8) !void {
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
const allocator = bus_ptr.bios.allocator;
const bios_len = 0x4000;
bus_ptr.bios.buf = try allocator.alloc(u8, bios_len);
try bus_ptr.bios.load(file_path);
while (true) if (timer.read() > wake_time) break;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,44 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const buf_len = 0x400;
const Self = @This();
buf: []u8,
allocator: Allocator,
pub fn read(self: *const Self, comptime T: type, address: usize) T {
const addr = address & 0x3FF;
return switch (T) {
u32, u16, u8 => std.mem.readInt(T, self.buf[addr..][0..@sizeOf(T)], .little),
else => @compileError("OAM: Unsupported read width"),
};
}
pub fn write(self: *Self, comptime T: type, address: usize, value: T) void {
const addr = address & 0x3FF;
switch (T) {
u32, u16 => std.mem.writeInt(T, self.buf[addr..][0..@sizeOf(T)], value, .little),
u8 => return, // 8-bit writes are explicitly ignored
else => @compileError("OAM: Unsupported write width"),
}
}
pub fn init(allocator: Allocator) !Self {
const buf = try allocator.alloc(u8, buf_len);
@memset(buf, 0);
return Self{ .buf = buf, .allocator = allocator };
}
pub fn reset(self: *Self) void {
@memset(self.buf, 0);
}
pub fn deinit(self: *Self) void {
self.allocator.free(self.buf);
self.* = undefined;
}

View File

@@ -1,51 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const buf_len = 0x400;
const Self = @This();
buf: []u8,
allocator: Allocator,
pub fn read(self: *const Self, comptime T: type, address: usize) T {
const addr = address & 0x3FF;
return switch (T) {
u32, u16, u8 => std.mem.readInt(T, self.buf[addr..][0..@sizeOf(T)], .little),
else => @compileError("PALRAM: Unsupported read width"),
};
}
pub fn write(self: *Self, comptime T: type, address: usize, value: T) void {
const addr = address & 0x3FF;
switch (T) {
u32, u16 => std.mem.writeInt(T, self.buf[addr..][0..@sizeOf(T)], value, .little),
u8 => {
const align_addr = addr & ~@as(u32, 1); // Aligned to Halfword boundary
std.mem.writeInt(u16, self.buf[align_addr..][0..@sizeOf(u16)], @as(u16, value) * 0x101, .little);
},
else => @compileError("PALRAM: Unsupported write width"),
}
}
pub fn init(allocator: Allocator) !Self {
const buf = try allocator.alloc(u8, buf_len);
@memset(buf, 0);
return Self{ .buf = buf, .allocator = allocator };
}
pub fn reset(self: *Self) void {
@memset(self.buf, 0);
}
pub fn deinit(self: *Self) void {
self.allocator.free(self.buf);
self.* = undefined;
}
pub inline fn backdrop(self: *const Self) u16 {
return std.mem.readInt(u16, self.buf[0..2], .little);
}

View File

@@ -1,64 +0,0 @@
const std = @import("std");
const io = @import("../bus/io.zig");
const Allocator = std.mem.Allocator;
const buf_len = 0x18000;
const Self = @This();
buf: []u8,
allocator: Allocator,
pub fn read(self: *const Self, comptime T: type, address: usize) T {
const addr = Self.mirror(address);
return switch (T) {
u32, u16, u8 => std.mem.readInt(T, self.buf[addr..][0..@sizeOf(T)], .little),
else => @compileError("VRAM: Unsupported read width"),
};
}
pub fn write(self: *Self, comptime T: type, dispcnt: io.DisplayControl, address: usize, value: T) void {
const mode: u3 = dispcnt.bg_mode.read();
const idx = Self.mirror(address);
switch (T) {
u32, u16 => std.mem.writeInt(T, self.buf[idx..][0..@sizeOf(T)], value, .little),
u8 => {
// Ignore write if it falls within the boundaries of OBJ VRAM
switch (mode) {
0, 1, 2 => if (0x0001_0000 <= idx) return,
else => if (0x0001_4000 <= idx) return,
}
const align_idx = idx & ~@as(u32, 1); // Aligned to a halfword boundary
std.mem.writeInt(u16, self.buf[align_idx..][0..@sizeOf(u16)], @as(u16, value) * 0x101, .little);
},
else => @compileError("VRAM: Unsupported write width"),
}
}
pub fn init(allocator: Allocator) !Self {
const buf = try allocator.alloc(u8, buf_len);
@memset(buf, 0);
return Self{ .buf = buf, .allocator = allocator };
}
pub fn reset(self: *Self) void {
@memset(self.buf, 0);
}
pub fn deinit(self: *Self) void {
self.allocator.free(self.buf);
self.* = undefined;
}
pub fn mirror(address: usize) usize {
// Mirrored in steps of 128K (64K + 32K + 32K) (abcc)
const addr = address & 0x1FFFF;
// If the address is within 96K we don't do anything,
// otherwise we want to mirror the last 32K (addresses between 64K and 96K)
return if (addr < buf_len) addr else 0x10000 + (addr & 0x7FFF);
}

View File

@@ -1,7 +1,7 @@
const std = @import("std");
const Arm7tdmi = @import("arm32").Arm7tdmi;
const Bus = @import("Bus.zig");
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
const Clock = @import("bus/gpio.zig").Clock;
const Order = std.math.Order;
@@ -12,11 +12,11 @@ const log = std.log.scoped(.Scheduler);
pub const Scheduler = struct {
const Self = @This();
tick: u64 = 0,
tick: u64,
queue: PriorityQueue(Event, void, lessThan),
pub fn init(allocator: Allocator) Self {
var sched = Self{ .queue = PriorityQueue(Event, void, lessThan).init(allocator, {}) };
var sched = Self{ .tick = 0, .queue = PriorityQueue(Event, void, lessThan).init(allocator, {}) };
sched.queue.add(.{ .kind = .HeatDeath, .tick = std.math.maxInt(u64) }) catch unreachable;
return sched;
@@ -27,73 +27,69 @@ pub const Scheduler = struct {
self.* = undefined;
}
pub fn reset(self: *Self) void {
// `std.PriorityQueue` provides no reset function, so we will just create a new one
const allocator = self.queue.allocator;
self.queue.deinit();
var new_queue = PriorityQueue(Event, void, lessThan).init(allocator, {});
new_queue.add(.{ .kind = .HeatDeath, .tick = std.math.maxInt(u64) }) catch unreachable;
self.* = .{ .queue = new_queue };
}
pub inline fn now(self: *const Self) u64 {
return self.tick;
}
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;
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
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.handleHDrawEnd(cpu, late);
},
.TimerOverflow => |id| {
switch (id) {
0 => cpu.bus.tim[0].handleOverflow(cpu, late),
1 => cpu.bus.tim[1].handleOverflow(cpu, late),
2 => cpu.bus.tim[2].handleOverflow(cpu, late),
3 => cpu.bus.tim[3].handleOverflow(cpu, late),
}
},
.ApuChannel => |id| {
switch (id) {
0 => cpu.bus.apu.ch1.channelTimerOverflow(late),
1 => cpu.bus.apu.ch2.channelTimerOverflow(late),
2 => cpu.bus.apu.ch3.channelTimerOverflow(late),
3 => cpu.bus.apu.ch4.channelTimerOverflow(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
bus_ptr.ppu.drawScanline();
bus_ptr.ppu.onHdrawEnd(cpu, late);
},
.TimerOverflow => |id| {
switch (id) {
inline 0...3 => |idx| bus_ptr.tim[idx].onTimerExpire(cpu, late),
}
},
.ApuChannel => |id| {
switch (id) {
0 => bus_ptr.apu.ch1.onToneSweepEvent(late),
1 => bus_ptr.apu.ch2.onToneEvent(late),
2 => bus_ptr.apu.ch3.onWaveEvent(late),
3 => bus_ptr.apu.ch4.onNoiseEvent(late),
}
},
.RealTimeClock => {
const device = &bus_ptr.pak.gpio.device;
if (device.kind != .Rtc or device.ptr == null) return;
const clock: *Clock = @ptrCast(@alignCast(device.ptr.?));
clock.onClockUpdate(late);
},
.FrameSequencer => bus_ptr.apu.onSequencerTick(late),
.SampleAudio => bus_ptr.apu.sampleAudio(late),
.HBlank => bus_ptr.ppu.onHblankEnd(cpu, late), // The end of a HBlank
.VBlank => bus_ptr.ppu.onHdrawEnd(cpu, late), // The end of a VBlank
const clock = @ptrCast(*Clock, @alignCast(@alignOf(*Clock), device.ptr.?));
clock.updateTime(late);
},
.FrameSequencer => cpu.bus.apu.tickFrameSequencer(late),
.SampleAudio => cpu.bus.apu.sampleAudio(late),
.HBlank => cpu.bus.ppu.handleHBlankEnd(cpu, late), // The end of a HBlank
.VBlank => cpu.bus.ppu.handleHDrawEnd(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

@@ -1,516 +0,0 @@
//! Namespace for dealing with ZBA's immediate-mode GUI
//! Currently, ZBA uses zgui from https://github.com/michal-z/zig-gamedev
//! which provides Zig bindings for https://github.com/ocornut/imgui under the hood
const std = @import("std");
const zgui = @import("zgui");
const gl = @import("gl");
const nfd = @import("nfd");
const config = @import("config.zig");
const emu = @import("core/emu.zig");
const Gui = @import("platform.zig").Gui;
const Arm7tdmi = @import("arm32").Arm7tdmi;
const Scheduler = @import("core/scheduler.zig").Scheduler;
const Bus = @import("core/Bus.zig");
const Synchro = @import("core/emu.zig").Synchro;
const RingBuffer = @import("zba_util").RingBuffer;
const Dimensions = @import("platform.zig").Dimensions;
const Allocator = std.mem.Allocator;
const GLuint = gl.uint;
const gba_width = @import("core/ppu.zig").width;
const gba_height = @import("core/ppu.zig").height;
const log = std.log.scoped(.Imgui);
// two seconds worth of fps values into the past
const histogram_len = 0x80;
/// Immediate-Mode GUI State
pub const State = struct {
title: [12:0]u8,
fps_hist: RingBuffer(u32),
should_quit: bool = false,
emulation: Emulation,
win_stat: WindowStatus = .{},
const WindowStatus = struct {
show_deps: bool = false,
show_regs: bool = false,
show_schedule: bool = false,
show_perf: bool = false,
show_palette: bool = false,
};
const Emulation = union(enum) {
Active,
Inactive,
Transition: enum { Active, Inactive },
};
/// if zba is initialized with a ROM already provided, this initializer should be called
/// with `title_opt` being non-null
pub fn init(allocator: Allocator, title_opt: ?*const [12]u8) !@This() {
const history = try allocator.alloc(u32, histogram_len);
return .{
.title = handleTitle(title_opt),
.emulation = if (title_opt == null) .Inactive else .{ .Transition = .Active },
.fps_hist = RingBuffer(u32).init(history),
};
}
pub fn deinit(self: *@This(), allocator: Allocator) void {
allocator.free(self.fps_hist.buf);
self.* = undefined;
}
};
pub fn draw(state: *State, sync: *Synchro, dim: Dimensions, cpu: *const Arm7tdmi, tex_id: GLuint) bool {
const scn_scale = config.config().host.win_scale;
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
zgui.backend.newFrame(dim.width, dim.height);
state.title = handleTitle(&bus_ptr.pak.title);
{
_ = zgui.beginMainMenuBar();
defer zgui.endMainMenuBar();
if (zgui.beginMenu("File", true)) {
defer zgui.endMenu();
if (zgui.menuItem("Quit", .{}))
state.should_quit = true;
if (zgui.menuItem("Insert ROM", .{})) blk: {
const file_path = tmp: {
const path_opt = nfd.openFileDialog("gba", null) catch |e| {
log.err("file dialog failed to open: {}", .{e});
break :blk;
};
break :tmp path_opt orelse {
log.warn("did not receive a file path", .{});
break :blk;
};
};
defer nfd.freePath(file_path);
log.info("user chose: \"{s}\"", .{file_path});
const message = tmp: {
var msg: Synchro.Message = .{ .rom_path = undefined };
@memcpy(msg.rom_path[0..file_path.len], file_path);
break :tmp msg;
};
sync.ch.push(message) catch |e| {
log.err("failed to send file path to emu thread: {}", .{e});
break :blk;
};
state.emulation = .{ .Transition = .Active };
}
if (zgui.menuItem("Load BIOS", .{})) blk: {
const file_path = tmp: {
const path_opt = nfd.openFileDialog("bin", null) catch |e| {
log.err("file dialog failed to open: {}", .{e});
break :blk;
};
break :tmp path_opt orelse {
log.warn("did not receive a file path", .{});
break :blk;
};
};
defer nfd.freePath(file_path);
log.info("user chose: \"{s}\"", .{file_path});
const message = tmp: {
var msg: Synchro.Message = .{ .bios_path = undefined };
@memcpy(msg.bios_path[0..file_path.len], file_path);
break :tmp msg;
};
sync.ch.push(message) catch |e| {
log.err("failed to send file path to emu thread: {}", .{e});
break :blk;
};
}
}
if (zgui.beginMenu("Emulation", true)) {
defer zgui.endMenu();
if (zgui.menuItem("Registers", .{ .selected = state.win_stat.show_regs }))
state.win_stat.show_regs = true;
if (zgui.menuItem("Palette", .{ .selected = state.win_stat.show_palette }))
state.win_stat.show_palette = true;
if (zgui.menuItem("Schedule", .{ .selected = state.win_stat.show_schedule }))
state.win_stat.show_schedule = true;
if (zgui.menuItem("Paused", .{ .selected = state.emulation == .Inactive })) {
state.emulation = switch (state.emulation) {
.Active => .{ .Transition = .Inactive },
.Inactive => .{ .Transition = .Active },
else => state.emulation,
};
}
if (zgui.menuItem("Restart", .{}))
sync.ch.push(.restart) catch |e| log.err("failed to send restart req to emu thread: {}", .{e});
}
if (zgui.beginMenu("Stats", true)) {
defer zgui.endMenu();
if (zgui.menuItem("Performance", .{ .selected = state.win_stat.show_perf }))
state.win_stat.show_perf = true;
}
if (zgui.beginMenu("Help", true)) {
defer zgui.endMenu();
if (zgui.menuItem("Dependencies", .{ .selected = state.win_stat.show_deps }))
state.win_stat.show_deps = true;
}
}
{
const w: f32 = @floatFromInt(gba_width * scn_scale);
const h: f32 = @floatFromInt(gba_height * scn_scale);
const window_title = std.mem.sliceTo(&state.title, 0);
_ = zgui.begin(window_title, .{ .flags = .{ .no_resize = true, .always_auto_resize = true } });
defer zgui.end();
// FIXME(paoda): go look at some documentation here
zgui.image(.{ .tex_data = null, .tex_id = @enumFromInt(tex_id) }, .{ .w = w, .h = h });
}
// TODO: Any other steps to respect the copyright of the libraries I use?
if (state.win_stat.show_deps) {
_ = zgui.begin("Dependencies", .{ .popen = &state.win_stat.show_deps });
defer zgui.end();
zgui.bulletText("known-folders by ziglibs", .{});
zgui.bulletText("nfd-zig ported by Fabio Arnold", .{});
{
zgui.indent(.{});
defer zgui.unindent(.{});
zgui.bulletText("nativefiledialog by Michael Labbe", .{});
}
zgui.bulletText("SDL ported by Carl Åstholm", .{});
{
zgui.indent(.{});
defer zgui.unindent(.{});
zgui.bulletText("SDL by Sam Lantinga", .{});
}
// zgui.bulletText("tomlz by Matthew Hall", .{});
zgui.bulletText("zba-gdbstub by Rekai Musuka", .{});
zgui.bulletText("zba-util by Rekai Musuka", .{});
zgui.bulletText("zgui ported by Michal Ziulek et al.", .{});
{
zgui.indent(.{});
defer zgui.unindent(.{});
zgui.bulletText("DearImGui by Omar Cornut", .{});
}
zgui.bulletText("zig-clap by Jimmi Holst Christensen", .{});
zgui.bulletText("zig-datetime by Jairus Martin", .{});
zgui.newLine();
zgui.bulletText("bitfield.zig by Hannes Bredberg et al.", .{});
zgui.bulletText("zigglgen ported by Carl Åstholm", .{});
{
zgui.indent(.{});
defer zgui.unindent(.{});
zgui.bulletText("OpenGL-Registry by The Khronos Group", .{});
}
}
if (state.win_stat.show_regs) {
_ = zgui.begin("Guest Registers", .{ .popen = &state.win_stat.show_regs });
defer zgui.end();
for (0..8) |i| {
zgui.text("R{}: 0x{X:0>8}", .{ i, cpu.r[i] });
zgui.sameLine(.{});
const padding = if (8 + i < 10) " " else "";
zgui.text("{s}R{}: 0x{X:0>8}", .{ padding, 8 + i, cpu.r[8 + i] });
}
zgui.separator();
widgets.psr("CPSR", cpu.cpsr);
widgets.psr("SPSR", cpu.spsr);
zgui.separator();
widgets.interrupts(" IE", bus_ptr.io.ie);
widgets.interrupts("IRQ", bus_ptr.io.irq);
}
if (state.win_stat.show_perf) {
_ = zgui.begin("Performance", .{ .popen = &state.win_stat.show_perf });
defer zgui.end();
// const tmp = blk: {
// var buf: [histogram_len]u32 = undefined;
// const len = state.fps_hist.copy(&buf);
// break :blk .{ buf, len };
// };
// const values = tmp[0];
// const len = tmp[1];
// if (len == values.len) _ = state.fps_hist.pop();
// const sorted = blk: {
// var buf: @TypeOf(values) = undefined;
// @memcpy(buf[0..len], values[0..len]);
// std.mem.sort(u32, buf[0..len], {}, std.sort.asc(u32));
// break :blk buf;
// };
// const y_max: f64 = 2 * if (len != 0) @as(f64, @floatFromInt(sorted[len - 1])) else emu.frame_rate;
// const x_max: f64 = @floatFromInt(values.len);
// const y_args = .{ .flags = .{ .no_grid_lines = true } };
// const x_args = .{ .flags = .{ .no_grid_lines = true, .no_tick_labels = true, .no_tick_marks = true } };
// if (zgui.plot.beginPlot("Emulation FPS", .{ .w = 0.0, .flags = .{ .no_title = true, .no_frame = true } })) {
// defer zgui.plot.endPlot();
// zgui.plot.setupLegend(.{ .north = true, .east = true }, .{});
// zgui.plot.setupAxis(.x1, x_args);
// zgui.plot.setupAxis(.y1, y_args);
// zgui.plot.setupAxisLimits(.y1, .{ .min = 0.0, .max = y_max, .cond = .always });
// zgui.plot.setupAxisLimits(.x1, .{ .min = 0.0, .max = x_max, .cond = .always });
// zgui.plot.setupFinish();
// zgui.plot.plotLineValues("FPS", u32, .{ .v = values[0..len] });
// }
// const stats: struct { u32, u32, u32 } = blk: {
// if (len == 0) break :blk .{ 0, 0, 0 };
// const average: u32 = average: {
// var sum: u32 = 0;
// for (sorted[0..len]) |value| sum += value;
// break :average @intCast(sum / len);
// };
// const median = sorted[len / 2];
// const low = sorted[len / 100]; // 1% Low
// break :blk .{ average, median, low };
// };
// zgui.text("Average: {:0>3} fps", .{stats[0]});
// zgui.text(" Median: {:0>3} fps", .{stats[1]});
// zgui.text(" 1% Low: {:0>3} fps", .{stats[2]});
}
if (state.win_stat.show_schedule) {
_ = zgui.begin("Schedule", .{ .popen = &state.win_stat.show_schedule });
defer zgui.end();
const scheduler = cpu.sched;
_ = scheduler;
// zgui.text("tick: {X:0>16}", .{scheduler.now()});
zgui.text("tick: (TODO: FIX THIS)", .{});
zgui.separator();
const sched_ptr: *Scheduler = @ptrCast(@alignCast(cpu.sched.ptr));
const Event = std.meta.Child(@TypeOf(sched_ptr.queue.items));
var items: [20]Event = undefined;
const len = @min(sched_ptr.queue.items.len, items.len);
@memcpy(items[0..len], sched_ptr.queue.items[0..len]);
std.mem.sort(Event, items[0..len], {}, widgets.eventDesc(Event));
for (items[0..len]) |event| {
// zgui.text("{X:0>16} | {?}", .{ event.tick, event.kind });
_ = event;
zgui.text("TODO: Fix This", .{});
}
}
if (state.win_stat.show_palette) {
_ = zgui.begin("Palette", .{ .popen = &state.win_stat.show_palette });
defer zgui.end();
widgets.paletteGrid(.Background, cpu);
zgui.sameLine(.{ .spacing = 20.0 });
widgets.paletteGrid(.Object, cpu);
}
// {
// zgui.showDemoWindow(null);
// }
return true; // request redraw
}
const widgets = struct {
const PaletteKind = enum { Background, Object };
fn paletteGrid(comptime kind: PaletteKind, cpu: *const Arm7tdmi) void {
_ = zgui.beginGroup();
defer zgui.endGroup();
const address: u32 = switch (kind) {
.Background => 0x0500_0000,
.Object => 0x0500_0200,
};
for (0..0x100) |i| {
const offset: u32 = @truncate(i);
const bgr555 = cpu.bus.dbgRead(u16, address + offset * @sizeOf(u16));
widgets.colourSquare(bgr555);
if ((i + 1) % 0x10 != 0) zgui.sameLine(.{});
}
zgui.text(@tagName(kind), .{});
}
fn colourSquare(bgr555: u16) void {
// FIXME: working with the packed struct enum is currently broken :pensive:
const ImguiColorEditFlags_NoInputs: u32 = 1 << 5;
const ImguiColorEditFlags_NoPicker: u32 = 1 << 2;
const flags: zgui.ColorEditFlags = @bitCast(ImguiColorEditFlags_NoInputs | ImguiColorEditFlags_NoPicker);
const b: f32 = @floatFromInt(bgr555 >> 10 & 0x1f);
const g: f32 = @floatFromInt(bgr555 >> 5 & 0x1F);
const r: f32 = @floatFromInt(bgr555 & 0x1F);
var col = [_]f32{ r / 31.0, g / 31.0, b / 31.0 };
_ = zgui.colorEdit3("", .{ .col = &col, .flags = flags });
}
fn interrupts(comptime label: []const u8, int: anytype) void {
const h = 15.0;
const w = 9.0 * 2 + 3.5;
const ww = 9.0 * 3;
{
zgui.text(label ++ ":", .{});
zgui.sameLine(.{});
_ = zgui.selectable("VBL", .{ .w = w, .h = h, .selected = int.vblank.read() });
zgui.sameLine(.{});
_ = zgui.selectable("HBL", .{ .w = w, .h = h, .selected = int.hblank.read() });
zgui.sameLine(.{});
_ = zgui.selectable("VCT", .{ .w = w, .h = h, .selected = int.coincidence.read() });
{
zgui.sameLine(.{});
_ = zgui.selectable("TIM0", .{ .w = ww, .h = h, .selected = int.tim0.read() });
zgui.sameLine(.{});
_ = zgui.selectable("TIM1", .{ .w = ww, .h = h, .selected = int.tim1.read() });
zgui.sameLine(.{});
_ = zgui.selectable("TIM2", .{ .w = ww, .h = h, .selected = int.tim2.read() });
zgui.sameLine(.{});
_ = zgui.selectable("TIM3", .{ .w = ww, .h = h, .selected = int.tim3.read() });
}
zgui.sameLine(.{});
_ = zgui.selectable("SRL", .{ .w = w, .h = h, .selected = int.serial.read() });
{
zgui.sameLine(.{});
_ = zgui.selectable("DMA0", .{ .w = ww, .h = h, .selected = int.dma0.read() });
zgui.sameLine(.{});
_ = zgui.selectable("DMA1", .{ .w = ww, .h = h, .selected = int.dma1.read() });
zgui.sameLine(.{});
_ = zgui.selectable("DMA2", .{ .w = ww, .h = h, .selected = int.dma2.read() });
zgui.sameLine(.{});
_ = zgui.selectable("DMA3", .{ .w = ww, .h = h, .selected = int.dma3.read() });
}
zgui.sameLine(.{});
_ = zgui.selectable("KPD", .{ .w = w, .h = h, .selected = int.keypad.read() });
zgui.sameLine(.{});
_ = zgui.selectable("GPK", .{ .w = w, .h = h, .selected = int.game_pak.read() });
}
}
fn psr(comptime label: []const u8, register: anytype) void {
const Mode = @import("arm32").arm.Mode;
const maybe_mode = std.meta.intToEnum(Mode, register.mode.read()) catch null;
const mode = if (maybe_mode) |mode| mode.toString() else "???";
const w = 9.0;
const h = 15.0;
zgui.text(label ++ ": 0x{X:0>8}", .{register.raw});
zgui.sameLine(.{});
_ = zgui.selectable("N", .{ .w = w, .h = h, .selected = register.n.read() });
zgui.sameLine(.{});
_ = zgui.selectable("Z", .{ .w = w, .h = h, .selected = register.z.read() });
zgui.sameLine(.{});
_ = zgui.selectable("C", .{ .w = w, .h = h, .selected = register.c.read() });
zgui.sameLine(.{});
_ = zgui.selectable("V", .{ .w = w, .h = h, .selected = register.v.read() });
zgui.sameLine(.{});
zgui.text("{s}", .{mode});
}
fn eventDesc(comptime T: type) fn (void, T, T) bool {
return struct {
fn inner(_: void, left: T, right: T) bool {
return left.tick > right.tick;
}
}.inner;
}
};
fn handleTitle(title_opt: ?*const [12]u8) [12:0]u8 {
if (title_opt == null) return "[N/A Title]\x00".*; // No ROM present
const title = title_opt.?;
// ROM Title is an empty string (ImGui hates these)
if (title[0] == '\x00') return "[No Title]\x00\x00".*;
return title.* ++ [_:0]u8{};
}

View File

@@ -1,28 +0,0 @@
pub const c = @cImport({
@cDefine("SDL_DISABLE_OLD_NAMES", {});
@cDefine("SDL_MAIN_HANDLED", {});
@cInclude("SDL3/SDL.h");
@cInclude("SDL3/SDL_main.h");
});
// https://github.com/castholm/zig-examples/blob/77a829c85b5ddbad673026d504626015db4093ac/opengl-sdl/main.zig#L200-L219
pub inline fn errify(value: anytype) error{sdl_error}!switch (@typeInfo(@TypeOf(value))) {
.bool => void,
.pointer, .optional => @TypeOf(value.?),
.int => |info| switch (info.signedness) {
.signed => @TypeOf(@max(0, value)),
.unsigned => @TypeOf(value),
},
else => @compileError("unerrifiable type: " ++ @typeName(@TypeOf(value))),
} {
return switch (@typeInfo(@TypeOf(value))) {
.bool => if (!value) error.sdl_error,
.pointer, .optional => value orelse error.sdl_error,
.int => |info| switch (info.signedness) {
.signed => if (value >= 0) @max(0, value) else error.sdl_error,
.unsigned => if (value != 0) value else error.sdl_error,
},
else => comptime unreachable,
};
}

View File

@@ -1,548 +0,0 @@
// FIFO of fixed size items
// Usually used for e.g. byte buffers
const std = @import("std");
const math = std.math;
const mem = std.mem;
const Allocator = mem.Allocator;
const assert = std.debug.assert;
const testing = std.testing;
pub const LinearFifoBufferType = union(enum) {
/// The buffer is internal to the fifo; it is of the specified size.
static: usize,
/// The buffer is passed as a slice to the initialiser.
slice,
/// The buffer is managed dynamically using a `mem.Allocator`.
dynamic,
};
pub fn LinearFifo(
comptime T: type,
comptime buffer_type: LinearFifoBufferType,
) type {
const autoalign = false;
const powers_of_two = switch (buffer_type) {
.static => std.math.isPowerOfTwo(buffer_type.static),
.slice => false, // Any size slice could be passed in
.dynamic => true, // This could be configurable in future
};
return struct {
allocator: if (buffer_type == .dynamic) Allocator else void,
buf: if (buffer_type == .static) [buffer_type.static]T else []T,
head: usize,
count: usize,
const Self = @This();
pub const Reader = std.io.Reader(*Self, error{}, readFn);
pub const Writer = std.io.Writer(*Self, error{OutOfMemory}, appendWrite);
// Type of Self argument for slice operations.
// If buffer is inline (Static) then we need to ensure we haven't
// returned a slice into a copy on the stack
const SliceSelfArg = if (buffer_type == .static) *Self else Self;
pub const init = switch (buffer_type) {
.static => initStatic,
.slice => initSlice,
.dynamic => initDynamic,
};
fn initStatic() Self {
comptime assert(buffer_type == .static);
return .{
.allocator = {},
.buf = undefined,
.head = 0,
.count = 0,
};
}
fn initSlice(buf: []T) Self {
comptime assert(buffer_type == .slice);
return .{
.allocator = {},
.buf = buf,
.head = 0,
.count = 0,
};
}
fn initDynamic(allocator: Allocator) Self {
comptime assert(buffer_type == .dynamic);
return .{
.allocator = allocator,
.buf = &.{},
.head = 0,
.count = 0,
};
}
pub fn deinit(self: Self) void {
if (buffer_type == .dynamic) self.allocator.free(self.buf);
}
pub fn realign(self: *Self) void {
if (self.buf.len - self.head >= self.count) {
mem.copyForwards(T, self.buf[0..self.count], self.buf[self.head..][0..self.count]);
self.head = 0;
} else {
var tmp: [4096 / 2 / @sizeOf(T)]T = undefined;
while (self.head != 0) {
const n = @min(self.head, tmp.len);
const m = self.buf.len - n;
@memcpy(tmp[0..n], self.buf[0..n]);
mem.copyForwards(T, self.buf[0..m], self.buf[n..][0..m]);
@memcpy(self.buf[m..][0..n], tmp[0..n]);
self.head -= n;
}
}
{ // set unused area to undefined
const unused = mem.sliceAsBytes(self.buf[self.count..]);
@memset(unused, undefined);
}
}
/// Reduce allocated capacity to `size`.
pub fn shrink(self: *Self, size: usize) void {
assert(size >= self.count);
if (buffer_type == .dynamic) {
self.realign();
self.buf = self.allocator.realloc(self.buf, size) catch |e| switch (e) {
error.OutOfMemory => return, // no problem, capacity is still correct then.
};
}
}
/// Ensure that the buffer can fit at least `size` items
pub fn ensureTotalCapacity(self: *Self, size: usize) !void {
if (self.buf.len >= size) return;
if (buffer_type == .dynamic) {
self.realign();
const new_size = if (powers_of_two) math.ceilPowerOfTwo(usize, size) catch return error.OutOfMemory else size;
self.buf = try self.allocator.realloc(self.buf, new_size);
} else {
return error.OutOfMemory;
}
}
/// Makes sure at least `size` items are unused
pub fn ensureUnusedCapacity(self: *Self, size: usize) error{OutOfMemory}!void {
if (self.writableLength() >= size) return;
return try self.ensureTotalCapacity(math.add(usize, self.count, size) catch return error.OutOfMemory);
}
/// Returns number of items currently in fifo
pub fn readableLength(self: Self) usize {
return self.count;
}
/// Returns a writable slice from the 'read' end of the fifo
fn readableSliceMut(self: SliceSelfArg, offset: usize) []T {
if (offset > self.count) return &[_]T{};
var start = self.head + offset;
if (start >= self.buf.len) {
start -= self.buf.len;
return self.buf[start .. start + (self.count - offset)];
} else {
const end = @min(self.head + self.count, self.buf.len);
return self.buf[start..end];
}
}
/// Returns a readable slice from `offset`
pub fn readableSlice(self: SliceSelfArg, offset: usize) []const T {
return self.readableSliceMut(offset);
}
pub fn readableSliceOfLen(self: *Self, len: usize) []const T {
assert(len <= self.count);
const buf = self.readableSlice(0);
if (buf.len >= len) {
return buf[0..len];
} else {
self.realign();
return self.readableSlice(0)[0..len];
}
}
/// Discard first `count` items in the fifo
pub fn discard(self: *Self, count: usize) void {
assert(count <= self.count);
{ // set old range to undefined. Note: may be wrapped around
const slice = self.readableSliceMut(0);
if (slice.len >= count) {
const unused = mem.sliceAsBytes(slice[0..count]);
@memset(unused, undefined);
} else {
const unused = mem.sliceAsBytes(slice[0..]);
@memset(unused, undefined);
const unused2 = mem.sliceAsBytes(self.readableSliceMut(slice.len)[0 .. count - slice.len]);
@memset(unused2, undefined);
}
}
if (autoalign and self.count == count) {
self.head = 0;
self.count = 0;
} else {
var head = self.head + count;
if (powers_of_two) {
// Note it is safe to do a wrapping subtract as
// bitwise & with all 1s is a noop
head &= self.buf.len -% 1;
} else {
head %= self.buf.len;
}
self.head = head;
self.count -= count;
}
}
/// Read the next item from the fifo
pub fn readItem(self: *Self) ?T {
if (self.count == 0) return null;
const c = self.buf[self.head];
self.discard(1);
return c;
}
/// Read data from the fifo into `dst`, returns number of items copied.
pub fn read(self: *Self, dst: []T) usize {
var dst_left = dst;
while (dst_left.len > 0) {
const slice = self.readableSlice(0);
if (slice.len == 0) break;
const n = @min(slice.len, dst_left.len);
@memcpy(dst_left[0..n], slice[0..n]);
self.discard(n);
dst_left = dst_left[n..];
}
return dst.len - dst_left.len;
}
/// Same as `read` except it returns an error union
/// The purpose of this function existing is to match `std.io.Reader` API.
fn readFn(self: *Self, dest: []u8) error{}!usize {
return self.read(dest);
}
pub fn reader(self: *Self) Reader {
return .{ .context = self };
}
/// Returns number of items available in fifo
pub fn writableLength(self: Self) usize {
return self.buf.len - self.count;
}
/// Returns the first section of writable buffer.
/// Note that this may be of length 0
pub fn writableSlice(self: SliceSelfArg, offset: usize) []T {
if (offset > self.buf.len) return &[_]T{};
const tail = self.head + offset + self.count;
if (tail < self.buf.len) {
return self.buf[tail..];
} else {
return self.buf[tail - self.buf.len ..][0 .. self.writableLength() - offset];
}
}
/// Returns a writable buffer of at least `size` items, allocating memory as needed.
/// Use `fifo.update` once you've written data to it.
pub fn writableWithSize(self: *Self, size: usize) ![]T {
try self.ensureUnusedCapacity(size);
// try to avoid realigning buffer
var slice = self.writableSlice(0);
if (slice.len < size) {
self.realign();
slice = self.writableSlice(0);
}
return slice;
}
/// Update the tail location of the buffer (usually follows use of writable/writableWithSize)
pub fn update(self: *Self, count: usize) void {
assert(self.count + count <= self.buf.len);
self.count += count;
}
/// Appends the data in `src` to the fifo.
/// You must have ensured there is enough space.
pub fn writeAssumeCapacity(self: *Self, src: []const T) void {
assert(self.writableLength() >= src.len);
var src_left = src;
while (src_left.len > 0) {
const writable_slice = self.writableSlice(0);
assert(writable_slice.len != 0);
const n = @min(writable_slice.len, src_left.len);
@memcpy(writable_slice[0..n], src_left[0..n]);
self.update(n);
src_left = src_left[n..];
}
}
/// Write a single item to the fifo
pub fn writeItem(self: *Self, item: T) !void {
try self.ensureUnusedCapacity(1);
return self.writeItemAssumeCapacity(item);
}
pub fn writeItemAssumeCapacity(self: *Self, item: T) void {
var tail = self.head + self.count;
if (powers_of_two) {
tail &= self.buf.len - 1;
} else {
tail %= self.buf.len;
}
self.buf[tail] = item;
self.update(1);
}
/// Appends the data in `src` to the fifo.
/// Allocates more memory as necessary
pub fn write(self: *Self, src: []const T) !void {
try self.ensureUnusedCapacity(src.len);
return self.writeAssumeCapacity(src);
}
/// Same as `write` except it returns the number of bytes written, which is always the same
/// as `bytes.len`. The purpose of this function existing is to match `std.io.Writer` API.
fn appendWrite(self: *Self, bytes: []const u8) error{OutOfMemory}!usize {
try self.write(bytes);
return bytes.len;
}
pub fn writer(self: *Self) Writer {
return .{ .context = self };
}
/// Make `count` items available before the current read location
fn rewind(self: *Self, count: usize) void {
assert(self.writableLength() >= count);
var head = self.head + (self.buf.len - count);
if (powers_of_two) {
head &= self.buf.len - 1;
} else {
head %= self.buf.len;
}
self.head = head;
self.count += count;
}
/// Place data back into the read stream
pub fn unget(self: *Self, src: []const T) !void {
try self.ensureUnusedCapacity(src.len);
self.rewind(src.len);
const slice = self.readableSliceMut(0);
if (src.len < slice.len) {
@memcpy(slice[0..src.len], src);
} else {
@memcpy(slice, src[0..slice.len]);
const slice2 = self.readableSliceMut(slice.len);
@memcpy(slice2[0 .. src.len - slice.len], src[slice.len..]);
}
}
/// Returns the item at `offset`.
/// Asserts offset is within bounds.
pub fn peekItem(self: Self, offset: usize) T {
assert(offset < self.count);
var index = self.head + offset;
if (powers_of_two) {
index &= self.buf.len - 1;
} else {
index %= self.buf.len;
}
return self.buf[index];
}
/// Pump data from a reader into a writer.
/// Stops when reader returns 0 bytes (EOF).
/// Buffer size must be set before calling; a buffer length of 0 is invalid.
pub fn pump(self: *Self, src_reader: anytype, dest_writer: anytype) !void {
assert(self.buf.len > 0);
while (true) {
if (self.writableLength() > 0) {
const n = try src_reader.read(self.writableSlice(0));
if (n == 0) break; // EOF
self.update(n);
}
self.discard(try dest_writer.write(self.readableSlice(0)));
}
// flush remaining data
while (self.readableLength() > 0) {
self.discard(try dest_writer.write(self.readableSlice(0)));
}
}
pub fn toOwnedSlice(self: *Self) Allocator.Error![]T {
if (self.head != 0) self.realign();
assert(self.head == 0);
assert(self.count <= self.buf.len);
const allocator = self.allocator;
if (allocator.resize(self.buf, self.count)) {
const result = self.buf[0..self.count];
self.* = Self.init(allocator);
return result;
}
const new_memory = try allocator.dupe(T, self.buf[0..self.count]);
allocator.free(self.buf);
self.* = Self.init(allocator);
return new_memory;
}
};
}
test "LinearFifo(u8, .Dynamic) discard(0) from empty buffer should not error on overflow" {
var fifo = LinearFifo(u8, .dynamic).init(testing.allocator);
defer fifo.deinit();
// If overflow is not explicitly allowed this will crash in debug / safe mode
fifo.discard(0);
}
test "LinearFifo(u8, .Dynamic)" {
var fifo = LinearFifo(u8, .dynamic).init(testing.allocator);
defer fifo.deinit();
try fifo.write("HELLO");
try testing.expectEqual(@as(usize, 5), fifo.readableLength());
try testing.expectEqualSlices(u8, "HELLO", fifo.readableSlice(0));
{
var i: usize = 0;
while (i < 5) : (i += 1) {
try fifo.write(&[_]u8{fifo.peekItem(i)});
}
try testing.expectEqual(@as(usize, 10), fifo.readableLength());
try testing.expectEqualSlices(u8, "HELLOHELLO", fifo.readableSlice(0));
}
{
try testing.expectEqual(@as(u8, 'H'), fifo.readItem().?);
try testing.expectEqual(@as(u8, 'E'), fifo.readItem().?);
try testing.expectEqual(@as(u8, 'L'), fifo.readItem().?);
try testing.expectEqual(@as(u8, 'L'), fifo.readItem().?);
try testing.expectEqual(@as(u8, 'O'), fifo.readItem().?);
}
try testing.expectEqual(@as(usize, 5), fifo.readableLength());
{ // Writes that wrap around
try testing.expectEqual(@as(usize, 11), fifo.writableLength());
try testing.expectEqual(@as(usize, 6), fifo.writableSlice(0).len);
fifo.writeAssumeCapacity("6<chars<11");
try testing.expectEqualSlices(u8, "HELLO6<char", fifo.readableSlice(0));
try testing.expectEqualSlices(u8, "s<11", fifo.readableSlice(11));
try testing.expectEqualSlices(u8, "11", fifo.readableSlice(13));
try testing.expectEqualSlices(u8, "", fifo.readableSlice(15));
fifo.discard(11);
try testing.expectEqualSlices(u8, "s<11", fifo.readableSlice(0));
fifo.discard(4);
try testing.expectEqual(@as(usize, 0), fifo.readableLength());
}
{
const buf = try fifo.writableWithSize(12);
try testing.expectEqual(@as(usize, 12), buf.len);
var i: u8 = 0;
while (i < 10) : (i += 1) {
buf[i] = i + 'a';
}
fifo.update(10);
try testing.expectEqualSlices(u8, "abcdefghij", fifo.readableSlice(0));
}
{
try fifo.unget("prependedstring");
var result: [30]u8 = undefined;
try testing.expectEqualSlices(u8, "prependedstringabcdefghij", result[0..fifo.read(&result)]);
try fifo.unget("b");
try fifo.unget("a");
try testing.expectEqualSlices(u8, "ab", result[0..fifo.read(&result)]);
}
fifo.shrink(0);
{
try fifo.writer().print("{s}, {s}!", .{ "Hello", "World" });
var result: [30]u8 = undefined;
try testing.expectEqualSlices(u8, "Hello, World!", result[0..fifo.read(&result)]);
try testing.expectEqual(@as(usize, 0), fifo.readableLength());
}
{
try fifo.writer().writeAll("This is a test");
var result: [30]u8 = undefined;
try testing.expectEqualSlices(u8, "This", (try fifo.reader().readUntilDelimiterOrEof(&result, ' ')).?);
try testing.expectEqualSlices(u8, "is", (try fifo.reader().readUntilDelimiterOrEof(&result, ' ')).?);
try testing.expectEqualSlices(u8, "a", (try fifo.reader().readUntilDelimiterOrEof(&result, ' ')).?);
try testing.expectEqualSlices(u8, "test", (try fifo.reader().readUntilDelimiterOrEof(&result, ' ')).?);
}
{
try fifo.ensureTotalCapacity(1);
var in_fbs = std.io.fixedBufferStream("pump test");
var out_buf: [50]u8 = undefined;
var out_fbs = std.io.fixedBufferStream(&out_buf);
try fifo.pump(in_fbs.reader(), out_fbs.writer());
try testing.expectEqualSlices(u8, in_fbs.buffer, out_fbs.getWritten());
}
}
test LinearFifo {
inline for ([_]type{ u1, u8, u16, u64 }) |T| {
inline for ([_]LinearFifoBufferType{ LinearFifoBufferType{ .static = 32 }, .slice, .dynamic }) |bt| {
const FifoType = LinearFifo(T, bt);
var buf: if (bt == .slice) [32]T else void = undefined;
var fifo = switch (bt) {
.static => FifoType.init(),
.slice => FifoType.init(buf[0..]),
.dynamic => FifoType.init(testing.allocator),
};
defer fifo.deinit();
try fifo.write(&[_]T{ 0, 1, 1, 0, 1 });
try testing.expectEqual(@as(usize, 5), fifo.readableLength());
{
try testing.expectEqual(@as(T, 0), fifo.readItem().?);
try testing.expectEqual(@as(T, 1), fifo.readItem().?);
try testing.expectEqual(@as(T, 1), fifo.readItem().?);
try testing.expectEqual(@as(T, 0), fifo.readItem().?);
try testing.expectEqual(@as(T, 1), fifo.readItem().?);
try testing.expectEqual(@as(usize, 0), fifo.readableLength());
}
{
try fifo.writeItem(1);
try fifo.writeItem(1);
try fifo.writeItem(1);
try testing.expectEqual(@as(usize, 3), fifo.readableLength());
}
{
var readBuf: [3]T = undefined;
const n = fifo.read(&readBuf);
try testing.expectEqual(@as(usize, 3), n); // NOTE: It should be the number of items.
}
}
}
}

View File

@@ -4,96 +4,54 @@ const known_folders = @import("known_folders");
const clap = @import("clap");
const config = @import("config.zig");
const emu = @import("core/emu.zig");
const Synchro = @import("core/emu.zig").Synchro;
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 Arm7tdmi = @import("arm32").Arm7tdmi;
const IBus = @import("arm32").Bus;
const IScheduler = @import("arm32").Scheduler;
const log = std.log.scoped(.Cli);
const width = @import("core/ppu.zig").width;
const height = @import("core/ppu.zig").height;
pub const log_level = if (builtin.mode != .Debug) .info else std.log.default_level;
// CLI Arguments + Help Text
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.
\\<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() == .ok);
defer std.debug.assert(!gpa.deinit());
const allocator = gpa.allocator();
// Determine the Data Directory (stores saves)
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 });
break :blk path;
};
// TODO: Make Error message not Linux Specific
const data_path = try known_folders.getPath(allocator, .data) orelse exit("Unable to Determine XDG Data Path", .{});
defer allocator.free(data_path);
// 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;
};
const config_path = try configFilePath(allocator, data_path);
defer allocator.free(config_path);
// Parse CLI
const save_path = try savePath(allocator, data_path);
defer allocator.free(save_path);
const result = clap.parse(clap.Help, &params, clap.parsers.default, .{ .allocator = allocator }) catch |e| exitln("failed to parse cli: {}", .{e});
try config.load(allocator, config_path);
// Handle CLI Input
const result = try clap.parse(clap.Help, &params, clap.parsers.default, .{});
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 paths = try handleArguments(allocator, data_path, &result);
defer if (paths.save) |path| allocator.free(path);
config.load(allocator, cfg_file_path) catch |e| exitln("failed to load config file: {}", .{e});
var paths = handleArguments(allocator, data_path, &result) catch |e| exitln("failed to handle cli arguments: {}", .{e});
defer paths.deinit(allocator);
// if paths.bios is null, then we want to see if it's in the data directory
if (paths.bios == null) blk: {
const bios_path = std.mem.join(allocator, "/", &.{ data_path, "zba", "gba_bios.bin" }) catch |e| exitln("failed to allocate backup bios dir path: {}", .{e});
defer allocator.free(bios_path);
_ = std.fs.cwd().statFile(bios_path) catch |e| switch (e) {
error.FileNotFound => { // ZBA will crash on attempt to read BIOS but that's fine
log.err("file located at {s} was not found", .{bios_path});
break :blk;
},
else => exitln("error when checking \"{s}\": {}", .{ bios_path, e }),
};
paths.bios = allocator.dupe(u8, bios_path) catch |e| exitln("failed to duplicate path to bios: {}", .{e});
}
const log_file = switch (config.config().debug.cpu_trace) {
true => std.fs.cwd().createFile("zba.log", .{}) catch |e| exitln("failed to create trace log file: {}", .{e}),
false => null,
};
const cpu_trace = config.config().debug.cpu_trace;
const log_file: ?std.fs.File = if (cpu_trace) try std.fs.cwd().createFile("zba.log", .{}) else null;
defer if (log_file) |file| file.close();
// TODO: Take Emulator Init Code out of main.zig
@@ -101,138 +59,70 @@ pub fn main() void {
defer scheduler.deinit();
var bus: Bus = undefined;
var cpu = Arm7tdmi.init(&scheduler, &bus, log_file);
if (paths.bios == null) cpu.fastBoot();
const ischeduler = IScheduler.init(&scheduler);
const ibus = IBus.init(&bus);
var cpu = Arm7tdmi.init(ischeduler, ibus);
bus.init(allocator, &scheduler, &cpu, paths) catch |e| exitln("failed to init zba bus: {}", .{e});
try bus.init(allocator, &scheduler, &cpu, paths);
defer bus.deinit();
if (config.config().guest.skip_bios or result.args.skip != 0 or paths.bios == null) {
@import("core/cpu_util.zig").fastBoot(&cpu);
}
const title_ptr = if (paths.rom != null) &bus.pak.title else null;
// TODO: Just copy the title instead of grabbing a pointer to it
var gui = Gui.init(allocator, &bus.apu, title_ptr) catch |e| exitln("failed to init gui: {}", .{e});
var gui = Gui.init(&bus.pak.title, &bus.apu, width, height);
defer gui.deinit();
var sync = Synchro.init(allocator) catch |e| exitln("failed to allocate sync types: {}", .{e});
defer sync.deinit(allocator);
if (result.args.gdb != 0) {
const Server = @import("gdbstub").Server;
const EmuThing = @import("core/emu.zig").EmuThing;
var wrapper = EmuThing.init(&cpu, &scheduler);
var emulator = wrapper.interface();
defer emulator.deinit(allocator);
log.info("Ready to connect", .{});
var server = Server.init(
emulator,
.{ .memory_map = EmuThing.map, .target = EmuThing.target },
) 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, &sync.should_quit }) catch |e| exitln("gdb server thread crashed: {}", .{e});
defer thread.join();
gui.run(.{
.cpu = &cpu,
.scheduler = &scheduler,
.sync = &sync,
}) catch |e| exitln("main thread panicked: {}", .{e});
} else {
var tracker = FpsTracker.init();
const thread = std.Thread.spawn(.{}, emu.run, .{ &cpu, &scheduler, &tracker, &sync }) catch |e| exitln("emu thread panicked: {}", .{e});
defer thread.join();
gui.run(.{
.cpu = &cpu,
.scheduler = &scheduler,
.tracker = &tracker,
.sync = &sync,
}) catch |e| exitln("main thread panicked: {}", .{e});
}
try gui.run(&cpu, &scheduler);
}
fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const clap.Result(clap.Help, &params, clap.parsers.default)) !FilePaths {
const rom_path = try romPath(allocator, result);
errdefer if (rom_path) |path| allocator.free(path);
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});
const bios_path: ?[]const u8 = if (result.args.bios) |path| try allocator.dupe(u8, path) else null;
errdefer if (bios_path) |path| allocator.free(path);
const bios_path = result.args.bios;
if (bios_path) |path| log.info("BIOS path: {s}", .{path}) else log.info("No BIOS provided", .{});
const save_path = try std.fs.path.join(allocator, &[_][]const u8{ data_path, "zba", "save" });
log.info("ROM path: {?s}", .{rom_path});
log.info("BIOS path: {?s}", .{bios_path});
const save_path = try savePath(allocator, data_path);
log.info("Save path: {s}", .{save_path});
return .{
return FilePaths{
.rom = rom_path,
.bios = bios_path,
.save = save_path,
};
}
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" });
errdefer allocator.free(path);
fn configFilePath(allocator: Allocator, data_path: []const u8) ![]const u8 {
const path = try std.fs.path.join(allocator, &[_][]const u8{ data_path, "zba", "config.toml" });
// We try to create the file exclusively, meaning that we err out if the file already exists.
// All we care about is a file being there so we can just ignore that error in particular and
// continue down the happy pathj
std.fs.accessAbsolute(path, .{}) catch |e| {
if (e != error.FileNotFound) return e;
const config_file = std.fs.createFileAbsolute(path, .{}) catch |err| exitln("failed to create \"{s}\": {}", .{ path, err });
defer config_file.close();
// FIXME(2025-09-22): re-enable
// try config_file.writeAll(@embedFile("example.toml"));
try config_file.writeAll("");
std.fs.accessAbsolute(path, .{}) catch {
const file_handle = try std.fs.createFileAbsolute(path, .{});
file_handle.close();
};
return path;
}
fn ensureDataDirsExist(data_path: []const u8) !void {
fn savePath(allocator: Allocator, data_path: []const u8) ![]const u8 {
var dir = try std.fs.openDirAbsolute(data_path, .{});
defer dir.close();
// Will recursively create directories
try dir.makePath("zba" ++ std.fs.path.sep_str ++ "save");
// Will either make the path recursively, or just exit early since it already exists
try dir.makePath("zba" ++ [_]u8{std.fs.path.sep} ++ "save");
// FIXME: Do we have to allocate? :sad:
return try std.fs.path.join(allocator, &[_][]const u8{ data_path, "zba", "save" });
}
fn ensureConfigDirExists(config_path: []const u8) !void {
var dir = try std.fs.openDirAbsolute(config_path, .{});
defer dir.close();
try dir.makePath("zba");
}
fn romPath(allocator: Allocator, result: *const clap.Result(clap.Help, &params, clap.parsers.default)) !?[]const u8 {
fn romPath(result: *const clap.Result(clap.Help, &params, clap.parsers.default)) []const u8 {
return switch (result.positionals.len) {
0 => null,
1 => if (result.positionals[0]) |path| try allocator.dupe(u8, path) else null,
else => exitln("ZBA received too many positional arguments.", .{}),
1 => result.positionals[0],
0 => exit("ZBA requires a path to a GamePak ROM\n", .{}),
else => exit("ZBA received too many positional arguments. \n", .{}),
};
}
fn exitln(comptime format: []const u8, args: anytype) noreturn {
var buf: [1024]u8 = undefined;
var stderr = std.fs.File.stderr().writer(&buf).interface;
fn exit(comptime format: []const u8, args: anytype) noreturn {
const stderr = std.io.getStdErr().writer();
stderr.print(format, args) catch {}; // Just exit already...
stderr.writeByte('\n') catch {};
std.process.exit(1);
std.os.exit(1);
}

View File

@@ -1,406 +1,192 @@
const std = @import("std");
const gl = @import("gl");
const zgui = @import("zgui");
const c = @import("lib.zig").c;
const SDL = @import("sdl2");
const emu = @import("core/emu.zig");
const config = @import("config.zig");
const imgui = @import("imgui.zig");
const Apu = @import("core/apu.zig").Apu;
const Arm7tdmi = @import("arm32").Arm7tdmi;
const Bus = @import("core/Bus.zig");
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
const Scheduler = @import("core/scheduler.zig").Scheduler;
const FpsTracker = @import("util.zig").FpsTracker;
const Synchro = @import("core/emu.zig").Synchro;
const KeyInput = @import("core/bus/io.zig").KeyInput;
const gba_width = @import("core/ppu.zig").width;
const gba_height = @import("core/ppu.zig").height;
const span = @import("util.zig").span;
const GLsizei = gl.sizei;
const SDL_GLContext = *c.SDL_GLContextState;
const Allocator = std.mem.Allocator;
pub const Dimensions = struct { width: u32, height: u32 };
const default_dim: Dimensions = .{ .width = 1280, .height = 720 };
pub const sample_rate = 1 << 15;
// pub const sample_format = SDL.AUDIO_U16;
const window_title = "ZBA";
const errify = @import("lib.zig").errify;
var gl_procs: gl.ProcTable = undefined;
const pitch = @import("core/ppu.zig").framebuf_pitch;
const default_title: []const u8 = "ZBA";
pub const Gui = struct {
const Self = @This();
const log = std.log.scoped(.Gui);
window: *c.SDL_Window,
ctx: SDL_GLContext,
window: *SDL.SDL_Window,
title: []const u8,
renderer: *SDL.SDL_Renderer,
texture: *SDL.SDL_Texture,
audio: Audio,
state: imgui.State,
allocator: Allocator,
pub fn init(title: *const [12]u8, apu: *Apu, width: i32, height: i32) Self {
const ret = SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO | SDL.SDL_INIT_GAMECONTROLLER);
if (ret < 0) panic();
pub fn init(allocator: Allocator, apu: *Apu, title_opt: ?*const [12]u8) !Self {
c.SDL_SetMainReady();
const win_scale = @intCast(c_int, config.config().host.win_scale);
try errify(c.SDL_Init(c.SDL_INIT_VIDEO | c.SDL_INIT_AUDIO | c.SDL_INIT_EVENTS));
const window = SDL.SDL_CreateWindow(
default_title.ptr,
SDL.SDL_WINDOWPOS_CENTERED,
SDL.SDL_WINDOWPOS_CENTERED,
@as(c_int, width * win_scale),
@as(c_int, height * win_scale),
SDL.SDL_WINDOW_SHOWN,
) orelse panic();
try errify(c.SDL_SetAppMetadata(window_title, "0.1.0", "moe.paoda.zba"));
try errify(c.SDL_GL_SetAttribute(c.SDL_GL_CONTEXT_MAJOR_VERSION, gl.info.version_major));
try errify(c.SDL_GL_SetAttribute(c.SDL_GL_CONTEXT_MINOR_VERSION, gl.info.version_minor));
try errify(c.SDL_GL_SetAttribute(c.SDL_GL_CONTEXT_PROFILE_MASK, c.SDL_GL_CONTEXT_PROFILE_CORE));
try errify(c.SDL_GL_SetAttribute(c.SDL_GL_CONTEXT_FLAGS, c.SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG));
const renderer_flags = SDL.SDL_RENDERER_ACCELERATED | if (config.config().host.vsync) SDL.SDL_RENDERER_PRESENTVSYNC else 0;
const renderer = SDL.SDL_CreateRenderer(window, -1, @bitCast(u32, renderer_flags)) orelse panic();
const window: *c.SDL_Window = try errify(c.SDL_CreateWindow(window_title, default_dim.width, default_dim.height, c.SDL_WINDOW_OPENGL | c.SDL_WINDOW_RESIZABLE));
errdefer c.SDL_DestroyWindow(window);
const gl_ctx = try errify(c.SDL_GL_CreateContext(window));
errdefer errify(c.SDL_GL_DestroyContext(gl_ctx)) catch {};
try errify(c.SDL_GL_MakeCurrent(window, gl_ctx));
errdefer errify(c.SDL_GL_MakeCurrent(window, null)) catch {};
if (!gl_procs.init(c.SDL_GL_GetProcAddress)) return error.gl_init_failed;
gl.makeProcTableCurrent(&gl_procs);
errdefer gl.makeProcTableCurrent(null);
try errify(c.SDL_GL_SetSwapInterval(@intFromBool(config.config().host.vsync)));
zgui.init(allocator);
zgui.plot.init();
zgui.backend.init(window, gl_ctx);
// zgui.io.setIniFilename(null);
const texture = SDL.SDL_CreateTexture(
renderer,
SDL.SDL_PIXELFORMAT_RGBA8888,
SDL.SDL_TEXTUREACCESS_STREAMING,
@as(c_int, width),
@as(c_int, height),
) orelse panic();
return Self{
.window = window,
.ctx = gl_ctx,
.audio = try Audio.init(apu),
.allocator = allocator,
.state = try imgui.State.init(allocator, title_opt),
.title = span(title),
.renderer = renderer,
.texture = texture,
.audio = Audio.init(apu),
};
}
pub fn deinit(self: *Self) void {
self.audio.deinit();
self.state.deinit(self.allocator);
pub fn run(self: *Self, cpu: *Arm7tdmi, scheduler: *Scheduler) !void {
var quit = std.atomic.Atomic(bool).init(false);
var tracker = FpsTracker.init();
zgui.backend.deinit();
zgui.plot.deinit();
zgui.deinit();
const thread = try std.Thread.spawn(.{}, emu.run, .{ &quit, scheduler, cpu, &tracker });
defer thread.join();
errify(c.SDL_GL_DestroyContext(self.ctx)) catch {};
c.SDL_DestroyWindow(self.window);
c.SDL_Quit();
self.* = undefined;
}
const RunOptions = struct {
sync: *Synchro,
tracker: ?*FpsTracker = null,
cpu: *Arm7tdmi,
scheduler: *Scheduler,
};
pub fn run(self: *Self, opt: RunOptions) !void {
const cpu = opt.cpu;
const tracker = opt.tracker;
const sync = opt.sync;
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
const vao_id = opengl_impl.vao();
defer gl.DeleteVertexArrays(1, vao_id[0..]);
const emu_tex = opengl_impl.screenTex(bus_ptr.ppu.framebuf.get(.Renderer));
defer gl.DeleteTextures(1, emu_tex[0..]);
const out_tex = opengl_impl.outTex();
defer gl.DeleteTextures(1, out_tex[0..]);
const fbo_id = try opengl_impl.frameBuffer(out_tex[0]);
defer gl.DeleteFramebuffers(1, fbo_id[0..]);
const prog_id = try opengl_impl.program(); // Dynamic Shaders?
defer gl.DeleteProgram(prog_id);
var win_dim: Dimensions = default_dim;
var title_buf: [0x100]u8 = [_]u8{0} ** 0x100;
emu_loop: while (true) {
// Outside of `SDL.SDL_QUIT` below, the DearImgui UI might signal that the program
// should exit, in which case we should also handle this
if (self.state.should_quit or sync.should_quit.load(.monotonic)) break :emu_loop;
var event: c.SDL_Event = undefined;
while (c.SDL_PollEvent(&event)) {
_ = zgui.backend.processEvent(&event);
var event: SDL.SDL_Event = undefined;
while (SDL.SDL_PollEvent(&event) != 0) {
switch (event.type) {
c.SDL_EVENT_QUIT => break :emu_loop,
c.SDL_EVENT_KEY_DOWN => {
// TODO: Make use of compare_and_xor?
var keyinput: KeyInput = .{ .raw = 0x0000 };
SDL.SDL_QUIT => break :emu_loop,
SDL.SDL_KEYDOWN => {
const io = &cpu.bus.io;
const key_code = event.key.keysym.sym;
switch (event.key.scancode) {
c.SDL_SCANCODE_UP => keyinput.up.write(true),
c.SDL_SCANCODE_DOWN => keyinput.down.write(true),
c.SDL_SCANCODE_LEFT => keyinput.left.write(true),
c.SDL_SCANCODE_RIGHT => keyinput.right.write(true),
c.SDL_SCANCODE_X => keyinput.a.write(true),
c.SDL_SCANCODE_Z => keyinput.b.write(true),
c.SDL_SCANCODE_A => keyinput.shoulder_l.write(true),
c.SDL_SCANCODE_S => keyinput.shoulder_r.write(true),
c.SDL_SCANCODE_RETURN => keyinput.start.write(true),
c.SDL_SCANCODE_RSHIFT => keyinput.select.write(true),
switch (key_code) {
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 => {},
}
bus_ptr.io.keyinput.fetchAnd(~keyinput.raw, .monotonic);
},
c.SDL_EVENT_KEY_UP => {
// FIXME(paoda): merge with above?
// TODO: Make use of compare_and_xor?
var keyinput: KeyInput = .{ .raw = 0x0000 };
SDL.SDL_KEYUP => {
const io = &cpu.bus.io;
const key_code = event.key.keysym.sym;
switch (event.key.scancode) {
c.SDL_SCANCODE_UP => keyinput.up.write(true),
c.SDL_SCANCODE_DOWN => keyinput.down.write(true),
c.SDL_SCANCODE_LEFT => keyinput.left.write(true),
c.SDL_SCANCODE_RIGHT => keyinput.right.write(true),
c.SDL_SCANCODE_X => keyinput.a.write(true),
c.SDL_SCANCODE_Z => keyinput.b.write(true),
c.SDL_SCANCODE_A => keyinput.shoulder_l.write(true),
c.SDL_SCANCODE_S => keyinput.shoulder_r.write(true),
c.SDL_SCANCODE_RETURN => keyinput.start.write(true),
c.SDL_SCANCODE_RSHIFT => keyinput.select.write(true),
switch (key_code) {
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);
},
else => {},
}
bus_ptr.io.keyinput.fetchOr(keyinput.raw, .monotonic);
},
c.SDL_EVENT_WINDOW_RESIZED => {
log.debug("window resized to: {}x{}", .{ event.window.data1, event.window.data2 });
win_dim.width = @intCast(event.window.data1);
win_dim.height = @intCast(event.window.data2);
},
else => {},
}
}
var zgui_redraw: bool = false;
// Emulator has an internal Double Buffer
const framebuf = cpu.bus.ppu.framebuf.get(.Renderer);
_ = SDL.SDL_UpdateTexture(self.texture, null, framebuf.ptr, pitch);
_ = SDL.SDL_RenderCopy(self.renderer, self.texture, null, null);
SDL.SDL_RenderPresent(self.renderer);
switch (self.state.emulation) {
.Transition => |inner| switch (inner) {
.Active => {
sync.paused.store(false, .monotonic);
if (!config.config().host.mute) try errify(c.SDL_PauseAudioDevice(c.SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK));
self.state.emulation = .Active;
},
.Inactive => {
// Assert that double pausing is impossible
try errify(c.SDL_ResumeAudioDevice(c.SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK));
sync.paused.store(true, .monotonic);
self.state.emulation = .Inactive;
},
},
.Active => {
// Add FPS count to the histogram
if (tracker) |t| self.state.fps_hist.push(t.value()) catch {};
// Draw GBA Screen to Texture
{
gl.BindFramebuffer(gl.FRAMEBUFFER, fbo_id[0]);
defer gl.BindFramebuffer(gl.FRAMEBUFFER, 0);
gl.Viewport(0, 0, gba_width, gba_height);
opengl_impl.drawScreen(emu_tex[0], prog_id, vao_id[0], bus_ptr.ppu.framebuf.get(.Renderer));
}
// FIXME: We only really care about locking the audio device (and therefore writing silence)
// since if nfd-zig is used the emu may be paused for way too long. Perhaps we should try and limit
// spurious calls to SDL_LockAudioDevice?
try errify(c.SDL_LockAudioStream(self.audio.stream));
defer errify(c.SDL_UnlockAudioStream(self.audio.stream)) catch @panic("TODO: FIXME");
zgui_redraw = imgui.draw(&self.state, sync, win_dim, cpu, out_tex[0]);
},
.Inactive => zgui_redraw = imgui.draw(&self.state, sync, win_dim, cpu, out_tex[0]),
}
if (zgui_redraw) {
// Background Colour
const size = zgui.io.getDisplaySize();
gl.Viewport(0, 0, @intFromFloat(size[0]), @intFromFloat(size[1]));
gl.ClearColor(0, 0, 0, 1.0);
gl.Clear(gl.COLOR_BUFFER_BIT);
zgui.backend.draw();
}
try errify(c.SDL_GL_SwapWindow(self.window));
const dyn_title = std.fmt.bufPrint(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, tracker.value() }) catch unreachable;
SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr);
}
sync.should_quit.store(true, .monotonic);
quit.store(true, .SeqCst); // Terminate Emulator Thread
}
pub fn deinit(self: *Self) void {
self.audio.deinit();
SDL.SDL_DestroyTexture(self.texture);
SDL.SDL_DestroyRenderer(self.renderer);
SDL.SDL_DestroyWindow(self.window);
SDL.SDL_Quit();
self.* = undefined;
}
};
const Audio = struct {
const Self = @This();
const log = std.log.scoped(.PlatformAudio);
const sample_rate = @import("core/apu.zig").host_sample_rate;
stream: *c.SDL_AudioStream,
device: SDL.SDL_AudioDeviceID,
fn init(apu: *Apu) !Self {
var desired: c.SDL_AudioSpec = std.mem.zeroes(c.SDL_AudioSpec);
desired.freq = sample_rate;
desired.format = c.SDL_AUDIO_S16LE;
desired.channels = 2;
fn init(apu: *Apu) Self {
var have: SDL.SDL_AudioSpec = undefined;
var want: SDL.SDL_AudioSpec = std.mem.zeroes(SDL.SDL_AudioSpec);
want.freq = sample_rate;
want.format = SDL.AUDIO_U16;
want.channels = 2;
want.samples = 0x100;
want.callback = Self.callback;
want.userdata = apu;
log.info("Host Sample Rate: {}Hz, Host Format: SDL_AUDIO_S16LE", .{sample_rate});
const device = SDL.SDL_OpenAudioDevice(null, 0, &want, &have, 0);
if (device == 0) panic();
const stream = try errify(c.SDL_OpenAudioDeviceStream(c.SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desired, null, null));
errdefer c.SDL_DestroyAudioStream(stream);
SDL.SDL_PauseAudioDevice(device, 0); // Unpause Audio
apu.stream = stream;
return .{ .stream = stream };
return .{ .device = device };
}
fn deinit(self: *Self) void {
c.SDL_DestroyAudioStream(self.stream);
SDL.SDL_CloseAudioDevice(self.device);
self.* = undefined;
}
export fn callback(userdata: ?*anyopaque, stream: [*c]u8, len: c_int) void {
const apu = @ptrCast(*Apu, @alignCast(@alignOf(*Apu), userdata));
_ = SDL.SDL_AudioStreamGet(apu.stream, stream, len);
// If we don't write anything, play silence otherwise garbage will be played
// FIXME: I don't think this hack to remove DC Offset is acceptable :thinking:
// if (written == 0) std.mem.set(u8, stream[0..@intCast(usize, len)], 0x40);
}
};
const opengl_impl = struct {
fn drawScreen(tex_id: gl.uint, prog_id: gl.uint, vao_id: gl.uint, buf: []const u8) void {
gl.BindTexture(gl.TEXTURE_2D, tex_id);
defer gl.BindTexture(gl.TEXTURE_2D, 0);
gl.TexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gba_width, gba_height, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, buf.ptr);
// Bind VAO
gl.BindVertexArray(vao_id);
defer gl.BindVertexArray(0);
// Use compiled frag + vertex shader
gl.UseProgram(prog_id);
defer gl.UseProgram(0);
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 3);
}
fn program() !gl.uint {
const vert_shader: [1][*]const u8 = .{@embedFile("shader/pixelbuf.vert")};
const frag_shader: [1][*]const u8 = .{@embedFile("shader/pixelbuf.frag")};
const vs = gl.CreateShader(gl.VERTEX_SHADER);
defer gl.DeleteShader(vs);
gl.ShaderSource(vs, 1, vert_shader[0..], null);
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, frag_shader[0..], null);
gl.CompileShader(fs);
if (!shader.didCompile(fs)) return error.FragmentCompileError;
const prog = gl.CreateProgram();
gl.AttachShader(prog, vs);
gl.AttachShader(prog, fs);
gl.LinkProgram(prog);
return prog;
}
fn vao() [1]gl.uint {
var vao_id: [1]gl.uint = undefined;
gl.GenVertexArrays(1, vao_id[0..]);
return vao_id;
}
fn screenTex(buf: []const u8) [1]gl.uint {
var tex_id: [1]gl.uint = undefined;
gl.GenTextures(1, tex_id[0..]);
gl.BindTexture(gl.TEXTURE_2D, tex_id[0]);
defer gl.BindTexture(gl.TEXTURE_2D, 0);
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gba_width, gba_height, 0, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, buf.ptr);
return tex_id;
}
fn outTex() [1]gl.uint {
var tex_id: [1]gl.uint = undefined;
gl.GenTextures(1, tex_id[0..]);
gl.BindTexture(gl.TEXTURE_2D, tex_id[0]);
defer gl.BindTexture(gl.TEXTURE_2D, 0);
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gba_width, gba_height, 0, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, null);
return tex_id;
}
fn frameBuffer(tex_id: gl.uint) ![1]gl.uint {
var fbo_id: [1]gl.uint = undefined;
gl.GenFramebuffers(1, fbo_id[0..]);
gl.BindFramebuffer(gl.FRAMEBUFFER, fbo_id[0]);
defer gl.BindFramebuffer(gl.FRAMEBUFFER, 0);
gl.FramebufferTexture(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, tex_id, 0);
gl.DrawBuffers(1, &.{gl.COLOR_ATTACHMENT0});
if (gl.CheckFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE)
return error.FrameBufferObejctInitFailed;
return fbo_id;
}
const shader = struct {
const log = std.log.scoped(.shader);
fn didCompile(id: gl.uint) bool {
var success: [1]gl.int = undefined;
gl.GetShaderiv(id, gl.COMPILE_STATUS, success[0..]);
if (success[0] == 0) err(id);
return success[0] == 1;
}
fn err(id: gl.uint) void {
const buf_len = 512;
var error_msg: [buf_len]u8 = undefined;
gl.GetShaderInfoLog(id, buf_len, null, &error_msg);
log.err("{s}", .{std.mem.sliceTo(&error_msg, 0)});
}
};
};
fn panic() noreturn {
const str = @as(?[*:0]const u8, SDL.SDL_GetError()) orelse "unknown error";
@panic(std.mem.sliceTo(str, 0));
}

View File

@@ -1,24 +0,0 @@
#version 330 core
out vec4 frag_color;
in vec2 uv;
uniform sampler2D screen;
void main() {
// https://near.sh/video/color-emulation
// Thanks to Talarubi + Near for the Colour Correction
// Thanks to fleur + mattrb for the Shader Impl
vec4 color = texture(screen, uv);
color.rgb = pow(color.rgb, vec3(4.0)); // LCD Gamma
frag_color = vec4(
pow(vec3(
0 * color.b + 50 * color.g + 255 * color.r,
30 * color.b + 230 * color.g + 10 * color.r,
220 * color.b + 10 * color.g + 50 * color.r
) / 255, vec3(1.0 / 2.2)), // Out Gamma
1.0);
}

View File

@@ -1,10 +0,0 @@
#version 330 core
out vec2 uv;
const vec2 pos[3] = vec2[3](vec2(-1.0f, -1.0f), vec2(-1.0f, 3.0f), vec2(3.0f, -1.0f));
const vec2 uvs[3] = vec2[3](vec2( 0.0f, 0.0f), vec2( 0.0f, 2.0f), vec2(2.0f, 0.0f));
void main() {
uv = uvs[gl_VertexID];
gl_Position = vec4(pos[gl_VertexID], 0.0, 1.0);
}

View File

@@ -3,32 +3,51 @@ const builtin = @import("builtin");
const config = @import("config.zig");
const Log2Int = std.math.Log2Int;
const Arm7tdmi = @import("arm32").Arm7tdmi;
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
const Allocator = std.mem.Allocator;
// Sign-Extend value of type `T` to type `U`
pub fn sext(comptime T: type, comptime U: type, value: T) T {
// U must have less bits than T
comptime std.debug.assert(@typeInfo(U).Int.bits <= @typeInfo(T).Int.bits);
const iT = std.meta.Int(.signed, @typeInfo(T).Int.bits);
const ExtU = if (@typeInfo(U).Int.signedness == .unsigned) T else iT;
const shift = @intCast(Log2Int(T), @typeInfo(T).Int.bits - @typeInfo(U).Int.bits);
return @bitCast(T, @bitCast(iT, @as(ExtU, @truncate(U, value)) << shift) >> shift);
}
/// See https://godbolt.org/z/W3en9Eche
pub inline fn rotr(comptime T: type, x: T, r: anytype) T {
if (@typeInfo(T).Int.signedness == .signed)
@compileError("cannot rotate signed integer");
const ar = @intCast(Log2Int(T), @mod(r, @typeInfo(T).Int.bits));
return x >> ar | x << (1 +% ~ar);
}
pub const FpsTracker = struct {
const Self = @This();
fps: u32,
count: std.atomic.Value(u32),
count: std.atomic.Atomic(u32),
timer: std.time.Timer,
pub fn init() Self {
return .{
.fps = 0,
.count = std.atomic.Value(u32).init(0),
.count = std.atomic.Atomic(u32).init(0),
.timer = std.time.Timer.start() catch unreachable,
};
}
pub fn tick(self: *Self) void {
_ = self.count.fetchAdd(1, .monotonic);
_ = self.count.fetchAdd(1, .Monotonic);
}
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();
}
@@ -36,6 +55,68 @@ pub const FpsTracker = struct {
}
};
pub fn intToBytes(comptime T: type, value: anytype) [@sizeOf(T)]u8 {
comptime std.debug.assert(@typeInfo(T) == .Int);
var result: [@sizeOf(T)]u8 = undefined;
var i: Log2Int(T) = 0;
while (i < result.len) : (i += 1) result[i] = @truncate(u8, value >> i * @bitSizeOf(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
@@ -50,15 +131,9 @@ pub fn escape(title: [12]u8) [12]u8 {
}
pub const FilePaths = struct {
rom: ?[]const u8,
rom: []const u8,
bios: ?[]const u8,
save: []const u8,
pub fn deinit(self: @This(), allocator: Allocator) void {
if (self.rom) |path| allocator.free(path);
if (self.bios) |path| allocator.free(path);
allocator.free(self.save);
}
save: ?[]const u8,
};
pub const io = struct {
@@ -68,9 +143,7 @@ pub const io = struct {
return 0;
}
pub fn undef(comptime T: type, comptime log: anytype, comptime format: []const u8, args: anytype) ?T {
@branchHint(.cold);
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);
@@ -78,13 +151,6 @@ pub const io = struct {
return null;
}
pub fn err(comptime T: type, comptime log: anytype, comptime format: []const u8, args: anytype) ?T {
@branchHint(.cold);
log.err(format, args);
return null;
}
};
pub const write = struct {
@@ -96,10 +162,20 @@ pub const io = struct {
}
};
};
pub fn readUndefined(log: anytype, comptime format: []const u8, args: anytype) u8 {
log.warn(format, args);
if (builtin.mode == .Debug) std.debug.panic("TODO: Implement I/O Register", .{});
return 0;
}
pub fn writeUndefined(log: anytype, comptime format: []const u8, args: anytype) void {
log.warn(format, args);
if (builtin.mode == .Debug) std.debug.panic("TODO: Implement I/O Register", .{});
}
pub const Logger = struct {
const Self = @This();
const FmtArgTuple = std.meta.Tuple(&.{ u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32 });
buf: std.io.BufferedWriter(4096 << 2, std.fs.File.Writer),
@@ -111,7 +187,6 @@ pub const Logger = struct {
pub fn print(self: *Self, comptime format: []const u8, args: anytype) !void {
try self.buf.writer().print(format, args);
try self.buf.flush(); // FIXME: On panics, whatever is in the buffer isn't written to file
}
pub fn mgbaLog(self: *Self, cpu: *const Arm7tdmi, opcode: u32) void {
@@ -122,7 +197,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");
@@ -151,175 +226,11 @@ pub const Logger = struct {
cpu.r[12],
cpu.r[13],
cpu.r[14],
cpu.r[15] - if (cpu.cpsr.t.read()) 2 else @as(u32, 4),
cpu.r[15],
cpu.cpsr.raw,
opcode,
};
}
};
pub const audio = struct {
const _io = @import("core/bus/io.zig");
const ToneSweep = @import("core/apu/ToneSweep.zig");
const Tone = @import("core/apu/Tone.zig");
const Wave = @import("core/apu/Wave.zig");
const Noise = @import("core/apu/Noise.zig");
pub const length = struct {
const FrameSequencer = @import("core/apu.zig").FrameSequencer;
/// Update State of Ch1, Ch2 and Ch3 length timer
pub fn update(comptime T: type, self: *T, fs: *const FrameSequencer, nrx34: _io.Frequency) void {
comptime std.debug.assert(T == ToneSweep or T == Tone or T == Wave);
// Write to NRx4 when FS's next step is not one that clocks the length counter
if (!fs.isLengthNext()) {
// If length_enable was disabled but is now enabled and length timer is not 0 already,
// decrement the length timer
if (!self.freq.length_enable.read() and nrx34.length_enable.read() and self.len_dev.timer != 0) {
self.len_dev.timer -= 1;
// If Length Timer is now 0 and trigger is clear, disable the channel
if (self.len_dev.timer == 0 and !nrx34.trigger.read()) self.enabled = false;
}
}
}
pub const ch4 = struct {
/// update state of ch4 length timer
pub fn update(self: *Noise, fs: *const FrameSequencer, nr44: _io.NoiseControl) void {
// Write to NRx4 when FS's next step is not one that clocks the length counter
if (!fs.isLengthNext()) {
// If length_enable was disabled but is now enabled and length timer is not 0 already,
// decrement the length timer
if (!self.cnt.length_enable.read() and nr44.length_enable.read() and self.len_dev.timer != 0) {
self.len_dev.timer -= 1;
// If Length Timer is now 0 and trigger is clear, disable the channel
if (self.len_dev.timer == 0 and !nr44.trigger.read()) self.enabled = false;
}
}
}
};
};
};
/// Sets 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: u2 = @truncate(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,
};
}
/// 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 @as(u4, @truncate(byte & 1)) << 3;
}
pub inline fn setHalf(comptime T: type, left: T, addr: u8, right: HalfInt(T)) T {
const offset: u1 = @truncate(addr >> if (T == u32) 1 else 0);
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,
},
else => @compileError("unsupported type"),
};
}
/// The Integer type which corresponds to T with exactly half the amount of bits
fn HalfInt(comptime T: type) type {
const type_info = @typeInfo(T);
comptime std.debug.assert(type_info == .int); // Type must be an integer
comptime std.debug.assert(type_info.int.bits % 2 == 0); // Type must have an even amount of bits
return std.meta.Int(type_info.int.signedness, type_info.int.bits >> 1);
}
/// Double Buffering Implementation
pub const FrameBuffer = struct {
const Self = @This();
layers: [2][]u8,
buf: []u8,
current: u1 = 0,
allocator: Allocator,
// TODO: Rename
const Device = enum { Emulator, Renderer };
pub fn init(allocator: Allocator, comptime len: comptime_int) !Self {
const buf = try allocator.alloc(u8, len * 2);
@memset(buf, 0);
return .{
// Front and Back Framebuffers
.layers = [_][]u8{ buf[0..][0..len], buf[len..][0..len] },
.buf = buf,
.allocator = allocator,
};
}
pub fn reset(self: *Self) void {
@memset(self.buf, 0);
self.current = 0;
}
pub fn deinit(self: *Self) void {
self.allocator.free(self.buf);
self.* = undefined;
}
pub fn swap(self: *Self) void {
self.current = ~self.current;
}
pub fn get(self: *Self, comptime dev: Device) []u8 {
return self.layers[if (dev == .Emulator) self.current else ~self.current];
}
};
const RingBuffer = @import("zba_util").RingBuffer;
// TODO: Lock Free Queue?
pub fn Queue(comptime T: type) type {
return struct {
inner: RingBuffer(T),
mtx: std.Thread.Mutex = .{},
pub fn init(buf: []T) @This() {
return .{ .inner = RingBuffer(T).init(buf) };
}
pub fn push(self: *@This(), value: T) !void {
self.mtx.lock();
defer self.mtx.unlock();
try self.inner.push(value);
}
pub fn pop(self: *@This()) ?T {
self.mtx.lock();
defer self.mtx.unlock();
return self.inner.pop();
}
};
}
const FmtArgTuple = std.meta.Tuple(&.{ u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32 });