Compare commits
192 Commits
ca67ca3183
...
main
Author | SHA1 | Date | |
---|---|---|---|
6cacdc7180 | |||
56ac755a73 | |||
c5ffa19e06 | |||
2d3c659b85 | |||
94894fadc6 | |||
0e02d9aaab | |||
b4830326ff | |||
ef93bbe084 | |||
f71aaafe41 | |||
66192daf6c | |||
05b7a9014d | |||
493d7aeede | |||
9183e6850d | |||
d54202bf8b | |||
d097dcc2f5 | |||
203971c91a | |||
78b849b6ff | |||
557b90a39f | |||
dd3158bcfc | |||
64cd373957 | |||
64a30b190c | |||
f2c728ef44 | |||
8b4faca80f | |||
f73b096d62 | |||
d4b7167e29 | |||
d96c9c01ff | |||
954fb279ad | |||
5b6650ef34 | |||
10215d4e99 | |||
e8bc798120 | |||
44818a4d5b | |||
07d85628ac | |||
a8cd510da6 | |||
3040a9f45c | |||
ccdc2cbad4 | |||
16c3eceffd | |||
8f5a0cab9c | |||
79514b0cd0 | |||
a048263fd6 | |||
d9e09a9cbe | |||
2b9a479b96 | |||
21295b8d03 | |||
89671f767e | |||
a92598d17d | |||
c677957725 | |||
a5e636d9c5 | |||
f6527da948 | |||
53fb1d163b | |||
df005d7fb6 | |||
3c619df3dc | |||
13f5e7a480 | |||
8519187d9b | |||
a66428f24e | |||
1d8b21d6b4 | |||
b879c76510 | |||
0dbba2fb9a | |||
49b0620c48 | |||
a6a9e3ac72 | |||
aeefff86f8 | |||
91aa98eef7 | |||
f3b6c4f3fe | |||
5aa5ac2a8b | |||
b1827ccea0 | |||
2629d15e2f | |||
c7b62d3202 | |||
85ec9a84c4 | |||
5adbc354d6 | |||
f8477714ae | |||
bd872ee1c0 | |||
11eae091db | |||
72b702cb21 | |||
d985eac0fc | |||
3fff4fd742 | |||
e90d5a17ba | |||
54143332ab | |||
baa3fb7905 | |||
57c7437f77 | |||
eef5a238a0 | |||
6048458f9b | |||
ff609c85ba | |||
3e98f4053a | |||
1d601dba39 | |||
a8fac5f3c6 | |||
ae78588b80 | |||
fe6fc0e517 | |||
3dcc4cb385 | |||
5e94cbfbea | |||
3b13102abb | |||
7234ecab37 | |||
ddf4599162 | |||
01f5410180 | |||
49706842af | |||
2798a90d83 | |||
518b868249 | |||
755115660b | |||
6709f8c551 | |||
1f3cdd9513 | |||
65af6aa499 | |||
024151a5c1 | |||
e380af7056 | |||
e654abfd1d | |||
3510a6cff8 | |||
3fb351e762 | |||
a11b96b84e | |||
c3be1c0a67 | |||
fdf7399e52 | |||
ed8155139a | |||
8112b1aab2 | |||
c0e583d20d | |||
3f72367aaf | |||
c27f487bf0 | |||
ae3bb94036 | |||
ddc54e2977 | |||
ed49d7c460 | |||
59baa14bde | |||
6bf1c44961 | |||
94702b9b51 | |||
0f148507e4 | |||
0cec779545 | |||
1ecbbc7d29 | |||
caaa60d1a8 | |||
39d50466c9 | |||
5a452d85c1 | |||
4326ae7a0a | |||
905c4448d0 | |||
0de44835e5 | |||
5aac04faf5 | |||
f98a1700e0 | |||
acdb270793 | |||
4ceed382ed | |||
52ce4f3d20 | |||
c1c8cac6e4 | |||
be7a34f719 | |||
f7a94634f9 | |||
7d4ab6db2c | |||
0a78587d8e | |||
b753ceef8e | |||
8963fe205b | |||
e906506e16 | |||
3195a45e3d | |||
6aad911985 | |||
e3b45ef794 | |||
8e1a539e70 | |||
63fa972afa | |||
bf95eee3f1 | |||
240fbcb1df | |||
26db340077 | |||
20f611b7b5 | |||
f9aefedf60 | |||
d7e3d34726 | |||
2294dc8832 | |||
4af86e1cb3 | |||
9fcbbe7d57 | |||
c3f67e38a1 | |||
46e29245b7 | |||
002e33b48b | |||
5bb25fe214 | |||
66db2e6049 | |||
c5cf471912 | |||
4ed4f8e143 | |||
f31699d921 | |||
96a9ae2ca5 | |||
ee1c0bb313 | |||
558c03b12b | |||
7d8fbbb086 | |||
9fd405a896 | |||
5d7cf3a8a2 | |||
1230aa1e91 | |||
accecb3350 | |||
1e0ade8f55 | |||
429676ad43 | |||
ef39d9a7b8 | |||
986bc9448e | |||
d34893ba72 | |||
b8a5fb95c1 | |||
102b2c946b | |||
505b1b9608 | |||
2851c140ea | |||
637d81ce44 | |||
bc52461f0f | |||
c395c04a6e | |||
9eb4f8f191 | |||
f774256c42 | |||
5c15d039e1 | |||
28e9342c25 | |||
af8ec4db5b | |||
5d47e5d167 | |||
5101fbd809 | |||
472457b9f3 | |||
2ef4bb7dcc | |||
9a732ea6f8 | |||
f80799a593 |
59
.github/workflows/main.yml
vendored
Normal file
59
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
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
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
/.vscode
|
||||
/bin
|
||||
**/zig-cache
|
||||
**/.zig-cache
|
||||
**/zig-out
|
||||
/docs
|
||||
**/*.log
|
||||
@@ -12,3 +13,7 @@
|
||||
|
||||
# Any Custom Scripts for Debugging purposes
|
||||
*.sh
|
||||
|
||||
|
||||
# Dear ImGui
|
||||
**/imgui.ini
|
||||
|
14
.gitmodules
vendored
14
.gitmodules
vendored
@@ -1,15 +1,3 @@
|
||||
[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
|
||||
url = https://github.com/paoda/SDL.zig
|
||||
|
8
.vscode/extensions.json
vendored
8
.vscode/extensions.json
vendored
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"augusterame.zls-vscode",
|
||||
"usernamehw.errorlens",
|
||||
"vadimcn.vscode-lldb",
|
||||
"dan-c-underwood.arm"
|
||||
]
|
||||
}
|
132
README.md
132
README.md
@@ -1,86 +1,96 @@
|
||||
# ZBA (working title)
|
||||
|
||||
A Game Boy Advance Emulator written in Zig ⚡!
|
||||
|
||||
## Scope
|
||||
I'm hardly the first to write a Game Boy Advance Emulator nor will I be the last. This project isn't going to compete with the GOATs like
|
||||
[mGBA](https://github.com/mgba-emu) or [NanoBoyAdvance](https://github.com/nba-emu/NanoBoyAdvance). There aren't any interesting
|
||||
ideas either like in [DSHBA](https://github.com/DenSinH/DSHBA).
|
||||

|
||||
|
||||
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:
|
||||
## Scope
|
||||
|
||||
I'm hardly the first to write a Game Boy Advance Emulator nor will I be the last. This project isn't going to compete with the GOATs like [mGBA](https://github.com/mgba-emu) or [NanoBoyAdvance](https://github.com/nba-emu/NanoBoyAdvance). There aren't any interesting ideas either like in [DSHBA](https://github.com/DenSinH/DSHBA).
|
||||
|
||||
This is a simple (read: incomplete) for-fun long-term project. I hope to get "mostly there", which to me means that I'm not missing any major hardware features and the set of possible improvements would be in memory timing or in UI/UX. With respect to that goal, here's what's outstanding:
|
||||
|
||||
### TODO
|
||||
- [ ] Affine Sprites
|
||||
|
||||
- [x] Affine Sprites
|
||||
- [ ] Windowing (see [this branch](https://git.musuka.dev/paoda/zba/src/branch/window))
|
||||
- [ ] Audio Resampler (Having issues with SDL2's)
|
||||
- [ ] Immediate Mode GUI
|
||||
- [ ] Refactoring for easy-ish perf boosts
|
||||
|
||||
## Tests
|
||||
- [x] [jsmolka's GBA Test Collection](https://github.com/jsmolka/gba-tests)
|
||||
- [x] `arm.gba` and `thumb.gba`
|
||||
- [x] `flash64.gba`, `flash128.gba`, `none.gba`, and `sram.gba`
|
||||
- [x] `hello.gba`, `shades.gba`, and `stripes.gba`
|
||||
- [x] `memory.gba`
|
||||
- [x] `bios.gba`
|
||||
- [x] `nes.gba`
|
||||
- [ ] [DenSinH's GBA ROMs](https://github.com/DenSinH/GBARoms)
|
||||
- [x] `eeprom-test` and `flash-test`
|
||||
- [x] `midikey2freq`
|
||||
- [ ] `swi-tests-random`
|
||||
- [ ] [destoer's GBA Tests](https://github.com/destoer/gba_tests)
|
||||
- [x] `cond_invalid.gba`
|
||||
- [x] `dma_priority.gba`
|
||||
- [x] `hello_world.gba`
|
||||
- [x] `if_ack.gba`
|
||||
- [ ] `line_timing.gba`
|
||||
- [ ] `lyc_midline.gba`
|
||||
- [ ] `window_midframe.gba`
|
||||
- [x] [ladystarbreeze's GBA Test Collection](https://github.com/ladystarbreeze/GBA-Test-Collection)
|
||||
- [x] `retAddr.gba`
|
||||
- [x] `helloWorld.gba`
|
||||
- [x] `helloAudio.gba`
|
||||
- [x] [`armwrestler-gba-fixed.gba`](https://github.com/destoer/armwrestler-gba-fixed)
|
||||
- [x] [FuzzARM](https://github.com/DenSinH/FuzzARM)
|
||||
## Usage
|
||||
|
||||
## 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)
|
||||
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 [0.10.0-dev.4474+b41b35f57](https://github.com/ziglang/zig/tree/b41b35f57)
|
||||
|
||||
Most recently built on Zig [v0.11.0](https://github.com/ziglang/zig/tree/0.11.0)
|
||||
|
||||
### Dependencies
|
||||
* [SDL.zig](https://github.com/MasterQ32/SDL.zig)
|
||||
* [SDL2](https://www.libsdl.org/download-2.0.php)
|
||||
* [zig-clap](https://github.com/Hejsil/zig-clap)
|
||||
* [known-folders](https://github.com/ziglibs/known-folders)
|
||||
* [zig-toml](https://github.com/aeronavery/zig-toml)
|
||||
* [zig-datetime](https://github.com/frmdstryr/zig-datetime)
|
||||
* [`bitfields.zig`](https://github.com/FlorenceOS/Florence/blob/aaa5a9e568/lib/util/bitfields.zig)
|
||||
|
||||
`bitfields.zig` from [FlorenceOS](https://github.com/FlorenceOS) is included under `lib/util/bitfield.zig`.
|
||||
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 submodules `SDL.zig`, `zig-clap`, `known-folders`, `zig-toml` and `zig-datetime`
|
||||
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: ¯\\\_(ツ)_/¯
|
||||
* Windows: [`vcpkg`](https://github.com/Microsoft/vcpkg) (install `sdl2:x64-windows`)
|
||||
|
||||
- Linux: Your distro's package manager
|
||||
- macOS: ¯\\\_(ツ)_/¯ (try [this formula](https://formulae.brew.sh/formula/sdl2)?)
|
||||
- Windows: [`vcpkg`](https://github.com/Microsoft/vcpkg) (install `sdl2:x64-windows`)
|
||||
|
||||
`SDL.zig` will provide a helpful compile error if the zig compiler is unable to find SDL2.
|
||||
|
||||
Once you've got all the dependencies, execute `zig build -Drelease-fast`. The executable is located at `zig-out/bin/`.
|
||||
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
|
||||
--- | ---
|
||||
<kbd>X</kbd> | A
|
||||
<kbd>Z</kbd> | B
|
||||
<kbd>A</kbd> | L
|
||||
<kbd>S</kbd> | R
|
||||
<kbd>Return</kbd> | Start
|
||||
<kbd>RShift</kbd> | Select
|
||||
|
||||
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
|
||||
|
||||
## 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)
|
||||
|
BIN
assets/screenshot.png
Normal file
BIN
assets/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
99
build.zig
99
build.zig
@@ -1,60 +1,65 @@
|
||||
const std = @import("std");
|
||||
const Sdk = @import("lib/SDL.zig/Sdk.zig");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
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(.{});
|
||||
const sdl = @import("lib/SDL.zig/build.zig");
|
||||
|
||||
// Standard release options allow the person running `zig build` to select
|
||||
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
|
||||
const mode = b.standardReleaseOptions();
|
||||
const SemVer = std.SemanticVersion;
|
||||
|
||||
const exe = b.addExecutable("zba", "src/main.zig");
|
||||
exe.setMainPkgPath("."); // Necessary so that src/main.zig can embed example.toml
|
||||
exe.setTarget(target);
|
||||
const target_version = "0.13.0";
|
||||
|
||||
// Known Folders (%APPDATA%, XDG, etc.)
|
||||
exe.addPackagePath("known_folders", "lib/known-folders/known-folders.zig");
|
||||
|
||||
// DateTime Library
|
||||
exe.addPackagePath("datetime", "lib/zig-datetime/src/main.zig");
|
||||
|
||||
// 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");
|
||||
|
||||
// Argument Parsing Library
|
||||
exe.addPackagePath("clap", "lib/zig-clap/clap.zig");
|
||||
|
||||
// TOML Library
|
||||
exe.addPackagePath("toml", "lib/zig-toml/src/toml.zig");
|
||||
|
||||
// OpenGL 3.3 Bindings
|
||||
exe.addPackagePath("gl", "lib/gl.zig");
|
||||
|
||||
// Zig SDL Bindings: https://github.com/MasterQ32/SDL.zig
|
||||
const sdk = Sdk.init(b);
|
||||
sdk.link(exe, .dynamic);
|
||||
exe.addPackage(sdk.getNativePackage("sdl2"));
|
||||
|
||||
exe.setBuildMode(mode);
|
||||
exe.install();
|
||||
|
||||
const run_cmd = exe.run();
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
pub fn build(b: *std.Build) void {
|
||||
const actual_version = builtin.zig_version;
|
||||
if (comptime actual_version.order(SemVer.parse(target_version) catch unreachable) != .eq) {
|
||||
@compileError("ZBA must be built with Zig v" ++ target_version ++ ".");
|
||||
}
|
||||
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "zba",
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const sdk = sdl.init(b, null, null);
|
||||
const zgui = b.dependency("zgui", .{ .shared = false, .with_implot = true, .backend = .sdl2_opengl3 });
|
||||
const imgui = zgui.artifact("imgui");
|
||||
|
||||
exe.root_module.addImport("known_folders", b.dependency("known-folders", .{}).module("known-folders")); // https://github.com/ziglibs/known-folders
|
||||
exe.root_module.addImport("datetime", b.dependency("zig-datetime", .{}).module("zig-datetime")); // https://github.com/frmdstryr/zig-datetime
|
||||
exe.root_module.addImport("clap", b.dependency("zig-clap", .{}).module("clap")); // https://github.com/Hejsil/zig-clap
|
||||
exe.root_module.addImport("zba-util", b.dependency("zba-util", .{}).module("zba-util")); // https://git.musuka.dev/paoda/zba-util
|
||||
exe.root_module.addImport("tomlz", b.dependency("tomlz", .{}).module("tomlz")); // https://github.com/mattyhall/tomlz
|
||||
exe.root_module.addImport("arm32", b.dependency("arm32", .{}).module("arm32")); // https://git.musuka.dev/paoda/arm32
|
||||
exe.root_module.addImport("gdbstub", b.dependency("zba-gdbstub", .{}).module("zba-gdbstub")); // https://git.musuka.dev/paoda/gdbstub
|
||||
exe.root_module.addImport("nfd", b.dependency("nfd", .{}).module("nfd")); // https://github.com/fabioarnold/nfd-zig
|
||||
exe.root_module.addImport("zgui", zgui.module("root")); // https://git.musuka.dev/paoda/zgui
|
||||
exe.root_module.addImport("sdl2", sdk.getNativeModule()); // https://github.com/MasterQ32/SDL.zig
|
||||
|
||||
exe.root_module.addAnonymousImport("bitfield", .{ .root_source_file = b.path("lib/bitfield.zig") }); // https://github.com/FlorenceOS/
|
||||
exe.root_module.addAnonymousImport("gl", .{ .root_source_file = b.path("lib/gl.zig") }); // https://github.com/MasterQ32/zig-opengl
|
||||
exe.root_module.addAnonymousImport("example.toml", .{ .root_source_file = b.path("example.toml") });
|
||||
|
||||
sdk.link(exe, .dynamic, .SDL2);
|
||||
sdk.link(imgui, .dynamic, .SDL2);
|
||||
exe.linkLibrary(imgui);
|
||||
|
||||
b.installArtifact(exe);
|
||||
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
if (b.args) |args| run_cmd.addArgs(args);
|
||||
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
|
||||
const exe_tests = b.addTest("src/main.zig");
|
||||
exe_tests.setTarget(target);
|
||||
exe_tests.setBuildMode(mode);
|
||||
const exe_tests = b.addTest(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const test_step = b.step("test", "Run unit tests");
|
||||
test_step.dependOn(&exe_tests.step);
|
||||
|
50
build.zig.zon
Normal file
50
build.zig.zon
Normal file
@@ -0,0 +1,50 @@
|
||||
.{
|
||||
.name = "zba",
|
||||
.version = "0.1.0",
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"lib/bitfield.zig",
|
||||
"lib/gl.zig",
|
||||
"src",
|
||||
},
|
||||
.minimum_zig_version = "0.13.0",
|
||||
.dependencies = .{
|
||||
.nfd = .{
|
||||
.url = "git+https://github.com/paoda/nfd-zig#ad81729d33da30d5f4fd23718debec48245121ca",
|
||||
.hash = "1220a679380847513262c8c5c474d4a415f9ecc4921c8c6aefbdbdce66cf2aa19ceb",
|
||||
},
|
||||
.@"known-folders" = .{
|
||||
.url = "git+https://github.com/ziglibs/known-folders#1cceeb70e77dec941a4178160ff6c8d05a74de6f",
|
||||
.hash = "12205f5e7505c96573f6fc5144592ec38942fb0a326d692f9cddc0c7dd38f9028f29",
|
||||
},
|
||||
.@"zig-datetime" = .{
|
||||
.url = "git+https://github.com/frmdstryr/zig-datetime#70aebf28fb3e137cd84123a9349d157a74708721",
|
||||
.hash = "122077215ce36e125a490e59ec1748ffd4f6ba00d4d14f7308978e5360711d72d77f",
|
||||
},
|
||||
.@"zig-clap" = .{
|
||||
.url = "git+https://github.com/Hejsil/zig-clap#c0193e9247335a6c1688b946325060289405de2a",
|
||||
.hash = "12207ee987ce045596cb992cfb15b0d6d9456e50d4721c3061c69dabc2962053644d",
|
||||
},
|
||||
.@"zba-util" = .{
|
||||
.url = "git+https://git.musuka.dev/paoda/zba-util#bf0e744047ce1ec90172dbcc0c72bfcc29a063e3",
|
||||
.hash = "1220d044ecfbeacc3b3cebeff131d587e24167d61435a3cb96dffd4d4521bb06aed0",
|
||||
},
|
||||
.@"zba-gdbstub" = .{
|
||||
.url = "git+https://git.musuka.dev/paoda/zba-gdbstub#9a50607d5f48293f950a4e823344f2bc24582a5a",
|
||||
.hash = "1220ac267744ed2a735f03c4620d7c6210fbd36d7bfb2b376ddc3436faebadee0f61",
|
||||
},
|
||||
.tomlz = .{
|
||||
.url = "git+https://github.com/paoda/tomlz#9a16dd53927ef2012478b6494bafb4475e44f4c9",
|
||||
.hash = "12204f922cab84980e36b5c058d354ec0ee169bda401c8e0e80a463580349b476569",
|
||||
},
|
||||
.arm32 = .{
|
||||
.url = "git+https://git.musuka.dev/paoda/arm32#814d081ea0983bc48841a6baad7158c157b17ad6",
|
||||
.hash = "12203c3dacf3a7aa7aee5fc5763dd7b40399bd1c34d1483330b6bd5a76bffef22d82",
|
||||
},
|
||||
.zgui = .{
|
||||
.url = "git+https://git.musuka.dev/paoda/zgui#7f8d05101e96c64314d7926c80ee157dcb89da4e",
|
||||
.hash = "1220bd81a1c7734892b1d4233ed047710487787873c85dd5fc76d1764a331ed2ff43",
|
||||
},
|
||||
},
|
||||
}
|
36
dl_sdl2.ps1
Normal file
36
dl_sdl2.ps1
Normal file
@@ -0,0 +1,36 @@
|
||||
$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
|
@@ -1,4 +1,4 @@
|
||||
[Host]
|
||||
[host]
|
||||
# Using nearest-neighbour scaling, how many times the native resolution
|
||||
# of the game bow should the screen be?
|
||||
win_scale = 3
|
||||
@@ -7,7 +7,7 @@ vsync = true
|
||||
# Mute ZBA
|
||||
mute = false
|
||||
|
||||
[Guest]
|
||||
[guest]
|
||||
# Sync Emulation to Audio
|
||||
audio_sync = true
|
||||
# Sync Emulation to Video
|
||||
@@ -17,7 +17,7 @@ force_rtc = false
|
||||
# Skip BIOS
|
||||
skip_bios = false
|
||||
|
||||
[Debug]
|
||||
[debug]
|
||||
# Enable detailed CPU logs
|
||||
cpu_trace = false
|
||||
# When false and builtin.mode == .Debug, ZBA will panic
|
||||
|
Submodule lib/SDL.zig updated: 6a9e37687a...fac81ec499
@@ -26,13 +26,13 @@ fn BitType(comptime FieldType: type, comptime ValueType: type, comptime shamt: u
|
||||
}
|
||||
|
||||
pub fn read(self: anytype) ValueType {
|
||||
return @bitCast(ValueType, @truncate(u1, self.bits.field().* >> shamt));
|
||||
return @bitCast(@as(u1, @truncate(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)) {
|
||||
if (@as(bool, @bitCast(val))) {
|
||||
self.set();
|
||||
} else {
|
||||
self.unset();
|
||||
@@ -67,17 +67,17 @@ pub fn Bitfield(comptime FieldType: type, comptime shamt: usize, comptime num_bi
|
||||
dummy: FieldType,
|
||||
|
||||
fn field(self: anytype) PtrCastPreserveCV(@This(), @TypeOf(self), FieldType) {
|
||||
return @ptrCast(PtrCastPreserveCV(@This(), @TypeOf(self), FieldType), self);
|
||||
return @ptrCast(self);
|
||||
}
|
||||
|
||||
pub fn write(self: anytype, val: ValueType) void {
|
||||
self.field().* &= ~self_mask;
|
||||
self.field().* |= @intCast(FieldType, val) << shamt;
|
||||
self.field().* |= @as(FieldType, @intCast(val)) << shamt;
|
||||
}
|
||||
|
||||
pub fn read(self: anytype) ValueType {
|
||||
const val: FieldType = self.field().*;
|
||||
return @intCast(ValueType, (val & self_mask) >> shamt);
|
||||
return @intCast((val & self_mask) >> shamt);
|
||||
}
|
||||
};
|
||||
}
|
4163
lib/gl.zig
4163
lib/gl.zig
File diff suppressed because it is too large
Load Diff
Submodule lib/known-folders deleted from 24845b0103
Submodule lib/zig-clap deleted from e5d09c4b2d
Submodule lib/zig-datetime deleted from 5ec1c36cf3
Submodule lib/zig-toml deleted from 5dfa919e03
@@ -1,5 +1,5 @@
|
||||
const std = @import("std");
|
||||
const toml = @import("toml");
|
||||
const tomlz = @import("tomlz");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
@@ -7,6 +7,7 @@ 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 = .{},
|
||||
@@ -49,35 +50,14 @@ pub fn config() *const Config {
|
||||
}
|
||||
|
||||
/// Reads a config file and then loads it into the global state
|
||||
pub fn load(allocator: Allocator, config_path: []const u8) !void {
|
||||
var config_file = try std.fs.cwd().openFile(config_path, .{});
|
||||
pub fn load(allocator: Allocator, file_path: []const u8) !void {
|
||||
var config_file = try std.fs.cwd().openFile(file_path, .{});
|
||||
defer config_file.close();
|
||||
|
||||
log.info("loaded from {s}", .{config_path});
|
||||
log.info("loaded from {s}", .{file_path});
|
||||
|
||||
const contents = try config_file.readToEndAlloc(allocator, try config_file.getEndPos());
|
||||
defer allocator.free(contents);
|
||||
|
||||
const table = try toml.parseContents(allocator, contents, null);
|
||||
defer table.deinit();
|
||||
|
||||
// TODO: Report unknown config options
|
||||
|
||||
if (table.keys.get("Host")) |host| {
|
||||
if (host.Table.keys.get("win_scale")) |scale| state.host.win_scale = scale.Integer;
|
||||
if (host.Table.keys.get("vsync")) |vsync| state.host.vsync = vsync.Boolean;
|
||||
if (host.Table.keys.get("mute")) |mute| state.host.mute = mute.Boolean;
|
||||
}
|
||||
|
||||
if (table.keys.get("Guest")) |guest| {
|
||||
if (guest.Table.keys.get("audio_sync")) |sync| state.guest.audio_sync = sync.Boolean;
|
||||
if (guest.Table.keys.get("video_sync")) |sync| state.guest.video_sync = sync.Boolean;
|
||||
if (guest.Table.keys.get("force_rtc")) |forced| state.guest.force_rtc = forced.Boolean;
|
||||
if (guest.Table.keys.get("skip_bios")) |skip| state.guest.skip_bios = skip.Boolean;
|
||||
}
|
||||
|
||||
if (table.keys.get("Debug")) |debug| {
|
||||
if (debug.Table.keys.get("cpu_trace")) |trace| state.debug.cpu_trace = trace.Boolean;
|
||||
if (debug.Table.keys.get("unhandled_io")) |unhandled| state.debug.unhandled_io = unhandled.Boolean;
|
||||
}
|
||||
state = try tomlz.parser.decode(Config, allocator, contents);
|
||||
}
|
||||
|
458
src/core/Bus.zig
458
src/core/Bus.zig
@@ -1,6 +1,6 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
||||
const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||
const Bios = @import("bus/Bios.zig");
|
||||
const Ewram = @import("bus/Ewram.zig");
|
||||
const GamePak = @import("bus/GamePak.zig");
|
||||
@@ -19,7 +19,7 @@ const log = std.log.scoped(.Bus);
|
||||
|
||||
const createDmaTuple = @import("bus/dma.zig").create;
|
||||
const createTimerTuple = @import("bus/timer.zig").create;
|
||||
const rotr = @import("../util.zig").rotr;
|
||||
const rotr = @import("zba-util").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,6 +33,11 @@ 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,
|
||||
@@ -48,7 +53,16 @@ io: Io,
|
||||
cpu: *Arm7tdmi,
|
||||
sched: *Scheduler,
|
||||
|
||||
read_table: *const [table_len]?*const anyopaque,
|
||||
write_tables: [2]*const [table_len]?*anyopaque,
|
||||
allocator: Allocator,
|
||||
|
||||
pub fn init(self: *Self, allocator: Allocator, sched: *Scheduler, cpu: *Arm7tdmi, paths: FilePaths) !void {
|
||||
const tables = try allocator.alloc(?*anyopaque, 3 * table_len); // Allocate all tables
|
||||
|
||||
const read_table = tables[0..table_len];
|
||||
const write_tables = .{ tables[table_len .. 2 * table_len], tables[2 * table_len .. 3 * table_len] };
|
||||
|
||||
self.* = .{
|
||||
.pak = try GamePak.init(allocator, cpu, paths.rom, paths.save),
|
||||
.bios = try Bios.init(allocator, paths.bios),
|
||||
@@ -61,7 +75,17 @@ pub fn init(self: *Self, allocator: Allocator, sched: *Scheduler, cpu: *Arm7tdmi
|
||||
.io = Io.init(),
|
||||
.cpu = cpu,
|
||||
.sched = sched,
|
||||
|
||||
.read_table = read_table,
|
||||
.write_tables = write_tables,
|
||||
.allocator = allocator,
|
||||
};
|
||||
|
||||
self.fillReadTable(read_table);
|
||||
|
||||
// Internal Display Memory behaves differently on 8-bit reads
|
||||
self.fillWriteTable(u32, write_tables[0]);
|
||||
self.fillWriteTable(u8, write_tables[1]);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
@@ -70,62 +94,170 @@ 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 dbgRead(self: *const Self, comptime T: type, address: u32) T {
|
||||
const page = @truncate(u8, address >> 24);
|
||||
const aligned_addr = forceAlign(T, address);
|
||||
pub fn reset(self: *Self) void {
|
||||
self.bios.reset();
|
||||
self.ppu.reset();
|
||||
self.apu.reset();
|
||||
self.iwram.reset();
|
||||
self.ewram.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.dma.len) : (i += 1) {
|
||||
self.dma[0].reset();
|
||||
}
|
||||
}
|
||||
|
||||
break :blk self.openBus(T, address);
|
||||
},
|
||||
0x02 => self.ewram.read(T, aligned_addr),
|
||||
0x03 => self.iwram.read(T, aligned_addr),
|
||||
0x04 => self.readIo(T, address),
|
||||
// https://github.com/ziglang/zig/issues/14705
|
||||
{
|
||||
comptime var i: usize = 0;
|
||||
inline while (i < self.tim.len) : (i += 1) {
|
||||
self.tim[0].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.openBus(T, address),
|
||||
};
|
||||
self.io.reset();
|
||||
}
|
||||
|
||||
/// TODO: Should open bus read addresses be force-aligned?
|
||||
fn readIo(self: *const Self, comptime T: type, unaligned_address: u32) T {
|
||||
const maybe_value = io.read(self, T, forceAlign(T, unaligned_address));
|
||||
return if (maybe_value) |value| value else self.openBus(T, unaligned_address);
|
||||
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);
|
||||
|
||||
const read_ptr: *[table_len]?*const anyopaque = @constCast(self.read_table);
|
||||
const write_ptrs: [2]*[table_len]?*anyopaque = .{ @constCast(self.write_tables[0]), @constCast(self.write_tables[1]) };
|
||||
|
||||
self.fillReadTable(read_ptr);
|
||||
self.fillWriteTable(u32, write_ptrs[0]);
|
||||
self.fillWriteTable(u8, write_ptrs[1]);
|
||||
}
|
||||
|
||||
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]?*const anyopaque) void {
|
||||
comptime std.debug.assert(T == u32 or T == u16 or T == u8);
|
||||
const vramMirror = @import("ppu/Vram.zig").mirror;
|
||||
|
||||
for (table, 0..) |*ptr, i| {
|
||||
const addr: 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 {
|
||||
@setCold(true);
|
||||
const r15 = self.cpu.r[15];
|
||||
|
||||
const word = blk: {
|
||||
// If Arm, get the most recently fetched instruction (PC + 8)
|
||||
//
|
||||
// FIXME: This is most likely a faulty assumption.
|
||||
// I think what *actually* happens is that the Bus has a latch for the most
|
||||
// recently fetched piece of data, which is then returned during Open Bus (also DMA open bus?)
|
||||
// I can "get away" with this because it's very statistically likely that the most recently latched value is
|
||||
// the most recently fetched instruction by the pipeline
|
||||
if (!self.cpu.cpsr.t.read()) break :blk self.cpu.pipe.stage[1].?;
|
||||
|
||||
const page = @truncate(u8, r15 >> 24);
|
||||
const page: u8 = @truncate(r15 >> 24);
|
||||
|
||||
// PC + 2 = stage[0]
|
||||
// PC + 4 = stage[1]
|
||||
@@ -134,7 +266,7 @@ fn openBus(self: *const Self, comptime T: type, address: u32) T {
|
||||
switch (page) {
|
||||
// EWRAM, PALRAM, VRAM, and Game ROM (16-bit)
|
||||
0x02, 0x05, 0x06, 0x08...0x0D => {
|
||||
const halfword: u32 = @truncate(u16, self.cpu.pipe.stage[1].?);
|
||||
const halfword: u32 = @as(u16, @truncate(self.cpu.pipe.stage[1].?));
|
||||
break :blk halfword << 16 | halfword;
|
||||
},
|
||||
|
||||
@@ -145,8 +277,8 @@ fn openBus(self: *const Self, comptime T: type, address: u32) T {
|
||||
const aligned = address & 3 == 0b00;
|
||||
|
||||
// TODO: What to do on PC + 6?
|
||||
const high: u32 = if (aligned) self.dbgRead(u16, r15 + 4) else @truncate(u16, self.cpu.pipe.stage[1].?);
|
||||
const low: u32 = @truncate(u16, self.cpu.pipe.stage[@boolToInt(aligned)].?);
|
||||
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;
|
||||
},
|
||||
@@ -157,8 +289,8 @@ fn openBus(self: *const Self, comptime T: type, address: u32) T {
|
||||
// Unaligned: (PC + 4) | (PC + 2)
|
||||
const aligned = address & 3 == 0b00;
|
||||
|
||||
const high: u32 = @truncate(u16, self.cpu.pipe.stage[1 - @boolToInt(aligned)].?);
|
||||
const low: u32 = @truncate(u16, self.cpu.pipe.stage[@boolToInt(aligned)].?);
|
||||
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;
|
||||
},
|
||||
@@ -169,36 +301,112 @@ fn openBus(self: *const Self, comptime T: type, address: u32) T {
|
||||
}
|
||||
};
|
||||
|
||||
return @truncate(T, word);
|
||||
return @truncate(word);
|
||||
}
|
||||
|
||||
pub fn read(self: *Self, comptime T: type, address: u32) T {
|
||||
const page = @truncate(u8, address >> 24);
|
||||
const aligned_addr = forceAlign(T, address);
|
||||
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);
|
||||
|
||||
self.sched.tick += timings[@boolToInt(T == u32)][@truncate(u4, page)];
|
||||
// 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 {
|
||||
@setCold(true);
|
||||
|
||||
const page: u8 = @truncate(unaligned_address >> 24);
|
||||
const address = forceAlign(T, unaligned_address);
|
||||
|
||||
return switch (page) {
|
||||
// General Internal Memory
|
||||
0x00 => blk: {
|
||||
if (address < Bios.size)
|
||||
break :blk self.bios.read(T, self.cpu.r[15], aligned_addr);
|
||||
break :blk self.bios.read(T, self.cpu.r[15], unaligned_address);
|
||||
|
||||
break :blk self.openBus(T, address);
|
||||
},
|
||||
0x02 => self.ewram.read(T, aligned_addr),
|
||||
0x03 => self.iwram.read(T, aligned_addr),
|
||||
0x02 => unreachable, // completely handled by fastmeme
|
||||
0x03 => unreachable, // completely handled by fastmeme
|
||||
0x04 => self.readIo(T, address),
|
||||
|
||||
// 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),
|
||||
0x05 => unreachable, // completely handled by fastmeme
|
||||
0x06 => unreachable, // completely handled by fastmeme
|
||||
0x07 => unreachable, // completely handled by fastmeme
|
||||
|
||||
// External Memory (Game Pak)
|
||||
0x08...0x0D => self.pak.read(T, aligned_addr),
|
||||
0x0E...0x0F => blk: {
|
||||
const value = self.pak.backup.read(address);
|
||||
0x08...0x0D => self.pak.read(T, address),
|
||||
0x0E...0x0F => self.readBackup(T, unaligned_address),
|
||||
else => self.openBus(T, 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);
|
||||
|
||||
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);
|
||||
},
|
||||
0x02 => unreachable, // handled by fastmem
|
||||
0x03 => unreachable, // handled by fastmem
|
||||
0x04 => self.readIo(T, address),
|
||||
|
||||
// Internal Display Memory
|
||||
0x05 => unreachable, // handled by fastmem
|
||||
0x06 => unreachable, // handled by fastmem
|
||||
0x07 => unreachable, // handled by fastmem
|
||||
|
||||
// External Memory (Game Pak)
|
||||
0x08...0x0D => self.pak.dbgRead(T, address),
|
||||
0x0E...0x0F => self.readBackup(T, unaligned_address),
|
||||
else => self.openBus(T, address),
|
||||
};
|
||||
}
|
||||
|
||||
fn readBackup(self: *const Self, comptime T: type, unaligned_address: u32) T {
|
||||
const value = self.pak.backup.read(unaligned_address);
|
||||
|
||||
const multiplier = switch (T) {
|
||||
u32 => 0x01010101,
|
||||
@@ -207,50 +415,122 @@ pub fn read(self: *Self, comptime T: type, address: u32) T {
|
||||
else => @compileError("Backup: Unsupported read width"),
|
||||
};
|
||||
|
||||
break :blk @as(T, value) * multiplier;
|
||||
},
|
||||
else => self.openBus(T, address),
|
||||
};
|
||||
return @as(T, value) * multiplier;
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
self.sched.tick += timings[@boolToInt(T == u32)][@truncate(u4, page)];
|
||||
// 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 {
|
||||
@setCold(true);
|
||||
|
||||
const page: u8 = @truncate(unaligned_address >> 24);
|
||||
const address = forceAlign(T, unaligned_address);
|
||||
|
||||
switch (page) {
|
||||
// General Internal Memory
|
||||
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),
|
||||
0x00 => self.bios.write(T, address, value),
|
||||
0x02 => unreachable, // completely handled by fastmem
|
||||
0x03 => unreachable, // completely handled by fastmem
|
||||
0x04 => io.write(self, T, address, value),
|
||||
|
||||
// Internal Display Memory
|
||||
0x05 => self.ppu.palette.write(T, aligned_addr, value),
|
||||
0x06 => self.ppu.vram.write(T, self.ppu.dispcnt, aligned_addr, value),
|
||||
0x07 => self.ppu.oam.write(T, aligned_addr, value),
|
||||
0x05 => self.ppu.palette.write(T, address, value),
|
||||
0x06 => self.ppu.vram.write(T, self.ppu.dispcnt, address, value),
|
||||
0x07 => unreachable, // completely handled by fastmem
|
||||
|
||||
// External Memory (Game Pak)
|
||||
0x08...0x0D => self.pak.write(T, self.dma[3].word_count, 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)));
|
||||
},
|
||||
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)))),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn forceAlign(comptime T: type, address: u32) u32 {
|
||||
fn dbgSlowWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
|
||||
@setCold(true);
|
||||
|
||||
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 {
|
||||
return switch (T) {
|
||||
u32 => address & 0xFFFF_FFFC,
|
||||
u16 => address & 0xFFFF_FFFE,
|
||||
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),
|
||||
u8 => address,
|
||||
else => @compileError("Bus: Invalid read/write type"),
|
||||
};
|
||||
|
191
src/core/apu.zig
191
src/core/apu.zig
@@ -3,7 +3,8 @@ const SDL = @import("sdl2");
|
||||
const io = @import("bus/io.zig");
|
||||
const util = @import("../util.zig");
|
||||
|
||||
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
||||
const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||
const Bus = @import("Bus.zig");
|
||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
||||
const ToneSweep = @import("apu/ToneSweep.zig");
|
||||
const Tone = @import("apu/Tone.zig");
|
||||
@@ -14,7 +15,6 @@ const SoundFifo = std.fifo.LinearFifo(u8, .{ .Static = 0x20 });
|
||||
|
||||
const getHalf = util.getHalf;
|
||||
const setHalf = util.setHalf;
|
||||
const intToBytes = util.intToBytes;
|
||||
|
||||
const log = std.log.scoped(.APU);
|
||||
|
||||
@@ -22,7 +22,7 @@ pub const host_rate = @import("../platform.zig").sample_rate;
|
||||
pub const host_format = @import("../platform.zig").sample_format;
|
||||
|
||||
pub fn read(comptime T: type, apu: *const Apu, addr: u32) ?T {
|
||||
const byte_addr = @truncate(u8, addr);
|
||||
const byte_addr: u8 = @truncate(addr);
|
||||
|
||||
return switch (T) {
|
||||
u32 => switch (byte_addr) {
|
||||
@@ -73,27 +73,27 @@ pub fn read(comptime T: type, apu: *const Apu, addr: u32) ?T {
|
||||
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
||||
},
|
||||
u8 => switch (byte_addr) {
|
||||
0x60, 0x61 => @truncate(T, @as(u16, apu.ch1.sound1CntL()) >> getHalf(byte_addr)),
|
||||
0x62, 0x63 => @truncate(T, apu.ch1.sound1CntH() >> getHalf(byte_addr)),
|
||||
0x64, 0x65 => @truncate(T, apu.ch1.sound1CntX() >> getHalf(byte_addr)),
|
||||
0x60, 0x61 => @truncate(@as(u16, apu.ch1.sound1CntL()) >> getHalf(byte_addr)),
|
||||
0x62, 0x63 => @truncate(apu.ch1.sound1CntH() >> getHalf(byte_addr)),
|
||||
0x64, 0x65 => @truncate(apu.ch1.sound1CntX() >> getHalf(byte_addr)),
|
||||
0x66, 0x67 => 0x00, // assuming behaviour is identical to that of 16-bit reads
|
||||
0x68, 0x69 => @truncate(T, apu.ch2.sound2CntL() >> getHalf(byte_addr)),
|
||||
0x68, 0x69 => @truncate(apu.ch2.sound2CntL() >> getHalf(byte_addr)),
|
||||
0x6A, 0x6B => 0x00,
|
||||
0x6C, 0x6D => @truncate(T, apu.ch2.sound2CntH() >> getHalf(byte_addr)),
|
||||
0x6C, 0x6D => @truncate(apu.ch2.sound2CntH() >> getHalf(byte_addr)),
|
||||
0x6E, 0x6F => 0x00,
|
||||
0x70, 0x71 => @truncate(T, @as(u16, apu.ch3.sound3CntL()) >> getHalf(byte_addr)), // SOUND3CNT_L
|
||||
0x72, 0x73 => @truncate(T, apu.ch3.sound3CntH() >> getHalf(byte_addr)),
|
||||
0x74, 0x75 => @truncate(T, apu.ch3.sound3CntX() >> getHalf(byte_addr)), // SOUND3CNT_L
|
||||
0x70, 0x71 => @truncate(@as(u16, apu.ch3.sound3CntL()) >> getHalf(byte_addr)), // SOUND3CNT_L
|
||||
0x72, 0x73 => @truncate(apu.ch3.sound3CntH() >> getHalf(byte_addr)),
|
||||
0x74, 0x75 => @truncate(apu.ch3.sound3CntX() >> getHalf(byte_addr)), // SOUND3CNT_L
|
||||
0x76, 0x77 => 0x00,
|
||||
0x78, 0x79 => @truncate(T, apu.ch4.sound4CntL() >> getHalf(byte_addr)),
|
||||
0x78, 0x79 => @truncate(apu.ch4.sound4CntL() >> getHalf(byte_addr)),
|
||||
0x7A, 0x7B => 0x00,
|
||||
0x7C, 0x7D => @truncate(T, apu.ch4.sound4CntH() >> getHalf(byte_addr)),
|
||||
0x7C, 0x7D => @truncate(apu.ch4.sound4CntH() >> getHalf(byte_addr)),
|
||||
0x7E, 0x7F => 0x00,
|
||||
0x80, 0x81 => @truncate(T, apu.soundCntL() >> getHalf(byte_addr)), // SOUNDCNT_L
|
||||
0x82, 0x83 => @truncate(T, apu.soundCntH() >> getHalf(byte_addr)), // SOUNDCNT_H
|
||||
0x84, 0x85 => @truncate(T, @as(u16, apu.soundCntX()) >> getHalf(byte_addr)),
|
||||
0x80, 0x81 => @truncate(apu.soundCntL() >> getHalf(byte_addr)), // SOUNDCNT_L
|
||||
0x82, 0x83 => @truncate(apu.soundCntH() >> getHalf(byte_addr)), // SOUNDCNT_H
|
||||
0x84, 0x85 => @truncate(@as(u16, apu.soundCntX()) >> getHalf(byte_addr)),
|
||||
0x86, 0x87 => 0x00,
|
||||
0x88, 0x89 => @truncate(T, apu.bias.raw >> getHalf(byte_addr)), // SOUNDBIAS
|
||||
0x88, 0x89 => @truncate(apu.bias.raw >> getHalf(byte_addr)), // SOUNDBIAS
|
||||
0x8A, 0x8B => 0x00,
|
||||
0x8C...0x8F => null,
|
||||
0x90...0x9F => apu.ch3.wave_dev.read(T, apu.ch3.select, addr),
|
||||
@@ -106,34 +106,44 @@ pub fn read(comptime T: type, apu: *const Apu, addr: u32) ?T {
|
||||
}
|
||||
|
||||
pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void {
|
||||
const byte_addr = @truncate(u8, addr);
|
||||
const byte_addr: u8 = @truncate(addr);
|
||||
|
||||
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return;
|
||||
|
||||
switch (T) {
|
||||
u32 => switch (byte_addr) {
|
||||
0x60 => apu.ch1.setSound1Cnt(value),
|
||||
0x64 => apu.ch1.setSound1CntX(&apu.fs, @truncate(u16, value)),
|
||||
u32 => {
|
||||
// 0x80 and 0x81 handled in setSoundCnt
|
||||
if (byte_addr < 0x80 and !apu.cnt.apu_enable.read()) return;
|
||||
|
||||
0x68 => apu.ch2.setSound2CntL(@truncate(u16, value)),
|
||||
0x6C => apu.ch2.setSound2CntH(&apu.fs, @truncate(u16, value)),
|
||||
switch (byte_addr) {
|
||||
0x60 => apu.ch1.setSound1Cnt(value),
|
||||
0x64 => apu.ch1.setSound1CntX(&apu.fs, @truncate(value)),
|
||||
|
||||
0x68 => apu.ch2.setSound2CntL(@truncate(value)),
|
||||
0x6C => apu.ch2.setSound2CntH(&apu.fs, @truncate(value)),
|
||||
|
||||
0x70 => apu.ch3.setSound3Cnt(value),
|
||||
0x74 => apu.ch3.setSound3CntX(&apu.fs, @truncate(u16, value)),
|
||||
0x74 => apu.ch3.setSound3CntX(&apu.fs, @truncate(value)),
|
||||
|
||||
0x78 => apu.ch4.setSound4CntL(@truncate(u16, value)),
|
||||
0x7C => apu.ch4.setSound4CntH(&apu.fs, @truncate(u16, value)),
|
||||
0x78 => apu.ch4.setSound4CntL(@truncate(value)),
|
||||
0x7C => apu.ch4.setSound4CntH(&apu.fs, @truncate(value)),
|
||||
|
||||
0x80 => apu.setSoundCnt(value),
|
||||
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1),
|
||||
0x88 => apu.bias.raw = @truncate(u16, value),
|
||||
0x88 => apu.bias.raw = @truncate(value),
|
||||
0x8C => {},
|
||||
|
||||
0x90, 0x94, 0x98, 0x9C => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
|
||||
0xA0 => apu.chA.push(value), // FIFO_A
|
||||
0xA4 => apu.chB.push(value), // FIFO_B
|
||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||
}
|
||||
},
|
||||
u16 => switch (byte_addr) {
|
||||
0x60 => apu.ch1.setSound1CntL(@truncate(u8, value)), // SOUND1CNT_L
|
||||
u16 => {
|
||||
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return;
|
||||
|
||||
switch (byte_addr) {
|
||||
0x60 => apu.ch1.setSound1CntL(@truncate(value)), // SOUND1CNT_L
|
||||
0x62 => apu.ch1.setSound1CntH(value),
|
||||
0x64 => apu.ch1.setSound1CntX(&apu.fs, value),
|
||||
0x66 => {},
|
||||
@@ -143,7 +153,7 @@ pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void {
|
||||
0x6C => apu.ch2.setSound2CntH(&apu.fs, value),
|
||||
0x6E => {},
|
||||
|
||||
0x70 => apu.ch3.setSound3CntL(@truncate(u8, value)),
|
||||
0x70 => apu.ch3.setSound3CntL(@truncate(value)),
|
||||
0x72 => apu.ch3.setSound3CntH(value),
|
||||
0x74 => apu.ch3.setSound3CntX(&apu.fs, value),
|
||||
0x76 => {},
|
||||
@@ -164,8 +174,12 @@ pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void {
|
||||
0xA0, 0xA2 => log.err("Tried to write 0x{X:0>4}{} to FIFO_A", .{ value, T }),
|
||||
0xA4, 0xA6 => log.err("Tried to write 0x{X:0>4}{} to FIFO_B", .{ value, T }),
|
||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||
}
|
||||
},
|
||||
u8 => switch (byte_addr) {
|
||||
u8 => {
|
||||
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return;
|
||||
|
||||
switch (byte_addr) {
|
||||
0x60 => apu.ch1.setSound1CntL(value),
|
||||
0x61 => {},
|
||||
0x62 => apu.ch1.setNr11(value),
|
||||
@@ -208,6 +222,7 @@ pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void {
|
||||
0xA0...0xA3 => log.err("Tried to write 0x{X:0>2}{} to FIFO_A", .{ value, T }),
|
||||
0xA4...0xA7 => log.err("Tried to write 0x{X:0>2}{} to FIFO_B", .{ value, T }),
|
||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||
}
|
||||
},
|
||||
else => @compileError("APU: Unsupported write width"),
|
||||
}
|
||||
@@ -264,27 +279,59 @@ pub const Apu = struct {
|
||||
.is_buffer_full = false,
|
||||
};
|
||||
|
||||
sched.push(.SampleAudio, apu.interval());
|
||||
sched.push(.{ .ApuChannel = 0 }, @import("apu/signal/Square.zig").interval);
|
||||
sched.push(.{ .ApuChannel = 1 }, @import("apu/signal/Square.zig").interval);
|
||||
sched.push(.{ .ApuChannel = 2 }, @import("apu/signal/Wave.zig").interval);
|
||||
sched.push(.{ .ApuChannel = 3 }, @import("apu/signal/Lfsr.zig").interval);
|
||||
sched.push(.FrameSequencer, FrameSequencer.interval);
|
||||
Self.initEvents(apu.sched, apu.interval());
|
||||
|
||||
return apu;
|
||||
}
|
||||
|
||||
fn reset(self: *Self) void {
|
||||
fn initEvents(scheduler: *Scheduler, apu_interval: u64) void {
|
||||
scheduler.push(.SampleAudio, apu_interval);
|
||||
scheduler.push(.{ .ApuChannel = 0 }, @import("apu/signal/Square.zig").interval);
|
||||
scheduler.push(.{ .ApuChannel = 1 }, @import("apu/signal/Square.zig").interval);
|
||||
scheduler.push(.{ .ApuChannel = 2 }, @import("apu/signal/Wave.zig").interval);
|
||||
scheduler.push(.{ .ApuChannel = 3 }, @import("apu/signal/Lfsr.zig").interval);
|
||||
scheduler.push(.FrameSequencer, FrameSequencer.interval);
|
||||
}
|
||||
|
||||
/// Used when resetting the emulator
|
||||
pub fn reset(self: *Self) void {
|
||||
// FIXME: These reset functions are meant to emulate obscure APU behaviour. Write proper emu reset fns
|
||||
self.ch1.reset();
|
||||
self.ch2.reset();
|
||||
self.ch3.reset();
|
||||
self.ch4.reset();
|
||||
|
||||
self.chA.reset();
|
||||
self.chB.reset();
|
||||
|
||||
self.psg_cnt = .{ .raw = 0 };
|
||||
self.dma_cnt = .{ .raw = 0 };
|
||||
self.cnt = .{ .raw = 0 };
|
||||
self.bias = .{ .raw = 0x200 };
|
||||
|
||||
self.sampling_cycle = 0;
|
||||
self.fs.reset();
|
||||
|
||||
Self.initEvents(self.sched, self.interval());
|
||||
}
|
||||
|
||||
/// Emulates the reset behaviour of the APU
|
||||
fn _reset(self: *Self) void {
|
||||
// All PSG Registers between 0x0400_0060..0x0400_0081 are zeroed
|
||||
// 0x0400_0082 and 0x0400_0088 retain their values
|
||||
self.ch1.reset();
|
||||
self.ch2.reset();
|
||||
self.ch3.reset();
|
||||
self.ch4.reset();
|
||||
|
||||
// GBATEK says 4000060h..4000081h I take this to mean inclusive
|
||||
self.psg_cnt.raw = 0x0000;
|
||||
}
|
||||
|
||||
/// SOUNDCNT
|
||||
fn setSoundCnt(self: *Self, value: u32) void {
|
||||
self.setSoundCntL(@truncate(u16, value));
|
||||
self.setSoundCntH(@truncate(u16, value >> 16));
|
||||
if (self.cnt.apu_enable.read()) self.setSoundCntL(@truncate(value));
|
||||
self.setSoundCntH(@truncate(value >> 16));
|
||||
}
|
||||
|
||||
/// SOUNDCNT_L
|
||||
@@ -322,24 +369,27 @@ pub const Apu = struct {
|
||||
self.fs.step = 0; // Reset Frame Sequencer
|
||||
|
||||
// Reset Square Wave Offsets
|
||||
self.ch1.square.pos = 0;
|
||||
self.ch2.square.pos = 0;
|
||||
self.ch1.square.reset();
|
||||
self.ch2.square.reset();
|
||||
|
||||
// Reset Wave Device Offsets
|
||||
self.ch3.wave_dev.offset = 0;
|
||||
// Reset Wave
|
||||
self.ch3.wave_dev.reset();
|
||||
|
||||
// Rest Noise
|
||||
self.ch4.lfsr.reset();
|
||||
} else {
|
||||
self.reset();
|
||||
self._reset();
|
||||
}
|
||||
}
|
||||
|
||||
/// NR52
|
||||
pub fn soundCntX(self: *const Self) u8 {
|
||||
const apu_enable: u8 = @boolToInt(self.cnt.apu_enable.read());
|
||||
const apu_enable: u8 = @intFromBool(self.cnt.apu_enable.read());
|
||||
|
||||
const ch1_enable: u8 = @boolToInt(self.ch1.enabled);
|
||||
const ch2_enable: u8 = @boolToInt(self.ch2.enabled);
|
||||
const ch3_enable: u8 = @boolToInt(self.ch3.enabled);
|
||||
const ch4_enable: u8 = @boolToInt(self.ch4.enabled);
|
||||
const ch1_enable: u8 = @intFromBool(self.ch1.enabled);
|
||||
const ch2_enable: u8 = @intFromBool(self.ch2.enabled);
|
||||
const ch3_enable: u8 = @intFromBool(self.ch3.enabled);
|
||||
const ch4_enable: u8 = @intFromBool(self.ch4.enabled);
|
||||
|
||||
return apu_enable << 7 | ch4_enable << 3 | ch3_enable << 2 | ch2_enable << 1 | ch1_enable;
|
||||
}
|
||||
@@ -395,19 +445,18 @@ pub const Apu = struct {
|
||||
right += if (self.dma_cnt.chB_right.read()) chB_sample else 0;
|
||||
|
||||
// Add SOUNDBIAS
|
||||
// FIXME: Is SOUNDBIAS 9-bit or 10-bit?
|
||||
const bias = @as(i16, self.bias.level.read()) << 1;
|
||||
// FIXME: SOUNDBIAS is 10-bit but The waveform is centered around 0 if I treat it as 11-bit
|
||||
const bias = @as(i16, self.bias.level.read()) << 2;
|
||||
left += bias;
|
||||
right += bias;
|
||||
|
||||
const clamped_left = std.math.clamp(@bitCast(u16, left), std.math.minInt(u11), std.math.maxInt(u11));
|
||||
const clamped_right = std.math.clamp(@bitCast(u16, right), std.math.minInt(u11), std.math.maxInt(u11));
|
||||
const clamped_left = std.math.clamp(@as(u16, @bitCast(left)), 0, std.math.maxInt(u11));
|
||||
const clamped_right = std.math.clamp(@as(u16, @bitCast(right)), 0, std.math.maxInt(u11));
|
||||
|
||||
// Extend to 16-bit signed audio samples
|
||||
const ext_left = (clamped_left << 5) | (clamped_left >> 6);
|
||||
const ext_right = (clamped_right << 5) | (clamped_right >> 6);
|
||||
|
||||
// FIXME: This rarely happens
|
||||
if (self.sampling_cycle != self.bias.sampling_cycle.read()) self.replaceSDLResampler();
|
||||
|
||||
_ = SDL.SDL_AudioStreamPut(self.stream, &[2]u16{ ext_left, ext_right }, 2 * @sizeOf(u16));
|
||||
@@ -424,7 +473,7 @@ pub const Apu = struct {
|
||||
defer SDL.SDL_FreeAudioStream(old_stream);
|
||||
|
||||
self.sampling_cycle = self.bias.sampling_cycle.read();
|
||||
self.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, @intCast(c_int, sample_rate), host_format, 2, host_rate).?;
|
||||
self.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, @intCast(sample_rate), host_format, 2, host_rate).?;
|
||||
}
|
||||
|
||||
fn interval(self: *const Self) u64 {
|
||||
@@ -472,18 +521,20 @@ pub const Apu = struct {
|
||||
pub fn onDmaAudioSampleRequest(self: *Self, cpu: *Arm7tdmi, tim_id: u3) void {
|
||||
if (!self.cnt.apu_enable.read()) return;
|
||||
|
||||
if (@boolToInt(self.dma_cnt.chA_timer.read()) == tim_id) {
|
||||
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
|
||||
|
||||
if (@intFromBool(self.dma_cnt.chA_timer.read()) == tim_id) {
|
||||
if (!self.chA.enabled) return;
|
||||
|
||||
self.chA.updateSample();
|
||||
if (self.chA.len() <= 15) cpu.bus.dma[1].requestAudio(0x0400_00A0);
|
||||
if (self.chA.len() <= 15) bus_ptr.dma[1].requestAudio(0x0400_00A0);
|
||||
}
|
||||
|
||||
if (@boolToInt(self.dma_cnt.chB_timer.read()) == tim_id) {
|
||||
if (@intFromBool(self.dma_cnt.chB_timer.read()) == tim_id) {
|
||||
if (!self.chB.enabled) return;
|
||||
|
||||
self.chB.updateSample();
|
||||
if (self.chB.len() <= 15) cpu.bus.dma[2].requestAudio(0x0400_00A4);
|
||||
if (self.chB.len() <= 15) bus_ptr.dma[2].requestAudio(0x0400_00A4);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -506,11 +557,15 @@ pub fn DmaSound(comptime kind: DmaSoundKind) type {
|
||||
};
|
||||
}
|
||||
|
||||
/// Used when resetting hte emulator (not emulation code)
|
||||
fn reset(self: *Self) void {
|
||||
self.* = Self.init();
|
||||
}
|
||||
|
||||
pub fn push(self: *Self, value: u32) void {
|
||||
// FIXME: I tried to communicate that this is unlikely to the compiler
|
||||
if (!self.enabled) self.enable();
|
||||
|
||||
self.fifo.write(&intToBytes(u32, value)) catch |e| log.err("{} Error: {}", .{ kind, e });
|
||||
self.fifo.write(std.mem.asBytes(&value)) catch |e| log.err("{} Error: {}", .{ kind, e });
|
||||
}
|
||||
|
||||
fn enable(self: *Self) void {
|
||||
@@ -523,11 +578,11 @@ pub fn DmaSound(comptime kind: DmaSoundKind) type {
|
||||
}
|
||||
|
||||
pub fn updateSample(self: *Self) void {
|
||||
if (self.fifo.readItem()) |sample| self.sample = @bitCast(i8, sample);
|
||||
if (self.fifo.readItem()) |sample| self.sample = @bitCast(sample);
|
||||
}
|
||||
|
||||
pub fn amplitude(self: *const Self) i16 {
|
||||
return @as(i16, self.sample);
|
||||
return self.sample;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -541,10 +596,14 @@ pub const FrameSequencer = struct {
|
||||
const Self = @This();
|
||||
pub const interval = (1 << 24) / 512;
|
||||
|
||||
step: u3,
|
||||
step: u3 = 0,
|
||||
|
||||
pub fn init() Self {
|
||||
return .{ .step = 0 };
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
self.* = .{};
|
||||
}
|
||||
|
||||
pub fn tick(self: *Self) void {
|
||||
|
@@ -49,10 +49,13 @@ pub fn init(sched: *Scheduler) Self {
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
self.len = 0;
|
||||
self.envelope.raw = 0;
|
||||
self.poly.raw = 0;
|
||||
self.cnt.raw = 0;
|
||||
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;
|
||||
@@ -73,14 +76,14 @@ pub fn sound4CntL(self: *const Self) u16 {
|
||||
|
||||
/// NR41, NR42
|
||||
pub fn setSound4CntL(self: *Self, value: u16) void {
|
||||
self.setNr41(@truncate(u8, value));
|
||||
self.setNr42(@truncate(u8, value >> 8));
|
||||
self.setNr41(@truncate(value));
|
||||
self.setNr42(@truncate(value >> 8));
|
||||
}
|
||||
|
||||
/// NR41
|
||||
pub fn setNr41(self: *Self, len: u8) void {
|
||||
self.len = @truncate(u6, len);
|
||||
self.len_dev.timer = @as(u7, 64) - @truncate(u6, len);
|
||||
self.len = @truncate(len);
|
||||
self.len_dev.timer = @as(u7, 64) - self.len;
|
||||
}
|
||||
|
||||
/// NR42
|
||||
@@ -96,8 +99,8 @@ pub fn sound4CntH(self: *const Self) u16 {
|
||||
|
||||
/// NR43, NR44
|
||||
pub fn setSound4CntH(self: *Self, fs: *const FrameSequencer, value: u16) void {
|
||||
self.poly.raw = @truncate(u8, value);
|
||||
self.setNr44(fs, @truncate(u8, value >> 8));
|
||||
self.poly.raw = @truncate(value);
|
||||
self.setNr44(fs, @truncate(value >> 8));
|
||||
}
|
||||
|
||||
/// NR44
|
||||
|
@@ -43,9 +43,12 @@ pub fn init(sched: *Scheduler) Self {
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
self.duty.raw = 0;
|
||||
self.envelope.raw = 0;
|
||||
self.freq.raw = 0;
|
||||
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;
|
||||
@@ -74,14 +77,14 @@ pub fn sound2CntL(self: *const Self) u16 {
|
||||
|
||||
/// NR21, NR22
|
||||
pub fn setSound2CntL(self: *Self, value: u16) void {
|
||||
self.setNr21(@truncate(u8, value));
|
||||
self.setNr22(@truncate(u8, value >> 8));
|
||||
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) - @truncate(u6, value);
|
||||
self.len_dev.timer = @as(u7, 64) - @as(u6, @truncate(value));
|
||||
}
|
||||
|
||||
/// NR22
|
||||
@@ -97,8 +100,8 @@ pub fn sound2CntH(self: *const Self) u16 {
|
||||
|
||||
/// NR23, NR24
|
||||
pub fn setSound2CntH(self: *Self, fs: *const FrameSequencer, value: u16) void {
|
||||
self.setNr23(@truncate(u8, value));
|
||||
self.setNr24(fs, @truncate(u8, value >> 8));
|
||||
self.setNr23(@truncate(value));
|
||||
self.setNr24(fs, @truncate(value >> 8));
|
||||
}
|
||||
|
||||
/// NR23
|
||||
|
@@ -50,12 +50,14 @@ pub fn init(sched: *Scheduler) Self {
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
self.sweep.raw = 0;
|
||||
self.sweep_dev.calc_performed = false;
|
||||
self.sweep.raw = 0; // NR10
|
||||
self.duty.raw = 0; // NR11
|
||||
self.envelope.raw = 0; // NR12
|
||||
self.freq.raw = 0; // NR13, NR14
|
||||
|
||||
self.duty.raw = 0;
|
||||
self.envelope.raw = 0;
|
||||
self.freq.raw = 0;
|
||||
self.len_dev.reset();
|
||||
self.sweep_dev.reset();
|
||||
self.env_dev.reset();
|
||||
|
||||
self.sample = 0;
|
||||
self.enabled = false;
|
||||
@@ -79,8 +81,8 @@ pub fn onToneSweepEvent(self: *Self, late: u64) void {
|
||||
|
||||
/// NR10, NR11, NR12
|
||||
pub fn setSound1Cnt(self: *Self, value: u32) void {
|
||||
self.setSound1CntL(@truncate(u8, value));
|
||||
self.setSound1CntH(@truncate(u16, value >> 16));
|
||||
self.setSound1CntL(@truncate(value));
|
||||
self.setSound1CntH(@truncate(value >> 16));
|
||||
}
|
||||
|
||||
/// NR10
|
||||
@@ -92,10 +94,9 @@ pub fn sound1CntL(self: *const Self) u8 {
|
||||
pub fn setSound1CntL(self: *Self, value: u8) void {
|
||||
const new = io.Sweep{ .raw = value };
|
||||
|
||||
if (self.sweep.direction.read() and !new.direction.read()) {
|
||||
// Sweep Negate bit has been cleared
|
||||
// If At least 1 Sweep Calculation has been made since
|
||||
// the last trigger, the channel is immediately disabled
|
||||
if (!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;
|
||||
}
|
||||
@@ -110,14 +111,14 @@ pub fn sound1CntH(self: *const Self) u16 {
|
||||
|
||||
/// NR11, NR12
|
||||
pub fn setSound1CntH(self: *Self, value: u16) void {
|
||||
self.setNr11(@truncate(u8, value));
|
||||
self.setNr12(@truncate(u8, value >> 8));
|
||||
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) - @truncate(u6, value);
|
||||
self.len_dev.timer = @as(u7, 64) - @as(u6, @truncate(value));
|
||||
}
|
||||
|
||||
/// NR12
|
||||
@@ -133,8 +134,8 @@ pub fn sound1CntX(self: *const Self) u16 {
|
||||
|
||||
/// NR13, NR14
|
||||
pub fn setSound1CntX(self: *Self, fs: *const FrameSequencer, value: u16) void {
|
||||
self.setNr13(@truncate(u8, value));
|
||||
self.setNr14(fs, @truncate(u8, value >> 8));
|
||||
self.setNr13(@truncate(value));
|
||||
self.setNr14(fs, @truncate(value >> 8));
|
||||
}
|
||||
|
||||
/// NR13
|
||||
|
@@ -42,10 +42,13 @@ pub fn init(sched: *Scheduler) Self {
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
self.select.raw = 0;
|
||||
self.length = 0;
|
||||
self.vol.raw = 0;
|
||||
self.freq.raw = 0;
|
||||
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;
|
||||
@@ -61,8 +64,8 @@ pub fn tick(self: *Self, comptime kind: Tick) void {
|
||||
|
||||
/// NR30, NR31, NR32
|
||||
pub fn setSound3Cnt(self: *Self, value: u32) void {
|
||||
self.setSound3CntL(@truncate(u8, value));
|
||||
self.setSound3CntH(@truncate(u16, value >> 16));
|
||||
self.setSound3CntL(@truncate(value));
|
||||
self.setSound3CntH(@truncate(value >> 16));
|
||||
}
|
||||
|
||||
/// NR30
|
||||
@@ -83,8 +86,8 @@ pub fn sound3CntH(self: *const Self) u16 {
|
||||
|
||||
/// NR31, NR32
|
||||
pub fn setSound3CntH(self: *Self, value: u16) void {
|
||||
self.setNr31(@truncate(u8, value));
|
||||
self.vol.raw = (@truncate(u8, value >> 8));
|
||||
self.setNr31(@truncate(value));
|
||||
self.vol.raw = @truncate(value >> 8);
|
||||
}
|
||||
|
||||
/// NR31
|
||||
@@ -95,8 +98,8 @@ pub fn setNr31(self: *Self, len: u8) void {
|
||||
|
||||
/// NR33, NR34
|
||||
pub fn setSound3CntX(self: *Self, fs: *const FrameSequencer, value: u16) void {
|
||||
self.setNr33(@truncate(u8, value));
|
||||
self.setNr34(fs, @truncate(u8, value >> 8));
|
||||
self.setNr33(@truncate(value));
|
||||
self.setNr34(fs, @truncate(value >> 8));
|
||||
}
|
||||
|
||||
/// NR33, NR34
|
||||
|
@@ -3,12 +3,16 @@ const io = @import("../../bus/io.zig");
|
||||
const Self = @This();
|
||||
|
||||
/// Period Timer
|
||||
timer: u3,
|
||||
timer: u3 = 0,
|
||||
/// Current Volume
|
||||
vol: u4,
|
||||
vol: u4 = 0,
|
||||
|
||||
pub fn create() Self {
|
||||
return .{ .timer = 0, .vol = 0 };
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
self.* = .{};
|
||||
}
|
||||
|
||||
pub fn tick(self: *Self, nrx2: io.Envelope) void {
|
||||
|
@@ -1,9 +1,13 @@
|
||||
const Self = @This();
|
||||
|
||||
timer: u9,
|
||||
timer: u9 = 0,
|
||||
|
||||
pub fn create() Self {
|
||||
return .{ .timer = 0 };
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
self.* = .{};
|
||||
}
|
||||
|
||||
pub fn tick(self: *Self, enabled: bool, ch_enable: *bool) void {
|
||||
|
@@ -3,19 +3,18 @@ const ToneSweep = @import("../ToneSweep.zig");
|
||||
|
||||
const Self = @This();
|
||||
|
||||
timer: u8,
|
||||
enabled: bool,
|
||||
shadow: u11,
|
||||
timer: u8 = 0,
|
||||
enabled: bool = false,
|
||||
shadow: u11 = 0,
|
||||
|
||||
calc_performed: bool,
|
||||
calc_performed: bool = false,
|
||||
|
||||
pub fn create() Self {
|
||||
return .{
|
||||
.timer = 0,
|
||||
.enabled = false,
|
||||
.shadow = 0,
|
||||
.calc_performed = false,
|
||||
};
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
self.* = .{};
|
||||
}
|
||||
|
||||
pub fn tick(self: *Self, ch1: *ToneSweep) void {
|
||||
@@ -24,14 +23,13 @@ pub fn tick(self: *Self, ch1: *ToneSweep) void {
|
||||
if (self.timer == 0) {
|
||||
const period = ch1.sweep.period.read();
|
||||
self.timer = if (period == 0) 8 else period;
|
||||
if (!self.calc_performed) self.calc_performed = true;
|
||||
|
||||
if (self.enabled and period != 0) {
|
||||
const new_freq = self.calculate(ch1.sweep, &ch1.enabled);
|
||||
|
||||
if (new_freq <= 0x7FF and ch1.sweep.shift.read() != 0) {
|
||||
ch1.freq.frequency.write(@truncate(u11, new_freq));
|
||||
self.shadow = @truncate(u11, new_freq);
|
||||
ch1.freq.frequency.write(@as(u11, @truncate(new_freq)));
|
||||
self.shadow = @truncate(new_freq);
|
||||
|
||||
_ = self.calculate(ch1.sweep, &ch1.enabled);
|
||||
}
|
||||
@@ -45,7 +43,10 @@ pub fn calculate(self: *Self, sweep: io.Sweep, ch_enable: *bool) u12 {
|
||||
const shadow_shifted = shadow >> sweep.shift.read();
|
||||
const decrease = sweep.direction.read();
|
||||
|
||||
const freq = if (decrease) shadow - shadow_shifted else shadow + shadow_shifted;
|
||||
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;
|
||||
|
@@ -19,6 +19,11 @@ pub fn create(sched: *Scheduler) Self {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
self.shift = 0;
|
||||
self.timer = 0;
|
||||
}
|
||||
|
||||
pub fn sample(self: *const Self) i8 {
|
||||
return if ((~self.shift & 1) == 1) 1 else -1;
|
||||
}
|
||||
@@ -33,7 +38,7 @@ pub fn reload(self: *Self, poly: io.PolyCounter) void {
|
||||
}
|
||||
|
||||
/// Scheduler Event Handler for LFSR Timer Expire
|
||||
/// FIXME: This gets called a lot, clogging up the Scheduler
|
||||
/// 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."
|
||||
|
@@ -20,6 +20,11 @@ pub fn init(sched: *Scheduler) Self {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
self.timer = 0;
|
||||
self.pos = 0;
|
||||
}
|
||||
|
||||
/// Scheduler Event Handler for Square Synth Timer Expire
|
||||
pub fn onSquareTimerExpire(self: *Self, comptime T: type, nrx34: io.Frequency, late: u64) void {
|
||||
comptime std.debug.assert(T == ToneSweep or T == Tone);
|
||||
|
@@ -18,7 +18,7 @@ pub fn read(self: *const Self, comptime T: type, nr30: io.WaveSelect, addr: u32)
|
||||
const base = if (!nr30.bank.read()) @as(u32, 0x10) else 0; // Read from the Opposite Bank in Use
|
||||
|
||||
const i = base + addr - 0x0400_0090;
|
||||
return std.mem.readIntSliceLittle(T, self.buf[i..][0..@sizeOf(T)]);
|
||||
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 {
|
||||
@@ -26,7 +26,7 @@ pub fn write(self: *Self, comptime T: type, nr30: io.WaveSelect, addr: u32, valu
|
||||
const base = if (!nr30.bank.read()) @as(u32, 0x10) else 0; // Write to the Opposite Bank in Use
|
||||
|
||||
const i = base + addr - 0x0400_0090;
|
||||
std.mem.writeIntSliceLittle(T, self.buf[i..][0..@sizeOf(T)], value);
|
||||
std.mem.writeInt(T, self.buf[i..][0..@sizeOf(T)], value, .little);
|
||||
}
|
||||
|
||||
pub fn init(sched: *Scheduler) Self {
|
||||
@@ -38,6 +38,13 @@ pub fn init(sched: *Scheduler) Self {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
self.timer = 0;
|
||||
self.offset = 0;
|
||||
|
||||
// sample buffer isn't reset because it's outside of the range of what NR52{7}'s effects
|
||||
}
|
||||
|
||||
/// Reload internal Wave Timer
|
||||
pub fn reload(self: *Self, value: u11) void {
|
||||
self.sched.removeScheduledEvent(.{ .ApuChannel = 2 });
|
||||
@@ -63,7 +70,7 @@ pub fn sample(self: *const Self, nr30: io.WaveSelect) u4 {
|
||||
const base = if (nr30.bank.read()) @as(u32, 0x10) else 0;
|
||||
|
||||
const value = self.buf[base + self.offset / 2];
|
||||
return if (self.offset & 1 == 0) @truncate(u4, value >> 4) else @truncate(u4, value);
|
||||
return if (self.offset & 1 == 0) @truncate(value >> 4) else @truncate(value);
|
||||
}
|
||||
|
||||
/// TODO: Write comment
|
||||
|
@@ -3,6 +3,9 @@ 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();
|
||||
@@ -10,21 +13,37 @@ const Self = @This();
|
||||
buf: ?[]u8,
|
||||
allocator: Allocator,
|
||||
|
||||
addr_latch: u32,
|
||||
addr_latch: u32 = 0,
|
||||
|
||||
pub fn read(self: *Self, comptime T: type, r15: u32, addr: u32) T {
|
||||
// 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.debug("Rejected read since r15=0x{X:0>8}", .{r15});
|
||||
return @truncate(T, self._read(T, self.addr_latch));
|
||||
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)));
|
||||
}
|
||||
|
||||
pub fn dbgRead(self: *const Self, comptime T: type, r15: u32, addr: u32) T {
|
||||
if (r15 < Self.size) return self._read(T, addr);
|
||||
return @truncate(T, self._read(T, self.addr_latch + 8));
|
||||
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
|
||||
@@ -32,7 +51,7 @@ fn _read(self: *const Self, comptime T: type, addr: u32) T {
|
||||
const buf = self.buf orelse std.debug.panic("[BIOS] ZBA tried to read {} from 0x{X:0>8} but not BIOS was present", .{ T, addr });
|
||||
|
||||
return switch (T) {
|
||||
u32, u16, u8 => std.mem.readIntSliceLittle(T, buf[addr..][0..@sizeOf(T)]),
|
||||
u32, u16, u8 => std.mem.readInt(T, buf[addr..][0..@sizeOf(T)], .little),
|
||||
else => @compileError("BIOS: Unsupported read width"),
|
||||
};
|
||||
}
|
||||
@@ -43,18 +62,28 @@ pub fn write(_: *Self, comptime T: type, addr: u32, value: T) void {
|
||||
}
|
||||
|
||||
pub fn init(allocator: Allocator, maybe_path: ?[]const u8) !Self {
|
||||
const buf: ?[]u8 = if (maybe_path) |path| blk: {
|
||||
const file = try std.fs.cwd().openFile(path, .{});
|
||||
if (maybe_path == null) return .{ .buf = null, .allocator = allocator };
|
||||
const file_path = maybe_path.?;
|
||||
|
||||
const buf = try allocator.alloc(u8, Self.size);
|
||||
errdefer allocator.free(buf);
|
||||
|
||||
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();
|
||||
|
||||
break :blk try file.readToEndAlloc(allocator, try file.getEndPos());
|
||||
} else null;
|
||||
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 });
|
||||
}
|
||||
|
||||
return Self{
|
||||
.buf = buf,
|
||||
.allocator = allocator,
|
||||
.addr_latch = 0,
|
||||
};
|
||||
pub fn reset(self: *Self) void {
|
||||
self.addr_latch = 0;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
|
@@ -11,7 +11,7 @@ pub fn read(self: *const Self, comptime T: type, address: usize) T {
|
||||
const addr = address & 0x3FFFF;
|
||||
|
||||
return switch (T) {
|
||||
u32, u16, u8 => std.mem.readIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)]),
|
||||
u32, u16, u8 => std.mem.readInt(T, self.buf[addr..][0..@sizeOf(T)], .little),
|
||||
else => @compileError("EWRAM: Unsupported read width"),
|
||||
};
|
||||
}
|
||||
@@ -20,14 +20,14 @@ 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.writeIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)], value),
|
||||
u32, u16, u8 => std.mem.writeInt(T, self.buf[addr..][0..@sizeOf(T)], value, .little),
|
||||
else => @compileError("EWRAM: Unsupported write width"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn init(allocator: Allocator) !Self {
|
||||
const buf = try allocator.alloc(u8, ewram_size);
|
||||
std.mem.set(u8, buf, 0);
|
||||
@memset(buf, 0);
|
||||
|
||||
return Self{
|
||||
.buf = buf,
|
||||
@@ -35,6 +35,10 @@ pub fn init(allocator: Allocator) !Self {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
@memset(self.buf, 0);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.allocator.free(self.buf);
|
||||
self.* = undefined;
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const std = @import("std");
|
||||
const config = @import("../../config.zig");
|
||||
|
||||
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||
const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||
const Backup = @import("backup.zig").Backup;
|
||||
const Gpio = @import("gpio.zig").Gpio;
|
||||
const Allocator = std.mem.Allocator;
|
||||
@@ -30,7 +30,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 (@truncate(u8, address >> 24) == 0x0D)
|
||||
if (@as(u8, @truncate(address >> 24)) == 0x0D)
|
||||
return self.backup.eeprom.read();
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ inline fn get(self: *const Self, i: u32) u8 {
|
||||
if (i < self.buf.len) return self.buf[i];
|
||||
|
||||
const lhs = i >> 1 & 0xFFFF;
|
||||
return @truncate(u8, lhs >> 8 * @truncate(u5, i & 1));
|
||||
return @truncate(lhs >> 8 * @as(u5, @truncate(i & 1)));
|
||||
}
|
||||
|
||||
pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
|
||||
@@ -94,7 +94,7 @@ 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 (@truncate(u8, address >> 24) == 0x0D)
|
||||
if (@as(u8, @truncate(address >> 24)) == 0x0D)
|
||||
return self.backup.eeprom.dbgRead();
|
||||
}
|
||||
}
|
||||
@@ -105,14 +105,13 @@ pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
|
||||
|
||||
switch (T) {
|
||||
u32 => switch (address) {
|
||||
// TODO: Do I even need to implement these?
|
||||
// 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) {
|
||||
// FIXME: What do 16-bit GPIO Reads look like?
|
||||
0x0800_00C4 => return self.gpio.read(.Data),
|
||||
0x0800_00C6 => return self.gpio.read(.Direction),
|
||||
0x0800_00C8 => return self.gpio.read(.Control),
|
||||
@@ -140,7 +139,7 @@ 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 = @truncate(u1, value);
|
||||
const bit: u1 = @truncate(value);
|
||||
|
||||
if (self.buf.len > 0x100_0000) { // Large
|
||||
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
|
||||
@@ -152,7 +151,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 (@truncate(u8, address >> 24) == 0x0D)
|
||||
if (@as(u8, @truncate(address >> 24)) == 0x0D)
|
||||
return self.backup.eeprom.write(word_count, &self.backup.buf, bit);
|
||||
}
|
||||
}
|
||||
@@ -160,19 +159,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, @truncate(u4, value));
|
||||
self.gpio.write(.Direction, @truncate(u4, value >> 16));
|
||||
self.gpio.write(.Data, @as(u4, @truncate(value)));
|
||||
self.gpio.write(.Direction, @as(u4, @truncate(value >> 16)));
|
||||
},
|
||||
0x0800_00C6 => {
|
||||
self.gpio.write(.Direction, @truncate(u4, value));
|
||||
self.gpio.write(.Control, @truncate(u1, value >> 16));
|
||||
self.gpio.write(.Direction, @as(u4, @truncate(value)));
|
||||
self.gpio.write(.Control, @as(u1, @truncate(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, @truncate(u4, value)),
|
||||
0x0800_00C6 => self.gpio.write(.Direction, @truncate(u4, value)),
|
||||
0x0800_00C8 => self.gpio.write(.Control, @truncate(u1, value)),
|
||||
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))),
|
||||
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 }),
|
||||
@@ -180,23 +179,30 @@ pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, rom_path: []const u8, save_path: ?[]const u8) !Self {
|
||||
const file = try std.fs.cwd().openFile(rom_path, .{});
|
||||
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, maybe_rom: ?[]const u8, maybe_save: ?[]const u8) !Self {
|
||||
const Device = Gpio.Device;
|
||||
|
||||
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 file_buf = try file.readToEndAlloc(allocator, try file.getEndPos());
|
||||
const title = file_buf[0xA0..0xAC].*;
|
||||
const kind = Backup.guess(file_buf);
|
||||
const device = if (config.config().guest.force_rtc) .Rtc else guessDevice(file_buf);
|
||||
const buffer = try file.readToEndAlloc(allocator, try file.getEndPos());
|
||||
const title = buffer[0xA0..0xAC];
|
||||
logHeader(buffer, title);
|
||||
|
||||
logHeader(file_buf, &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 = file_buf,
|
||||
.buf = items[0],
|
||||
.allocator = allocator,
|
||||
.title = title,
|
||||
.backup = try Backup.init(allocator, kind, title, save_path),
|
||||
.gpio = try Gpio.init(allocator, cpu, device),
|
||||
.backup = try Backup.init(allocator, items[2], title, maybe_save),
|
||||
.gpio = try Gpio.init(allocator, cpu, items[3]),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -214,25 +220,24 @@ 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 code = buf[0xAC..0xB0];
|
||||
const maker = buf[0xB0..0xB2];
|
||||
const version = buf[0xBC];
|
||||
|
||||
log.info("Title: {s}", .{title});
|
||||
if (version != 0) log.info("Version: {}", .{version});
|
||||
log.info("Game Code: {s}", .{code});
|
||||
log.info("Maker Code: {s}", .{maker});
|
||||
|
||||
log.info("Game Code: {s}", .{buf[0xAC..0xB0]});
|
||||
log.info("Maker Code: {s}", .{buf[0xB0..0xB2]});
|
||||
}
|
||||
|
||||
test "OOB Access" {
|
||||
|
@@ -11,7 +11,7 @@ pub fn read(self: *const Self, comptime T: type, address: usize) T {
|
||||
const addr = address & 0x7FFF;
|
||||
|
||||
return switch (T) {
|
||||
u32, u16, u8 => std.mem.readIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)]),
|
||||
u32, u16, u8 => std.mem.readInt(T, self.buf[addr..][0..@sizeOf(T)], .little),
|
||||
else => @compileError("IWRAM: Unsupported read width"),
|
||||
};
|
||||
}
|
||||
@@ -20,14 +20,14 @@ 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.writeIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)], value),
|
||||
u32, u16, u8 => std.mem.writeInt(T, self.buf[addr..][0..@sizeOf(T)], value, .little),
|
||||
else => @compileError("IWRAM: Unsupported write width"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn init(allocator: Allocator) !Self {
|
||||
const buf = try allocator.alloc(u8, iwram_size);
|
||||
std.mem.set(u8, buf, 0);
|
||||
@memset(buf, 0);
|
||||
|
||||
return Self{
|
||||
.buf = buf,
|
||||
@@ -35,6 +35,10 @@ pub fn init(allocator: Allocator) !Self {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
@memset(self.buf, 0);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.allocator.free(self.buf);
|
||||
self.* = undefined;
|
||||
|
@@ -6,7 +6,6 @@ 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{
|
||||
@@ -33,7 +32,7 @@ pub const Backup = struct {
|
||||
flash: Flash,
|
||||
eeprom: Eeprom,
|
||||
|
||||
const Kind = enum {
|
||||
pub const Kind = enum {
|
||||
Eeprom,
|
||||
Sram,
|
||||
Flash,
|
||||
@@ -78,7 +77,7 @@ pub const Backup = struct {
|
||||
|
||||
switch (addr) {
|
||||
0x0000 => if (self.kind == .Flash1M and self.flash.set_bank) {
|
||||
self.flash.bank = @truncate(u1, byte);
|
||||
self.flash.bank = @truncate(byte);
|
||||
},
|
||||
0x5555 => {
|
||||
if (self.flash.state == .Command) {
|
||||
@@ -111,7 +110,7 @@ pub const Backup = struct {
|
||||
};
|
||||
|
||||
const buf = try allocator.alloc(u8, buf_size);
|
||||
std.mem.set(u8, buf, 0xFF);
|
||||
@memset(buf, 0xFF);
|
||||
|
||||
var backup = Self{
|
||||
.buf = buf,
|
||||
@@ -138,6 +137,7 @@ pub const Backup = struct {
|
||||
for (backup_kinds) |needle| {
|
||||
const needle_len = needle.str.len;
|
||||
|
||||
// TODO: Use new for loop syntax?
|
||||
var i: usize = 0;
|
||||
while ((i + needle_len) < rom.len) : (i += 1) {
|
||||
if (std.mem.eql(u8, needle.str, rom[i..][0..needle_len])) return needle.kind;
|
||||
@@ -151,8 +151,8 @@ pub const Backup = struct {
|
||||
const file_path = try self.savePath(allocator, path);
|
||||
defer allocator.free(file_path);
|
||||
|
||||
// FIXME: Don't rely on this lol
|
||||
if (std.mem.eql(u8, file_path[file_path.len - 12 .. file_path.len], "untitled.sav")) {
|
||||
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", .{});
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ pub const Backup = struct {
|
||||
switch (self.kind) {
|
||||
.Sram, .Flash, .Flash1M => {
|
||||
if (self.buf.len == file_buf.len) {
|
||||
std.mem.copy(u8, self.buf, file_buf);
|
||||
@memcpy(self.buf, file_buf);
|
||||
return log.info("Loaded Save from {s}", .{file_path});
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ pub const Backup = struct {
|
||||
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);
|
||||
@memcpy(self.buf, file_buf);
|
||||
return log.info("Loaded Save from {s}", .{file_path});
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ pub const Backup = struct {
|
||||
}
|
||||
|
||||
fn saveName(self: *const Self, allocator: Allocator) ![]const u8 {
|
||||
const title_str = span(&escape(self.title));
|
||||
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" });
|
||||
|
@@ -44,7 +44,7 @@ pub fn handleCommand(self: *Self, buf: []u8, byte: u8) void {
|
||||
0xB0 => self.set_bank = true,
|
||||
0x80 => self.prep_erase = true,
|
||||
0x10 => {
|
||||
std.mem.set(u8, buf, 0xFF);
|
||||
@memset(buf, 0xFF);
|
||||
self.prep_erase = false;
|
||||
},
|
||||
0xA0 => self.prep_write = true,
|
||||
@@ -61,7 +61,7 @@ pub fn shouldEraseSector(self: *const Self, addr: usize, byte: u8) bool {
|
||||
pub fn erase(self: *Self, buf: []u8, sector: usize) void {
|
||||
const start = self.address() + (sector & 0xF000);
|
||||
|
||||
std.mem.set(u8, buf[start..][0..0x1000], 0xFF);
|
||||
@memset(buf[start..][0..0x1000], 0xFF);
|
||||
self.prep_erase = false;
|
||||
self.state = .Ready;
|
||||
}
|
||||
|
@@ -58,12 +58,14 @@ pub const Eeprom = struct {
|
||||
log.err("Failed to resize EEPROM buf to {} bytes", .{len});
|
||||
std.debug.panic("EEPROM entered irrecoverable state {}", .{e});
|
||||
};
|
||||
std.mem.set(u8, buf.*, 0xFF);
|
||||
|
||||
// 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?", .{});
|
||||
// if (bit != 0) log.debug("EEPROM Request did not end in 0u1. TODO: is this ok?", .{});
|
||||
self.state = .Ready;
|
||||
return;
|
||||
}
|
||||
@@ -106,7 +108,7 @@ pub const Eeprom = struct {
|
||||
switch (self.state) {
|
||||
.Ready => {
|
||||
if (self.writer.len() == 2) {
|
||||
const req = @intCast(u2, self.writer.finish());
|
||||
const req: u2 = @intCast(self.writer.finish());
|
||||
switch (req) {
|
||||
0b11 => self.state = .Read,
|
||||
0b10 => self.state = .Write,
|
||||
@@ -118,8 +120,8 @@ pub const Eeprom = struct {
|
||||
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]);
|
||||
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;
|
||||
@@ -128,8 +130,8 @@ pub const Eeprom = struct {
|
||||
.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]);
|
||||
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;
|
||||
@@ -142,13 +144,13 @@ pub const Eeprom = struct {
|
||||
switch (self.kind) {
|
||||
.Large => {
|
||||
if (self.writer.len() == 14) {
|
||||
self.addr = @intCast(u10, self.writer.finish());
|
||||
self.addr = @as(u10, @intCast(self.writer.finish()));
|
||||
self.state = .WriteTransfer;
|
||||
}
|
||||
},
|
||||
.Small => {
|
||||
if (self.writer.len() == 6) {
|
||||
self.addr = @intCast(u6, self.writer.finish());
|
||||
self.addr = @as(u6, @intCast(self.writer.finish()));
|
||||
self.state = .WriteTransfer;
|
||||
}
|
||||
},
|
||||
@@ -157,7 +159,7 @@ pub const Eeprom = struct {
|
||||
},
|
||||
.WriteTransfer => {
|
||||
if (self.writer.len() == 64) {
|
||||
std.mem.writeIntSliceLittle(u64, buf[self.addr * 8 ..][0..8], self.writer.finish());
|
||||
std.mem.writeInt(u64, buf[self.addr * 8 ..][0..8], self.writer.finish(), .little);
|
||||
self.state = .RequestEnd;
|
||||
}
|
||||
},
|
||||
@@ -184,11 +186,9 @@ const Reader = struct {
|
||||
fn read(self: *Self) u1 {
|
||||
if (!self.enabled) return 1;
|
||||
|
||||
const bit = if (self.i < 4) blk: {
|
||||
break :blk 0;
|
||||
} else blk: {
|
||||
const idx = @intCast(u6, 63 - (self.i - 4));
|
||||
break :blk @truncate(u1, self.data >> idx);
|
||||
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);
|
||||
@@ -200,11 +200,11 @@ const Reader = struct {
|
||||
fn dbgRead(self: *const Self) u1 {
|
||||
if (!self.enabled) return 1;
|
||||
|
||||
const bit = if (self.i < 4) blk: {
|
||||
const bit: u1 = 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);
|
||||
const idx: u6 = @intCast(63 - (self.i - 4));
|
||||
break :blk @truncate(self.data >> idx);
|
||||
};
|
||||
|
||||
return bit;
|
||||
@@ -228,7 +228,7 @@ const Writer = struct {
|
||||
}
|
||||
|
||||
fn requestWrite(self: *Self, bit: u1) void {
|
||||
const idx = @intCast(u1, 1 - self.i);
|
||||
const idx: u1 = @intCast(1 - self.i);
|
||||
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
|
||||
self.i += 1;
|
||||
}
|
||||
@@ -242,13 +242,13 @@ const Writer = struct {
|
||||
.Unknown => unreachable,
|
||||
};
|
||||
|
||||
const idx = @intCast(u4, size - self.i);
|
||||
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 = @intCast(u6, 63 - self.i);
|
||||
const idx: u6 = @intCast(63 - self.i);
|
||||
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
|
||||
self.i += 1;
|
||||
}
|
||||
|
@@ -3,23 +3,24 @@ const util = @import("../../util.zig");
|
||||
|
||||
const DmaControl = @import("io.zig").DmaControl;
|
||||
const Bus = @import("../Bus.zig");
|
||||
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||
const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||
|
||||
pub const DmaTuple = std.meta.Tuple(&[_]type{ DmaController(0), DmaController(1), DmaController(2), DmaController(3) });
|
||||
pub const DmaTuple = struct { 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("../../util.zig").rotr;
|
||||
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 = @truncate(u8, addr);
|
||||
const byte_addr: u8 = @truncate(addr);
|
||||
|
||||
return switch (T) {
|
||||
u32 => switch (byte_addr) {
|
||||
@@ -54,19 +55,19 @@ pub fn read(comptime T: type, dma: *const DmaTuple, addr: u32) ?T {
|
||||
u8 => switch (byte_addr) {
|
||||
0xB0...0xB7 => null, // DMA0SAD, DMA0DAD
|
||||
0xB8, 0xB9 => 0x00, // DMA0CNT_L
|
||||
0xBA, 0xBB => @truncate(T, dma.*[0].dmacntH() >> getHalf(byte_addr)),
|
||||
0xBA, 0xBB => @truncate(dma.*[0].dmacntH() >> getHalf(byte_addr)),
|
||||
|
||||
0xBC...0xC3 => null, // DMA1SAD, DMA1DAD
|
||||
0xC4, 0xC5 => 0x00, // DMA1CNT_L
|
||||
0xC6, 0xC7 => @truncate(T, dma.*[1].dmacntH() >> getHalf(byte_addr)),
|
||||
0xC6, 0xC7 => @truncate(dma.*[1].dmacntH() >> getHalf(byte_addr)),
|
||||
|
||||
0xC8...0xCF => null, // DMA2SAD, DMA2DAD
|
||||
0xD0, 0xD1 => 0x00, // DMA2CNT_L
|
||||
0xD2, 0xD3 => @truncate(T, dma.*[2].dmacntH() >> getHalf(byte_addr)),
|
||||
0xD2, 0xD3 => @truncate(dma.*[2].dmacntH() >> getHalf(byte_addr)),
|
||||
|
||||
0xD4...0xDB => null, // DMA3SAD, DMA3DAD
|
||||
0xDC, 0xDD => 0x00, // DMA3CNT_L
|
||||
0xDE, 0xDF => @truncate(T, dma.*[3].dmacntH() >> getHalf(byte_addr)),
|
||||
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 }),
|
||||
},
|
||||
else => @compileError("DMA: Unsupported read width"),
|
||||
@@ -74,7 +75,7 @@ pub fn read(comptime T: type, dma: *const DmaTuple, addr: u32) ?T {
|
||||
}
|
||||
|
||||
pub fn write(comptime T: type, dma: *DmaTuple, addr: u32, value: T) void {
|
||||
const byte_addr = @truncate(u8, addr);
|
||||
const byte_addr: u8 = @truncate(addr);
|
||||
|
||||
switch (T) {
|
||||
u32 => switch (byte_addr) {
|
||||
@@ -195,6 +196,10 @@ fn DmaController(comptime id: u2) type {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
self.* = Self.init();
|
||||
}
|
||||
|
||||
pub fn setDmasad(self: *Self, addr: u32) void {
|
||||
self.sad = addr & sad_mask;
|
||||
}
|
||||
@@ -204,7 +209,7 @@ fn DmaController(comptime id: u2) type {
|
||||
}
|
||||
|
||||
pub fn setDmacntL(self: *Self, halfword: u16) void {
|
||||
self.word_count = @truncate(@TypeOf(self.word_count), halfword);
|
||||
self.word_count = @truncate(halfword);
|
||||
}
|
||||
|
||||
pub fn dmacntH(self: *const Self) u16 {
|
||||
@@ -228,14 +233,16 @@ fn DmaController(comptime id: u2) type {
|
||||
}
|
||||
|
||||
pub fn setDmacnt(self: *Self, word: u32) void {
|
||||
self.setDmacntL(@truncate(u16, word));
|
||||
self.setDmacntH(@truncate(u16, word >> 16));
|
||||
self.setDmacntL(@truncate(word));
|
||||
self.setDmacntH(@truncate(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 = @intToEnum(Adjustment, self.cnt.sad_adj.read());
|
||||
const dad_adj = if (is_fifo) .Fixed else @intToEnum(Adjustment, self.cnt.dad_adj.read());
|
||||
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 transfer_type = is_fifo or self.cnt.transfer_type.read();
|
||||
const offset: u32 = if (transfer_type) @sizeOf(u32) else @sizeOf(u16);
|
||||
@@ -253,15 +260,19 @@ fn DmaController(comptime id: u2) type {
|
||||
self.data_latch = value << 16 | value;
|
||||
}
|
||||
|
||||
cpu.bus.write(u16, dad_addr, @truncate(u16, rotr(u32, self.data_latch, 8 * (dad_addr & 3))));
|
||||
cpu.bus.write(u16, dad_addr, @as(u16, @truncate(rotr(u32, self.data_latch, 8 * (dad_addr & 3)))));
|
||||
}
|
||||
|
||||
switch (sad_adj) {
|
||||
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,
|
||||
// FIXME: Is just ignoring this ok?
|
||||
.IncrementReload => log.err("{} is a prohibited adjustment on SAD", .{sad_adj}),
|
||||
.Fixed => {},
|
||||
},
|
||||
}
|
||||
|
||||
switch (dad_adj) {
|
||||
@@ -275,13 +286,13 @@ fn DmaController(comptime id: u2) type {
|
||||
if (self._word_count == 0) {
|
||||
if (self.cnt.irq.read()) {
|
||||
switch (id) {
|
||||
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(),
|
||||
0 => bus_ptr.io.irq.dma0.set(),
|
||||
1 => bus_ptr.io.irq.dma1.set(),
|
||||
2 => bus_ptr.io.irq.dma2.set(),
|
||||
3 => bus_ptr.io.irq.dma3.set(),
|
||||
}
|
||||
|
||||
cpu.handleInterrupt();
|
||||
handleInterrupt(cpu);
|
||||
}
|
||||
|
||||
// If we're not repeating, Fire the IRQs and disable the DMA
|
||||
@@ -310,7 +321,7 @@ 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 (@intToEnum(Adjustment, self.cnt.dad_adj.read()) == .IncrementReload) self.dad_latch = self.dad;
|
||||
if (@as(Adjustment, @enumFromInt(self.cnt.dad_adj.read())) == .IncrementReload) self.dad_latch = self.dad;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,11 +345,8 @@ fn DmaController(comptime id: u2) type {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn pollDmaOnBlank(bus: *Bus, comptime kind: DmaKind) void {
|
||||
bus.dma[0].poll(kind);
|
||||
bus.dma[1].poll(kind);
|
||||
bus.dma[2].poll(kind);
|
||||
bus.dma[3].poll(kind);
|
||||
pub fn onBlanking(bus: *Bus, comptime kind: DmaKind) void {
|
||||
inline for (0..4) |i| bus.dma[i].poll(kind);
|
||||
}
|
||||
|
||||
const Adjustment = enum(u2) {
|
||||
|
@@ -2,9 +2,13 @@ const std = @import("std");
|
||||
const Bit = @import("bitfield").Bit;
|
||||
const DateTime = @import("datetime").datetime.Datetime;
|
||||
|
||||
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||
const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||
const Bus = @import("../Bus.zig");
|
||||
const Scheduler = @import("../scheduler.zig").Scheduler;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const handleInterrupt = @import("../cpu_util.zig").handleInterrupt;
|
||||
|
||||
/// GPIO Register Implementation
|
||||
pub const Gpio = struct {
|
||||
const Self = @This();
|
||||
@@ -27,8 +31,8 @@ pub const Gpio = struct {
|
||||
fn step(self: *Device, value: u4) u4 {
|
||||
return switch (self.kind) {
|
||||
.Rtc => blk: {
|
||||
const clock = @ptrCast(*Clock, @alignCast(@alignOf(*Clock), self.ptr.?));
|
||||
break :blk clock.step(Clock.Data{ .raw = value });
|
||||
const clock: *Clock = @ptrCast(@alignCast(self.ptr.?));
|
||||
break :blk clock.step(.{ .raw = value });
|
||||
},
|
||||
.None => value,
|
||||
};
|
||||
@@ -71,7 +75,7 @@ pub const Gpio = struct {
|
||||
|
||||
self.* = .{
|
||||
.data = 0b0000,
|
||||
.direction = 0b1111, // TODO: What is GPIO DIrection set to by default?
|
||||
.direction = 0b1111, // TODO: What is GPIO Direction set to by default?
|
||||
.cnt = 0b0,
|
||||
|
||||
.device = switch (kind) {
|
||||
@@ -90,7 +94,7 @@ pub const Gpio = struct {
|
||||
|
||||
pub fn deinit(self: *Self, allocator: Allocator) void {
|
||||
switch (self.device.kind) {
|
||||
.Rtc => allocator.destroy(@ptrCast(*Clock, @alignCast(@alignOf(*Clock), self.device.ptr.?))),
|
||||
.Rtc => allocator.destroy(@as(*Clock, @ptrCast(@alignCast(self.device.ptr.?)))),
|
||||
.None => {},
|
||||
}
|
||||
|
||||
@@ -142,16 +146,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 = @intCast(u3, self.i);
|
||||
const idx: u3 = @intCast(self.i);
|
||||
defer self.i += 1;
|
||||
|
||||
// FIXME: What do I do about the unused bits?
|
||||
return switch (register) {
|
||||
.Control => @truncate(u1, switch (self.count) {
|
||||
.Control => @truncate(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(u1, switch (self.count) {
|
||||
.DateTime => @truncate(switch (self.count) {
|
||||
// Date
|
||||
0 => clock.year >> idx,
|
||||
1 => @as(u8, clock.month) >> idx,
|
||||
@@ -164,7 +168,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(u1, switch (self.count) {
|
||||
.Time => @truncate(switch (self.count) {
|
||||
0 => @as(u8, clock.hour) >> idx,
|
||||
1 => @as(u8, clock.minute) >> idx,
|
||||
2 => @as(u8, clock.second) >> idx,
|
||||
@@ -203,7 +207,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 = @intCast(u3, self.i);
|
||||
const idx: u3 = @intCast(self.i);
|
||||
self.buf = (self.buf & ~(@as(u8, 1) << idx)) | @as(u8, value) << idx;
|
||||
self.i += 1;
|
||||
}
|
||||
@@ -286,20 +290,22 @@ pub const Clock = struct {
|
||||
.gpio = gpio, // Can't use Arm7tdmi ptr b/c not initialized yet
|
||||
};
|
||||
|
||||
cpu.sched.push(.RealTimeClock, 1 << 24); // Every Second
|
||||
const sched_ptr: *Scheduler = @ptrCast(@alignCast(cpu.sched.ptr));
|
||||
sched_ptr.push(.RealTimeClock, 1 << 24); // Every Second
|
||||
}
|
||||
|
||||
pub fn onClockUpdate(self: *Self, late: u64) void {
|
||||
self.cpu.sched.push(.RealTimeClock, (1 << 24) -| late); // Reschedule
|
||||
const sched_ptr: *Scheduler = @ptrCast(@alignCast(self.cpu.sched.ptr));
|
||||
sched_ptr.push(.RealTimeClock, (1 << 24) -| late); // Reschedule
|
||||
|
||||
const now = DateTime.now();
|
||||
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);
|
||||
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));
|
||||
}
|
||||
|
||||
fn step(self: *Self, value: Data) u4 {
|
||||
@@ -315,14 +321,14 @@ pub const Clock = struct {
|
||||
}
|
||||
}
|
||||
|
||||
break :blk @truncate(u4, value.raw);
|
||||
break :blk @truncate(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(@boolToInt(value.sio.read()));
|
||||
self.writer.push(@intFromBool(value.sio.read()));
|
||||
|
||||
if (self.writer.finished()) {
|
||||
self.state = self.processCommand(self.writer.buf);
|
||||
@@ -332,14 +338,14 @@ pub const Clock = struct {
|
||||
}
|
||||
}
|
||||
|
||||
break :blk @truncate(u4, value.raw);
|
||||
break :blk @truncate(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(@boolToInt(value.sio.read()));
|
||||
self.writer.push(@intFromBool(value.sio.read()));
|
||||
|
||||
const register_width: u32 = switch (register) {
|
||||
.Control => 1,
|
||||
@@ -358,7 +364,7 @@ pub const Clock = struct {
|
||||
}
|
||||
}
|
||||
|
||||
break :blk @truncate(u4, value.raw);
|
||||
break :blk @truncate(value.raw);
|
||||
},
|
||||
.Read => |register| blk: {
|
||||
if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state});
|
||||
@@ -384,7 +390,7 @@ pub const Clock = struct {
|
||||
}
|
||||
}
|
||||
|
||||
break :blk @truncate(u4, ret.raw);
|
||||
break :blk @truncate(ret.raw);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -397,11 +403,13 @@ 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", .{});
|
||||
|
||||
self.cpu.bus.io.irq.game_pak.set();
|
||||
self.cpu.handleInterrupt();
|
||||
bus_ptr.io.irq.game_pak.set();
|
||||
handleInterrupt(self.cpu);
|
||||
}
|
||||
|
||||
fn processCommand(self: *Self, raw_command: u8) State {
|
||||
@@ -421,7 +429,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 = @truncate(u3, command >> 1 & 0x7);
|
||||
const rtc_register: u3 = @truncate(command >> 1 & 0x7);
|
||||
|
||||
if (is_write) {
|
||||
return switch (rtc_register) {
|
||||
@@ -449,16 +457,8 @@ pub const Clock = struct {
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
/// 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);
|
||||
}
|
||||
|
@@ -9,6 +9,9 @@ const Bit = @import("bitfield").Bit;
|
||||
const Bitfield = @import("bitfield").Bitfield;
|
||||
const Bus = @import("../Bus.zig");
|
||||
|
||||
const getHalf = util.getHalf;
|
||||
const setHalf = util.setHalf;
|
||||
|
||||
const log = std.log.scoped(.@"I/O");
|
||||
|
||||
pub const Io = struct {
|
||||
@@ -19,23 +22,29 @@ pub const Io = struct {
|
||||
ie: InterruptEnable,
|
||||
irq: InterruptRequest,
|
||||
postflg: PostFlag,
|
||||
waitcnt: WaitControl,
|
||||
haltcnt: HaltControl,
|
||||
keyinput: KeyInput,
|
||||
keyinput: AtomicKeyInput,
|
||||
|
||||
pub fn init() Self {
|
||||
return .{
|
||||
.ime = false,
|
||||
.ie = .{ .raw = 0x0000 },
|
||||
.irq = .{ .raw = 0x0000 },
|
||||
.keyinput = .{ .raw = 0x03FF },
|
||||
.keyinput = AtomicKeyInput.init(.{ .raw = 0x03FF }),
|
||||
.waitcnt = .{ .raw = 0x0000_0000 }, // Bit 15 == 0 for GBA
|
||||
.postflg = .FirstBoot,
|
||||
.haltcnt = .Execute,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
self.* = Self.init();
|
||||
}
|
||||
|
||||
fn setIrqs(self: *Io, word: u32) void {
|
||||
self.ie.raw = @truncate(u16, word);
|
||||
self.irq.raw &= ~@truncate(u16, word >> 16);
|
||||
self.ie.raw = @truncate(word);
|
||||
self.irq.raw &= ~@as(u16, @truncate(word >> 16));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -64,8 +73,10 @@ 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(T, bus.io.irq.raw) << 16 | bus.io.ie.raw,
|
||||
0x0400_0208 => @boolToInt(bus.io.ime),
|
||||
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),
|
||||
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
|
||||
},
|
||||
u16 => switch (address) {
|
||||
@@ -85,16 +96,23 @@ 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.raw,
|
||||
0x0400_0130 => bus.io.keyinput.load(.monotonic),
|
||||
|
||||
// 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 => util.io.read.todo(log, "Read {} from WAITCNT", .{T}),
|
||||
0x0400_0208 => @boolToInt(bus.io.ime),
|
||||
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,
|
||||
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
|
||||
},
|
||||
u8 => return switch (address) {
|
||||
@@ -118,10 +136,20 @@ 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 => @truncate(T, bus.io.ie.raw),
|
||||
0x0400_0300 => @enumToInt(bus.io.postflg),
|
||||
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,
|
||||
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"),
|
||||
@@ -168,9 +196,12 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
|
||||
|
||||
// Interrupts
|
||||
0x0400_0200 => bus.io.setIrqs(value),
|
||||
0x0400_0204 => log.debug("Wrote 0x{X:0>8} to WAITCNT", .{value}),
|
||||
0x0400_0204 => bus.io.waitcnt.set(@truncate(value)),
|
||||
0x0400_0208 => bus.io.ime = value & 1 == 1,
|
||||
0x0400_020C...0x0400_021C => {}, // Unused
|
||||
0x0400_0300 => {
|
||||
bus.io.postflg = @enumFromInt(value & 1);
|
||||
bus.io.haltcnt = if (value >> 15 & 1 == 0) .Halt else @panic("TODO: Implement STOP");
|
||||
},
|
||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, address }),
|
||||
},
|
||||
u16 => switch (address) {
|
||||
@@ -186,7 +217,7 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
|
||||
|
||||
// Timers
|
||||
0x0400_0100...0x0400_010E => timer.write(T, &bus.tim, address, value),
|
||||
0x0400_0114 => {}, // TODO: Gyakuten Saiban writes 0x8000 to 0x0400_0114
|
||||
0x0400_0114 => {},
|
||||
0x0400_0110 => {}, // Not Used,
|
||||
|
||||
// Serial Communication 1
|
||||
@@ -210,9 +241,14 @@ 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 => log.debug("Wrote 0x{X:0>4} to WAITCNT", .{value}),
|
||||
0x0400_0204 => bus.io.waitcnt.set(value),
|
||||
0x0400_0206 => {},
|
||||
0x0400_0208 => bus.io.ime = value & 1 == 1,
|
||||
0x0400_0206, 0x0400_020A => {}, // Not Used
|
||||
0x0400_020A => {},
|
||||
0x0400_0300 => {
|
||||
bus.io.postflg = @enumFromInt(value & 1);
|
||||
bus.io.haltcnt = if (value >> 15 & 1 == 0) .Halt else @panic("TODO: Implement STOP");
|
||||
},
|
||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, address }),
|
||||
},
|
||||
u8 => switch (address) {
|
||||
@@ -237,9 +273,16 @@ 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_0300 => bus.io.postflg = std.meta.intToEnum(PostFlag, value & 1) catch unreachable,
|
||||
0x0400_0209 => {},
|
||||
0x0400_020A, 0x0400_020B => {},
|
||||
|
||||
0x0400_0300 => bus.io.postflg = @enumFromInt(value & 1),
|
||||
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 }),
|
||||
@@ -278,14 +321,22 @@ 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
|
||||
@@ -299,10 +350,10 @@ const InterruptEnable = extern union {
|
||||
vblank: Bit(u16, 0),
|
||||
hblank: Bit(u16, 1),
|
||||
coincidence: Bit(u16, 2),
|
||||
tm0_overflow: Bit(u16, 3),
|
||||
tm1_overflow: Bit(u16, 4),
|
||||
tm2_overflow: Bit(u16, 5),
|
||||
tm3_overflow: Bit(u16, 6),
|
||||
tim0: Bit(u16, 3),
|
||||
tim1: Bit(u16, 4),
|
||||
tim2: Bit(u16, 5),
|
||||
tim3: Bit(u16, 6),
|
||||
serial: Bit(u16, 7),
|
||||
dma0: Bit(u16, 8),
|
||||
dma1: Bit(u16, 9),
|
||||
@@ -315,7 +366,7 @@ const InterruptEnable = extern union {
|
||||
|
||||
/// Read Only
|
||||
/// 0 = Pressed, 1 = Released
|
||||
const KeyInput = extern union {
|
||||
pub const KeyInput = extern union {
|
||||
a: Bit(u16, 0),
|
||||
b: Bit(u16, 1),
|
||||
select: Bit(u16, 2),
|
||||
@@ -329,6 +380,32 @@ 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),
|
||||
@@ -377,6 +454,8 @@ pub const BldY = extern union {
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
const u8WriteKind = enum { Hi, Lo };
|
||||
|
||||
/// Write-only
|
||||
pub const WinH = extern union {
|
||||
x2: Bitfield(u16, 0, 8),
|
||||
@@ -386,6 +465,8 @@ 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,
|
||||
@@ -394,20 +475,20 @@ pub const WinV = extern union {
|
||||
pub const WinIn = extern union {
|
||||
w0_bg: Bitfield(u16, 0, 4),
|
||||
w0_obj: Bit(u16, 4),
|
||||
w0_colour: Bit(u16, 5),
|
||||
w0_bld: Bit(u16, 5),
|
||||
w1_bg: Bitfield(u16, 8, 4),
|
||||
w1_obj: Bit(u16, 12),
|
||||
w1_colour: Bit(u16, 13),
|
||||
w1_bld: Bit(u16, 13),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
pub const WinOut = extern union {
|
||||
out_bg: Bitfield(u16, 0, 4),
|
||||
out_obj: Bit(u16, 4),
|
||||
out_colour: Bit(u16, 5),
|
||||
out_bld: Bit(u16, 5),
|
||||
obj_bg: Bitfield(u16, 8, 4),
|
||||
obj_obj: Bit(u16, 12),
|
||||
obj_colour: Bit(u16, 13),
|
||||
obj_bld: Bit(u16, 13),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
@@ -576,3 +657,24 @@ 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);
|
||||
}
|
||||
};
|
||||
|
@@ -3,9 +3,12 @@ const util = @import("../../util.zig");
|
||||
|
||||
const TimerControl = @import("io.zig").TimerControl;
|
||||
const Scheduler = @import("../scheduler.zig").Scheduler;
|
||||
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||
const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||
const Bus = @import("../Bus.zig");
|
||||
|
||||
pub const TimerTuple = std.meta.Tuple(&[_]type{ Timer(0), Timer(1), Timer(2), Timer(3) });
|
||||
const handleInterrupt = @import("../cpu_util.zig").handleInterrupt;
|
||||
|
||||
pub const TimerTuple = struct { Timer(0), Timer(1), Timer(2), Timer(3) };
|
||||
const log = std.log.scoped(.Timer);
|
||||
|
||||
const getHalf = util.getHalf;
|
||||
@@ -16,7 +19,7 @@ pub fn create(sched: *Scheduler) TimerTuple {
|
||||
}
|
||||
|
||||
pub fn read(comptime T: type, tim: *const TimerTuple, addr: u32) ?T {
|
||||
const nybble_addr = @truncate(u4, addr);
|
||||
const nybble_addr: u4 = @truncate(addr);
|
||||
|
||||
return switch (T) {
|
||||
u32 => switch (nybble_addr) {
|
||||
@@ -41,24 +44,24 @@ pub fn read(comptime T: type, tim: *const TimerTuple, addr: u32) ?T {
|
||||
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
||||
},
|
||||
u8 => switch (nybble_addr) {
|
||||
0x0, 0x1 => @truncate(T, tim.*[0].timcntL() >> getHalf(nybble_addr)),
|
||||
0x2, 0x3 => @truncate(T, tim.*[0].cnt.raw >> getHalf(nybble_addr)),
|
||||
0x0, 0x1 => @truncate(tim.*[0].timcntL() >> getHalf(nybble_addr)),
|
||||
0x2, 0x3 => @truncate(tim.*[0].cnt.raw >> getHalf(nybble_addr)),
|
||||
|
||||
0x4, 0x5 => @truncate(T, tim.*[1].timcntL() >> getHalf(nybble_addr)),
|
||||
0x6, 0x7 => @truncate(T, tim.*[1].cnt.raw >> getHalf(nybble_addr)),
|
||||
0x4, 0x5 => @truncate(tim.*[1].timcntL() >> getHalf(nybble_addr)),
|
||||
0x6, 0x7 => @truncate(tim.*[1].cnt.raw >> getHalf(nybble_addr)),
|
||||
|
||||
0x8, 0x9 => @truncate(T, tim.*[2].timcntL() >> getHalf(nybble_addr)),
|
||||
0xA, 0xB => @truncate(T, tim.*[2].cnt.raw >> getHalf(nybble_addr)),
|
||||
0x8, 0x9 => @truncate(tim.*[2].timcntL() >> getHalf(nybble_addr)),
|
||||
0xA, 0xB => @truncate(tim.*[2].cnt.raw >> getHalf(nybble_addr)),
|
||||
|
||||
0xC, 0xD => @truncate(T, tim.*[3].timcntL() >> getHalf(nybble_addr)),
|
||||
0xE, 0xF => @truncate(T, tim.*[3].cnt.raw >> getHalf(nybble_addr)),
|
||||
0xC, 0xD => @truncate(tim.*[3].timcntL() >> getHalf(nybble_addr)),
|
||||
0xE, 0xF => @truncate(tim.*[3].cnt.raw >> getHalf(nybble_addr)),
|
||||
},
|
||||
else => @compileError("TIM: Unsupported read width"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write(comptime T: type, tim: *TimerTuple, addr: u32, value: T) void {
|
||||
const nybble_addr = @truncate(u4, addr);
|
||||
const nybble_addr: u4 = @truncate(addr);
|
||||
|
||||
return switch (T) {
|
||||
u32 => switch (nybble_addr) {
|
||||
@@ -128,11 +131,17 @@ 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 {
|
||||
if (self.cnt.cascade.read() or !self.cnt.enabled.read()) return self._counter;
|
||||
|
||||
return self._counter +% @truncate(u16, (self.sched.now() - self._start_timestamp) / self.frequency());
|
||||
return self._counter +% @as(u16, @truncate((self.sched.now() - self._start_timestamp) / self.frequency()));
|
||||
}
|
||||
|
||||
/// TIMCNT_L Setter
|
||||
@@ -142,35 +151,52 @@ fn Timer(comptime id: u2) type {
|
||||
|
||||
/// TIMCNT_L & TIMCNT_H
|
||||
pub fn setTimcnt(self: *Self, word: u32) void {
|
||||
self.setTimcntL(@truncate(u16, word));
|
||||
self.setTimcntH(@truncate(u16, word >> 16));
|
||||
self.setTimcntL(@truncate(word));
|
||||
self.setTimcntH(@truncate(word >> 16));
|
||||
}
|
||||
|
||||
/// TIMCNT_H
|
||||
pub fn setTimcntH(self: *Self, halfword: u16) void {
|
||||
const new = TimerControl{ .raw = halfword };
|
||||
|
||||
// If Timer happens to be enabled, It will either be resheduled or disabled
|
||||
if (self.cnt.enabled.read()) {
|
||||
// timer was already enabled
|
||||
|
||||
// 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 });
|
||||
|
||||
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());
|
||||
// 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 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;
|
||||
// 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
|
||||
|
||||
// If Timer is enabled and we're not cascading, we need to schedule an overflow event
|
||||
if (new.enabled.read() and !new.cascade.read()) self.rescheduleTimerExpire(0);
|
||||
self.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);
|
||||
}
|
||||
}
|
||||
|
||||
self.cnt.raw = halfword;
|
||||
}
|
||||
|
||||
pub fn onTimerExpire(self: *Self, cpu: *Arm7tdmi, late: u64) void {
|
||||
// Fire IRQ if enabled
|
||||
const io = &cpu.bus.io;
|
||||
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
|
||||
|
||||
const io = &bus_ptr.io;
|
||||
|
||||
if (self.cnt.irq.read()) {
|
||||
switch (id) {
|
||||
@@ -180,33 +206,30 @@ fn Timer(comptime id: u2) type {
|
||||
3 => io.irq.tim3.set(),
|
||||
}
|
||||
|
||||
cpu.handleInterrupt();
|
||||
handleInterrupt(cpu);
|
||||
}
|
||||
|
||||
// DMA Sound Things
|
||||
if (id == 0 or id == 1) {
|
||||
cpu.bus.apu.onDmaAudioSampleRequest(cpu, id);
|
||||
bus_ptr.apu.onDmaAudioSampleRequest(cpu, id);
|
||||
}
|
||||
|
||||
// Perform Cascade Behaviour
|
||||
switch (id) {
|
||||
0 => if (cpu.bus.tim[1].cnt.cascade.read()) {
|
||||
cpu.bus.tim[1]._counter +%= 1;
|
||||
if (cpu.bus.tim[1]._counter == 0) cpu.bus.tim[1].onTimerExpire(cpu, late);
|
||||
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);
|
||||
}
|
||||
},
|
||||
1 => if (cpu.bus.tim[2].cnt.cascade.read()) {
|
||||
cpu.bus.tim[2]._counter +%= 1;
|
||||
if (cpu.bus.tim[2]._counter == 0) cpu.bus.tim[2].onTimerExpire(cpu, late);
|
||||
},
|
||||
2 => if (cpu.bus.tim[3].cnt.cascade.read()) {
|
||||
cpu.bus.tim[3]._counter +%= 1;
|
||||
if (cpu.bus.tim[3]._counter == 0) cpu.bus.tim[3].onTimerExpire(cpu, late);
|
||||
},
|
||||
3 => {}, // There is no Timer for TIM3 to "cascade" to,
|
||||
3 => {}, // THere is no timer for TIM3 to cascade to
|
||||
}
|
||||
|
||||
// Reschedule Timer if we're not cascading
|
||||
if (!self.cnt.cascade.read()) {
|
||||
// TIM0 cascade value is N/A
|
||||
if (id == 0 or !self.cnt.cascade.read()) {
|
||||
self._counter = self._reload;
|
||||
self.rescheduleTimerExpire(late);
|
||||
}
|
||||
|
725
src/core/cpu.zig
725
src/core/cpu.zig
@@ -1,725 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Bus = @import("Bus.zig");
|
||||
const Bit = @import("bitfield").Bit;
|
||||
const Bitfield = @import("bitfield").Bitfield;
|
||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
||||
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,
|
||||
pipe: Pipeline,
|
||||
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,
|
||||
.pipe = Pipeline.init(),
|
||||
.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));
|
||||
}
|
||||
|
||||
/// Advances state so that the BIOS is skipped
|
||||
///
|
||||
/// Note: This accesses the CPU's bus ptr so it only may be called
|
||||
/// once the Bus has been properly initialized
|
||||
///
|
||||
/// TODO: Make above notice impossible to do in code
|
||||
pub fn fastBoot(self: *Self) void {
|
||||
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;
|
||||
self.cpsr.raw = 0x0000_001F;
|
||||
|
||||
self.bus.bios.addr_latch = 0x0000_00DC + 8;
|
||||
}
|
||||
|
||||
pub fn step(self: *Self) void {
|
||||
defer {
|
||||
if (!self.pipe.flushed) self.r[15] += if (self.cpsr.t.read()) 2 else @as(u32, 4);
|
||||
self.pipe.flushed = false;
|
||||
}
|
||||
|
||||
if (self.cpsr.t.read()) {
|
||||
const opcode = @truncate(u16, self.pipe.step(self, u16) orelse return);
|
||||
if (self.logger) |*trace| trace.mgbaLog(self, opcode);
|
||||
|
||||
thumb.lut[thumb.idx(opcode)](self, self.bus, opcode);
|
||||
} else {
|
||||
const opcode = self.pipe.step(self, u32) orelse return;
|
||||
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;
|
||||
|
||||
// Return if IME is disabled, CPSR I is set or there is nothing to handle
|
||||
if (!self.bus.io.ime or self.cpsr.i.read() or should_handle == 0) return;
|
||||
|
||||
// If Pipeline isn't full, we have a bug
|
||||
std.debug.assert(self.pipe.isFull());
|
||||
|
||||
// log.debug("Handling Interrupt!", .{});
|
||||
self.bus.io.haltcnt = .Execute;
|
||||
|
||||
// FIXME: This seems weird, but retAddr.gba suggests I need to make these changes
|
||||
const ret_addr = self.r[15] - if (self.cpsr.t.read()) 0 else @as(u32, 4);
|
||||
const new_spsr = self.cpsr.raw;
|
||||
|
||||
self.changeMode(.Irq);
|
||||
self.cpsr.t.write(false);
|
||||
self.cpsr.i.write(true);
|
||||
|
||||
self.r[14] = ret_addr;
|
||||
self.spsr.raw = new_spsr;
|
||||
self.r[15] = 0x0000_0018;
|
||||
self.pipe.reload(self);
|
||||
}
|
||||
|
||||
inline fn fetch(self: *Self, comptime T: type, address: u32) T {
|
||||
comptime std.debug.assert(T == u32 or T == u16); // Opcode may be 32-bit (ARM) or 16-bit (THUMB)
|
||||
|
||||
// Bus.read will advance the scheduler. There are different timings for CPU fetches,
|
||||
// so we want to undo what Bus.read will apply. We can do this by caching the current tick
|
||||
// This is very dumb.
|
||||
//
|
||||
// FIXME: Please rework this
|
||||
const tick_cache = self.sched.tick;
|
||||
defer self.sched.tick = tick_cache + Bus.fetch_timings[@boolToInt(T == u32)][@truncate(u4, address >> 24)];
|
||||
|
||||
return self.bus.read(T, address);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
std.debug.print("pipeline: {??X:0>8}\n", .{self.pipe.stage});
|
||||
|
||||
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] -| if (self.cpsr.t.read()) 2 else @as(u32, 4);
|
||||
|
||||
const c_psr = self.cpsr.raw;
|
||||
|
||||
var log_str: []u8 = undefined;
|
||||
if (self.cpsr.t.read()) {
|
||||
if (opcode >> 11 == 0x1E) {
|
||||
// Instruction 1 of a BL Opcode, print in ARM mode
|
||||
const other_half = self.bus.debugRead(u16, self.r[15] - 2);
|
||||
const bl_opcode = @as(u32, opcode) << 16 | other_half;
|
||||
|
||||
log_str = try std.fmt.bufPrint(&buf, arm_fmt, .{ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, c_psr, bl_opcode });
|
||||
} else {
|
||||
log_str = try std.fmt.bufPrint(&buf, thumb_fmt, .{ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, c_psr, opcode });
|
||||
}
|
||||
} else {
|
||||
log_str = try std.fmt.bufPrint(&buf, arm_fmt, .{ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, c_psr, opcode });
|
||||
}
|
||||
|
||||
_ = try file.writeAll(log_str);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn checkCond(cpsr: PSR, cond: u4) bool {
|
||||
return switch (cond) {
|
||||
0x0 => cpsr.z.read(), // EQ - Equal
|
||||
0x1 => !cpsr.z.read(), // NE - Not equal
|
||||
0x2 => cpsr.c.read(), // CS - Unsigned higher or same
|
||||
0x3 => !cpsr.c.read(), // CC - Unsigned lower
|
||||
0x4 => cpsr.n.read(), // MI - Negative
|
||||
0x5 => !cpsr.n.read(), // PL - Positive or zero
|
||||
0x6 => cpsr.v.read(), // VS - Overflow
|
||||
0x7 => !cpsr.v.read(), // VC - No overflow
|
||||
0x8 => cpsr.c.read() and !cpsr.z.read(), // HI - unsigned higher
|
||||
0x9 => !cpsr.c.read() or cpsr.z.read(), // LS - unsigned lower or same
|
||||
0xA => cpsr.n.read() == cpsr.v.read(), // GE - Greater or equal
|
||||
0xB => cpsr.n.read() != cpsr.v.read(), // LT - Less than
|
||||
0xC => !cpsr.z.read() and (cpsr.n.read() == cpsr.v.read()), // GT - Greater than
|
||||
0xD => cpsr.z.read() or (cpsr.n.read() != cpsr.v.read()), // LE - Less than or equal
|
||||
0xE => true, // AL - Always
|
||||
0xF => false, // NV - Never (reserved in ARMv3 and up, but seems to have not changed?)
|
||||
};
|
||||
}
|
||||
|
||||
const Pipeline = struct {
|
||||
const Self = @This();
|
||||
stage: [2]?u32,
|
||||
flushed: bool,
|
||||
|
||||
fn init() Self {
|
||||
return .{
|
||||
.stage = [_]?u32{null} ** 2,
|
||||
.flushed = false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isFull(self: *const Self) bool {
|
||||
return self.stage[0] != null and self.stage[1] != null;
|
||||
}
|
||||
|
||||
pub fn step(self: *Self, cpu: *Arm7tdmi, comptime T: type) ?u32 {
|
||||
comptime std.debug.assert(T == u32 or T == u16);
|
||||
|
||||
// FIXME: https://github.com/ziglang/zig/issues/12642
|
||||
var opcode = self.stage[0];
|
||||
|
||||
self.stage[0] = self.stage[1];
|
||||
self.stage[1] = cpu.fetch(T, cpu.r[15]);
|
||||
|
||||
return opcode;
|
||||
}
|
||||
|
||||
pub fn reload(self: *Self, cpu: *Arm7tdmi) void {
|
||||
if (cpu.cpsr.t.read()) {
|
||||
self.stage[0] = cpu.fetch(u16, cpu.r[15]);
|
||||
self.stage[1] = cpu.fetch(u16, cpu.r[15] + 2);
|
||||
cpu.r[15] += 4;
|
||||
} else {
|
||||
self.stage[0] = cpu.fetch(u32, cpu.r[15]);
|
||||
self.stage[1] = cpu.fetch(u32, cpu.r[15] + 4);
|
||||
cpu.r[15] += 8;
|
||||
}
|
||||
|
||||
self.flushed = true;
|
||||
}
|
||||
};
|
||||
|
||||
pub const PSR = extern union {
|
||||
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});
|
||||
}
|
@@ -1,112 +0,0 @@
|
||||
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);
|
||||
cpu.pipe.reload(cpu);
|
||||
} else {
|
||||
// FIXME: Should r15 on write be +12 ahead?
|
||||
bus.write(u32, und_addr, cpu.r[15] + 4);
|
||||
}
|
||||
|
||||
cpu.r[rn] = if (U) cpu.r[rn] + 0x40 else cpu.r[rn] - 0x40;
|
||||
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] = value;
|
||||
if (i == 0xF) {
|
||||
cpu.r[i] &= ~@as(u32, 3); // Align r15
|
||||
cpu.pipe.reload(cpu);
|
||||
|
||||
if (S) 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) 4 else @as(u32, 0)); // PC is already 8 ahead to make 12
|
||||
} else {
|
||||
bus.write(u32, address, cpu.r[i] + if (i == 0xF) 4 else @as(u32, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}.inner;
|
||||
}
|
@@ -1,26 +0,0 @@
|
||||
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] - 4;
|
||||
|
||||
cpu.r[15] +%= sext(u32, u24, opcode) << 2;
|
||||
cpu.pipe.reload(cpu);
|
||||
}
|
||||
}.inner;
|
||||
}
|
||||
|
||||
pub fn branchAndExchange(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
||||
const rn = opcode & 0xF;
|
||||
|
||||
const thumb = cpu.r[rn] & 1 == 1;
|
||||
cpu.r[15] = cpu.r[rn] & if (thumb) ~@as(u32, 1) else ~@as(u32, 3);
|
||||
|
||||
cpu.cpsr.t.write(thumb);
|
||||
cpu.pipe.reload(cpu);
|
||||
}
|
@@ -1,183 +0,0 @@
|
||||
const Bus = @import("../../Bus.zig");
|
||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||
|
||||
const exec = @import("../barrel_shifter.zig").exec;
|
||||
const ror = @import("../barrel_shifter.zig").ror;
|
||||
|
||||
pub fn dataProcessing(comptime I: bool, comptime S: bool, comptime kind: 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
|
||||
// TODO: Why these conditions?
|
||||
if (!I and opcode >> 4 & 1 == 1) cpu.r[15] += 4;
|
||||
const op1 = cpu.r[rn];
|
||||
|
||||
const amount = @truncate(u8, (opcode >> 8 & 0xF) << 1);
|
||||
const op2 = if (I) ror(S, &cpu.cpsr, opcode & 0xFF, amount) else exec(S, cpu, opcode);
|
||||
|
||||
// Undo special condition from above
|
||||
if (!I and opcode >> 4 & 1 == 1) cpu.r[15] -= 4;
|
||||
|
||||
var result: u32 = undefined;
|
||||
var overflow: bool = undefined;
|
||||
|
||||
// Perform Data Processing Logic
|
||||
switch (kind) {
|
||||
0x0 => result = op1 & op2, // AND
|
||||
0x1 => result = op1 ^ op2, // EOR
|
||||
0x2 => result = op1 -% op2, // SUB
|
||||
0x3 => result = op2 -% op1, // RSB
|
||||
0x4 => result = add(&overflow, op1, op2), // ADD
|
||||
0x5 => result = adc(&overflow, op1, op2, old_carry), // ADC
|
||||
0x6 => result = sbc(op1, op2, old_carry), // SBC
|
||||
0x7 => result = sbc(op2, op1, old_carry), // RSC
|
||||
0x8 => {
|
||||
// TST
|
||||
if (rd == 0xF)
|
||||
return undefinedTestBehaviour(cpu);
|
||||
|
||||
result = op1 & op2;
|
||||
},
|
||||
0x9 => {
|
||||
// TEQ
|
||||
if (rd == 0xF)
|
||||
return undefinedTestBehaviour(cpu);
|
||||
|
||||
result = op1 ^ op2;
|
||||
},
|
||||
0xA => {
|
||||
// CMP
|
||||
if (rd == 0xF)
|
||||
return undefinedTestBehaviour(cpu);
|
||||
|
||||
result = op1 -% op2;
|
||||
},
|
||||
0xB => {
|
||||
// CMN
|
||||
if (rd == 0xF)
|
||||
return undefinedTestBehaviour(cpu);
|
||||
|
||||
overflow = @addWithOverflow(u32, op1, op2, &result);
|
||||
},
|
||||
0xC => result = op1 | op2, // ORR
|
||||
0xD => result = op2, // MOV
|
||||
0xE => result = op1 & ~op2, // BIC
|
||||
0xF => result = ~op2, // MVN
|
||||
}
|
||||
|
||||
// Write to Destination Register
|
||||
switch (kind) {
|
||||
0x8, 0x9, 0xA, 0xB => {}, // Test Operations
|
||||
else => {
|
||||
cpu.r[rd] = result;
|
||||
if (rd == 0xF) {
|
||||
if (S) cpu.setCpsr(cpu.spsr.raw);
|
||||
cpu.pipe.reload(cpu);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Write Flags
|
||||
switch (kind) {
|
||||
0x0, 0x1, 0xC, 0xD, 0xE, 0xF => if (S and rd != 0xF) {
|
||||
// Logic Operation Flags
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
// C set by Barrel Shifter, V is unaffected
|
||||
|
||||
},
|
||||
0x2, 0x3 => if (S and rd != 0xF) {
|
||||
// SUB, RSB Flags
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
|
||||
if (kind == 0x2) {
|
||||
// SUB specific
|
||||
cpu.cpsr.c.write(op2 <= op1);
|
||||
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
||||
} else {
|
||||
// RSB Specific
|
||||
cpu.cpsr.c.write(op1 <= op2);
|
||||
cpu.cpsr.v.write(((op2 ^ result) & (~op1 ^ result)) >> 31 & 1 == 1);
|
||||
}
|
||||
},
|
||||
0x4, 0x5 => if (S and rd != 0xF) {
|
||||
// ADD, ADC Flags
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
cpu.cpsr.c.write(overflow);
|
||||
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
|
||||
},
|
||||
0x6, 0x7 => if (S and rd != 0xF) {
|
||||
// SBC, RSC Flags
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
|
||||
if (kind == 0x6) {
|
||||
// SBC specific
|
||||
const subtrahend = @as(u64, op2) -% old_carry +% 1;
|
||||
cpu.cpsr.c.write(subtrahend <= op1);
|
||||
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
||||
} else {
|
||||
// RSC Specific
|
||||
const subtrahend = @as(u64, op1) -% old_carry +% 1;
|
||||
cpu.cpsr.c.write(subtrahend <= op2);
|
||||
cpu.cpsr.v.write(((op2 ^ result) & (~op1 ^ result)) >> 31 & 1 == 1);
|
||||
}
|
||||
},
|
||||
0x8, 0x9, 0xA, 0xB => {
|
||||
// Test Operation Flags
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
|
||||
if (kind == 0xA) {
|
||||
// CMP specific
|
||||
cpu.cpsr.c.write(op2 <= op1);
|
||||
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
||||
} else if (kind == 0xB) {
|
||||
// CMN specific
|
||||
cpu.cpsr.c.write(overflow);
|
||||
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
|
||||
} else {
|
||||
// TST, TEQ specific
|
||||
// Barrel Shifter should always calc CPSR C in TST
|
||||
if (!S) _ = exec(true, cpu, opcode);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}.inner;
|
||||
}
|
||||
|
||||
pub fn sbc(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 ret = @truncate(u32, left -% subtrahend);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
pub fn add(overflow: *bool, left: u32, right: u32) u32 {
|
||||
var ret: u32 = undefined;
|
||||
overflow.* = @addWithOverflow(u32, left, right, &ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
pub fn adc(overflow: *bool, left: u32, right: u32, old_carry: u1) u32 {
|
||||
var ret: u32 = undefined;
|
||||
const first = @addWithOverflow(u32, left, right, &ret);
|
||||
const second = @addWithOverflow(u32, ret, old_carry, &ret);
|
||||
|
||||
overflow.* = first or second;
|
||||
return ret;
|
||||
}
|
||||
|
||||
fn undefinedTestBehaviour(cpu: *Arm7tdmi) void {
|
||||
@setCold(true);
|
||||
cpu.setCpsr(cpu.spsr.raw);
|
||||
}
|
@@ -1,53 +0,0 @@
|
||||
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;
|
||||
|
||||
const base = cpu.r[rn] + if (!L and rn == 0xF) 4 else @as(u32, 0);
|
||||
const offset = if (I) imm_offset_high << 4 | rm else 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
|
||||
const value = bus.read(u16, address);
|
||||
result = if (address & 1 == 1) sext(u32, u8, @truncate(u8, value >> 8)) else sext(u32, u16, value);
|
||||
},
|
||||
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;
|
||||
}
|
@@ -1,57 +0,0 @@
|
||||
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;
|
||||
}
|
@@ -1,59 +0,0 @@
|
||||
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);
|
||||
}
|
@@ -1,29 +0,0 @@
|
||||
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;
|
||||
}
|
@@ -1,57 +0,0 @@
|
||||
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;
|
||||
|
||||
// rn is r15 and L is not set, the PC is 12 ahead
|
||||
const base = cpu.r[rn] + if (!L and rn == 0xF) 4 else @as(u32, 0);
|
||||
|
||||
const offset = if (I) shifter.immediate(false, cpu, opcode) else opcode & 0xFFF;
|
||||
|
||||
const modified_base = if (U) base +% offset else base -% offset;
|
||||
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 = cpu.r[rd] + if (rd == 0xF) 4 else @as(u32, 0); // PC is 12 ahead
|
||||
bus.write(u8, address, @truncate(u8, value));
|
||||
} else {
|
||||
// STR
|
||||
const value = cpu.r[rd] + if (rd == 0xF) 4 else @as(u32, 0);
|
||||
bus.write(u32, address, value);
|
||||
}
|
||||
}
|
||||
|
||||
address = modified_base;
|
||||
if (W and P or !P) {
|
||||
cpu.r[rn] = address;
|
||||
if (rn == 0xF) cpu.pipe.reload(cpu);
|
||||
}
|
||||
|
||||
if (L) {
|
||||
// This emulates the LDR rd == rn behaviour
|
||||
cpu.r[rd] = result;
|
||||
if (rd == 0xF) cpu.pipe.reload(cpu);
|
||||
}
|
||||
}
|
||||
}.inner;
|
||||
}
|
@@ -1,23 +0,0 @@
|
||||
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 ret_addr = cpu.r[15] - 4;
|
||||
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] = ret_addr; // Resume Execution
|
||||
cpu.spsr.raw = cpsr; // Previous mode CPSR
|
||||
cpu.r[15] = 0x0000_0008;
|
||||
cpu.pipe.reload(cpu);
|
||||
}
|
||||
}.inner;
|
||||
}
|
@@ -1,147 +0,0 @@
|
||||
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||
const CPSR = @import("../cpu.zig").PSR;
|
||||
|
||||
const rotr = @import("../../util.zig").rotr;
|
||||
|
||||
pub fn exec(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
||||
var result: u32 = undefined;
|
||||
if (opcode >> 4 & 1 == 1) {
|
||||
result = register(S, cpu, opcode);
|
||||
} else {
|
||||
result = immediate(S, cpu, opcode);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fn register(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
||||
const rs_idx = opcode >> 8 & 0xF;
|
||||
const rm = cpu.r[opcode & 0xF];
|
||||
const rs = @truncate(u8, cpu.r[rs_idx]);
|
||||
|
||||
return switch (@truncate(u2, opcode >> 5)) {
|
||||
0b00 => lsl(S, &cpu.cpsr, rm, rs),
|
||||
0b01 => lsr(S, &cpu.cpsr, rm, rs),
|
||||
0b10 => asr(S, &cpu.cpsr, rm, rs),
|
||||
0b11 => ror(S, &cpu.cpsr, rm, rs),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn immediate(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
||||
const amount = @truncate(u8, opcode >> 7 & 0x1F);
|
||||
const rm = cpu.r[opcode & 0xF];
|
||||
|
||||
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 = lsl(S, &cpu.cpsr, rm, amount),
|
||||
0b01 => result = lsr(S, &cpu.cpsr, rm, amount),
|
||||
0b10 => result = asr(S, &cpu.cpsr, rm, amount),
|
||||
0b11 => result = ror(S, &cpu.cpsr, rm, amount),
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn lsl(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 lsr(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 asr(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 ror(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;
|
||||
}
|
@@ -1,102 +0,0 @@
|
||||
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 lsl = @import("../barrel_shifter.zig").lsl;
|
||||
const lsr = @import("../barrel_shifter.zig").lsr;
|
||||
const asr = @import("../barrel_shifter.zig").asr;
|
||||
const ror = @import("../barrel_shifter.zig").ror;
|
||||
|
||||
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());
|
||||
|
||||
const op1 = cpu.r[rd];
|
||||
const op2 = cpu.r[rs];
|
||||
|
||||
var result: u32 = undefined;
|
||||
var overflow: bool = undefined;
|
||||
switch (op) {
|
||||
0x0 => result = op1 & op2, // AND
|
||||
0x1 => result = op1 ^ op2, // EOR
|
||||
0x2 => result = lsl(true, &cpu.cpsr, op1, @truncate(u8, op2)), // LSL
|
||||
0x3 => result = lsr(true, &cpu.cpsr, op1, @truncate(u8, op2)), // LSR
|
||||
0x4 => result = asr(true, &cpu.cpsr, op1, @truncate(u8, op2)), // ASR
|
||||
0x5 => result = adc(&overflow, op1, op2, carry), // ADC
|
||||
0x6 => result = sbc(op1, op2, carry), // SBC
|
||||
0x7 => result = ror(true, &cpu.cpsr, op1, @truncate(u8, op2)), // ROR
|
||||
0x8 => result = op1 & op2, // TST
|
||||
0x9 => result = 0 -% op2, // NEG
|
||||
0xA => result = op1 -% op2, // CMP
|
||||
0xB => overflow = @addWithOverflow(u32, op1, op2, &result), // CMN
|
||||
0xC => result = op1 | op2, // ORR
|
||||
0xD => result = @truncate(u32, @as(u64, op2) * @as(u64, op1)),
|
||||
0xE => result = op1 & ~op2,
|
||||
0xF => result = ~op2,
|
||||
}
|
||||
|
||||
// Write to Destination Register
|
||||
switch (op) {
|
||||
0x8, 0xA, 0xB => {},
|
||||
else => cpu.r[rd] = result,
|
||||
}
|
||||
|
||||
// Write Flags
|
||||
switch (op) {
|
||||
0x0, 0x1, 0x2, 0x3, 0x4, 0x7, 0xC, 0xE, 0xF => {
|
||||
// Logic Operations
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
// C set by Barrel Shifter, V is unaffected
|
||||
},
|
||||
0x8, 0xA => {
|
||||
// Test Flags
|
||||
// CMN (0xB) is handled with ADC
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
|
||||
if (op == 0xA) {
|
||||
// CMP specific
|
||||
cpu.cpsr.c.write(op2 <= op1);
|
||||
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
||||
}
|
||||
},
|
||||
0x5, 0xB => {
|
||||
// ADC, CMN
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
cpu.cpsr.c.write(overflow);
|
||||
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
|
||||
},
|
||||
0x6 => {
|
||||
// SBC
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
|
||||
const subtrahend = @as(u64, op2) -% carry +% 1;
|
||||
cpu.cpsr.c.write(subtrahend <= op1);
|
||||
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
||||
},
|
||||
0x9 => {
|
||||
// NEG
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
cpu.cpsr.c.write(op2 <= 0);
|
||||
cpu.cpsr.v.write(((0 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
||||
},
|
||||
0xD => {
|
||||
// Multiplication
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
}.inner;
|
||||
}
|
@@ -1,101 +0,0 @@
|
||||
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 & ~@as(u32, 1);
|
||||
cpu.pipe.reload(cpu);
|
||||
} 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);
|
||||
cpu.pipe.reload(cpu);
|
||||
} else {
|
||||
bus.write(u32, address, cpu.r[15] + 2);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
@@ -1,54 +0,0 @@
|
||||
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
|
||||
if (cond == 0xE or cond == 0xF)
|
||||
cpu.panic("[CPU/THUMB.16] Undefined conditional branch with condition {}", .{cond});
|
||||
|
||||
if (!checkCond(cpu.cpsr, cond)) return;
|
||||
|
||||
cpu.r[15] +%= sext(u32, u8, opcode & 0xFF) << 1;
|
||||
cpu.pipe.reload(cpu);
|
||||
}
|
||||
}.inner;
|
||||
}
|
||||
|
||||
pub fn fmt18() InstrFn {
|
||||
return struct {
|
||||
// B but conditional
|
||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||
cpu.r[15] +%= sext(u32, u11, opcode & 0x7FF) << 1;
|
||||
cpu.pipe.reload(cpu);
|
||||
}
|
||||
}.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 next_opcode = cpu.r[15] - 2;
|
||||
|
||||
cpu.r[15] = cpu.r[14] +% (offset << 1);
|
||||
cpu.r[14] = next_opcode | 1;
|
||||
|
||||
cpu.pipe.reload(cpu);
|
||||
} else {
|
||||
// Instruction 1
|
||||
const lr_offset = sext(u32, u11, offset) << 12;
|
||||
cpu.r[14] = (cpu.r[15] +% lr_offset) & ~@as(u32, 1);
|
||||
}
|
||||
}
|
||||
}.inner;
|
||||
}
|
@@ -1,199 +0,0 @@
|
||||
const Bus = @import("../../Bus.zig");
|
||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
||||
|
||||
const add = @import("../arm/data_processing.zig").add;
|
||||
|
||||
const lsl = @import("../barrel_shifter.zig").lsl;
|
||||
const lsr = @import("../barrel_shifter.zig").lsr;
|
||||
const asr = @import("../barrel_shifter.zig").asr;
|
||||
|
||||
pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
|
||||
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 lsl(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 lsr(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 asr(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;
|
||||
|
||||
// Write Flags
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
}
|
||||
}.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 rs = @as(u4, h2) << 3 | (opcode >> 3 & 0x7);
|
||||
const rd = @as(u4, h1) << 3 | (opcode & 0x7);
|
||||
|
||||
const op1 = cpu.r[rd];
|
||||
const op2 = cpu.r[rs];
|
||||
|
||||
var result: u32 = undefined;
|
||||
var overflow: bool = undefined;
|
||||
switch (op) {
|
||||
0b00 => result = add(&overflow, op1, op2), // ADD
|
||||
0b01 => result = op1 -% op2, // CMP
|
||||
0b10 => result = op2, // MOV
|
||||
0b11 => {},
|
||||
}
|
||||
|
||||
// Write to Destination Register
|
||||
switch (op) {
|
||||
0b01 => {}, // Test Instruction
|
||||
0b11 => {
|
||||
// BX
|
||||
const is_thumb = op2 & 1 == 1;
|
||||
cpu.r[15] = op2 & ~@as(u32, 1);
|
||||
|
||||
cpu.cpsr.t.write(is_thumb);
|
||||
cpu.pipe.reload(cpu);
|
||||
},
|
||||
else => {
|
||||
cpu.r[rd] = result;
|
||||
if (rd == 0xF) {
|
||||
cpu.r[15] &= ~@as(u32, 1);
|
||||
cpu.pipe.reload(cpu);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Write Flags
|
||||
switch (op) {
|
||||
0b01 => {
|
||||
// CMP
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
cpu.cpsr.c.write(op2 <= op1);
|
||||
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
||||
},
|
||||
0b00, 0b10, 0b11 => {}, // MOV and Branch Instruction
|
||||
}
|
||||
}
|
||||
}.inner;
|
||||
}
|
||||
|
||||
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);
|
||||
const op1 = cpu.r[rs];
|
||||
const op2: u32 = if (I) rn else cpu.r[rn];
|
||||
|
||||
if (is_sub) {
|
||||
// SUB
|
||||
const result = op1 -% op2;
|
||||
cpu.r[rd] = result;
|
||||
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
cpu.cpsr.c.write(op2 <= op1);
|
||||
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
||||
} else {
|
||||
// ADD
|
||||
var overflow: bool = undefined;
|
||||
const result = add(&overflow, op1, op2);
|
||||
cpu.r[rd] = result;
|
||||
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
cpu.cpsr.c.write(overflow);
|
||||
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
|
||||
}
|
||||
}
|
||||
}.inner;
|
||||
}
|
||||
|
||||
pub fn fmt3(comptime op: u2, comptime rd: u3) InstrFn {
|
||||
return struct {
|
||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||
const op1 = cpu.r[rd];
|
||||
const op2: u32 = opcode & 0xFF; // Offset
|
||||
|
||||
var overflow: bool = undefined;
|
||||
const result: u32 = switch (op) {
|
||||
0b00 => op2, // MOV
|
||||
0b01 => op1 -% op2, // CMP
|
||||
0b10 => add(&overflow, op1, op2), // ADD
|
||||
0b11 => op1 -% op2, // SUB
|
||||
};
|
||||
|
||||
// Write to Register
|
||||
if (op != 0b01) cpu.r[rd] = result;
|
||||
|
||||
// Write Flags
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
|
||||
switch (op) {
|
||||
0b00 => {}, // MOV | C set by Barrel Shifter, V is unaffected
|
||||
0b01, 0b11 => {
|
||||
// SUB, CMP
|
||||
cpu.cpsr.c.write(op2 <= op1);
|
||||
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
||||
},
|
||||
0b10 => {
|
||||
// ADD
|
||||
cpu.cpsr.c.write(overflow);
|
||||
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
|
||||
},
|
||||
}
|
||||
}
|
||||
}.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] & ~@as(u32, 2);
|
||||
const right = (opcode & 0xFF) << 2;
|
||||
cpu.r[rd] = left + right;
|
||||
}
|
||||
}.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;
|
||||
}
|
@@ -1,145 +0,0 @@
|
||||
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;
|
||||
|
||||
// Bit 1 of the PC intentionally ignored
|
||||
cpu.r[rd] = bus.read(u32, (cpu.r[15] & ~@as(u32, 2)) + 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
|
||||
const value = bus.read(u16, address);
|
||||
cpu.r[rd] = if (address & 1 == 1) sext(u32, u8, @truncate(u8, value >> 8)) else sext(u32, u16, value);
|
||||
},
|
||||
}
|
||||
} 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;
|
||||
}
|
@@ -1,23 +0,0 @@
|
||||
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 ret_addr = cpu.r[15] - 2;
|
||||
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] = ret_addr; // Resume Execution
|
||||
cpu.spsr.raw = cpsr; // Previous mode CPSR
|
||||
cpu.r[15] = 0x0000_0008;
|
||||
cpu.pipe.reload(cpu);
|
||||
}
|
||||
}.inner;
|
||||
}
|
75
src/core/cpu_util.zig
Normal file
75
src/core/cpu_util.zig
Normal file
@@ -0,0 +1,75 @@
|
||||
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;
|
||||
}
|
223
src/core/emu.zig
223
src/core/emu.zig
@@ -3,11 +3,41 @@ const SDL = @import("sdl2");
|
||||
const config = @import("../config.zig");
|
||||
|
||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
||||
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
||||
const FpsTracker = @import("../util.zig").FpsTracker;
|
||||
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 Timer = std.time.Timer;
|
||||
const Atomic = std.atomic.Atomic;
|
||||
|
||||
pub const Synchro = struct {
|
||||
const AtomicBool = std.atomic.Value(bool);
|
||||
|
||||
// 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,
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -24,7 +54,7 @@ const frame_period = (std.time.ns_per_s * cycles_per_frame) / clock_rate;
|
||||
|
||||
/// Exact Value: 59.7275005696Hz
|
||||
/// The inverse of the frame period
|
||||
pub const frame_rate: f64 = @intToFloat(f64, clock_rate) / cycles_per_frame;
|
||||
pub const frame_rate: f64 = @as(f64, @floatFromInt(clock_rate)) / cycles_per_frame;
|
||||
|
||||
const log = std.log.scoped(.Emulation);
|
||||
|
||||
@@ -35,30 +65,37 @@ const RunKind = enum {
|
||||
LimitedFPS,
|
||||
};
|
||||
|
||||
pub fn run(quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: *FpsTracker) void {
|
||||
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;
|
||||
if (audio_sync) log.info("Audio sync enabled", .{});
|
||||
|
||||
if (config.config().guest.video_sync) {
|
||||
inner(.LimitedFPS, audio_sync, quit, scheduler, cpu, tracker);
|
||||
inner(.LimitedFPS, audio_sync, cpu, scheduler, tracker, sync);
|
||||
} else {
|
||||
inner(.UnlimitedFPS, audio_sync, quit, scheduler, cpu, tracker);
|
||||
inner(.UnlimitedFPS, audio_sync, cpu, scheduler, tracker, sync);
|
||||
}
|
||||
}
|
||||
|
||||
fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: ?*FpsTracker) void {
|
||||
fn inner(comptime kind: RunKind, audio_sync: bool, cpu: *Arm7tdmi, scheduler: *Scheduler, tracker: ?*Tracker, sync: *Synchro) 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 (!quit.load(.SeqCst)) {
|
||||
while (!sync.should_quit.load(.monotonic)) {
|
||||
handleChannel(cpu, &sync.ch);
|
||||
if (sync.paused.load(.monotonic)) continue;
|
||||
|
||||
runFrame(scheduler, cpu);
|
||||
audioSync(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
|
||||
audioSync(audio_sync, bus_ptr.apu.stream, &bus_ptr.apu.is_buffer_full);
|
||||
|
||||
if (kind == .UnlimitedFPS) tracker.?.tick();
|
||||
}
|
||||
@@ -68,7 +105,10 @@ fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), schedule
|
||||
var timer = Timer.start() catch @panic("failed to initalize std.timer.Timer");
|
||||
var wake_time: u64 = frame_period;
|
||||
|
||||
while (!quit.load(.SeqCst)) {
|
||||
while (!sync.should_quit.load(.monotonic)) {
|
||||
handleChannel(cpu, &sync.ch);
|
||||
if (sync.paused.load(.monotonic)) continue;
|
||||
|
||||
runFrame(scheduler, cpu);
|
||||
const new_wake_time = videoSync(&timer, wake_time);
|
||||
|
||||
@@ -77,7 +117,7 @@ fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), schedule
|
||||
// the amount of time needed for audio to catch up rather than
|
||||
// our expected wake-up time
|
||||
|
||||
audioSync(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
|
||||
audioSync(audio_sync, bus_ptr.apu.stream, &bus_ptr.apu.is_buffer_full);
|
||||
if (!audio_sync) spinLoop(&timer, wake_time);
|
||||
wake_time = new_wake_time;
|
||||
|
||||
@@ -87,14 +127,30 @@ fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), schedule
|
||||
}
|
||||
}
|
||||
|
||||
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 (!cpu.stepDmaTransfer()) {
|
||||
if (cpu.isHalted()) {
|
||||
if (!stepDmaTransfer(cpu)) {
|
||||
if (isHalted(cpu)) {
|
||||
// Fast-forward to next Event
|
||||
sched.tick = sched.queue.peek().?.tick;
|
||||
sched.tick = sched.nextTimestamp();
|
||||
} else {
|
||||
cpu.step();
|
||||
}
|
||||
@@ -105,8 +161,8 @@ pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
|
||||
}
|
||||
|
||||
fn audioSync(audio_sync: bool, stream: *SDL.SDL_AudioStream, is_buffer_full: *bool) void {
|
||||
comptime std.debug.assert(@import("../platform.zig").sample_format == SDL.AUDIO_F32);
|
||||
const sample_size = 2 * @sizeOf(f32);
|
||||
comptime std.debug.assert(@import("../platform.zig").sample_format == SDL.AUDIO_U16);
|
||||
const sample_size = 2 * @sizeOf(u16);
|
||||
const max_buf_size: c_int = 0x400;
|
||||
|
||||
// Determine whether the APU is busy right at this moment
|
||||
@@ -116,6 +172,10 @@ fn audioSync(audio_sync: bool, stream: *SDL.SDL_AudioStream, is_buffer_full: *bo
|
||||
// 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;
|
||||
@@ -146,9 +206,8 @@ fn sleep(timer: *Timer, wake_time: u64) ?u64 {
|
||||
|
||||
const step = 2 * std.time.ns_per_ms; // Granularity of 2ms
|
||||
const times = sleep_for / step;
|
||||
var i: usize = 0;
|
||||
|
||||
while (i < times) : (i += 1) {
|
||||
for (0..times) |_| {
|
||||
std.time.sleep(step);
|
||||
|
||||
// Upon wakeup, check to see if this particular sleep was longer than expected
|
||||
@@ -161,5 +220,129 @@ fn sleep(timer: *Timer, wake_time: u64) ?u64 {
|
||||
}
|
||||
|
||||
fn spinLoop(timer: *Timer, wake_time: u64) void {
|
||||
while (true) if (timer.read() > wake_time) break;
|
||||
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, allocator: Allocator) Interface {
|
||||
return Interface.init(allocator, self);
|
||||
}
|
||||
|
||||
pub fn read(self: *const Self, addr: u32) u8 {
|
||||
return self.cpu.bus.dbgRead(u8, addr);
|
||||
}
|
||||
|
||||
pub fn write(self: *Self, addr: u32, value: u8) void {
|
||||
self.cpu.bus.dbgWrite(u8, addr, value);
|
||||
}
|
||||
|
||||
pub fn registers(self: *const Self) *[16]u32 {
|
||||
return &self.cpu.r;
|
||||
}
|
||||
|
||||
pub fn cpsr(self: *const Self) u32 {
|
||||
return self.cpu.cpsr.raw;
|
||||
}
|
||||
|
||||
pub fn step(self: *Self) void {
|
||||
const cpu = self.cpu;
|
||||
const sched = self.scheduler;
|
||||
|
||||
// Is true when we have executed one (1) instruction
|
||||
var did_step: bool = false;
|
||||
|
||||
// TODO: How can I make it easier to keep this in lock-step with runFrame?
|
||||
while (!did_step) {
|
||||
if (!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);
|
||||
}
|
||||
|
1102
src/core/ppu.zig
1102
src/core/ppu.zig
File diff suppressed because it is too large
Load Diff
44
src/core/ppu/Oam.zig
Normal file
44
src/core/ppu/Oam.zig
Normal file
@@ -0,0 +1,44 @@
|
||||
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;
|
||||
}
|
51
src/core/ppu/Palette.zig
Normal file
51
src/core/ppu/Palette.zig
Normal file
@@ -0,0 +1,51 @@
|
||||
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);
|
||||
}
|
64
src/core/ppu/Vram.zig
Normal file
64
src/core/ppu/Vram.zig
Normal file
@@ -0,0 +1,64 @@
|
||||
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);
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
||||
const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||
const Bus = @import("Bus.zig");
|
||||
const Clock = @import("bus/gpio.zig").Clock;
|
||||
|
||||
const Order = std.math.Order;
|
||||
@@ -11,11 +12,11 @@ const log = std.log.scoped(.Scheduler);
|
||||
pub const Scheduler = struct {
|
||||
const Self = @This();
|
||||
|
||||
tick: u64,
|
||||
tick: u64 = 0,
|
||||
queue: PriorityQueue(Event, void, lessThan),
|
||||
|
||||
pub fn init(allocator: Allocator) Self {
|
||||
var sched = Self{ .tick = 0, .queue = PriorityQueue(Event, void, lessThan).init(allocator, {}) };
|
||||
var sched = Self{ .queue = PriorityQueue(Event, void, lessThan).init(allocator, {}) };
|
||||
sched.queue.add(.{ .kind = .HeatDeath, .tick = std.math.maxInt(u64) }) catch unreachable;
|
||||
|
||||
return sched;
|
||||
@@ -26,14 +27,27 @@ 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 {
|
||||
if (self.queue.removeOrNull()) |event| {
|
||||
const event = self.queue.remove();
|
||||
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.", .{});
|
||||
@@ -41,54 +55,45 @@ pub const Scheduler = struct {
|
||||
},
|
||||
.Draw => {
|
||||
// The end of a VDraw
|
||||
cpu.bus.ppu.drawScanline();
|
||||
cpu.bus.ppu.onHdrawEnd(cpu, late);
|
||||
bus_ptr.ppu.drawScanline();
|
||||
bus_ptr.ppu.onHdrawEnd(cpu, late);
|
||||
},
|
||||
.TimerOverflow => |id| {
|
||||
switch (id) {
|
||||
0 => cpu.bus.tim[0].onTimerExpire(cpu, late),
|
||||
1 => cpu.bus.tim[1].onTimerExpire(cpu, late),
|
||||
2 => cpu.bus.tim[2].onTimerExpire(cpu, late),
|
||||
3 => cpu.bus.tim[3].onTimerExpire(cpu, late),
|
||||
inline 0...3 => |idx| bus_ptr.tim[idx].onTimerExpire(cpu, late),
|
||||
}
|
||||
},
|
||||
.ApuChannel => |id| {
|
||||
switch (id) {
|
||||
0 => cpu.bus.apu.ch1.onToneSweepEvent(late),
|
||||
1 => cpu.bus.apu.ch2.onToneEvent(late),
|
||||
2 => cpu.bus.apu.ch3.onWaveEvent(late),
|
||||
3 => cpu.bus.apu.ch4.onNoiseEvent(late),
|
||||
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 = &cpu.bus.pak.gpio.device;
|
||||
const device = &bus_ptr.pak.gpio.device;
|
||||
if (device.kind != .Rtc or device.ptr == null) return;
|
||||
|
||||
const clock = @ptrCast(*Clock, @alignCast(@alignOf(*Clock), device.ptr.?));
|
||||
const clock: *Clock = @ptrCast(@alignCast(device.ptr.?));
|
||||
clock.onClockUpdate(late);
|
||||
},
|
||||
.FrameSequencer => cpu.bus.apu.onSequencerTick(late),
|
||||
.SampleAudio => cpu.bus.apu.sampleAudio(late),
|
||||
.HBlank => cpu.bus.ppu.onHblankEnd(cpu, late), // The end of a HBlank
|
||||
.VBlank => cpu.bus.ppu.onHdrawEnd(cpu, late), // The end of a VBlank
|
||||
}
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the **first** scheduled event of type `needle`
|
||||
pub fn removeScheduledEvent(self: *Self, needle: EventKind) void {
|
||||
var it = self.queue.iterator();
|
||||
|
||||
var i: usize = 0;
|
||||
while (it.next()) |event| : (i += 1) {
|
||||
for (self.queue.items, 0..) |event, i| {
|
||||
if (std.meta.eql(event.kind, needle)) {
|
||||
|
||||
// This invalidates the iterator
|
||||
// invalidates the slice we're iterating over
|
||||
_ = self.queue.removeIndex(i);
|
||||
|
||||
// Since removing something from the PQ invalidates the iterator,
|
||||
// this implementation can safely only remove the first instance of
|
||||
// a Scheduled Event. Exit Early
|
||||
// log.debug("Removed {?}@{}", .{ event.kind, event.tick });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
511
src/imgui.zig
Normal file
511
src/imgui.zig
Normal file
@@ -0,0 +1,511 @@
|
||||
//! 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.GLuint;
|
||||
|
||||
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(@floatFromInt(dim.width), @floatFromInt(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();
|
||||
|
||||
zgui.image(@ptrFromInt(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 by Fabio Arnold", .{});
|
||||
{
|
||||
zgui.indent(.{});
|
||||
defer zgui.unindent(.{});
|
||||
|
||||
zgui.bulletText("nativefiledialog by Michael Labbe", .{});
|
||||
}
|
||||
|
||||
zgui.bulletText("SDL.zig by Felix Queißner", .{});
|
||||
{
|
||||
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 by Michal Ziulek", .{});
|
||||
{
|
||||
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 and FlorenceOS contributors", .{});
|
||||
zgui.bulletText("zig-opengl by Felix Queißner", .{});
|
||||
{
|
||||
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;
|
||||
|
||||
zgui.text("tick: {X:0>16}", .{scheduler.now()});
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
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{};
|
||||
}
|
171
src/main.zig
171
src/main.zig
@@ -4,17 +4,21 @@ 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
|
||||
@@ -22,44 +26,74 @@ const params = clap.parseParamsComptime(
|
||||
\\-h, --help Display this help and exit.
|
||||
\\-s, --skip Skip BIOS.
|
||||
\\-b, --bios <str> Optional path to a GBA BIOS ROM.
|
||||
\\ --gdb Run ZBA from the context of a GDB Server
|
||||
\\<str> Path to the GBA GamePak ROM.
|
||||
\\
|
||||
);
|
||||
|
||||
pub fn main() anyerror!void {
|
||||
pub fn main() void {
|
||||
// Main Allocator for ZBA
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer std.debug.assert(!gpa.deinit());
|
||||
defer std.debug.assert(gpa.deinit() == .ok);
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
// Determine the Data Directory (stores saves, config file, etc.)
|
||||
// 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 attempting to find a data directory: {}", .{e});
|
||||
const path = option orelse exitln("no valid data directory could be found", .{});
|
||||
ensureDirectoriesExist(path) catch |e| exitln("failed to create directories under \"{s}\": {}", .{ path, e });
|
||||
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;
|
||||
};
|
||||
defer allocator.free(data_path);
|
||||
|
||||
// Determine the Config Directory
|
||||
const config_path = blk: {
|
||||
const result = known_folders.getPath(allocator, .roaming_configuration);
|
||||
const option = result catch |e| exitln("interreupted while determining the config folder: {}", .{e});
|
||||
const path = option orelse exitln("no valid config folder found", .{});
|
||||
ensureConfigDirExists(path) catch |e| exitln("failed to create required folder \"{s}\": {}", .{ path, e });
|
||||
|
||||
break :blk path;
|
||||
};
|
||||
defer allocator.free(config_path);
|
||||
|
||||
// Parse CLI
|
||||
const result = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{}) catch |e| exitln("failed to parse cli: {}", .{e});
|
||||
|
||||
const result = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{ .allocator = allocator }) catch |e| exitln("failed to parse cli: {}", .{e});
|
||||
defer result.deinit();
|
||||
|
||||
// TODO: Move config file to XDG Config directory?
|
||||
const config_path = configFilePath(allocator, data_path) catch |e| exitln("failed to determine the config file path for ZBA: {}", .{e});
|
||||
defer allocator.free(config_path);
|
||||
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);
|
||||
|
||||
config.load(allocator, config_path) catch |e| exitln("failed to read config file: {}", .{e});
|
||||
config.load(allocator, cfg_file_path) catch |e| exitln("failed to load config file: {}", .{e});
|
||||
|
||||
const paths = handleArguments(allocator, data_path, &result) catch |e| exitln("failed to handle cli arguments: {}", .{e});
|
||||
defer if (paths.save) |path| allocator.free(path);
|
||||
var paths = handleArguments(allocator, data_path, &result) catch |e| exitln("failed to handle cli arguments: {}", .{e});
|
||||
defer paths.deinit(allocator);
|
||||
|
||||
const log_file = if (config.config().debug.cpu_trace) blk: {
|
||||
break :blk std.fs.cwd().createFile("zba.log", .{}) catch |e| exitln("failed to create trace log file: {}", .{e});
|
||||
} else null;
|
||||
// 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,
|
||||
};
|
||||
defer if (log_file) |file| file.close();
|
||||
|
||||
// TODO: Take Emulator Init Code out of main.zig
|
||||
@@ -67,29 +101,80 @@ pub fn main() anyerror!void {
|
||||
defer scheduler.deinit();
|
||||
|
||||
var bus: Bus = undefined;
|
||||
var cpu = Arm7tdmi.init(&scheduler, &bus, log_file);
|
||||
|
||||
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});
|
||||
defer bus.deinit();
|
||||
|
||||
if (config.config().guest.skip_bios or result.args.skip or paths.bios == null) {
|
||||
cpu.fastBoot();
|
||||
if (config.config().guest.skip_bios or result.args.skip != 0 or paths.bios == null) {
|
||||
@import("core/cpu_util.zig").fastBoot(&cpu);
|
||||
}
|
||||
|
||||
var gui = Gui.init(&bus.pak.title, &bus.apu, width, height);
|
||||
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});
|
||||
defer gui.deinit();
|
||||
|
||||
gui.run(&cpu, &scheduler) catch |e| exitln("failed to run gui thread: {}", .{e});
|
||||
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(allocator);
|
||||
defer emulator.deinit();
|
||||
|
||||
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});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) !FilePaths {
|
||||
const rom_path = romPath(result);
|
||||
log.info("ROM path: {s}", .{rom_path});
|
||||
fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) !FilePaths {
|
||||
const rom_path = try romPath(allocator, result);
|
||||
errdefer if (rom_path) |path| allocator.free(path);
|
||||
|
||||
const bios_path = result.args.bios;
|
||||
if (bios_path) |path| log.info("BIOS path: {s}", .{path}) else log.warn("No BIOS provided", .{});
|
||||
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 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});
|
||||
log.info("Save path: {s}", .{save_path});
|
||||
|
||||
return .{
|
||||
@@ -99,8 +184,8 @@ pub fn handleArguments(allocator: Allocator, data_path: []const u8, result: *con
|
||||
};
|
||||
}
|
||||
|
||||
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" });
|
||||
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);
|
||||
|
||||
// We try to create the file exclusively, meaning that we err out if the file already exists.
|
||||
@@ -109,30 +194,34 @@ fn configFilePath(allocator: Allocator, data_path: []const u8) ![]const u8 {
|
||||
std.fs.accessAbsolute(path, .{}) catch |e| {
|
||||
if (e != error.FileNotFound) return e;
|
||||
|
||||
const config_file = try std.fs.createFileAbsolute(path, .{});
|
||||
const config_file = std.fs.createFileAbsolute(path, .{}) catch |err| exitln("failed to create \"{s}\": {}", .{ path, err });
|
||||
defer config_file.close();
|
||||
|
||||
try config_file.writeAll(@embedFile("../example.toml"));
|
||||
try config_file.writeAll(@embedFile("example.toml"));
|
||||
};
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
fn ensureDirectoriesExist(data_path: []const u8) !void {
|
||||
fn ensureDataDirsExist(data_path: []const u8) !void {
|
||||
var dir = try std.fs.openDirAbsolute(data_path, .{});
|
||||
defer dir.close();
|
||||
|
||||
// We want to make sure: %APPDATA%/zba and %APPDATA%/zba/save exist
|
||||
// (~/.local/share/zba/save for linux, ??? for macOS)
|
||||
|
||||
// Will recursively create directories
|
||||
try dir.makePath("zba" ++ std.fs.path.sep_str ++ "save");
|
||||
}
|
||||
|
||||
fn romPath(result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) []const u8 {
|
||||
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, ¶ms, clap.parsers.default)) !?[]const u8 {
|
||||
return switch (result.positionals.len) {
|
||||
1 => result.positionals[0],
|
||||
0 => exitln("ZBA requires a path to a GamePak ROM", .{}),
|
||||
0 => null,
|
||||
1 => try allocator.dupe(u8, result.positionals[0]),
|
||||
else => exitln("ZBA received too many positional arguments.", .{}),
|
||||
};
|
||||
}
|
||||
@@ -141,5 +230,5 @@ fn exitln(comptime format: []const u8, args: anytype) noreturn {
|
||||
const stderr = std.io.getStdErr().writer();
|
||||
stderr.print(format, args) catch {}; // Just exit already...
|
||||
stderr.writeByte('\n') catch {};
|
||||
std.os.exit(1);
|
||||
std.process.exit(1);
|
||||
}
|
||||
|
524
src/platform.zig
524
src/platform.zig
@@ -1,254 +1,251 @@
|
||||
const std = @import("std");
|
||||
const SDL = @import("sdl2");
|
||||
const gl = @import("gl");
|
||||
const zgui = @import("zgui");
|
||||
|
||||
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("core/cpu.zig").Arm7tdmi;
|
||||
const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||
const Bus = @import("core/Bus.zig");
|
||||
const Scheduler = @import("core/scheduler.zig").Scheduler;
|
||||
const FpsTracker = @import("util.zig").FpsTracker;
|
||||
|
||||
const span = @import("util.zig").span;
|
||||
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;
|
||||
|
||||
pub const sample_rate = 44100;
|
||||
pub const sample_format = SDL.AUDIO_F32;
|
||||
const GLuint = gl.GLuint;
|
||||
const GLsizei = gl.GLsizei;
|
||||
const SDL_GLContext = *anyopaque;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const default_title: []const u8 = "ZBA";
|
||||
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";
|
||||
|
||||
pub const Gui = struct {
|
||||
const Self = @This();
|
||||
const SDL_GLContext = *anyopaque; // SDL.SDL_GLContext is a ?*anyopaque
|
||||
const log = std.log.scoped(.Gui);
|
||||
|
||||
// zig fmt: off
|
||||
const vertices: [32]f32 = [_]f32{
|
||||
// Positions // Colours // Texture Coords
|
||||
1.0, -1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, // Top Right
|
||||
1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, // Bottom Right
|
||||
-1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, // Bottom Left
|
||||
-1.0, -1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, // Top Left
|
||||
};
|
||||
|
||||
const indices: [6]u32 = [_]u32{
|
||||
0, 1, 3, // First Triangle
|
||||
1, 2, 3, // Second Triangle
|
||||
};
|
||||
// zig fmt: on
|
||||
|
||||
window: *SDL.SDL_Window,
|
||||
ctx: SDL_GLContext,
|
||||
title: []const u8,
|
||||
audio: Audio,
|
||||
|
||||
program_id: gl.GLuint,
|
||||
state: imgui.State,
|
||||
allocator: Allocator,
|
||||
|
||||
pub fn init(title: *const [12]u8, apu: *Apu, width: i32, height: i32) Self {
|
||||
pub fn init(allocator: Allocator, apu: *Apu, title_opt: ?*const [12]u8) !Self {
|
||||
if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO) < 0) panic();
|
||||
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_PROFILE_MASK, SDL.SDL_GL_CONTEXT_PROFILE_CORE) < 0) panic();
|
||||
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic();
|
||||
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic();
|
||||
|
||||
const win_scale = @intCast(c_int, config.config().host.win_scale);
|
||||
|
||||
const window = SDL.SDL_CreateWindow(
|
||||
default_title.ptr,
|
||||
window_title,
|
||||
SDL.SDL_WINDOWPOS_CENTERED,
|
||||
SDL.SDL_WINDOWPOS_CENTERED,
|
||||
@as(c_int, width * win_scale),
|
||||
@as(c_int, height * win_scale),
|
||||
SDL.SDL_WINDOW_OPENGL | SDL.SDL_WINDOW_SHOWN,
|
||||
default_dim.width,
|
||||
default_dim.height,
|
||||
SDL.SDL_WINDOW_OPENGL | SDL.SDL_WINDOW_SHOWN | SDL.SDL_WINDOW_RESIZABLE,
|
||||
) orelse panic();
|
||||
|
||||
const ctx = SDL.SDL_GL_CreateContext(window) orelse panic();
|
||||
if (SDL.SDL_GL_MakeCurrent(window, ctx) < 0) panic();
|
||||
|
||||
gl.load(ctx, Self.glGetProcAddress) catch @panic("gl.load failed");
|
||||
if (SDL.SDL_GL_SetSwapInterval(@boolToInt(config.config().host.vsync)) < 0) panic();
|
||||
gl.load(ctx, Self.glGetProcAddress) catch {};
|
||||
if (SDL.SDL_GL_SetSwapInterval(@intFromBool(config.config().host.vsync)) < 0) panic();
|
||||
|
||||
const program_id = compileShaders();
|
||||
zgui.init(allocator);
|
||||
zgui.plot.init();
|
||||
zgui.backend.init(window, ctx, "#version 330 core");
|
||||
|
||||
// zgui.io.setIniFilename(null);
|
||||
|
||||
return Self{
|
||||
.window = window,
|
||||
.title = span(title),
|
||||
.ctx = ctx,
|
||||
.program_id = program_id,
|
||||
.audio = Audio.init(apu),
|
||||
|
||||
.allocator = allocator,
|
||||
.state = try imgui.State.init(allocator, title_opt),
|
||||
};
|
||||
}
|
||||
|
||||
fn compileShaders() gl.GLuint {
|
||||
// TODO: Panic on Shader Compiler Failure + Error Message
|
||||
const vert_shader = @embedFile("shader/pixelbuf.vert");
|
||||
const frag_shader = @embedFile("shader/pixelbuf.frag");
|
||||
|
||||
const vs = gl.createShader(gl.VERTEX_SHADER);
|
||||
defer gl.deleteShader(vs);
|
||||
|
||||
gl.shaderSource(vs, 1, &[_][*c]const u8{vert_shader}, 0);
|
||||
gl.compileShader(vs);
|
||||
|
||||
const fs = gl.createShader(gl.FRAGMENT_SHADER);
|
||||
defer gl.deleteShader(fs);
|
||||
|
||||
gl.shaderSource(fs, 1, &[_][*c]const u8{frag_shader}, 0);
|
||||
gl.compileShader(fs);
|
||||
|
||||
const program = gl.createProgram();
|
||||
gl.attachShader(program, vs);
|
||||
gl.attachShader(program, fs);
|
||||
gl.linkProgram(program);
|
||||
|
||||
return program;
|
||||
}
|
||||
|
||||
// Returns the VAO ID since it's used in run()
|
||||
fn generateBuffers() [3]c_uint {
|
||||
var vao_id: c_uint = undefined;
|
||||
var vbo_id: c_uint = undefined;
|
||||
var ebo_id: c_uint = undefined;
|
||||
gl.genVertexArrays(1, &vao_id);
|
||||
gl.genBuffers(1, &vbo_id);
|
||||
gl.genBuffers(1, &ebo_id);
|
||||
|
||||
gl.bindVertexArray(vao_id);
|
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, vbo_id);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, @sizeOf(@TypeOf(vertices)), &vertices, gl.STATIC_DRAW);
|
||||
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo_id);
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, @sizeOf(@TypeOf(indices)), &indices, gl.STATIC_DRAW);
|
||||
|
||||
// Position
|
||||
gl.vertexAttribPointer(0, 3, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, 0)); // lmao
|
||||
gl.enableVertexAttribArray(0);
|
||||
// Colour
|
||||
gl.vertexAttribPointer(1, 3, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, (3 * @sizeOf(f32))));
|
||||
gl.enableVertexAttribArray(1);
|
||||
// Texture Coord
|
||||
gl.vertexAttribPointer(2, 2, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, (6 * @sizeOf(f32))));
|
||||
gl.enableVertexAttribArray(2);
|
||||
|
||||
return .{ vao_id, vbo_id, ebo_id };
|
||||
}
|
||||
|
||||
fn generateTexture(buf: []const u8) c_uint {
|
||||
var tex_id: c_uint = undefined;
|
||||
gl.genTextures(1, &tex_id);
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex_id);
|
||||
|
||||
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gba_width, gba_height, 0, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, buf.ptr);
|
||||
// gl.generateMipmap(gl.TEXTURE_2D); // TODO: Remove?
|
||||
|
||||
return tex_id;
|
||||
}
|
||||
|
||||
pub fn run(self: *Self, cpu: *Arm7tdmi, scheduler: *Scheduler) !void {
|
||||
var quit = std.atomic.Atomic(bool).init(false);
|
||||
var tracker = FpsTracker.init();
|
||||
|
||||
const thread = try std.Thread.spawn(.{}, emu.run, .{ &quit, scheduler, cpu, &tracker });
|
||||
defer thread.join();
|
||||
|
||||
var title_buf: [0x100]u8 = [_]u8{0} ** 0x100;
|
||||
|
||||
const vao_id = Self.generateBuffers()[0];
|
||||
_ = Self.generateTexture(cpu.bus.ppu.framebuf.get(.Renderer));
|
||||
|
||||
emu_loop: while (true) {
|
||||
var event: SDL.SDL_Event = undefined;
|
||||
while (SDL.SDL_PollEvent(&event) != 0) {
|
||||
switch (event.type) {
|
||||
SDL.SDL_QUIT => break :emu_loop,
|
||||
SDL.SDL_KEYDOWN => {
|
||||
const io = &cpu.bus.io;
|
||||
const key_code = event.key.keysym.sym;
|
||||
|
||||
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 => {},
|
||||
}
|
||||
},
|
||||
SDL.SDL_KEYUP => {
|
||||
const io = &cpu.bus.io;
|
||||
const key_code = event.key.keysym.sym;
|
||||
|
||||
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 => {
|
||||
comptime std.debug.assert(sample_format == SDL.AUDIO_F32);
|
||||
log.err("Sample Count: {}", .{@intCast(u32, SDL.SDL_AudioStreamAvailable(cpu.bus.apu.stream)) / (2 * @sizeOf(f32))});
|
||||
},
|
||||
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 => {},
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
// Emulator has an internal Double Buffer
|
||||
const framebuf = cpu.bus.ppu.framebuf.get(.Renderer);
|
||||
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gba_width, gba_height, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, framebuf.ptr);
|
||||
|
||||
gl.useProgram(self.program_id);
|
||||
gl.bindVertexArray(vao_id);
|
||||
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, null);
|
||||
SDL.SDL_GL_SwapWindow(self.window);
|
||||
|
||||
const dyn_title = std.fmt.bufPrint(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, tracker.value() }) catch unreachable;
|
||||
SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr);
|
||||
}
|
||||
|
||||
quit.store(true, .SeqCst); // Terminate Emulator Thread
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.audio.deinit();
|
||||
// TODO: Buffer deletions
|
||||
gl.deleteProgram(self.program_id);
|
||||
self.state.deinit(self.allocator);
|
||||
|
||||
zgui.backend.deinit();
|
||||
zgui.plot.deinit();
|
||||
zgui.deinit();
|
||||
|
||||
SDL.SDL_GL_DeleteContext(self.ctx);
|
||||
SDL.SDL_DestroyWindow(self.window);
|
||||
SDL.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, &[_]GLuint{vao_id});
|
||||
|
||||
const emu_tex = opengl_impl.screenTex(bus_ptr.ppu.framebuf.get(.Renderer));
|
||||
const out_tex = opengl_impl.outTex();
|
||||
defer gl.deleteTextures(2, &[_]GLuint{ emu_tex, out_tex });
|
||||
|
||||
const fbo_id = try opengl_impl.frameBuffer(out_tex);
|
||||
defer gl.deleteFramebuffers(1, &fbo_id);
|
||||
|
||||
const prog_id = try opengl_impl.program(); // Dynamic Shaders?
|
||||
defer gl.deleteProgram(prog_id);
|
||||
|
||||
var win_dim: Dimensions = default_dim;
|
||||
|
||||
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: SDL.SDL_Event = undefined;
|
||||
while (SDL.SDL_PollEvent(&event) != 0) {
|
||||
_ = zgui.backend.processEvent(&event);
|
||||
|
||||
switch (event.type) {
|
||||
SDL.SDL_QUIT => break :emu_loop,
|
||||
SDL.SDL_KEYDOWN => {
|
||||
// TODO: Make use of compare_and_xor?
|
||||
const key_code = event.key.keysym.sym;
|
||||
var keyinput: KeyInput = .{ .raw = 0x0000 };
|
||||
|
||||
switch (key_code) {
|
||||
SDL.SDLK_UP => keyinput.up.set(),
|
||||
SDL.SDLK_DOWN => keyinput.down.set(),
|
||||
SDL.SDLK_LEFT => keyinput.left.set(),
|
||||
SDL.SDLK_RIGHT => keyinput.right.set(),
|
||||
SDL.SDLK_x => keyinput.a.set(),
|
||||
SDL.SDLK_z => keyinput.b.set(),
|
||||
SDL.SDLK_a => keyinput.shoulder_l.set(),
|
||||
SDL.SDLK_s => keyinput.shoulder_r.set(),
|
||||
SDL.SDLK_RETURN => keyinput.start.set(),
|
||||
SDL.SDLK_RSHIFT => keyinput.select.set(),
|
||||
else => {},
|
||||
}
|
||||
|
||||
bus_ptr.io.keyinput.fetchAnd(~keyinput.raw, .monotonic);
|
||||
},
|
||||
SDL.SDL_KEYUP => {
|
||||
// TODO: Make use of compare_and_xor?
|
||||
const key_code = event.key.keysym.sym;
|
||||
var keyinput: KeyInput = .{ .raw = 0x0000 };
|
||||
|
||||
switch (key_code) {
|
||||
SDL.SDLK_UP => keyinput.up.set(),
|
||||
SDL.SDLK_DOWN => keyinput.down.set(),
|
||||
SDL.SDLK_LEFT => keyinput.left.set(),
|
||||
SDL.SDLK_RIGHT => keyinput.right.set(),
|
||||
SDL.SDLK_x => keyinput.a.set(),
|
||||
SDL.SDLK_z => keyinput.b.set(),
|
||||
SDL.SDLK_a => keyinput.shoulder_l.set(),
|
||||
SDL.SDLK_s => keyinput.shoulder_r.set(),
|
||||
SDL.SDLK_RETURN => keyinput.start.set(),
|
||||
SDL.SDLK_RSHIFT => keyinput.select.set(),
|
||||
else => {},
|
||||
}
|
||||
|
||||
bus_ptr.io.keyinput.fetchOr(keyinput.raw, .monotonic);
|
||||
},
|
||||
SDL.SDL_WINDOWEVENT => {
|
||||
if (event.window.event == SDL.SDL_WINDOWEVENT_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;
|
||||
|
||||
switch (self.state.emulation) {
|
||||
.Transition => |inner| switch (inner) {
|
||||
.Active => {
|
||||
sync.paused.store(false, .monotonic);
|
||||
if (!config.config().host.mute) SDL.SDL_PauseAudioDevice(self.audio.device, 0);
|
||||
|
||||
self.state.emulation = .Active;
|
||||
},
|
||||
.Inactive => {
|
||||
// Assert that double pausing is impossible
|
||||
SDL.SDL_PauseAudioDevice(self.audio.device, 1);
|
||||
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);
|
||||
defer gl.bindFramebuffer(gl.FRAMEBUFFER, 0);
|
||||
|
||||
gl.viewport(0, 0, gba_width, gba_height);
|
||||
opengl_impl.drawScreen(emu_tex, prog_id, vao_id, 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?
|
||||
SDL.SDL_LockAudioDevice(self.audio.device);
|
||||
defer SDL.SDL_UnlockAudioDevice(self.audio.device);
|
||||
|
||||
zgui_redraw = imgui.draw(&self.state, sync, win_dim, cpu, out_tex);
|
||||
},
|
||||
.Inactive => zgui_redraw = imgui.draw(&self.state, sync, win_dim, cpu, out_tex),
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
SDL.SDL_GL_SwapWindow(self.window);
|
||||
}
|
||||
|
||||
sync.should_quit.store(true, .monotonic);
|
||||
}
|
||||
|
||||
fn glGetProcAddress(ctx: SDL.SDL_GLContext, proc: [:0]const u8) ?*anyopaque {
|
||||
_ = ctx;
|
||||
return SDL.SDL_GL_GetProcAddress(proc.ptr);
|
||||
@@ -271,17 +268,12 @@ const Audio = struct {
|
||||
want.callback = Self.callback;
|
||||
want.userdata = apu;
|
||||
|
||||
std.debug.assert(sample_format == SDL.AUDIO_F32);
|
||||
log.info("Host Sample Rate: {}Hz, Host Format: SDL.AUDIO_F32", .{sample_rate});
|
||||
std.debug.assert(sample_format == SDL.AUDIO_U16);
|
||||
log.info("Host Sample Rate: {}Hz, Host Format: SDL.AUDIO_U16", .{sample_rate});
|
||||
|
||||
const device = SDL.SDL_OpenAudioDevice(null, 0, &want, &have, 0);
|
||||
if (device == 0) panic();
|
||||
|
||||
if (!config.config().host.mute) {
|
||||
SDL.SDL_PauseAudioDevice(device, 0); // Unpause Audio
|
||||
log.info("Unpaused Device", .{});
|
||||
}
|
||||
|
||||
return .{ .device = device };
|
||||
}
|
||||
|
||||
@@ -291,8 +283,7 @@ const Audio = struct {
|
||||
}
|
||||
|
||||
export fn callback(userdata: ?*anyopaque, stream: [*c]u8, len: c_int) void {
|
||||
const T = *Apu;
|
||||
const apu = @ptrCast(T, @alignCast(@alignOf(T), userdata));
|
||||
const apu: *Apu = @ptrCast(@alignCast(userdata));
|
||||
|
||||
_ = SDL.SDL_AudioStreamGet(apu.stream, stream, len);
|
||||
}
|
||||
@@ -302,3 +293,124 @@ fn panic() noreturn {
|
||||
const str = @as(?[*:0]const u8, SDL.SDL_GetError()) orelse "unknown error";
|
||||
@panic(std.mem.sliceTo(str, 0));
|
||||
}
|
||||
|
||||
const opengl_impl = struct {
|
||||
fn drawScreen(tex_id: GLuint, prog_id: GLuint, vao_id: GLuint, 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() !GLuint {
|
||||
const vert_shader = @embedFile("shader/pixelbuf.vert");
|
||||
const frag_shader = @embedFile("shader/pixelbuf.frag");
|
||||
|
||||
const vs = gl.createShader(gl.VERTEX_SHADER);
|
||||
defer gl.deleteShader(vs);
|
||||
|
||||
gl.shaderSource(vs, 1, &[_][*c]const u8{vert_shader}, 0);
|
||||
gl.compileShader(vs);
|
||||
|
||||
if (!shader.didCompile(vs)) return error.VertexCompileError;
|
||||
|
||||
const fs = gl.createShader(gl.FRAGMENT_SHADER);
|
||||
defer gl.deleteShader(fs);
|
||||
|
||||
gl.shaderSource(fs, 1, &[_][*c]const u8{frag_shader}, 0);
|
||||
gl.compileShader(fs);
|
||||
|
||||
if (!shader.didCompile(fs)) return error.FragmentCompileError;
|
||||
|
||||
const prog = gl.createProgram();
|
||||
gl.attachShader(prog, vs);
|
||||
gl.attachShader(prog, fs);
|
||||
gl.linkProgram(prog);
|
||||
|
||||
return prog;
|
||||
}
|
||||
|
||||
fn vao() GLuint {
|
||||
var vao_id: GLuint = undefined;
|
||||
gl.genVertexArrays(1, &vao_id);
|
||||
|
||||
return vao_id;
|
||||
}
|
||||
|
||||
fn screenTex(buf: []const u8) GLuint {
|
||||
var tex_id: GLuint = undefined;
|
||||
gl.genTextures(1, &tex_id);
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex_id);
|
||||
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() GLuint {
|
||||
var tex_id: GLuint = undefined;
|
||||
gl.genTextures(1, &tex_id);
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex_id);
|
||||
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: GLuint) !GLuint {
|
||||
var fbo_id: GLuint = undefined;
|
||||
gl.genFramebuffers(1, &fbo_id);
|
||||
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo_id);
|
||||
defer gl.bindFramebuffer(gl.FRAMEBUFFER, 0);
|
||||
|
||||
gl.framebufferTexture(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, tex_id, 0);
|
||||
gl.drawBuffers(1, &@as(GLuint, 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.GLuint) bool {
|
||||
var success: gl.GLint = undefined;
|
||||
gl.getShaderiv(id, gl.COMPILE_STATUS, &success);
|
||||
|
||||
if (success == 0) err(id);
|
||||
|
||||
return success == 1;
|
||||
}
|
||||
|
||||
fn err(id: gl.GLuint) void {
|
||||
const buf_len = 512;
|
||||
var error_msg: [buf_len]u8 = undefined;
|
||||
|
||||
gl.getShaderInfoLog(id, buf_len, 0, &error_msg);
|
||||
log.err("{s}", .{std.mem.sliceTo(&error_msg, 0)});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@@ -1,7 +1,6 @@
|
||||
#version 330 core
|
||||
out vec4 frag_color;
|
||||
|
||||
in vec3 color;
|
||||
in vec2 uv;
|
||||
|
||||
uniform sampler2D screen;
|
||||
|
@@ -1,13 +1,10 @@
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 pos;
|
||||
layout (location = 1) in vec3 in_color;
|
||||
layout (location = 2) in vec2 in_uv;
|
||||
|
||||
out vec3 color;
|
||||
out vec2 uv;
|
||||
|
||||
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() {
|
||||
color = in_color;
|
||||
uv = in_uv;
|
||||
gl_Position = vec4(pos, 1.0);
|
||||
uv = uvs[gl_VertexID];
|
||||
gl_Position = vec4(pos[gl_VertexID], 0.0, 1.0);
|
||||
}
|
211
src/util.zig
211
src/util.zig
@@ -3,51 +3,32 @@ const builtin = @import("builtin");
|
||||
const config = @import("config.zig");
|
||||
|
||||
const Log2Int = std.math.Log2Int;
|
||||
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
|
||||
const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||
|
||||
// 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_amt = @intCast(Log2Int(T), @typeInfo(T).Int.bits - @typeInfo(U).Int.bits);
|
||||
|
||||
return @bitCast(T, @bitCast(iT, @as(ExtU, @truncate(U, value)) << shift_amt) >> shift_amt);
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const FpsTracker = struct {
|
||||
const Self = @This();
|
||||
|
||||
fps: u32,
|
||||
count: std.atomic.Atomic(u32),
|
||||
count: std.atomic.Value(u32),
|
||||
timer: std.time.Timer,
|
||||
|
||||
pub fn init() Self {
|
||||
return .{
|
||||
.fps = 0,
|
||||
.count = std.atomic.Atomic(u32).init(0),
|
||||
.count = std.atomic.Value(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, .SeqCst);
|
||||
self.fps = self.count.swap(0, .monotonic);
|
||||
self.timer.reset();
|
||||
}
|
||||
|
||||
@@ -55,68 +36,6 @@ 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
|
||||
@@ -131,9 +50,15 @@ pub fn escape(title: [12]u8) [12]u8 {
|
||||
}
|
||||
|
||||
pub const FilePaths = struct {
|
||||
rom: []const u8,
|
||||
rom: ?[]const u8,
|
||||
bios: ?[]const u8,
|
||||
save: ?[]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);
|
||||
}
|
||||
};
|
||||
|
||||
pub const io = struct {
|
||||
@@ -174,6 +99,7 @@ pub const io = struct {
|
||||
|
||||
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),
|
||||
|
||||
@@ -196,7 +122,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]);
|
||||
const low = cpu.bus.dbgRead(u16, cpu.r[15] - 2);
|
||||
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");
|
||||
@@ -232,8 +158,6 @@ pub const Logger = struct {
|
||||
}
|
||||
};
|
||||
|
||||
const FmtArgTuple = std.meta.Tuple(&.{ u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32 });
|
||||
|
||||
pub const audio = struct {
|
||||
const _io = @import("core/bus/io.zig");
|
||||
|
||||
@@ -285,7 +209,7 @@ pub const audio = struct {
|
||||
|
||||
/// Sets a quarter (8) of the bits of the u32 `left` to the value of u8 `right`
|
||||
pub inline fn setQuart(left: u32, addr: u8, right: u8) u32 {
|
||||
const offset = @truncate(u2, addr);
|
||||
const offset: u2 = @truncate(addr);
|
||||
|
||||
return switch (offset) {
|
||||
0b00 => (left & 0xFFFF_FF00) | right,
|
||||
@@ -299,13 +223,11 @@ pub inline fn setQuart(left: u32, addr: u8, right: u8) u32 {
|
||||
///
|
||||
/// TODO: Support u16 reads of u32 values?
|
||||
pub inline fn getHalf(byte: u8) u4 {
|
||||
return @truncate(u4, byte & 1) << 3;
|
||||
return @as(u4, @truncate(byte & 1)) << 3;
|
||||
}
|
||||
|
||||
// TODO: Maybe combine SetLo and SetHi, use addr alignment to deduplicate code
|
||||
|
||||
pub inline fn setHalf(comptime T: type, left: T, addr: u8, right: HalfInt(T)) T {
|
||||
const offset = @truncate(u1, addr >> if (T == u32) 1 else 0);
|
||||
const offset: u1 = @truncate(addr >> if (T == u32) 1 else 0);
|
||||
|
||||
return switch (T) {
|
||||
u32 => switch (offset) {
|
||||
@@ -314,32 +236,12 @@ pub inline fn setHalf(comptime T: type, left: T, addr: u8, right: HalfInt(T)) T
|
||||
},
|
||||
u16 => switch (offset) {
|
||||
0b0 => (left & 0xFF00) | right,
|
||||
0b1 => (right & 0x00FF) | @as(u16, right) << 8,
|
||||
0b1 => (left & 0x00FF) | @as(u16, right) << 8,
|
||||
},
|
||||
else => @compileError("unsupported type"),
|
||||
};
|
||||
}
|
||||
|
||||
/// Sets the high bits of an integer to a value
|
||||
pub inline fn setLo(comptime T: type, left: T, right: HalfInt(T)) T {
|
||||
return switch (T) {
|
||||
u32 => (left & 0xFFFF_0000) | right,
|
||||
u16 => (left & 0xFF00) | right,
|
||||
u8 => (left & 0xF0) | right,
|
||||
else => @compileError("unsupported type"),
|
||||
};
|
||||
}
|
||||
|
||||
/// sets the low bits of an integer to a value
|
||||
pub inline fn setHi(comptime T: type, left: T, right: HalfInt(T)) T {
|
||||
return switch (T) {
|
||||
u32 => (left & 0x0000_FFFF) | @as(u32, right) << 16,
|
||||
u16 => (left & 0x00FF) | @as(u16, right) << 8,
|
||||
u8 => (left & 0x0F) | @as(u8, right) << 4,
|
||||
else => @compileError("unsupported type"),
|
||||
};
|
||||
}
|
||||
|
||||
/// The Integer type which corresponds to T with exactly half the amount of bits
|
||||
fn HalfInt(comptime T: type) type {
|
||||
const type_info = @typeInfo(T);
|
||||
@@ -348,3 +250,76 @@ fn HalfInt(comptime T: type) type {
|
||||
|
||||
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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
Reference in New Issue
Block a user