Compare commits
214 Commits
86d2224cfc
...
april-fool
Author | SHA1 | Date | |
---|---|---|---|
9844d657b0 | |||
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 | |||
ca67ca3183 | |||
47fc49deb6 | |||
472215b4c2 | |||
c9a423d094 | |||
1d163fa56f | |||
13710a3236 | |||
6154585e77 | |||
7debdc490d | |||
58375795bf | |||
f0dca29836 | |||
c75682dbd4 | |||
36832ba1fb | |||
647bd83224 | |||
c831f67d1a | |||
268961262d | |||
3e62feacba | |||
d859cee365 | |||
371cf4cc12 | |||
10aec67ee0 | |||
4eb715a138 | |||
14b24787ab | |||
eb7ffa29f4 | |||
4b8ed3cebb | |||
928ce674d9 | |||
945dbec013 | |||
dd98066a34 | |||
a2868dfe9e | |||
22979d9450 | |||
712c58391d | |||
407774d798 | |||
16f8f4c953 | |||
143ffd95f7 | |||
250ff25ed7 | |||
eff52ac1bb | |||
e60b556f72 | |||
3a3e6acc6a | |||
4b4bc7f894 | |||
325208d460 | |||
f44a1a49fd | |||
1575f517a9 | |||
26dba16789 | |||
b133880064 | |||
2474daa3ae | |||
fc53a40b3c | |||
7097e21361 | |||
a9fe24b1b4 | |||
f38c840d32 | |||
19e70c39d1 | |||
5a72a8e7f3 | |||
7b146ad7ca | |||
822eed1f3a | |||
b37a14900c | |||
f5bd20bc2a | |||
d3514b14f3 | |||
06c60dad74 | |||
870e991862 | |||
5bb5bdf389 | |||
a3996cbc58 | |||
a948c6f900 | |||
014180cbd0 | |||
e4451738b5 | |||
48b81c8e7a | |||
3cf1bf54e9 | |||
1f9eeedfe8 | |||
72a63eeb98 | |||
2799c3f202 | |||
b3ada64e64 | |||
62162ba492 | |||
aa100de581 | |||
7142831284 | |||
97f48c730e | |||
293fbd9f55 | |||
622f479e07 | |||
0204eb6f94 |
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"
|
||||
branches:
|
||||
- main
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
# os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
runs-on: ${{matrix.os}}
|
||||
steps:
|
||||
- uses: goto-bus-stop/setup-zig@v2
|
||||
with:
|
||||
version: master
|
||||
- name: prepare-linux
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install libgtk-3-dev libsdl2-dev
|
||||
- name: prepare-windows
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
vcpkg integrate install
|
||||
vcpkg install sdl2:x64-windows
|
||||
git config --global core.autocrlf false
|
||||
- name: prepare-macos
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
brew install sdl2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: build
|
||||
run: zig build -Doptimize=ReleaseSafe -Dcpu=baseline
|
||||
- name: upload
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: zba-${{matrix.os}}
|
||||
path: zig-out/bin
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: goto-bus-stop/setup-zig@v2
|
||||
with:
|
||||
version: master
|
||||
- run: zig fmt src/**/*.zig
|
||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,7 +1,7 @@
|
||||
/.vscode
|
||||
/bin
|
||||
/zig-cache
|
||||
/zig-out
|
||||
**/zig-cache
|
||||
**/zig-out
|
||||
/docs
|
||||
**/*.log
|
||||
**/*.bin
|
||||
@@ -12,3 +12,7 @@
|
||||
|
||||
# Any Custom Scripts for Debugging purposes
|
||||
*.sh
|
||||
|
||||
|
||||
# Dear ImGui
|
||||
**/imgui.ini
|
15
.gitmodules
vendored
15
.gitmodules
vendored
@@ -10,3 +10,18 @@
|
||||
[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
|
||||
[submodule "lib/zba-gdbstub"]
|
||||
path = lib/zba-gdbstub
|
||||
url = https://git.musuka.dev/paoda/zba-gdbstub
|
||||
[submodule "lib/zgui"]
|
||||
path = lib/zgui
|
||||
url = https://git.musuka.dev/paoda/zgui
|
||||
[submodule "lib/nfd-zig"]
|
||||
path = lib/nfd-zig
|
||||
url = https://github.com/fabioarnold/nfd-zig
|
||||
[submodule "lib/zba-util"]
|
||||
path = lib/zba-util
|
||||
url = https://git.musuka.dev/paoda/zba-util.git
|
||||
|
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"
|
||||
]
|
||||
}
|
134
README.md
134
README.md
@@ -1,63 +1,113 @@
|
||||
# ZBA (working title)
|
||||
An in-progress Game Boy Advance Emulator written in Zig ⚡!
|
||||
|
||||
A Game Boy Advance Emulator written in Zig ⚡!
|
||||
|
||||

|
||||
|
||||
## Scope
|
||||
|
||||
I'm hardly the first to write a Game Boy Advance Emulator nor will I be the last. This project isn't going to compete with the GOATs like [mGBA](https://github.com/mgba-emu) or [NanoBoyAdvance](https://github.com/nba-emu/NanoBoyAdvance). There aren't any interesting ideas either like in [DSHBA](https://github.com/DenSinH/DSHBA).
|
||||
|
||||
This is a simple (read: incomplete) for-fun long-term project. I hope to get "mostly there", which to me means that I'm not missing any major hardware features and the set of possible improvements would be in memory timing or in UI/UX. With respect to that goal, here's what's outstanding:
|
||||
|
||||
### TODO
|
||||
|
||||
- [x] Affine Sprites
|
||||
- [ ] Windowing (see [this branch](https://git.musuka.dev/paoda/zba/src/branch/window))
|
||||
- [ ] Audio Resampler (Having issues with SDL2's)
|
||||
- [ ] Refactoring for easy-ish perf boosts
|
||||
|
||||
## Usage
|
||||
|
||||
As it currently exists, ZBA is run from the terminal. In your console of choice, type `./zba --help` to see what you can do.
|
||||
|
||||
I typically find myself typing `./zba -b ./bin/bios.bin` and then going to File -> Insert ROM to load the title of my choice.
|
||||
|
||||
Need a BIOS? Why not try using the open-source [Cult-Of-GBA BIOS](https://github.com/Cult-of-GBA/BIOS) written by [fleroviux](https://github.com/fleroviux) and [DenSinH](https://github.com/DenSinH)?
|
||||
|
||||
Finally it's worth noting that ZBA uses a TOML config file it'll store in your OS's data directory. See `example.toml` to learn about the defaults and what exactly you can mess around with.
|
||||
|
||||
## Tests
|
||||
- [ ] [jsmolka's GBA Test Collection](https://github.com/jsmolka/gba-tests)
|
||||
- [x] `arm.gba` and `thumb.gba`
|
||||
- [x] `flash64.gba`, `flash128.gba`, `none.gba`, and `sram.gba`
|
||||
- [x] `hello.gba`, `shades.gba`, and `stripes.gba`
|
||||
- [x] `memory.gba`
|
||||
- [x] `bios.gba`
|
||||
- [ ] `nes.gba`
|
||||
- [ ] [DenSinH's GBA ROMs](https://github.com/DenSinH/GBARoms)
|
||||
- [x] `eeprom-test` and `flash-test`
|
||||
- [x] `midikey2freq`
|
||||
- [ ] `swi-tests-random`
|
||||
- [ ] [destoer's GBA Tests](https://github.com/destoer/gba_tests)
|
||||
- [x] `cond_invalid.gba`
|
||||
- [x] `dma_priority.gba`
|
||||
- [x] `hello_world.gba`
|
||||
- [x] `if_ack.gba`
|
||||
- [ ] `line_timing.gba`
|
||||
- [ ] `lyc_midline.gba`
|
||||
- [ ] `window_midframe.gba`
|
||||
- [x] [ladystarbreeze's GBA Test Collection](https://github.com/ladystarbreeze/GBA-Test-Collection)
|
||||
- [x] `retAddr.gba`
|
||||
- [x] `helloWorld.gba`
|
||||
- [x] `helloAudio.gba`
|
||||
- [x] [`armwrestler-gba-fixed.gba`](https://github.com/destoer/armwrestler-gba-fixed)
|
||||
- [x] [FuzzARM](https://github.com/DenSinH/FuzzARM)
|
||||
|
||||
GBA Tests | [jsmolka](https://github.com/jsmolka/)
|
||||
--- | ---
|
||||
`arm.gba`, `thumb.gba` | PASS
|
||||
`memory.gba`, `bios.gba` | PASS
|
||||
`flash64.gba`, `flash128.gba` | PASS
|
||||
`sram.gba` | PASS
|
||||
`none.gba` | PASS
|
||||
`hello.gba`, `shades.gba`, `stripes.gba` | PASS
|
||||
`nes.gba` | PASS
|
||||
|
||||
GBARoms | [DenSinH](https://github.com/DenSinH/)
|
||||
--- | ---
|
||||
`eeprom-test`, `flash-test` | PASS
|
||||
`midikey2freq` | PASS
|
||||
`swi-tests-random` | FAIL
|
||||
|
||||
gba_tests | [destoer](https://github.com/destoer/)
|
||||
--- | ---
|
||||
`cond_invalid.gba` | PASS
|
||||
`dma_priority.gba` | PASS
|
||||
`hello_world.gba` | PASS
|
||||
`if_ack.gba` | PASS
|
||||
`line_timing.gba` | FAIL
|
||||
`lyc_midline.gba` | FAIL
|
||||
`window_midframe.gba` | FAIL
|
||||
|
||||
GBA Test Collection | [ladystarbreeze](https://github.com/ladystarbreeze)
|
||||
--- | ---
|
||||
`retAddr.gba` | PASS
|
||||
`helloWorld.gba` | PASS
|
||||
`helloAudio.gba` | PASS
|
||||
|
||||
FuzzARM | [DenSinH](https://github.com/DenSinH/)
|
||||
--- | ---
|
||||
`main.gba` | PASS
|
||||
|
||||
arm7wrestler GBA Fixed | [destoer](https://github.com/destoer)
|
||||
--- | ---
|
||||
`armwrestler-gba-fixed.gba` | PASS
|
||||
|
||||
## Resources
|
||||
* [GBATEK](https://problemkaputt.de/gbatek.htm)
|
||||
* [TONC](https://coranac.com/tonc/text/toc.htm)
|
||||
* [ARM Architecture Reference Manual](https://www.intel.com/content/dam/www/programmable/us/en/pdfs/literature/third-party/ddi0100e_arm_arm.pdf)
|
||||
* [ARM7TDMI Data Sheet](https://www.dca.fee.unicamp.br/cursos/EA871/references/ARM/ARM7TDMIDataSheet.pdf)
|
||||
|
||||
- [GBATEK](https://problemkaputt.de/gbatek.htm)
|
||||
- [TONC](https://coranac.com/tonc/text/toc.htm)
|
||||
- [ARM Architecture Reference Manual](https://www.intel.com/content/dam/www/programmable/us/en/pdfs/literature/third-party/ddi0100e_arm_arm.pdf)
|
||||
- [ARM7TDMI Data Sheet](https://www.dca.fee.unicamp.br/cursos/EA871/references/ARM/ARM7TDMIDataSheet.pdf)
|
||||
|
||||
## Compiling
|
||||
Most recently built on Zig [0.10.0-dev.3900+ab4b26d8a](https://github.com/ziglang/zig/tree/ab4b26d8a)
|
||||
|
||||
Most recently built on Zig [v0.11.0-dev.2168+322ace70f](https://github.com/ziglang/zig/tree/322ace70f)
|
||||
|
||||
### Dependencies
|
||||
* [SDL.zig](https://github.com/MasterQ32/SDL.zig)
|
||||
* [SDL2](https://www.libsdl.org/download-2.0.php)
|
||||
* [zig-clap](https://github.com/Hejsil/zig-clap)
|
||||
* [known-folders](https://github.com/ziglibs/known-folders)
|
||||
* [`bitfields.zig`](https://github.com/FlorenceOS/Florence/blob/aaa5a9e568197ad24780ec9adb421217530d4466/lib/util/bitfields.zig)
|
||||
|
||||
`bitfields.zig` from [FlorenceOS](https://github.com/FlorenceOS) is included under `lib/util/bitfield.zig`.
|
||||
Dependency | Source
|
||||
--- | ---
|
||||
SDL.zig | <https://github.com/MasterQ32/SDL.zig>
|
||||
known-folders | <https://github.com/ziglibs/known-folders>
|
||||
nfd-zig | <https://github.com/fabioarnold/nfd-zig>
|
||||
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>
|
||||
zig-toml | <https://github.com/aeronavery/zig-toml>
|
||||
`bitfields.zig` | [https://github.com/FlorenceOS/Florence](https://github.com/FlorenceOS/Florence/blob/aaa5a9e568/lib/util/bitfields.zig)
|
||||
`gl.zig` | <https://github.com/MasterQ32/zig-opengl>
|
||||
|
||||
Use `git submodule update --init` from the project root to pull the git submodules `SDL.zig`, `zig-clap`, and `known-folders`
|
||||
Use `git submodule update --init` from the project root to pull the git 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 is located at `zig-out/bin/`.
|
||||
|
||||
## Controls
|
||||
|
||||
Key | Button
|
||||
--- | ---
|
||||
<kbd>X</kbd> | A
|
||||
|
BIN
assets/screenshot.png
Normal file
BIN
assets/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
75
build.zig
75
build.zig
@@ -1,39 +1,70 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const Sdk = @import("lib/SDL.zig/Sdk.zig");
|
||||
const gdbstub = @import("lib/zba-gdbstub/build.zig");
|
||||
const zgui = @import("lib/zgui/build.zig");
|
||||
const nfd = @import("lib/nfd-zig/build.zig");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
// Minimum Zig Version
|
||||
const min_ver = std.SemanticVersion.parse("0.11.0-dev.2168+322ace70f") catch return; // https://github.com/ziglang/zig/commit/322ace70f
|
||||
if (builtin.zig_version.order(min_ver).compare(.lt)) {
|
||||
std.log.err("{s}", .{b.fmt("Zig v{} does not meet the minimum version requirement. (Zig v{})", .{ builtin.zig_version, min_ver })});
|
||||
std.os.exit(1);
|
||||
}
|
||||
|
||||
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 optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
// Standard release options allow the person running `zig build` to select
|
||||
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
|
||||
const mode = b.standardReleaseOptions();
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "zba",
|
||||
.root_source_file = .{ .path = "src/main.zig" },
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const exe = b.addExecutable("zba", "src/main.zig");
|
||||
exe.setTarget(target);
|
||||
exe.setMainPkgPath("."); // Necessary so that src/main.zig can embed example.toml
|
||||
|
||||
// Known Folders (%APPDATA%, XDG, etc.)
|
||||
exe.addPackagePath("known_folders", "lib/known-folders/known-folders.zig");
|
||||
exe.addAnonymousModule("known_folders", .{ .source_file = .{ .path = "lib/known-folders/known-folders.zig" } });
|
||||
|
||||
// DateTime Library
|
||||
exe.addPackagePath("datetime", "lib/zig-datetime/src/main.zig");
|
||||
exe.addAnonymousModule("datetime", .{ .source_file = .{ .path = "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");
|
||||
exe.addAnonymousModule("bitfield", .{ .source_file = .{ .path = "lib/bitfield.zig" } });
|
||||
|
||||
// Argument Parsing Library
|
||||
exe.addPackagePath("clap", "lib/zig-clap/clap.zig");
|
||||
exe.addAnonymousModule("clap", .{ .source_file = .{ .path = "lib/zig-clap/clap.zig" } });
|
||||
|
||||
// TOML Library
|
||||
exe.addAnonymousModule("toml", .{ .source_file = .{ .path = "lib/zig-toml/src/toml.zig" } });
|
||||
|
||||
// OpenGL 3.3 Bindings
|
||||
exe.addAnonymousModule("gl", .{ .source_file = .{ .path = "lib/gl.zig" } });
|
||||
|
||||
// ZBA utility code
|
||||
exe.addAnonymousModule("zba-util", .{ .source_file = .{ .path = "lib/zba-util/src/lib.zig" } });
|
||||
|
||||
// gdbstub
|
||||
exe.addModule("gdbstub", gdbstub.getModule(b));
|
||||
|
||||
// NativeFileDialog(ue) Bindings
|
||||
exe.linkLibrary(nfd.makeLib(b, target, optimize));
|
||||
exe.addModule("nfd", nfd.getModule(b));
|
||||
|
||||
// Zig SDL Bindings: https://github.com/MasterQ32/SDL.zig
|
||||
const sdk = Sdk.init(b);
|
||||
const sdk = Sdk.init(b, null);
|
||||
sdk.link(exe, .dynamic);
|
||||
exe.addPackage(sdk.getNativePackage("sdl2"));
|
||||
exe.addModule("sdl2", sdk.getNativeModule());
|
||||
|
||||
// Dear ImGui bindings
|
||||
|
||||
// .shared option should stay in sync with SDL.zig call above where true == .dynamic, and false == .static
|
||||
const zgui_pkg = zgui.package(b, target, optimize, .{ .options = .{ .backend = .sdl2_opengl3, .shared = true } });
|
||||
zgui_pkg.link(exe);
|
||||
|
||||
exe.setBuildMode(mode);
|
||||
exe.install();
|
||||
|
||||
const run_cmd = exe.run();
|
||||
@@ -45,9 +76,11 @@ pub fn build(b: *std.build.Builder) void {
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
|
||||
const exe_tests = b.addTest("src/main.zig");
|
||||
exe_tests.setTarget(target);
|
||||
exe_tests.setBuildMode(mode);
|
||||
const exe_tests = b.addTest(.{
|
||||
.root_source_file = .{ .path = "src/main.zig" },
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const test_step = b.step("test", "Run unit tests");
|
||||
test_step.dependOn(&exe_tests.step);
|
||||
|
25
example.toml
Normal file
25
example.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[Host]
|
||||
# Using nearest-neighbour scaling, how many times the native resolution
|
||||
# of the game bow should the screen be?
|
||||
win_scale = 3
|
||||
# Enable VSYNC on the UI thread
|
||||
vsync = true
|
||||
# Mute ZBA
|
||||
mute = false
|
||||
|
||||
[Guest]
|
||||
# Sync Emulation to Audio
|
||||
audio_sync = true
|
||||
# Sync Emulation to Video
|
||||
video_sync = true
|
||||
# Force RTC support
|
||||
force_rtc = false
|
||||
# Skip BIOS
|
||||
skip_bios = false
|
||||
|
||||
[Debug]
|
||||
# Enable detailed CPU logs
|
||||
cpu_trace = false
|
||||
# When false and builtin.mode == .Debug, ZBA will panic
|
||||
# on unknown I/O reads
|
||||
unhandled_io = true
|
Submodule lib/SDL.zig updated: 6a9e37687a...cc3b023f50
5053
lib/gl.zig
Normal file
5053
lib/gl.zig
Normal file
File diff suppressed because it is too large
Load Diff
Submodule lib/known-folders updated: 24845b0103...d13ba61370
1
lib/nfd-zig
Submodule
1
lib/nfd-zig
Submodule
Submodule lib/nfd-zig added at 5e5098bcaf
1
lib/zba-gdbstub
Submodule
1
lib/zba-gdbstub
Submodule
Submodule lib/zba-gdbstub added at 215e053b9a
1
lib/zba-util
Submodule
1
lib/zba-util
Submodule
Submodule lib/zba-util added at d5e66caf21
1
lib/zgui
Submodule
1
lib/zgui
Submodule
Submodule lib/zgui added at 5b2b64a9de
Submodule lib/zig-clap updated: e5d09c4b2d...6310cbd576
Submodule lib/zig-datetime updated: 5ec1c36cf3...b570d61187
1
lib/zig-toml
Submodule
1
lib/zig-toml
Submodule
Submodule lib/zig-toml added at 016b8bcf98
86
src/config.zig
Normal file
86
src/config.zig
Normal file
@@ -0,0 +1,86 @@
|
||||
const std = @import("std");
|
||||
const toml = @import("toml");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = std.log.scoped(.Config);
|
||||
var state: Config = .{};
|
||||
|
||||
const Config = struct {
|
||||
host: Host = .{},
|
||||
guest: Guest = .{},
|
||||
debug: Debug = .{},
|
||||
|
||||
/// Settings related to the Computer the Emulator is being run on
|
||||
const Host = struct {
|
||||
/// Using Nearest-Neighbor, multiply the resolution of the GBA Window
|
||||
win_scale: i64 = 3,
|
||||
/// Enable Vsync
|
||||
///
|
||||
/// Note: This does not affect whether Emulation is synced to 59Hz
|
||||
vsync: bool = true,
|
||||
/// Mute ZBA
|
||||
mute: bool = false,
|
||||
};
|
||||
|
||||
// Settings realted to the emulation itself
|
||||
const Guest = struct {
|
||||
/// Whether Emulation thread to sync to Audio Callbacks
|
||||
audio_sync: bool = true,
|
||||
/// Whether Emulation thread should sync to 59Hz
|
||||
video_sync: bool = true,
|
||||
/// Whether RTC I/O should always be enabled
|
||||
force_rtc: bool = false,
|
||||
/// Skip BIOS
|
||||
skip_bios: bool = false,
|
||||
};
|
||||
|
||||
/// Settings related to debugging ZBA
|
||||
const Debug = struct {
|
||||
/// Enable CPU Trace logs
|
||||
cpu_trace: bool = false,
|
||||
/// If false and ZBA is built in debug mode, ZBA will panic on unhandled I/O
|
||||
unhandled_io: bool = true,
|
||||
};
|
||||
};
|
||||
|
||||
pub fn config() *const Config {
|
||||
return &state;
|
||||
}
|
||||
|
||||
/// Reads a config file and then loads it into the global state
|
||||
pub fn load(allocator: Allocator, file_path: []const u8) !void {
|
||||
var config_file = try std.fs.cwd().openFile(file_path, .{});
|
||||
defer config_file.close();
|
||||
|
||||
log.info("loaded from {s}", .{file_path});
|
||||
|
||||
const contents = try config_file.readToEndAlloc(allocator, try config_file.getEndPos());
|
||||
defer allocator.free(contents);
|
||||
|
||||
var parser = try toml.parseFile(allocator, file_path);
|
||||
defer parser.deinit();
|
||||
|
||||
const table = try parser.parse();
|
||||
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;
|
||||
}
|
||||
}
|
485
src/core/Bus.zig
485
src/core/Bus.zig
@@ -1,6 +1,5 @@
|
||||
const std = @import("std");
|
||||
|
||||
const AudioDeviceId = @import("sdl2").SDL_AudioDeviceID;
|
||||
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
||||
const Bios = @import("bus/Bios.zig");
|
||||
const Ewram = @import("bus/Ewram.zig");
|
||||
@@ -20,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
|
||||
@@ -34,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,
|
||||
@@ -46,10 +50,19 @@ iwram: Iwram,
|
||||
ewram: Ewram,
|
||||
io: Io,
|
||||
|
||||
cpu: ?*Arm7tdmi,
|
||||
cpu: *Arm7tdmi,
|
||||
sched: *Scheduler,
|
||||
|
||||
read_table: *const [table_len]?*const anyopaque,
|
||||
write_tables: [2]*const [table_len]?*anyopaque,
|
||||
allocator: Allocator,
|
||||
|
||||
pub fn init(self: *Self, allocator: Allocator, sched: *Scheduler, cpu: *Arm7tdmi, paths: FilePaths) !void {
|
||||
const tables = try allocator.alloc(?*anyopaque, 3 * table_len); // Allocate all tables
|
||||
|
||||
const read_table = tables[0..table_len];
|
||||
const write_tables = .{ tables[table_len .. 2 * table_len], tables[2 * table_len .. 3 * table_len] };
|
||||
|
||||
self.* = .{
|
||||
.pak = try GamePak.init(allocator, cpu, paths.rom, paths.save),
|
||||
.bios = try Bios.init(allocator, paths.bios),
|
||||
@@ -62,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 {
|
||||
@@ -71,119 +94,321 @@ pub fn deinit(self: *Self) void {
|
||||
self.pak.deinit();
|
||||
self.bios.deinit();
|
||||
self.ppu.deinit();
|
||||
|
||||
// This is so I can deallocate the original `allocator.alloc`. I have to re-make the type
|
||||
// since I'm not keeping it around, This is very jank and bad though
|
||||
// FIXME: please figure out another way
|
||||
self.allocator.free(@ptrCast([*]const ?*anyopaque, self.read_table[0..])[0 .. 3 * table_len]);
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
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) {
|
||||
// https://github.com/ziglang/zig/issues/14705
|
||||
{
|
||||
comptime var i: usize = 0;
|
||||
inline while (i < self.dma.len) : (i += 1) {
|
||||
self.dma[0].reset();
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/ziglang/zig/issues/14705
|
||||
{
|
||||
comptime var i: usize = 0;
|
||||
inline while (i < self.tim.len) : (i += 1) {
|
||||
self.tim[0].reset();
|
||||
}
|
||||
}
|
||||
|
||||
self.io.reset();
|
||||
}
|
||||
|
||||
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 = @intCast(u32, page_size * i);
|
||||
|
||||
ptr.* = switch (addr) {
|
||||
// General Internal Memory
|
||||
0x00 => blk: {
|
||||
if (address < Bios.size)
|
||||
break :blk self.bios.dbgRead(T, self.cpu.?.r[15], aligned_addr);
|
||||
|
||||
break :blk self.readOpenBus(T, address);
|
||||
},
|
||||
0x02 => self.ewram.read(T, aligned_addr),
|
||||
0x03 => self.iwram.read(T, aligned_addr),
|
||||
0x04 => self.readIo(T, address),
|
||||
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
|
||||
0x05 => self.ppu.palette.read(T, aligned_addr),
|
||||
0x06 => self.ppu.vram.read(T, aligned_addr),
|
||||
0x07 => self.ppu.oam.read(T, aligned_addr),
|
||||
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)
|
||||
0x08...0x0D => self.pak.dbgRead(T, aligned_addr),
|
||||
0x0E...0x0F => blk: {
|
||||
const value = self.pak.backup.read(address);
|
||||
|
||||
const multiplier = switch (T) {
|
||||
u32 => 0x01010101,
|
||||
u16 => 0x0101,
|
||||
u8 => 1,
|
||||
else => @compileError("Backup: Unsupported read width"),
|
||||
};
|
||||
|
||||
break :blk @as(T, value) * multiplier;
|
||||
},
|
||||
else => self.readOpenBus(T, address),
|
||||
0x0800_0000...0x0DFF_FFFF => self.fillReadTableExternal(addr),
|
||||
0x0E00_0000...0x0FFF_FFFF => null, // SRAM
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn readIo(self: *const Self, comptime T: type, unaligned_address: u32) T {
|
||||
const maybe_value = io.read(self, T, forceAlign(T, unaligned_address));
|
||||
return if (maybe_value) |value| value else self.readOpenBus(T, unaligned_address);
|
||||
fn fillWriteTable(self: *Self, comptime T: type, table: *[table_len]?*const anyopaque) void {
|
||||
comptime std.debug.assert(T == u32 or T == u16 or T == u8);
|
||||
const vramMirror = @import("ppu/Vram.zig").mirror;
|
||||
|
||||
for (table, 0..) |*ptr, i| {
|
||||
const addr = @intCast(u32, page_size * i);
|
||||
|
||||
ptr.* = switch (addr) {
|
||||
// General Internal Memory
|
||||
0x0000_0000...0x0000_3FFF => null, // BIOS has it's own checks
|
||||
0x0200_0000...0x02FF_FFFF => &self.ewram.buf[addr & 0x3FFFF],
|
||||
0x0300_0000...0x03FF_FFFF => &self.iwram.buf[addr & 0x7FFF],
|
||||
0x0400_0000...0x0400_03FF => null, // I/O
|
||||
|
||||
// Internal Display Memory
|
||||
0x0500_0000...0x05FF_FFFF => if (T != u8) &self.ppu.palette.buf[addr & 0x3FF] else null,
|
||||
0x0600_0000...0x06FF_FFFF => if (T != u8) &self.ppu.vram.buf[vramMirror(addr)] else null,
|
||||
0x0700_0000...0x07FF_FFFF => if (T != u8) &self.ppu.oam.buf[addr & 0x3FF] else null,
|
||||
|
||||
// External Memory (Game Pak)
|
||||
0x0800_0000...0x0DFF_FFFF => null, // ROM
|
||||
0x0E00_0000...0x0FFF_FFFF => null, // SRAM
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn readOpenBus(self: *const Self, comptime T: type, address: u32) T {
|
||||
const r15 = self.cpu.?.r[15];
|
||||
fn fillReadTableExternal(self: *Self, addr: u32) ?*anyopaque {
|
||||
// see `GamePak.zig` for more information about what conditions need to be true
|
||||
// so that a simple pointer dereference isn't possible
|
||||
|
||||
std.debug.assert(addr & @as(u32, page_size - 1) == 0); // addr is guaranteed to be page-aligned
|
||||
|
||||
const start_addr = addr;
|
||||
const end_addr = start_addr + page_size;
|
||||
|
||||
{
|
||||
const data = start_addr <= 0x0800_00C4 and 0x0800_00C4 < end_addr; // GPIO Data
|
||||
const direction = start_addr <= 0x0800_00C6 and 0x0800_00C6 < end_addr; // GPIO Direction
|
||||
const control = start_addr <= 0x0800_00C8 and 0x0800_00C8 < end_addr; // GPIO Control
|
||||
|
||||
const has_gpio = data or direction or control;
|
||||
const gpio_kind = self.pak.gpio.device.kind;
|
||||
|
||||
// There is a GPIO Device, and the current page contains at least one memory-mapped GPIO register
|
||||
if (gpio_kind != .None and has_gpio) return null;
|
||||
}
|
||||
|
||||
if (self.pak.backup.kind == .Eeprom) {
|
||||
if (self.pak.buf.len > 0x100_000) {
|
||||
// We are using a "large" EEPROM which means that if the below check is true
|
||||
// this page has an address that's reserved for the EEPROM and therefore must
|
||||
// be handled in slowmem
|
||||
if (addr & 0x1FF_FFFF > 0x1FF_FEFF) return null;
|
||||
} else {
|
||||
// We are using a "small" EEPROM which means that if the below check is true
|
||||
// (that is, we're in the 0xD address page) then we must handle at least one
|
||||
// address in this page in slowmem
|
||||
if (@truncate(u4, addr >> 24) == 0xD) return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, the GamePak has some unique behaviour for reads past the end of the ROM,
|
||||
// so those will be handled by slowmem as well
|
||||
const masked_addr = addr & 0x1FF_FFFF;
|
||||
if (masked_addr >= self.pak.buf.len) return null;
|
||||
|
||||
return &self.pak.buf[masked_addr];
|
||||
}
|
||||
|
||||
fn readIo(self: *const Self, comptime T: type, address: u32) T {
|
||||
return io.read(self, T, address) orelse self.openBus(T, address);
|
||||
}
|
||||
|
||||
fn openBus(self: *const Self, comptime T: type, address: u32) T {
|
||||
@setCold(true);
|
||||
const r15 = self.cpu.r[15];
|
||||
|
||||
const word = blk: {
|
||||
// If u32 Open Bus, read recently fetched opcode (PC + 8)
|
||||
if (!self.cpu.?.cpsr.t.read()) break :blk self.dbgRead(u32, r15 + 4);
|
||||
// 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);
|
||||
|
||||
// PC + 2 = stage[0]
|
||||
// PC + 4 = stage[1]
|
||||
// PC + 6 = Need a Debug Read for this?
|
||||
|
||||
switch (page) {
|
||||
// EWRAM, PALRAM, VRAM, and Game ROM (16-bit)
|
||||
0x02, 0x05, 0x06, 0x08...0x0D => {
|
||||
// (PC + 4)
|
||||
const halfword = self.dbgRead(u16, r15 + 2);
|
||||
|
||||
break :blk @as(u32, halfword) << 16 | halfword;
|
||||
const halfword: u32 = @truncate(u16, self.cpu.pipe.stage[1].?);
|
||||
break :blk halfword << 16 | halfword;
|
||||
},
|
||||
|
||||
// BIOS or OAM (32-bit)
|
||||
0x00, 0x07 => {
|
||||
// Aligned: (PC + 6) | (PC + 4)
|
||||
// Unaligned: (PC + 4) | (PC + 2)
|
||||
const offset: u32 = if (address & 3 == 0b00) 2 else 0;
|
||||
const aligned = address & 3 == 0b00;
|
||||
|
||||
break :blk @as(u32, self.dbgRead(u16, r15 + 2 + offset)) << 16 | self.dbgRead(u16, r15 + offset);
|
||||
// TODO: What to do on PC + 6?
|
||||
const high: u32 = if (aligned) self.dbgRead(u16, r15 + 4) else @truncate(u16, self.cpu.pipe.stage[1].?);
|
||||
const low: u32 = @truncate(u16, self.cpu.pipe.stage[@boolToInt(aligned)].?);
|
||||
|
||||
break :blk high << 16 | low;
|
||||
},
|
||||
|
||||
// IWRAM (16-bit but special)
|
||||
0x03 => {
|
||||
// Aligned: (PC + 2) | (PC + 4)
|
||||
// Unaligned: (PC + 4) | (PC + 2)
|
||||
const offset: u32 = if (address & 3 == 0b00) 2 else 0;
|
||||
const aligned = address & 3 == 0b00;
|
||||
|
||||
break :blk @as(u32, self.dbgRead(u16, r15 + 2 - offset)) << 16 | self.dbgRead(u16, r15 + offset);
|
||||
const high: u32 = @truncate(u16, self.cpu.pipe.stage[1 - @boolToInt(aligned)].?);
|
||||
const low: u32 = @truncate(u16, self.cpu.pipe.stage[@boolToInt(aligned)].?);
|
||||
|
||||
break :blk high << 16 | low;
|
||||
},
|
||||
else => {
|
||||
log.err("THUMB open bus read from 0x{X:0>2} page @0x{X:0>8}", .{ page, address });
|
||||
@panic("invariant most-likely broken");
|
||||
},
|
||||
else => unreachable,
|
||||
}
|
||||
};
|
||||
|
||||
return @truncate(T, rotr(u32, word, 8 * (address & 3)));
|
||||
return @truncate(T, 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[@boolToInt(T == u32)][@truncate(u4, unaligned_address >> 24)];
|
||||
|
||||
// We're doing some serious out-of-bounds open-bus reads
|
||||
if (page >= table_len) return self.openBus(T, unaligned_address);
|
||||
|
||||
if (self.read_table[page]) |some_ptr| {
|
||||
// We have a pointer to a page, cast the pointer to it's underlying type
|
||||
const Ptr = [*]const T;
|
||||
const ptr = @ptrCast(Ptr, @alignCast(@alignOf(std.meta.Child(Ptr)), some_ptr));
|
||||
|
||||
// Note: We don't check array length, since we force align the
|
||||
// lower bits of the address as the GBA would
|
||||
return ptr[forceAlign(T, offset) / @sizeOf(T)];
|
||||
}
|
||||
|
||||
return self.slowRead(T, unaligned_address);
|
||||
}
|
||||
|
||||
pub fn dbgRead(self: *const Self, comptime T: type, unaligned_address: u32) T {
|
||||
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits;
|
||||
const page = unaligned_address >> bits;
|
||||
const offset = unaligned_address & (page_size - 1);
|
||||
|
||||
// We're doing some serious out-of-bounds open-bus reads
|
||||
if (page >= table_len) return self.openBus(T, unaligned_address);
|
||||
|
||||
if (self.read_table[page]) |some_ptr| {
|
||||
// We have a pointer to a page, cast the pointer to it's underlying type
|
||||
const Ptr = [*]const T;
|
||||
const ptr = @ptrCast(Ptr, @alignCast(@alignOf(std.meta.Child(Ptr)), some_ptr));
|
||||
|
||||
// Note: We don't check array length, since we force align the
|
||||
// lower bits of the address as the GBA would
|
||||
return ptr[forceAlign(T, offset) / @sizeOf(T)];
|
||||
}
|
||||
|
||||
return self.dbgSlowRead(T, unaligned_address);
|
||||
}
|
||||
|
||||
fn slowRead(self: *Self, comptime T: type, unaligned_address: u32) T {
|
||||
@setCold(true);
|
||||
|
||||
const page = @truncate(u8, unaligned_address >> 24);
|
||||
const address = forceAlign(T, unaligned_address);
|
||||
|
||||
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.readOpenBus(T, 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 = @truncate(u8, 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,
|
||||
@@ -192,50 +417,124 @@ 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.readOpenBus(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[@boolToInt(T == u32)][@truncate(u4, unaligned_address >> 24)];
|
||||
|
||||
// We're doing some serious out-of-bounds open-bus writes, they do nothing though
|
||||
if (page >= table_len) return;
|
||||
|
||||
if (self.write_tables[@boolToInt(T == u8)][page]) |some_ptr| {
|
||||
// We have a pointer to a page, cast the pointer to it's underlying type
|
||||
const Ptr = [*]T;
|
||||
const ptr = @ptrCast(Ptr, @alignCast(@alignOf(std.meta.Child(Ptr)), some_ptr));
|
||||
|
||||
// Note: We don't check array length, since we force align the
|
||||
// lower bits of the address as the GBA would
|
||||
ptr[forceAlign(T, offset) / @sizeOf(T)] = value;
|
||||
} else {
|
||||
// we can return early if this is an 8-bit OAM write
|
||||
if (T == u8 and @truncate(u8, unaligned_address >> 24) == 0x07) return;
|
||||
|
||||
self.slowWrite(T, unaligned_address, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mostly Identical to `Bus.write`, slowmeme is handled by `Bus.dbgSlowWrite`
|
||||
pub fn dbgWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
|
||||
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits;
|
||||
const page = unaligned_address >> bits;
|
||||
const offset = unaligned_address & (page_size - 1);
|
||||
|
||||
// We're doing some serious out-of-bounds open-bus writes, they do nothing though
|
||||
if (page >= table_len) return;
|
||||
|
||||
if (self.write_tables[@boolToInt(T == u8)][page]) |some_ptr| {
|
||||
// We have a pointer to a page, cast the pointer to it's underlying type
|
||||
const Ptr = [*]T;
|
||||
const ptr = @ptrCast(Ptr, @alignCast(@alignOf(std.meta.Child(Ptr)), some_ptr));
|
||||
|
||||
// Note: We don't check array length, since we force align the
|
||||
// lower bits of the address as the GBA would
|
||||
ptr[forceAlign(T, offset) / @sizeOf(T)] = value;
|
||||
} else {
|
||||
// we can return early if this is an 8-bit OAM write
|
||||
if (T == u8 and @truncate(u8, unaligned_address >> 24) == 0x07) return;
|
||||
|
||||
self.dbgSlowWrite(T, unaligned_address, value);
|
||||
}
|
||||
}
|
||||
|
||||
fn slowWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
|
||||
@setCold(true);
|
||||
|
||||
const page = @truncate(u8, unaligned_address >> 24);
|
||||
const address = forceAlign(T, unaligned_address);
|
||||
|
||||
switch (page) {
|
||||
// General Internal Memory
|
||||
0x00 => self.bios.write(T, 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(u8, 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 = @truncate(u8, unaligned_address >> 24);
|
||||
const address = forceAlign(T, unaligned_address);
|
||||
|
||||
switch (page) {
|
||||
// General Internal Memory
|
||||
0x00 => self.bios.write(T, address, value),
|
||||
0x02 => unreachable, // completely handled by fastmem
|
||||
0x03 => unreachable, // completely handled by fastmem
|
||||
0x04 => return, // FIXME: Let debug writes mess with I/O
|
||||
|
||||
// Internal Display Memory
|
||||
0x05 => self.ppu.palette.write(T, address, value),
|
||||
0x06 => self.ppu.vram.write(T, self.ppu.dispcnt, address, value),
|
||||
0x07 => unreachable, // completely handled by fastmem
|
||||
|
||||
// External Memory (Game Pak)
|
||||
0x08...0x0D => return, // FIXME: Debug Write to Backup/GPIO w/out messing with state
|
||||
0x0E...0x0F => return, // FIXME: Debug Write to Backup w/out messing with state
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
inline fn rotateBy(comptime T: type, address: u32) u32 {
|
||||
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"),
|
||||
};
|
||||
|
308
src/core/apu.zig
308
src/core/apu.zig
@@ -3,8 +3,6 @@ const SDL = @import("sdl2");
|
||||
const io = @import("bus/io.zig");
|
||||
const util = @import("../util.zig");
|
||||
|
||||
const AudioDeviceId = SDL.SDL_AudioDeviceID;
|
||||
|
||||
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
||||
const ToneSweep = @import("apu/ToneSweep.zig");
|
||||
@@ -14,133 +12,216 @@ const Noise = @import("apu/Noise.zig");
|
||||
|
||||
const SoundFifo = std.fifo.LinearFifo(u8, .{ .Static = 0x20 });
|
||||
|
||||
const intToBytes = @import("../util.zig").intToBytes;
|
||||
const setHi = @import("../util.zig").setHi;
|
||||
const setLo = @import("../util.zig").setLo;
|
||||
const getHalf = util.getHalf;
|
||||
const setHalf = util.setHalf;
|
||||
|
||||
const log = std.log.scoped(.APU);
|
||||
|
||||
pub const host_sample_rate = 1 << 15;
|
||||
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 = @truncate(u8, addr);
|
||||
const byte_addr = @truncate(u8, addr);
|
||||
|
||||
return switch (T) {
|
||||
u16 => switch (byte) {
|
||||
0x60 => apu.ch1.sound1CntL(),
|
||||
0x62 => apu.ch1.sound1CntH(),
|
||||
u32 => switch (byte_addr) {
|
||||
0x60 => @as(T, apu.ch1.sound1CntH()) << 16 | apu.ch1.sound1CntL(),
|
||||
0x64 => apu.ch1.sound1CntX(),
|
||||
0x68 => apu.ch2.sound2CntL(),
|
||||
0x6C => apu.ch2.sound2CntH(),
|
||||
|
||||
0x70 => apu.ch3.select.raw & 0xE0, // SOUND3CNT_L
|
||||
0x72 => apu.ch3.sound3CntH(),
|
||||
0x74 => apu.ch3.freq.raw & 0x4000, // SOUND3CNT_X
|
||||
|
||||
0x70 => @as(T, apu.ch3.sound3CntH()) << 16 | apu.ch3.sound3CntL(),
|
||||
0x74 => apu.ch3.sound3CntX(),
|
||||
0x78 => apu.ch4.sound4CntL(),
|
||||
0x7C => apu.ch4.sound4CntH(),
|
||||
|
||||
0x80 => apu.psg_cnt.raw & 0xFF77, // SOUNDCNT_L
|
||||
0x82 => apu.dma_cnt.raw & 0x770F, // SOUNDCNT_H
|
||||
0x80 => @as(T, apu.dma_cnt.raw) << 16 | apu.psg_cnt.raw, // SOUNDCNT_H, SOUNDCNT_L
|
||||
0x84 => apu.soundCntX(),
|
||||
0x88 => apu.bias.raw, // SOUNDBIAS, high is unused
|
||||
0x8C => null,
|
||||
0x90, 0x94, 0x98, 0x9C => apu.ch3.wave_dev.read(T, apu.ch3.select, addr),
|
||||
0xA0 => null, // FIFO_A
|
||||
0xA4 => null, // FIFO_B
|
||||
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
||||
},
|
||||
u16 => switch (byte_addr) {
|
||||
0x60 => apu.ch1.sound1CntL(),
|
||||
0x62 => apu.ch1.sound1CntH(),
|
||||
0x64 => apu.ch1.sound1CntX(),
|
||||
0x66 => 0x0000, // suite.gba expects 0x0000, not 0xDEAD
|
||||
0x68 => apu.ch2.sound2CntL(),
|
||||
0x6A => 0x0000,
|
||||
0x6C => apu.ch2.sound2CntH(),
|
||||
0x6E => 0x0000,
|
||||
0x70 => apu.ch3.sound3CntL(),
|
||||
0x72 => apu.ch3.sound3CntH(),
|
||||
0x74 => apu.ch3.sound3CntX(),
|
||||
0x76 => 0x0000,
|
||||
0x78 => apu.ch4.sound4CntL(),
|
||||
0x7A => 0x0000,
|
||||
0x7C => apu.ch4.sound4CntH(),
|
||||
0x7E => 0x0000,
|
||||
0x80 => apu.soundCntL(),
|
||||
0x82 => apu.soundCntH(),
|
||||
0x84 => apu.soundCntX(),
|
||||
0x86 => 0x0000,
|
||||
0x88 => apu.bias.raw, // SOUNDBIAS
|
||||
0x8A => 0x0000,
|
||||
0x8C, 0x8E => null,
|
||||
0x90, 0x92, 0x94, 0x96, 0x98, 0x9A, 0x9C, 0x9E => apu.ch3.wave_dev.read(T, apu.ch3.select, addr),
|
||||
0xA0, 0xA2 => null, // FIFO_A
|
||||
0xA4, 0xA6 => null, // FIFO_B
|
||||
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
||||
},
|
||||
u8 => switch (byte_addr) {
|
||||
0x60, 0x61 => @truncate(T, @as(u16, apu.ch1.sound1CntL()) >> getHalf(byte_addr)),
|
||||
0x62, 0x63 => @truncate(T, apu.ch1.sound1CntH() >> getHalf(byte_addr)),
|
||||
0x64, 0x65 => @truncate(T, apu.ch1.sound1CntX() >> getHalf(byte_addr)),
|
||||
0x66, 0x67 => 0x00, // assuming behaviour is identical to that of 16-bit reads
|
||||
0x68, 0x69 => @truncate(T, apu.ch2.sound2CntL() >> getHalf(byte_addr)),
|
||||
0x6A, 0x6B => 0x00,
|
||||
0x6C, 0x6D => @truncate(T, apu.ch2.sound2CntH() >> getHalf(byte_addr)),
|
||||
0x6E, 0x6F => 0x00,
|
||||
0x70, 0x71 => @truncate(T, @as(u16, apu.ch3.sound3CntL()) >> getHalf(byte_addr)), // SOUND3CNT_L
|
||||
0x72, 0x73 => @truncate(T, apu.ch3.sound3CntH() >> getHalf(byte_addr)),
|
||||
0x74, 0x75 => @truncate(T, apu.ch3.sound3CntX() >> getHalf(byte_addr)), // SOUND3CNT_L
|
||||
0x76, 0x77 => 0x00,
|
||||
0x78, 0x79 => @truncate(T, apu.ch4.sound4CntL() >> getHalf(byte_addr)),
|
||||
0x7A, 0x7B => 0x00,
|
||||
0x7C, 0x7D => @truncate(T, apu.ch4.sound4CntH() >> getHalf(byte_addr)),
|
||||
0x7E, 0x7F => 0x00,
|
||||
0x80, 0x81 => @truncate(T, apu.soundCntL() >> getHalf(byte_addr)), // SOUNDCNT_L
|
||||
0x82, 0x83 => @truncate(T, apu.soundCntH() >> getHalf(byte_addr)), // SOUNDCNT_H
|
||||
0x84, 0x85 => @truncate(T, @as(u16, apu.soundCntX()) >> getHalf(byte_addr)),
|
||||
0x86, 0x87 => 0x00,
|
||||
0x88, 0x89 => @truncate(T, apu.bias.raw >> getHalf(byte_addr)), // SOUNDBIAS
|
||||
0x8A, 0x8B => 0x00,
|
||||
0x8C...0x8F => null,
|
||||
0x90...0x9F => apu.ch3.wave_dev.read(T, apu.ch3.select, addr),
|
||||
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
||||
0xA0, 0xA1, 0xA2, 0xA3 => null, // FIFO_A
|
||||
0xA4, 0xA5, 0xA6, 0xA7 => null, // FIFO_B
|
||||
else => util.io.read.err(T, log, "unexpected {} read from 0x{X:0>8}", .{ T, addr }),
|
||||
},
|
||||
u8 => switch (byte) {
|
||||
0x60 => apu.ch1.sound1CntL(), // NR10
|
||||
0x62 => apu.ch1.duty.raw, // NR11
|
||||
0x63 => apu.ch1.envelope.raw, // NR12
|
||||
0x68 => apu.ch2.duty.raw, // NR21
|
||||
0x69 => apu.ch2.envelope.raw, // NR22
|
||||
0x73 => apu.ch3.vol.raw, // NR32
|
||||
0x79 => apu.ch4.envelope.raw, // NR42
|
||||
0x7C => apu.ch4.poly.raw, // NR43
|
||||
0x81 => @truncate(u8, apu.psg_cnt.raw >> 8), // NR51
|
||||
0x84 => apu.soundCntX(),
|
||||
0x89 => @truncate(u8, apu.bias.raw >> 8), // SOUNDBIAS_H
|
||||
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
||||
},
|
||||
u32 => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
||||
else => @compileError("APU: Unsupported read width"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void {
|
||||
const byte = @truncate(u8, addr);
|
||||
const byte_addr = @truncate(u8, addr);
|
||||
|
||||
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return;
|
||||
|
||||
switch (T) {
|
||||
u32 => switch (byte) {
|
||||
u32 => {
|
||||
// 0x80 and 0x81 handled in setSoundCnt
|
||||
if (byte_addr < 0x80 and !apu.cnt.apu_enable.read()) return;
|
||||
|
||||
switch (byte_addr) {
|
||||
0x60 => apu.ch1.setSound1Cnt(value),
|
||||
0x64 => apu.ch1.setSound1CntX(&apu.fs, @truncate(u16, value)),
|
||||
|
||||
0x68 => apu.ch2.setSound2CntL(@truncate(u16, value)),
|
||||
0x6C => apu.ch2.setSound2CntH(&apu.fs, @truncate(u16, value)),
|
||||
|
||||
0x70 => apu.ch3.setSound3Cnt(value),
|
||||
0x74 => apu.ch3.setSound3CntX(&apu.fs, @truncate(u16, value)),
|
||||
|
||||
0x78 => apu.ch4.setSound4CntL(@truncate(u16, value)),
|
||||
0x7C => apu.ch4.setSound4CntH(&apu.fs, @truncate(u16, value)),
|
||||
|
||||
0x80 => apu.setSoundCnt(value),
|
||||
// WAVE_RAM
|
||||
0x90...0x9F => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
|
||||
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1),
|
||||
0x88 => apu.bias.raw = @truncate(u16, value),
|
||||
0x8C => {},
|
||||
|
||||
0x90, 0x94, 0x98, 0x9C => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
|
||||
0xA0 => apu.chA.push(value), // FIFO_A
|
||||
0xA4 => apu.chB.push(value), // FIFO_B
|
||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||
}
|
||||
},
|
||||
u16 => switch (byte) {
|
||||
u16 => {
|
||||
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return;
|
||||
|
||||
switch (byte_addr) {
|
||||
0x60 => apu.ch1.setSound1CntL(@truncate(u8, value)), // SOUND1CNT_L
|
||||
0x62 => apu.ch1.setSound1CntH(value),
|
||||
0x64 => apu.ch1.setSound1CntX(&apu.fs, value),
|
||||
0x66 => {},
|
||||
|
||||
0x68 => apu.ch2.setSound2CntL(value),
|
||||
0x6A => {},
|
||||
0x6C => apu.ch2.setSound2CntH(&apu.fs, value),
|
||||
0x6E => {},
|
||||
|
||||
0x70 => apu.ch3.setSound3CntL(@truncate(u8, value)),
|
||||
0x72 => apu.ch3.setSound3CntH(value),
|
||||
0x74 => apu.ch3.setSound3CntX(&apu.fs, value),
|
||||
0x76 => {},
|
||||
|
||||
0x78 => apu.ch4.setSound4CntL(value),
|
||||
0x7A => {},
|
||||
0x7C => apu.ch4.setSound4CntH(&apu.fs, value),
|
||||
0x7E => {},
|
||||
|
||||
0x80 => apu.psg_cnt.raw = value, // SOUNDCNT_L
|
||||
0x80 => apu.setSoundCntL(value),
|
||||
0x82 => apu.setSoundCntH(value),
|
||||
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1),
|
||||
0x86 => {},
|
||||
0x88 => apu.bias.raw = value, // SOUNDBIAS
|
||||
// WAVE_RAM
|
||||
0x90...0x9F => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
|
||||
0x8A, 0x8C, 0x8E => {},
|
||||
|
||||
0x90, 0x92, 0x94, 0x96, 0x98, 0x9A, 0x9C, 0x9E => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
|
||||
0xA0, 0xA2 => log.err("Tried to write 0x{X:0>4}{} to FIFO_A", .{ value, T }),
|
||||
0xA4, 0xA6 => log.err("Tried to write 0x{X:0>4}{} to FIFO_B", .{ value, T }),
|
||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||
}
|
||||
},
|
||||
u8 => switch (byte) {
|
||||
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),
|
||||
0x63 => apu.ch1.setNr12(value),
|
||||
0x64 => apu.ch1.setNr13(value),
|
||||
0x65 => apu.ch1.setNr14(&apu.fs, value),
|
||||
0x66, 0x67 => {},
|
||||
|
||||
0x68 => apu.ch2.setNr21(value),
|
||||
0x69 => apu.ch2.setNr22(value),
|
||||
0x6A, 0x6B => {},
|
||||
0x6C => apu.ch2.setNr23(value),
|
||||
0x6D => apu.ch2.setNr24(&apu.fs, value),
|
||||
0x6E, 0x6F => {},
|
||||
|
||||
0x70 => apu.ch3.setSound3CntL(value), // NR30
|
||||
0x71 => {},
|
||||
0x72 => apu.ch3.setNr31(value),
|
||||
0x73 => apu.ch3.vol.raw = value, // NR32
|
||||
0x74 => apu.ch3.setNr33(value),
|
||||
0x75 => apu.ch3.setNr34(&apu.fs, value),
|
||||
0x76, 0x77 => {},
|
||||
|
||||
0x78 => apu.ch4.setNr41(value),
|
||||
0x79 => apu.ch4.setNr42(value),
|
||||
0x7A, 0x7B => {},
|
||||
0x7C => apu.ch4.poly.raw = value, // NR 43
|
||||
0x7D => apu.ch4.setNr44(&apu.fs, value),
|
||||
0x7E, 0x7F => {},
|
||||
|
||||
0x80, 0x81 => apu.setSoundCntL(setHalf(u16, apu.psg_cnt.raw, byte_addr, value)),
|
||||
0x82, 0x83 => apu.setSoundCntH(setHalf(u16, apu.dma_cnt.raw, byte_addr, value)),
|
||||
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1),
|
||||
0x85 => {},
|
||||
0x86, 0x87 => {},
|
||||
0x88, 0x89 => apu.bias.raw = setHalf(u16, apu.bias.raw, byte_addr, value), // SOUNDBIAS
|
||||
0x8A...0x8F => {},
|
||||
|
||||
0x80 => apu.setNr50(value),
|
||||
0x81 => apu.setNr51(value),
|
||||
0x82 => apu.setSoundCntH(setLo(u16, apu.dma_cnt.raw, value)),
|
||||
0x83 => apu.setSoundCntH(setHi(u16, apu.dma_cnt.raw, value)),
|
||||
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1), // NR52
|
||||
0x89 => apu.setSoundBiasH(value),
|
||||
0x90...0x9F => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
|
||||
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"),
|
||||
}
|
||||
@@ -189,7 +270,7 @@ pub const Apu = struct {
|
||||
.bias = .{ .raw = 0x0200 },
|
||||
|
||||
.sampling_cycle = 0b00,
|
||||
.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, 1 << 15, SDL.AUDIO_U16, 2, host_sample_rate).?,
|
||||
.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, 1 << 15, host_format, 2, host_rate).?,
|
||||
.sched = sched,
|
||||
|
||||
.capacitor = 0,
|
||||
@@ -197,29 +278,71 @@ 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.psg_cnt.raw = @truncate(u16, value);
|
||||
if (self.cnt.apu_enable.read()) self.setSoundCntL(@truncate(u16, value));
|
||||
self.setSoundCntH(@truncate(u16, value >> 16));
|
||||
}
|
||||
|
||||
/// SOUNDCNT_L
|
||||
pub fn soundCntL(self: *const Self) u16 {
|
||||
return self.psg_cnt.raw & 0xFF77;
|
||||
}
|
||||
|
||||
/// SOUNDCNT_L
|
||||
pub fn setSoundCntL(self: *Self, value: u16) void {
|
||||
self.psg_cnt.raw = value;
|
||||
}
|
||||
|
||||
/// SOUNDCNT_H
|
||||
pub fn setSoundCntH(self: *Self, value: u16) void {
|
||||
const new: io.DmaSoundControl = .{ .raw = value };
|
||||
@@ -232,6 +355,11 @@ pub const Apu = struct {
|
||||
self.dma_cnt = new;
|
||||
}
|
||||
|
||||
/// SOUNDCNT_H
|
||||
pub fn soundCntH(self: *const Self) u16 {
|
||||
return self.dma_cnt.raw & 0x770F;
|
||||
}
|
||||
|
||||
/// NR52
|
||||
pub fn setSoundCntX(self: *Self, value: bool) void {
|
||||
self.cnt.apu_enable.write(value);
|
||||
@@ -240,13 +368,16 @@ 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,20 +393,6 @@ pub const Apu = struct {
|
||||
return apu_enable << 7 | ch4_enable << 3 | ch3_enable << 2 | ch2_enable << 1 | ch1_enable;
|
||||
}
|
||||
|
||||
/// NR50
|
||||
pub fn setNr50(self: *Self, byte: u8) void {
|
||||
self.psg_cnt.raw = (self.psg_cnt.raw & 0xFF00) | byte;
|
||||
}
|
||||
|
||||
/// NR51
|
||||
pub fn setNr51(self: *Self, byte: u8) void {
|
||||
self.psg_cnt.raw = @as(u16, byte) << 8 | (self.psg_cnt.raw & 0xFF);
|
||||
}
|
||||
|
||||
pub fn setSoundBiasH(self: *Self, byte: u8) void {
|
||||
self.bias.raw = (@as(u16, byte) << 8) | (self.bias.raw & 0xFF);
|
||||
}
|
||||
|
||||
pub fn sampleAudio(self: *Self, late: u64) void {
|
||||
self.sched.push(.SampleAudio, self.interval() -| late);
|
||||
|
||||
@@ -327,8 +444,8 @@ 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;
|
||||
|
||||
@@ -339,7 +456,6 @@ pub const Apu = struct {
|
||||
const ext_left = (clamped_left << 5) | (clamped_left >> 6);
|
||||
const ext_right = (clamped_right << 5) | (clamped_right >> 6);
|
||||
|
||||
// FIXME: This rarely happens
|
||||
if (self.sampling_cycle != self.bias.sampling_cycle.read()) self.replaceSDLResampler();
|
||||
|
||||
_ = SDL.SDL_AudioStreamPut(self.stream, &[2]u16{ ext_left, ext_right }, 2 * @sizeOf(u16));
|
||||
@@ -356,7 +472,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), SDL.AUDIO_U16, 2, host_sample_rate).?;
|
||||
self.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, @intCast(c_int, sample_rate), host_format, 2, host_rate).?;
|
||||
}
|
||||
|
||||
fn interval(self: *const Self) u64 {
|
||||
@@ -405,11 +521,15 @@ pub const Apu = struct {
|
||||
if (!self.cnt.apu_enable.read()) return;
|
||||
|
||||
if (@boolToInt(self.dma_cnt.chA_timer.read()) == tim_id) {
|
||||
if (!self.chA.enabled) return;
|
||||
|
||||
self.chA.updateSample();
|
||||
if (self.chA.len() <= 15) cpu.bus.dma[1].requestAudio(0x0400_00A0);
|
||||
}
|
||||
|
||||
if (@boolToInt(self.dma_cnt.chB_timer.read()) == tim_id) {
|
||||
if (!self.chB.enabled) return;
|
||||
|
||||
self.chB.updateSample();
|
||||
if (self.chB.len() <= 15) cpu.bus.dma[2].requestAudio(0x0400_00A4);
|
||||
}
|
||||
@@ -423,17 +543,31 @@ pub fn DmaSound(comptime kind: DmaSoundKind) type {
|
||||
fifo: SoundFifo,
|
||||
kind: DmaSoundKind,
|
||||
sample: i8,
|
||||
enabled: bool,
|
||||
|
||||
fn init() Self {
|
||||
return .{
|
||||
.fifo = SoundFifo.init(),
|
||||
.kind = kind,
|
||||
.sample = 0,
|
||||
.enabled = false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Used when resetting hte emulator (not emulation code)
|
||||
fn reset(self: *Self) void {
|
||||
self.* = Self.init();
|
||||
}
|
||||
|
||||
pub fn push(self: *Self, value: u32) void {
|
||||
self.fifo.write(&intToBytes(u32, value)) catch |e| log.err("{} Error: {}", .{ kind, e });
|
||||
if (!self.enabled) self.enable();
|
||||
|
||||
self.fifo.write(std.mem.asBytes(&value)) catch |e| log.err("{} Error: {}", .{ kind, e });
|
||||
}
|
||||
|
||||
fn enable(self: *Self) void {
|
||||
@setCold(true);
|
||||
self.enabled = true;
|
||||
}
|
||||
|
||||
pub fn len(self: *const Self) usize {
|
||||
@@ -456,13 +590,17 @@ const DmaSoundKind = enum {
|
||||
};
|
||||
|
||||
pub const FrameSequencer = struct {
|
||||
const interval = (1 << 24) / 512;
|
||||
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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
@@ -71,6 +74,11 @@ pub fn setSound3CntL(self: *Self, value: u8) void {
|
||||
if (!self.select.enabled.read()) self.enabled = false;
|
||||
}
|
||||
|
||||
/// NR30
|
||||
pub fn sound3CntL(self: *const Self) u8 {
|
||||
return self.select.raw & 0xE0;
|
||||
}
|
||||
|
||||
/// NR31, NR32
|
||||
pub fn sound3CntH(self: *const Self) u16 {
|
||||
return @as(u16, self.length & 0xE0) << 8;
|
||||
@@ -94,6 +102,11 @@ pub fn setSound3CntX(self: *Self, fs: *const FrameSequencer, value: u16) void {
|
||||
self.setNr34(fs, @truncate(u8, value >> 8));
|
||||
}
|
||||
|
||||
/// NR33, NR34
|
||||
pub fn sound3CntX(self: *const Self) u16 {
|
||||
return self.freq.raw & 0x4000;
|
||||
}
|
||||
|
||||
/// NR33
|
||||
pub fn setNr33(self: *Self, byte: u8) void {
|
||||
self.freq.raw = (self.freq.raw & 0xFF00) | byte;
|
||||
|
@@ -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,7 +23,6 @@ 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);
|
||||
@@ -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;
|
||||
|
@@ -1,9 +1,7 @@
|
||||
//! Linear Feedback Shift Register
|
||||
const io = @import("../../bus/io.zig");
|
||||
|
||||
/// Linear Feedback Shift Register
|
||||
const Scheduler = @import("../../scheduler.zig").Scheduler;
|
||||
const FrameSequencer = @import("../../apu.zig").FrameSequencer;
|
||||
const Noise = @import("../Noise.zig");
|
||||
|
||||
const Self = @This();
|
||||
pub const interval: u64 = (1 << 24) / (1 << 22);
|
||||
@@ -21,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;
|
||||
}
|
||||
@@ -35,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."
|
||||
|
@@ -2,7 +2,6 @@ const std = @import("std");
|
||||
const io = @import("../../bus/io.zig");
|
||||
|
||||
const Scheduler = @import("../../scheduler.zig").Scheduler;
|
||||
const FrameSequencer = @import("../../apu.zig").FrameSequencer;
|
||||
const ToneSweep = @import("../ToneSweep.zig");
|
||||
const Tone = @import("../Tone.zig");
|
||||
|
||||
@@ -21,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);
|
||||
|
@@ -2,8 +2,6 @@ const std = @import("std");
|
||||
const io = @import("../../bus/io.zig");
|
||||
|
||||
const Scheduler = @import("../../scheduler.zig").Scheduler;
|
||||
const FrameSequencer = @import("../../apu.zig").FrameSequencer;
|
||||
const Wave = @import("../Wave.zig");
|
||||
|
||||
const buf_len = 0x20;
|
||||
pub const interval: u64 = (1 << 24) / (1 << 22);
|
||||
@@ -40,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 });
|
||||
|
@@ -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 + 8));
|
||||
log.warn("Open Bus! Read from 0x{X:0>8}, but PC was 0x{X:0>8}", .{ address, r15 });
|
||||
const value = self._read(u32, self.addr_latch);
|
||||
|
||||
return @truncate(T, rotr(u32, value, 8 * rotateBy(T, address)));
|
||||
}
|
||||
|
||||
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(T, rotr(u32, value, 8 * rotateBy(T, address)));
|
||||
}
|
||||
|
||||
/// Read without the GBA safety checks
|
||||
@@ -43,18 +62,23 @@ 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: {
|
||||
if (maybe_path == null) return .{ .buf = null, .allocator = allocator };
|
||||
const path = maybe_path.?;
|
||||
|
||||
const buf = try allocator.alloc(u8, Self.size);
|
||||
errdefer allocator.free(buf);
|
||||
|
||||
const file = try std.fs.cwd().openFile(path, .{});
|
||||
defer file.close();
|
||||
|
||||
break :blk try file.readToEndAlloc(allocator, try file.getEndPos());
|
||||
} else null;
|
||||
const file_len = try file.readAll(buf);
|
||||
if (file_len != Self.size) log.err("Expected BIOS to be {}B, was {}B", .{ Self.size, file_len });
|
||||
|
||||
return Self{
|
||||
.buf = buf,
|
||||
.allocator = allocator,
|
||||
.addr_latch = 0,
|
||||
};
|
||||
return Self{ .buf = buf, .allocator = allocator };
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
self.addr_latch = 0;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
|
@@ -35,6 +35,10 @@ pub fn init(allocator: Allocator) !Self {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
std.mem.set(u8, self.buf, 0);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.allocator.free(self.buf);
|
||||
self.* = undefined;
|
||||
|
@@ -1,14 +1,11 @@
|
||||
const std = @import("std");
|
||||
const Bit = @import("bitfield").Bit;
|
||||
const Bitfield = @import("bitfield").Bitfield;
|
||||
const DateTime = @import("datetime").datetime.Datetime;
|
||||
const config = @import("../../config.zig");
|
||||
|
||||
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||
const Backup = @import("backup.zig").Backup;
|
||||
const Gpio = @import("gpio.zig").Gpio;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const force_rtc = @import("../emu.zig").force_rtc;
|
||||
const log = std.log.scoped(.GamePak);
|
||||
|
||||
const Self = @This();
|
||||
@@ -108,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),
|
||||
@@ -183,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 (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]),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -217,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" {
|
||||
|
@@ -35,6 +35,10 @@ pub fn init(allocator: Allocator) !Self {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
std.mem.set(u8, 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,
|
||||
@@ -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", .{});
|
||||
}
|
||||
|
||||
@@ -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" });
|
||||
@@ -205,6 +205,10 @@ pub const Backup = struct {
|
||||
const file_path = try self.savePath(allocator, path);
|
||||
defer allocator.free(file_path);
|
||||
|
||||
// FIXME: communicate edge case to the user?
|
||||
if (std.mem.eql(u8, &self.title, "ACE LIGHTNIN"))
|
||||
return;
|
||||
|
||||
switch (self.kind) {
|
||||
.Sram, .Flash, .Flash1M, .Eeprom => {
|
||||
const file = try std.fs.createFileAbsolute(file_path, .{});
|
||||
|
@@ -63,7 +63,7 @@ pub const Eeprom = struct {
|
||||
}
|
||||
|
||||
if (self.state == .RequestEnd) {
|
||||
if (bit != 0) log.debug("EEPROM Request did not end in 0u1. TODO: is this ok?", .{});
|
||||
// if (bit != 0) log.debug("EEPROM Request did not end in 0u1. TODO: is this ok?", .{});
|
||||
self.state = .Ready;
|
||||
return;
|
||||
}
|
||||
|
@@ -5,89 +5,140 @@ const DmaControl = @import("io.zig").DmaControl;
|
||||
const Bus = @import("../Bus.zig");
|
||||
const Arm7tdmi = @import("../cpu.zig").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 setHi = util.setHi;
|
||||
const setLo = util.setLo;
|
||||
const getHalf = util.getHalf;
|
||||
const setHalf = util.setHalf;
|
||||
const setQuart = util.setQuart;
|
||||
|
||||
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 = @truncate(u8, addr);
|
||||
const byte_addr = @truncate(u8, addr);
|
||||
|
||||
return switch (T) {
|
||||
u32 => switch (byte) {
|
||||
0xB8 => @as(T, dma.*[0].cnt.raw) << 16,
|
||||
0xC4 => @as(T, dma.*[1].cnt.raw) << 16,
|
||||
0xD0 => @as(T, dma.*[2].cnt.raw) << 16,
|
||||
0xDC => @as(T, dma.*[3].cnt.raw) << 16,
|
||||
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
||||
u32 => switch (byte_addr) {
|
||||
0xB0, 0xB4 => null, // DMA0SAD, DMA0DAD,
|
||||
0xB8 => @as(T, dma.*[0].dmacntH()) << 16, // DMA0CNT_L is write-only
|
||||
0xBC, 0xC0 => null, // DMA1SAD, DMA1DAD
|
||||
0xC4 => @as(T, dma.*[1].dmacntH()) << 16, // DMA1CNT_L is write-only
|
||||
0xC8, 0xCC => null, // DMA2SAD, DMA2DAD
|
||||
0xD0 => @as(T, dma.*[2].dmacntH()) << 16, // DMA2CNT_L is write-only
|
||||
0xD4, 0xD8 => null, // DMA3SAD, DMA3DAD
|
||||
0xDC => @as(T, dma.*[3].dmacntH()) << 16, // DMA3CNT_L is write-only
|
||||
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
||||
},
|
||||
u16 => switch (byte) {
|
||||
0xBA => dma.*[0].cnt.raw,
|
||||
0xC6 => dma.*[1].cnt.raw,
|
||||
0xD2 => dma.*[2].cnt.raw,
|
||||
0xDE => dma.*[3].cnt.raw,
|
||||
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
||||
u16 => switch (byte_addr) {
|
||||
0xB0, 0xB2, 0xB4, 0xB6 => null, // DMA0SAD, DMA0DAD
|
||||
0xB8 => 0x0000, // DMA0CNT_L, suite.gba expects 0x0000 instead of 0xDEAD
|
||||
0xBA => dma.*[0].dmacntH(),
|
||||
|
||||
0xBC, 0xBE, 0xC0, 0xC2 => null, // DMA1SAD, DMA1DAD
|
||||
0xC4 => 0x0000, // DMA1CNT_L
|
||||
0xC6 => dma.*[1].dmacntH(),
|
||||
|
||||
0xC8, 0xCA, 0xCC, 0xCE => null, // DMA2SAD, DMA2DAD
|
||||
0xD0 => 0x0000, // DMA2CNT_L
|
||||
0xD2 => dma.*[2].dmacntH(),
|
||||
|
||||
0xD4, 0xD6, 0xD8, 0xDA => null, // DMA3SAD, DMA3DAD
|
||||
0xDC => 0x0000, // DMA3CNT_L
|
||||
0xDE => dma.*[3].dmacntH(),
|
||||
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
||||
},
|
||||
u8 => switch (byte_addr) {
|
||||
0xB0...0xB7 => null, // DMA0SAD, DMA0DAD
|
||||
0xB8, 0xB9 => 0x00, // DMA0CNT_L
|
||||
0xBA, 0xBB => @truncate(T, dma.*[0].dmacntH() >> getHalf(byte_addr)),
|
||||
|
||||
0xBC...0xC3 => null, // DMA1SAD, DMA1DAD
|
||||
0xC4, 0xC5 => 0x00, // DMA1CNT_L
|
||||
0xC6, 0xC7 => @truncate(T, dma.*[1].dmacntH() >> getHalf(byte_addr)),
|
||||
|
||||
0xC8...0xCF => null, // DMA2SAD, DMA2DAD
|
||||
0xD0, 0xD1 => 0x00, // DMA2CNT_L
|
||||
0xD2, 0xD3 => @truncate(T, dma.*[2].dmacntH() >> getHalf(byte_addr)),
|
||||
|
||||
0xD4...0xDB => null, // DMA3SAD, DMA3DAD
|
||||
0xDC, 0xDD => 0x00, // DMA3CNT_L
|
||||
0xDE, 0xDF => @truncate(T, dma.*[3].dmacntH() >> getHalf(byte_addr)),
|
||||
else => util.io.read.err(T, log, "unexpected {} read from 0x{X:0>8}", .{ T, addr }),
|
||||
},
|
||||
u8 => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
||||
else => @compileError("DMA: Unsupported read width"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write(comptime T: type, dma: *DmaTuple, addr: u32, value: T) void {
|
||||
const byte = @truncate(u8, addr);
|
||||
const byte_addr = @truncate(u8, addr);
|
||||
|
||||
switch (T) {
|
||||
u32 => switch (byte) {
|
||||
u32 => switch (byte_addr) {
|
||||
0xB0 => dma.*[0].setDmasad(value),
|
||||
0xB4 => dma.*[0].setDmadad(value),
|
||||
0xB8 => dma.*[0].setDmacnt(value),
|
||||
|
||||
0xBC => dma.*[1].setDmasad(value),
|
||||
0xC0 => dma.*[1].setDmadad(value),
|
||||
0xC4 => dma.*[1].setDmacnt(value),
|
||||
|
||||
0xC8 => dma.*[2].setDmasad(value),
|
||||
0xCC => dma.*[2].setDmadad(value),
|
||||
0xD0 => dma.*[2].setDmacnt(value),
|
||||
|
||||
0xD4 => dma.*[3].setDmasad(value),
|
||||
0xD8 => dma.*[3].setDmadad(value),
|
||||
0xDC => dma.*[3].setDmacnt(value),
|
||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||
},
|
||||
u16 => switch (byte) {
|
||||
0xB0 => dma.*[0].setDmasad(setLo(u32, dma.*[0].sad, value)),
|
||||
0xB2 => dma.*[0].setDmasad(setHi(u32, dma.*[0].sad, value)),
|
||||
0xB4 => dma.*[0].setDmadad(setLo(u32, dma.*[0].dad, value)),
|
||||
0xB6 => dma.*[0].setDmadad(setHi(u32, dma.*[0].dad, value)),
|
||||
u16 => switch (byte_addr) {
|
||||
0xB0, 0xB2 => dma.*[0].setDmasad(setHalf(u32, dma.*[0].sad, byte_addr, value)),
|
||||
0xB4, 0xB6 => dma.*[0].setDmadad(setHalf(u32, dma.*[0].dad, byte_addr, value)),
|
||||
0xB8 => dma.*[0].setDmacntL(value),
|
||||
0xBA => dma.*[0].setDmacntH(value),
|
||||
|
||||
0xBC => dma.*[1].setDmasad(setLo(u32, dma.*[1].sad, value)),
|
||||
0xBE => dma.*[1].setDmasad(setHi(u32, dma.*[1].sad, value)),
|
||||
0xC0 => dma.*[1].setDmadad(setLo(u32, dma.*[1].dad, value)),
|
||||
0xC2 => dma.*[1].setDmadad(setHi(u32, dma.*[1].dad, value)),
|
||||
0xBC, 0xBE => dma.*[1].setDmasad(setHalf(u32, dma.*[1].sad, byte_addr, value)),
|
||||
0xC0, 0xC2 => dma.*[1].setDmadad(setHalf(u32, dma.*[1].dad, byte_addr, value)),
|
||||
0xC4 => dma.*[1].setDmacntL(value),
|
||||
0xC6 => dma.*[1].setDmacntH(value),
|
||||
|
||||
0xC8 => dma.*[2].setDmasad(setLo(u32, dma.*[2].sad, value)),
|
||||
0xCA => dma.*[2].setDmasad(setHi(u32, dma.*[2].sad, value)),
|
||||
0xCC => dma.*[2].setDmadad(setLo(u32, dma.*[2].dad, value)),
|
||||
0xCE => dma.*[2].setDmadad(setHi(u32, dma.*[2].dad, value)),
|
||||
0xC8, 0xCA => dma.*[2].setDmasad(setHalf(u32, dma.*[2].sad, byte_addr, value)),
|
||||
0xCC, 0xCE => dma.*[2].setDmadad(setHalf(u32, dma.*[2].dad, byte_addr, value)),
|
||||
0xD0 => dma.*[2].setDmacntL(value),
|
||||
0xD2 => dma.*[2].setDmacntH(value),
|
||||
|
||||
0xD4 => dma.*[3].setDmasad(setLo(u32, dma.*[3].sad, value)),
|
||||
0xD6 => dma.*[3].setDmasad(setHi(u32, dma.*[3].sad, value)),
|
||||
0xD8 => dma.*[3].setDmadad(setLo(u32, dma.*[3].dad, value)),
|
||||
0xDA => dma.*[3].setDmadad(setHi(u32, dma.*[3].dad, value)),
|
||||
0xD4, 0xD6 => dma.*[3].setDmasad(setHalf(u32, dma.*[3].sad, byte_addr, value)),
|
||||
0xD8, 0xDA => dma.*[3].setDmadad(setHalf(u32, dma.*[3].dad, byte_addr, value)),
|
||||
0xDC => dma.*[3].setDmacntL(value),
|
||||
0xDE => dma.*[3].setDmacntH(value),
|
||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||
},
|
||||
u8 => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||
u8 => switch (byte_addr) {
|
||||
0xB0, 0xB1, 0xB2, 0xB3 => dma.*[0].setDmasad(setQuart(dma.*[0].sad, byte_addr, value)),
|
||||
0xB4, 0xB5, 0xB6, 0xB7 => dma.*[0].setDmadad(setQuart(dma.*[0].dad, byte_addr, value)),
|
||||
0xB8, 0xB9 => dma.*[0].setDmacntL(setHalf(u16, dma.*[0].word_count, byte_addr, value)),
|
||||
0xBA, 0xBB => dma.*[0].setDmacntH(setHalf(u16, dma.*[0].cnt.raw, byte_addr, value)),
|
||||
|
||||
0xBC, 0xBD, 0xBE, 0xBF => dma.*[1].setDmasad(setQuart(dma.*[1].sad, byte_addr, value)),
|
||||
0xC0, 0xC1, 0xC2, 0xC3 => dma.*[1].setDmadad(setQuart(dma.*[1].dad, byte_addr, value)),
|
||||
0xC4, 0xC5 => dma.*[1].setDmacntL(setHalf(u16, dma.*[1].word_count, byte_addr, value)),
|
||||
0xC6, 0xC7 => dma.*[1].setDmacntH(setHalf(u16, dma.*[1].cnt.raw, byte_addr, value)),
|
||||
|
||||
0xC8, 0xC9, 0xCA, 0xCB => dma.*[2].setDmasad(setQuart(dma.*[2].sad, byte_addr, value)),
|
||||
0xCC, 0xCD, 0xCE, 0xCF => dma.*[2].setDmadad(setQuart(dma.*[2].dad, byte_addr, value)),
|
||||
0xD0, 0xD1 => dma.*[2].setDmacntL(setHalf(u16, dma.*[2].word_count, byte_addr, value)),
|
||||
0xD2, 0xD3 => dma.*[2].setDmacntH(setHalf(u16, dma.*[2].cnt.raw, byte_addr, value)),
|
||||
|
||||
0xD4, 0xD5, 0xD6, 0xD7 => dma.*[3].setDmasad(setQuart(dma.*[3].sad, byte_addr, value)),
|
||||
0xD8, 0xD9, 0xDA, 0xDB => dma.*[3].setDmadad(setQuart(dma.*[3].dad, byte_addr, value)),
|
||||
0xDC, 0xDD => dma.*[3].setDmacntL(setHalf(u16, dma.*[3].word_count, byte_addr, value)),
|
||||
0xDE, 0xDF => dma.*[3].setDmacntH(setHalf(u16, dma.*[3].cnt.raw, byte_addr, value)),
|
||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||
},
|
||||
else => @compileError("DMA: Unsupported write width"),
|
||||
}
|
||||
}
|
||||
@@ -99,6 +150,7 @@ fn DmaController(comptime id: u2) type {
|
||||
|
||||
const sad_mask: u32 = if (id == 0) 0x07FF_FFFF else 0x0FFF_FFFF;
|
||||
const dad_mask: u32 = if (id != 3) 0x07FF_FFFF else 0x0FFF_FFFF;
|
||||
const WordCount = if (id == 3) u16 else u14;
|
||||
|
||||
/// Write-only. The first address in a DMA transfer. (DMASAD)
|
||||
/// Note: use writeSrc instead of manipulating src_addr directly
|
||||
@@ -107,17 +159,19 @@ fn DmaController(comptime id: u2) type {
|
||||
/// Note: Use writeDst instead of manipulatig dst_addr directly
|
||||
dad: u32,
|
||||
/// Write-only. The Word Count for the DMA Transfer (DMACNT_L)
|
||||
word_count: if (id == 3) u16 else u14,
|
||||
word_count: WordCount,
|
||||
/// Read / Write. DMACNT_H
|
||||
/// Note: Use writeControl instead of manipulating cnt directly.
|
||||
cnt: DmaControl,
|
||||
|
||||
/// Internal. The last successfully read value
|
||||
data_latch: u32,
|
||||
/// Internal. Currrent Source Address
|
||||
sad_latch: u32,
|
||||
/// Internal. Current Destination Address
|
||||
dad_latch: u32,
|
||||
/// Internal. Word Count
|
||||
_word_count: if (id == 3) u16 else u14,
|
||||
_word_count: WordCount,
|
||||
|
||||
/// Some DMA Transfers are enabled during Hblank / VBlank and / or
|
||||
/// have delays. Thefore bit 15 of DMACNT isn't actually something
|
||||
@@ -134,11 +188,17 @@ fn DmaController(comptime id: u2) type {
|
||||
// Internals
|
||||
.sad_latch = 0,
|
||||
.dad_latch = 0,
|
||||
.data_latch = 0,
|
||||
|
||||
._word_count = 0,
|
||||
.in_progress = false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
self.* = Self.init();
|
||||
}
|
||||
|
||||
pub fn setDmasad(self: *Self, addr: u32) void {
|
||||
self.sad = addr & sad_mask;
|
||||
}
|
||||
@@ -151,6 +211,10 @@ fn DmaController(comptime id: u2) type {
|
||||
self.word_count = @truncate(@TypeOf(self.word_count), halfword);
|
||||
}
|
||||
|
||||
pub fn dmacntH(self: *const Self) u16 {
|
||||
return self.cnt.raw & if (id == 3) 0xFFE0 else 0xF7E0;
|
||||
}
|
||||
|
||||
pub fn setDmacntH(self: *Self, halfword: u16) void {
|
||||
const new = DmaControl{ .raw = halfword };
|
||||
|
||||
@@ -158,7 +222,7 @@ fn DmaController(comptime id: u2) type {
|
||||
// Reload Internals on Rising Edge.
|
||||
self.sad_latch = self.sad;
|
||||
self.dad_latch = self.dad;
|
||||
self._word_count = if (self.word_count == 0) std.math.maxInt(@TypeOf(self._word_count)) else self.word_count;
|
||||
self._word_count = if (self.word_count == 0) std.math.maxInt(WordCount) else self.word_count;
|
||||
|
||||
// Only a Start Timing of 00 has a DMA Transfer immediately begin
|
||||
self.in_progress = new.start_timing.read() == 0b00;
|
||||
@@ -181,19 +245,31 @@ fn DmaController(comptime id: u2) type {
|
||||
const offset: u32 = if (transfer_type) @sizeOf(u32) else @sizeOf(u16);
|
||||
|
||||
const mask = if (transfer_type) ~@as(u32, 3) else ~@as(u32, 1);
|
||||
const sad_addr = self.sad_latch & mask;
|
||||
const dad_addr = self.dad_latch & mask;
|
||||
|
||||
if (transfer_type) {
|
||||
cpu.bus.write(u32, self.dad_latch & mask, cpu.bus.read(u32, self.sad_latch & mask));
|
||||
if (sad_addr >= 0x0200_0000) self.data_latch = cpu.bus.read(u32, sad_addr);
|
||||
cpu.bus.write(u32, dad_addr, self.data_latch);
|
||||
} else {
|
||||
cpu.bus.write(u16, self.dad_latch & mask, cpu.bus.read(u16, self.sad_latch & mask));
|
||||
if (sad_addr >= 0x0200_0000) {
|
||||
const value: u32 = cpu.bus.read(u16, sad_addr);
|
||||
self.data_latch = value << 16 | value;
|
||||
}
|
||||
|
||||
switch (sad_adj) {
|
||||
cpu.bus.write(u16, dad_addr, @truncate(u16, rotr(u32, self.data_latch, 8 * (dad_addr & 3))));
|
||||
}
|
||||
|
||||
switch (@truncate(u8, sad_addr >> 24)) {
|
||||
// according to fleroviux, DMAs with a source address in ROM misbehave
|
||||
// the resultant behaviour is that the source address will increment despite what DMAXCNT says
|
||||
0x08...0x0D => self.sad_latch +%= offset, // obscure behaviour
|
||||
else => switch (sad_adj) {
|
||||
.Increment => self.sad_latch +%= offset,
|
||||
.Decrement => self.sad_latch -%= offset,
|
||||
// FIXME: Is just ignoring this ok?
|
||||
.IncrementReload => log.err("{} is a prohibited adjustment on SAD", .{sad_adj}),
|
||||
.Fixed => {},
|
||||
},
|
||||
}
|
||||
|
||||
switch (dad_adj) {
|
||||
@@ -266,11 +342,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) {
|
||||
|
@@ -1,6 +1,5 @@
|
||||
const std = @import("std");
|
||||
const Bit = @import("bitfield").Bit;
|
||||
const Bitfield = @import("bitfield").Bitfield;
|
||||
const DateTime = @import("datetime").datetime.Datetime;
|
||||
|
||||
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||
@@ -68,9 +67,11 @@ pub const Gpio = struct {
|
||||
log.info("Device: {}", .{kind});
|
||||
|
||||
const self = try allocator.create(Self);
|
||||
errdefer allocator.destroy(self);
|
||||
|
||||
self.* = .{
|
||||
.data = 0b0000,
|
||||
.direction = 0b1111, // TODO: What is GPIO DIrection set to by default?
|
||||
.direction = 0b1111, // TODO: What is GPIO Direction set to by default?
|
||||
.cnt = 0b0,
|
||||
|
||||
.device = switch (kind) {
|
||||
@@ -292,13 +293,13 @@ pub const Clock = struct {
|
||||
self.cpu.sched.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(u8, now.date.year - 2000));
|
||||
self.month = @truncate(u5, bcd(now.date.month));
|
||||
self.day = @truncate(u6, bcd(now.date.day));
|
||||
self.weekday = @truncate(u3, bcd((now.date.weekday() + 1) % 7)); // API is Monday = 0, Sunday = 6. We want Sunday = 0, Saturday = 6
|
||||
self.hour = @truncate(u6, bcd(now.time.hour));
|
||||
self.minute = @truncate(u7, bcd(now.time.minute));
|
||||
self.second = @truncate(u7, bcd(now.time.second));
|
||||
}
|
||||
|
||||
fn step(self: *Self, value: Data) u4 {
|
||||
@@ -448,16 +449,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);
|
||||
}
|
||||
|
@@ -1,18 +1,16 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const timer = @import("timer.zig");
|
||||
const dma = @import("dma.zig");
|
||||
const apu = @import("../apu.zig");
|
||||
const ppu = @import("../ppu.zig");
|
||||
const util = @import("../../util.zig");
|
||||
|
||||
const Bit = @import("bitfield").Bit;
|
||||
const Bitfield = @import("bitfield").Bitfield;
|
||||
const Bus = @import("../Bus.zig");
|
||||
const DmaController = @import("dma.zig").DmaController;
|
||||
const Scheduler = @import("../scheduler.zig").Scheduler;
|
||||
|
||||
const setHi = util.setLo;
|
||||
const setLo = util.setHi;
|
||||
const getHalf = util.getHalf;
|
||||
const setHalf = util.setHalf;
|
||||
|
||||
const log = std.log.scoped(.@"I/O");
|
||||
|
||||
@@ -24,20 +22,26 @@ 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);
|
||||
@@ -48,9 +52,10 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) ?T {
|
||||
return switch (T) {
|
||||
u32 => switch (address) {
|
||||
// Display
|
||||
0x0400_0000 => bus.ppu.dispcnt.raw,
|
||||
0x0400_0004 => @as(T, bus.ppu.vcount.raw) << 16 | bus.ppu.dispstat.raw,
|
||||
0x0400_0006 => @as(T, bus.ppu.bg[0].cnt.raw) << 16 | bus.ppu.vcount.raw,
|
||||
0x0400_0000...0x0400_0054 => ppu.read(T, &bus.ppu, address),
|
||||
|
||||
// Sound
|
||||
0x0400_0060...0x0400_00A4 => apu.read(T, &bus.apu, address),
|
||||
|
||||
// DMA Transfers
|
||||
0x0400_00B0...0x0400_00DC => dma.read(T, &bus.dma, address),
|
||||
@@ -68,26 +73,18 @@ 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_0200 => @as(u32, bus.io.irq.raw) << 16 | bus.io.ie.raw,
|
||||
0x0400_0204 => bus.io.waitcnt.raw,
|
||||
0x0400_0208 => @boolToInt(bus.io.ime),
|
||||
0x0400_0300 => @enumToInt(bus.io.postflg),
|
||||
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
|
||||
},
|
||||
u16 => switch (address) {
|
||||
// Display
|
||||
0x0400_0000 => bus.ppu.dispcnt.raw,
|
||||
0x0400_0004 => bus.ppu.dispstat.raw,
|
||||
0x0400_0006 => bus.ppu.vcount.raw,
|
||||
0x0400_0008 => bus.ppu.bg[0].cnt.raw,
|
||||
0x0400_000A => bus.ppu.bg[1].cnt.raw,
|
||||
0x0400_000C => bus.ppu.bg[2].cnt.raw,
|
||||
0x0400_000E => bus.ppu.bg[3].cnt.raw,
|
||||
0x0400_004C => util.io.read.todo(log, "Read {} from MOSAIC", .{T}),
|
||||
0x0400_0050 => bus.ppu.bldcnt.raw,
|
||||
0x0400_0052 => bus.ppu.bldalpha.raw,
|
||||
0x0400_0054 => bus.ppu.bldy.raw,
|
||||
0x0400_0000...0x0400_0054 => ppu.read(T, &bus.ppu, address),
|
||||
|
||||
// Sound
|
||||
0x0400_0060...0x0400_009E => apu.read(T, &bus.apu, address),
|
||||
0x0400_0060...0x0400_00A6 => apu.read(T, &bus.apu, address),
|
||||
|
||||
// DMA Transfers
|
||||
0x0400_00B0...0x0400_00DE => dma.read(T, &bus.dma, address),
|
||||
@@ -99,32 +96,38 @@ 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).raw,
|
||||
|
||||
// Serial Communication 2
|
||||
0x0400_0134 => util.io.read.todo(log, "Read {} from RCNT", .{T}),
|
||||
0x0400_0136 => 0x0000,
|
||||
0x0400_0142 => 0x0000,
|
||||
0x0400_015A => 0x0000,
|
||||
|
||||
// Interrupts
|
||||
0x0400_0200 => bus.io.ie.raw,
|
||||
0x0400_0202 => bus.io.irq.raw,
|
||||
0x0400_0204 => util.io.read.todo(log, "Read {} from WAITCNT", .{T}),
|
||||
0x0400_0204 => bus.io.waitcnt.raw,
|
||||
0x0400_0206 => 0x0000,
|
||||
0x0400_0208 => @boolToInt(bus.io.ime),
|
||||
0x0400_020A => 0x0000,
|
||||
0x0400_0300 => @enumToInt(bus.io.postflg),
|
||||
0x0400_0302 => 0x0000,
|
||||
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
|
||||
},
|
||||
u8 => return switch (address) {
|
||||
// Display
|
||||
0x0400_0000 => @truncate(T, bus.ppu.dispcnt.raw),
|
||||
0x0400_0004 => @truncate(T, bus.ppu.dispstat.raw),
|
||||
0x0400_0005 => @truncate(T, bus.ppu.dispcnt.raw >> 8),
|
||||
0x0400_0006 => @truncate(T, bus.ppu.vcount.raw),
|
||||
0x0400_0008 => @truncate(T, bus.ppu.bg[0].cnt.raw),
|
||||
0x0400_0009 => @truncate(T, bus.ppu.bg[0].cnt.raw >> 8),
|
||||
0x0400_000A => @truncate(T, bus.ppu.bg[1].cnt.raw),
|
||||
0x0400_000B => @truncate(T, bus.ppu.bg[1].cnt.raw >> 8),
|
||||
0x0400_0000...0x0400_0055 => ppu.read(T, &bus.ppu, address),
|
||||
|
||||
// Sound
|
||||
0x0400_0060...0x0400_00A7 => apu.read(T, &bus.apu, address),
|
||||
|
||||
// DMA Transfers
|
||||
0x0400_00B0...0x0400_00DF => dma.read(T, &bus.dma, address),
|
||||
|
||||
// Timers
|
||||
0x0400_0100...0x0400_010F => timer.read(T, &bus.tim, address),
|
||||
|
||||
// Serial Communication 1
|
||||
0x0400_0128 => util.io.read.todo(log, "Read {} from SIOCNT_L", .{T}),
|
||||
|
||||
@@ -133,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_0200, 0x0400_0201 => @truncate(T, bus.io.ie.raw >> getHalf(@truncate(u8, address))),
|
||||
0x0400_0202, 0x0400_0203 => @truncate(T, bus.io.irq.raw >> getHalf(@truncate(u8, address))),
|
||||
0x0400_0204, 0x0400_0205 => @truncate(T, bus.io.waitcnt.raw >> getHalf(@truncate(u8, address))),
|
||||
0x0400_0206, 0x0400_0207 => 0x00,
|
||||
0x0400_0208, 0x0400_0209 => @truncate(T, @as(u16, @boolToInt(bus.io.ime)) >> getHalf(@truncate(u8, address))),
|
||||
0x0400_020A, 0x0400_020B => 0x00,
|
||||
0x0400_0300 => @enumToInt(bus.io.postflg),
|
||||
0x0400_0301 => null,
|
||||
0x0400_0302, 0x0400_0303 => 0x00,
|
||||
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
|
||||
},
|
||||
else => @compileError("I/O: Unsupported read width"),
|
||||
@@ -147,34 +160,7 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
|
||||
return switch (T) {
|
||||
u32 => switch (address) {
|
||||
// Display
|
||||
0x0400_0000 => bus.ppu.dispcnt.raw = @truncate(u16, value),
|
||||
0x0400_0004 => {
|
||||
bus.ppu.dispstat.raw = @truncate(u16, value);
|
||||
bus.ppu.vcount.raw = @truncate(u16, value >> 16);
|
||||
},
|
||||
0x0400_0008 => bus.ppu.setAdjCnts(0, value),
|
||||
0x0400_000C => bus.ppu.setAdjCnts(2, value),
|
||||
0x0400_0010 => bus.ppu.setBgOffsets(0, value),
|
||||
0x0400_0014 => bus.ppu.setBgOffsets(1, value),
|
||||
0x0400_0018 => bus.ppu.setBgOffsets(2, value),
|
||||
0x0400_001C => bus.ppu.setBgOffsets(3, value),
|
||||
0x0400_0020 => bus.ppu.aff_bg[0].writePaPb(value),
|
||||
0x0400_0024 => bus.ppu.aff_bg[0].writePcPd(value),
|
||||
0x0400_0028 => bus.ppu.aff_bg[0].setX(bus.ppu.dispstat.vblank.read(), value),
|
||||
0x0400_002C => bus.ppu.aff_bg[0].setY(bus.ppu.dispstat.vblank.read(), value),
|
||||
0x0400_0030 => bus.ppu.aff_bg[1].writePaPb(value),
|
||||
0x0400_0034 => bus.ppu.aff_bg[1].writePcPd(value),
|
||||
0x0400_0038 => bus.ppu.aff_bg[1].setX(bus.ppu.dispstat.vblank.read(), value),
|
||||
0x0400_003C => bus.ppu.aff_bg[1].setY(bus.ppu.dispstat.vblank.read(), value),
|
||||
0x0400_0040 => bus.ppu.win.setH(value),
|
||||
0x0400_0044 => bus.ppu.win.setV(value),
|
||||
0x0400_0048 => bus.ppu.win.setIo(value),
|
||||
0x0400_004C => log.debug("Wrote 0x{X:0>8} to MOSAIC", .{value}),
|
||||
0x0400_0050 => {
|
||||
bus.ppu.bldcnt.raw = @truncate(u16, value);
|
||||
bus.ppu.bldalpha.raw = @truncate(u16, value >> 16);
|
||||
},
|
||||
0x0400_0054 => bus.ppu.bldy.raw = @truncate(u16, value),
|
||||
0x0400_0000...0x0400_0054 => ppu.write(T, &bus.ppu, address, value),
|
||||
0x0400_0058...0x0400_005C => {}, // Unused
|
||||
|
||||
// Sound
|
||||
@@ -210,65 +196,28 @@ 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(u16, value)),
|
||||
0x0400_0208 => bus.io.ime = value & 1 == 1,
|
||||
0x0400_020C...0x0400_021C => {}, // Unused
|
||||
0x0400_0300 => {
|
||||
bus.io.postflg = @intToEnum(PostFlag, 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) {
|
||||
// Display
|
||||
0x0400_0000 => bus.ppu.dispcnt.raw = value,
|
||||
0x0400_0004 => bus.ppu.dispstat.raw = value,
|
||||
0x0400_0006 => {}, // vcount is read-only
|
||||
0x0400_0008 => bus.ppu.bg[0].cnt.raw = value,
|
||||
0x0400_000A => bus.ppu.bg[1].cnt.raw = value,
|
||||
0x0400_000C => bus.ppu.bg[2].cnt.raw = value,
|
||||
0x0400_000E => bus.ppu.bg[3].cnt.raw = value,
|
||||
0x0400_0010 => bus.ppu.bg[0].hofs.raw = value, // TODO: Don't write out every HOFS / VOFS?
|
||||
0x0400_0012 => bus.ppu.bg[0].vofs.raw = value,
|
||||
0x0400_0014 => bus.ppu.bg[1].hofs.raw = value,
|
||||
0x0400_0016 => bus.ppu.bg[1].vofs.raw = value,
|
||||
0x0400_0018 => bus.ppu.bg[2].hofs.raw = value,
|
||||
0x0400_001A => bus.ppu.bg[2].vofs.raw = value,
|
||||
0x0400_001C => bus.ppu.bg[3].hofs.raw = value,
|
||||
0x0400_001E => bus.ppu.bg[3].vofs.raw = value,
|
||||
0x0400_0020 => bus.ppu.aff_bg[0].pa = @bitCast(i16, value),
|
||||
0x0400_0022 => bus.ppu.aff_bg[0].pb = @bitCast(i16, value),
|
||||
0x0400_0024 => bus.ppu.aff_bg[0].pc = @bitCast(i16, value),
|
||||
0x0400_0026 => bus.ppu.aff_bg[0].pd = @bitCast(i16, value),
|
||||
0x0400_0028 => bus.ppu.aff_bg[0].x = @bitCast(i32, setLo(u32, @bitCast(u32, bus.ppu.aff_bg[0].x), value)),
|
||||
0x0400_002A => bus.ppu.aff_bg[0].x = @bitCast(i32, setHi(u32, @bitCast(u32, bus.ppu.aff_bg[0].x), value)),
|
||||
0x0400_002C => bus.ppu.aff_bg[0].y = @bitCast(i32, setLo(u32, @bitCast(u32, bus.ppu.aff_bg[0].y), value)),
|
||||
0x0400_002E => bus.ppu.aff_bg[0].y = @bitCast(i32, setHi(u32, @bitCast(u32, bus.ppu.aff_bg[0].y), value)),
|
||||
0x0400_0030 => bus.ppu.aff_bg[1].pa = @bitCast(i16, value),
|
||||
0x0400_0032 => bus.ppu.aff_bg[1].pb = @bitCast(i16, value),
|
||||
0x0400_0034 => bus.ppu.aff_bg[1].pc = @bitCast(i16, value),
|
||||
0x0400_0036 => bus.ppu.aff_bg[1].pd = @bitCast(i16, value),
|
||||
0x0400_0038 => bus.ppu.aff_bg[1].x = @bitCast(i32, setLo(u32, @bitCast(u32, bus.ppu.aff_bg[1].x), value)),
|
||||
0x0400_003A => bus.ppu.aff_bg[1].x = @bitCast(i32, setHi(u32, @bitCast(u32, bus.ppu.aff_bg[1].x), value)),
|
||||
0x0400_003C => bus.ppu.aff_bg[1].y = @bitCast(i32, setLo(u32, @bitCast(u32, bus.ppu.aff_bg[1].y), value)),
|
||||
0x0400_003E => bus.ppu.aff_bg[1].y = @bitCast(i32, setHi(u32, @bitCast(u32, bus.ppu.aff_bg[1].y), value)),
|
||||
0x0400_0040 => bus.ppu.win.h[0].raw = value,
|
||||
0x0400_0042 => bus.ppu.win.h[1].raw = value,
|
||||
0x0400_0044 => bus.ppu.win.v[0].raw = value,
|
||||
0x0400_0046 => bus.ppu.win.v[1].raw = value,
|
||||
0x0400_0048 => bus.ppu.win.in.raw = value,
|
||||
0x0400_004A => bus.ppu.win.out.raw = value,
|
||||
0x0400_004C => log.debug("Wrote 0x{X:0>4} to MOSAIC", .{value}),
|
||||
0x0400_0050 => bus.ppu.bldcnt.raw = value,
|
||||
0x0400_0052 => bus.ppu.bldalpha.raw = value,
|
||||
0x0400_0054 => bus.ppu.bldy.raw = value,
|
||||
0x0400_004E, 0x0400_0056 => {}, // Not used
|
||||
0x0400_0000...0x0400_0054 => ppu.write(T, &bus.ppu, address, value),
|
||||
0x0400_0056 => {}, // Not used
|
||||
|
||||
// Sound
|
||||
0x0400_0060...0x0400_009E => apu.write(T, &bus.apu, address, value),
|
||||
0x0400_0060...0x0400_00A6 => apu.write(T, &bus.apu, address, value),
|
||||
|
||||
// Dma Transfers
|
||||
0x0400_00B0...0x0400_00DE => dma.write(T, &bus.dma, address, value),
|
||||
|
||||
// Timers
|
||||
0x0400_0100...0x0400_010E => timer.write(T, &bus.tim, address, value),
|
||||
0x0400_0114 => {}, // TODO: Gyakuten Saiban writes 0x8000 to 0x0400_0114
|
||||
0x0400_0114 => {},
|
||||
0x0400_0110 => {}, // Not Used,
|
||||
|
||||
// Serial Communication 1
|
||||
@@ -292,27 +241,29 @@ 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 = @intToEnum(PostFlag, 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) {
|
||||
// Display
|
||||
0x0400_0004 => bus.ppu.dispstat.raw = setLo(u16, bus.ppu.dispstat.raw, value),
|
||||
0x0400_0005 => bus.ppu.dispstat.raw = setHi(u16, bus.ppu.dispstat.raw, value),
|
||||
0x0400_0008 => bus.ppu.bg[0].cnt.raw = setLo(u16, bus.ppu.bg[0].cnt.raw, value),
|
||||
0x0400_0009 => bus.ppu.bg[0].cnt.raw = setHi(u16, bus.ppu.bg[0].cnt.raw, value),
|
||||
0x0400_000A => bus.ppu.bg[1].cnt.raw = setLo(u16, bus.ppu.bg[1].cnt.raw, value),
|
||||
0x0400_000B => bus.ppu.bg[1].cnt.raw = setHi(u16, bus.ppu.bg[1].cnt.raw, value),
|
||||
0x0400_0048 => bus.ppu.win.in.raw = setLo(u16, bus.ppu.win.in.raw, value),
|
||||
0x0400_0049 => bus.ppu.win.in.raw = setHi(u16, bus.ppu.win.in.raw, value),
|
||||
0x0400_004A => bus.ppu.win.out.raw = setLo(u16, bus.ppu.win.out.raw, value),
|
||||
0x0400_0054 => bus.ppu.bldy.raw = setLo(u16, bus.ppu.bldy.raw, value),
|
||||
0x0400_0000...0x0400_0055 => ppu.write(T, &bus.ppu, address, value),
|
||||
|
||||
// Sound
|
||||
0x0400_0060...0x0400_00A7 => apu.write(T, &bus.apu, address, value),
|
||||
|
||||
// Dma Transfers
|
||||
0x0400_00B0...0x0400_00DF => dma.write(T, &bus.dma, address, value),
|
||||
|
||||
// Timers
|
||||
0x0400_0100...0x0400_010F => timer.write(T, &bus.tim, address, value),
|
||||
|
||||
// Serial Communication 1
|
||||
0x0400_0120 => log.debug("Wrote 0x{X:0>2} to SIODATA32_L_L", .{value}),
|
||||
0x0400_0128 => log.debug("Wrote 0x{X:0>2} to SIOCNT_L", .{value}),
|
||||
@@ -322,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(u8, address), value),
|
||||
0x0400_0202 => bus.io.irq.raw &= ~@as(u16, value),
|
||||
0x0400_0203 => bus.io.irq.raw &= ~@as(u16, value) << 8, // TODO: Is this good?
|
||||
0x0400_0204, 0x0400_0205 => bus.io.waitcnt.set(setHalf(u16, @truncate(u16, bus.io.waitcnt.raw), @truncate(u8, address), value)),
|
||||
0x0400_0206, 0x0400_0207 => {},
|
||||
0x0400_0208 => bus.io.ime = value & 1 == 1,
|
||||
0x0400_0300 => bus.io.postflg = std.meta.intToEnum(PostFlag, value & 1) catch unreachable,
|
||||
0x0400_0209 => {},
|
||||
0x0400_020A, 0x0400_020B => {},
|
||||
|
||||
0x0400_0300 => bus.io.postflg = @intToEnum(PostFlag, 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 }),
|
||||
@@ -363,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
|
||||
@@ -384,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),
|
||||
@@ -414,6 +380,31 @@ const KeyInput = extern union {
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
const AtomicKeyInput = struct {
|
||||
const Self = @This();
|
||||
const Ordering = std.atomic.Ordering;
|
||||
|
||||
inner: KeyInput,
|
||||
|
||||
pub fn init(value: KeyInput) Self {
|
||||
return .{ .inner = value };
|
||||
}
|
||||
|
||||
pub inline fn load(self: *const Self, comptime ordering: Ordering) KeyInput {
|
||||
return .{ .raw = switch (ordering) {
|
||||
.AcqRel, .Release => @compileError("not supported for atomic loads"),
|
||||
else => @atomicLoad(u16, &self.inner.raw, ordering),
|
||||
} };
|
||||
}
|
||||
|
||||
pub inline fn store(self: *Self, value: u16, comptime ordering: Ordering) void {
|
||||
switch (ordering) {
|
||||
.AcqRel, .Acquire => @compileError("not supported for atomic stores"),
|
||||
else => @atomicStore(u16, &self.inner.raw, value, ordering),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Read / Write
|
||||
pub const BackgroundControl = extern union {
|
||||
priority: Bitfield(u16, 0, 2),
|
||||
@@ -462,6 +453,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),
|
||||
@@ -471,6 +464,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,
|
||||
@@ -479,20 +474,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,
|
||||
};
|
||||
|
||||
@@ -661,3 +656,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);
|
||||
}
|
||||
};
|
||||
|
@@ -2,68 +2,99 @@ const std = @import("std");
|
||||
const util = @import("../../util.zig");
|
||||
|
||||
const TimerControl = @import("io.zig").TimerControl;
|
||||
const Io = @import("io.zig").Io;
|
||||
const Scheduler = @import("../scheduler.zig").Scheduler;
|
||||
const Event = @import("../scheduler.zig").Event;
|
||||
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||
|
||||
pub const TimerTuple = std.meta.Tuple(&[_]type{ Timer(0), Timer(1), Timer(2), Timer(3) });
|
||||
pub const TimerTuple = struct { Timer(0), Timer(1), Timer(2), Timer(3) };
|
||||
const log = std.log.scoped(.Timer);
|
||||
|
||||
const getHalf = util.getHalf;
|
||||
const setHalf = util.setHalf;
|
||||
|
||||
pub fn create(sched: *Scheduler) TimerTuple {
|
||||
return .{ Timer(0).init(sched), Timer(1).init(sched), Timer(2).init(sched), Timer(3).init(sched) };
|
||||
}
|
||||
|
||||
pub fn read(comptime T: type, tim: *const TimerTuple, addr: u32) ?T {
|
||||
const nybble = @truncate(u4, addr);
|
||||
const nybble_addr = @truncate(u4, addr);
|
||||
|
||||
return switch (T) {
|
||||
u32 => switch (nybble) {
|
||||
u32 => switch (nybble_addr) {
|
||||
0x0 => @as(T, tim.*[0].cnt.raw) << 16 | tim.*[0].timcntL(),
|
||||
0x4 => @as(T, tim.*[1].cnt.raw) << 16 | tim.*[1].timcntL(),
|
||||
0x8 => @as(T, tim.*[2].cnt.raw) << 16 | tim.*[2].timcntL(),
|
||||
0xC => @as(T, tim.*[3].cnt.raw) << 16 | tim.*[3].timcntL(),
|
||||
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
||||
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
||||
},
|
||||
u16 => switch (nybble) {
|
||||
u16 => switch (nybble_addr) {
|
||||
0x0 => tim.*[0].timcntL(),
|
||||
0x2 => tim.*[0].cnt.raw,
|
||||
|
||||
0x4 => tim.*[1].timcntL(),
|
||||
0x6 => tim.*[1].cnt.raw,
|
||||
|
||||
0x8 => tim.*[2].timcntL(),
|
||||
0xA => tim.*[2].cnt.raw,
|
||||
|
||||
0xC => tim.*[3].timcntL(),
|
||||
0xE => tim.*[3].cnt.raw,
|
||||
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
||||
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
||||
},
|
||||
u8 => switch (nybble_addr) {
|
||||
0x0, 0x1 => @truncate(T, tim.*[0].timcntL() >> getHalf(nybble_addr)),
|
||||
0x2, 0x3 => @truncate(T, tim.*[0].cnt.raw >> getHalf(nybble_addr)),
|
||||
|
||||
0x4, 0x5 => @truncate(T, tim.*[1].timcntL() >> getHalf(nybble_addr)),
|
||||
0x6, 0x7 => @truncate(T, tim.*[1].cnt.raw >> getHalf(nybble_addr)),
|
||||
|
||||
0x8, 0x9 => @truncate(T, tim.*[2].timcntL() >> getHalf(nybble_addr)),
|
||||
0xA, 0xB => @truncate(T, tim.*[2].cnt.raw >> getHalf(nybble_addr)),
|
||||
|
||||
0xC, 0xD => @truncate(T, tim.*[3].timcntL() >> getHalf(nybble_addr)),
|
||||
0xE, 0xF => @truncate(T, tim.*[3].cnt.raw >> getHalf(nybble_addr)),
|
||||
},
|
||||
u8 => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
||||
else => @compileError("TIM: Unsupported read width"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write(comptime T: type, tim: *TimerTuple, addr: u32, value: T) void {
|
||||
const nybble = @truncate(u4, addr);
|
||||
const nybble_addr = @truncate(u4, addr);
|
||||
|
||||
return switch (T) {
|
||||
u32 => switch (nybble) {
|
||||
u32 => switch (nybble_addr) {
|
||||
0x0 => tim.*[0].setTimcnt(value),
|
||||
0x4 => tim.*[1].setTimcnt(value),
|
||||
0x8 => tim.*[2].setTimcnt(value),
|
||||
0xC => tim.*[3].setTimcnt(value),
|
||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||
},
|
||||
u16 => switch (nybble) {
|
||||
u16 => switch (nybble_addr) {
|
||||
0x0 => tim.*[0].setTimcntL(value),
|
||||
0x2 => tim.*[0].setTimcntH(value),
|
||||
|
||||
0x4 => tim.*[1].setTimcntL(value),
|
||||
0x6 => tim.*[1].setTimcntH(value),
|
||||
|
||||
0x8 => tim.*[2].setTimcntL(value),
|
||||
0xA => tim.*[2].setTimcntH(value),
|
||||
|
||||
0xC => tim.*[3].setTimcntL(value),
|
||||
0xE => tim.*[3].setTimcntH(value),
|
||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||
},
|
||||
u8 => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||
u8 => switch (nybble_addr) {
|
||||
0x0, 0x1 => tim.*[0].setTimcntL(setHalf(u16, tim.*[0]._reload, nybble_addr, value)),
|
||||
0x2, 0x3 => tim.*[0].setTimcntH(setHalf(u16, tim.*[0].cnt.raw, nybble_addr, value)),
|
||||
|
||||
0x4, 0x5 => tim.*[1].setTimcntL(setHalf(u16, tim.*[1]._reload, nybble_addr, value)),
|
||||
0x6, 0x7 => tim.*[1].setTimcntH(setHalf(u16, tim.*[1].cnt.raw, nybble_addr, value)),
|
||||
|
||||
0x8, 0x9 => tim.*[2].setTimcntL(setHalf(u16, tim.*[2]._reload, nybble_addr, value)),
|
||||
0xA, 0xB => tim.*[2].setTimcntH(setHalf(u16, tim.*[2].cnt.raw, nybble_addr, value)),
|
||||
|
||||
0xC, 0xD => tim.*[3].setTimcntL(setHalf(u16, tim.*[3]._reload, nybble_addr, value)),
|
||||
0xE, 0xF => tim.*[3].setTimcntH(setHalf(u16, tim.*[3].cnt.raw, nybble_addr, value)),
|
||||
},
|
||||
else => @compileError("TIM: Unsupported write width"),
|
||||
};
|
||||
}
|
||||
@@ -97,6 +128,12 @@ 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;
|
||||
@@ -119,20 +156,35 @@ fn Timer(comptime id: u2) type {
|
||||
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
|
||||
// Counter should hold the value it stopped at meaning we have to calculate it now
|
||||
self._counter +%= @truncate(u16, (self.sched.now() - self._start_timestamp) / self.frequency());
|
||||
}
|
||||
|
||||
// The 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;
|
||||
}
|
||||
@@ -159,23 +211,20 @@ fn Timer(comptime id: u2) type {
|
||||
|
||||
// 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 (cpu.bus.tim[next].cnt.cascade.read()) {
|
||||
cpu.bus.tim[next]._counter +%= 1;
|
||||
if (cpu.bus.tim[next]._counter == 0) cpu.bus.tim[next].onTimerExpire(cpu, late);
|
||||
}
|
||||
},
|
||||
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);
|
||||
}
|
||||
|
439
src/core/cpu.zig
439
src/core/cpu.zig
@@ -1,14 +1,13 @@
|
||||
const std = @import("std");
|
||||
const util = @import("../util.zig");
|
||||
|
||||
const Bus = @import("Bus.zig");
|
||||
const Bit = @import("bitfield").Bit;
|
||||
const Bitfield = @import("bitfield").Bitfield;
|
||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
||||
const FilePaths = @import("../util.zig").FilePaths;
|
||||
const Logger = @import("../util.zig").Logger;
|
||||
|
||||
const File = std.fs.File;
|
||||
const log = std.log.scoped(.Arm7Tdmi);
|
||||
|
||||
// ARM Instructions
|
||||
pub const arm = struct {
|
||||
@@ -40,13 +39,12 @@ pub const arm = struct {
|
||||
}
|
||||
|
||||
fn populate() [0x1000]InstrFn {
|
||||
return comptime {
|
||||
comptime {
|
||||
@setEvalBranchQuota(0xE000);
|
||||
var ret = [_]InstrFn{und} ** 0x1000;
|
||||
var table = [_]InstrFn{und} ** 0x1000;
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < ret.len) : (i += 1) {
|
||||
ret[i] = switch (@as(u2, i >> 10)) {
|
||||
for (&table, 0..) |*handler, i| {
|
||||
handler.* = switch (@as(u2, i >> 10)) {
|
||||
0b00 => if (i == 0x121) blk: {
|
||||
break :blk branchExchange;
|
||||
} else if (i & 0xFCF == 0x009) blk: {
|
||||
@@ -108,8 +106,8 @@ pub const arm = struct {
|
||||
};
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
return table;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -137,13 +135,12 @@ pub const thumb = struct {
|
||||
}
|
||||
|
||||
fn populate() [0x400]InstrFn {
|
||||
return comptime {
|
||||
comptime {
|
||||
@setEvalBranchQuota(5025); // This is exact
|
||||
var ret = [_]InstrFn{und} ** 0x400;
|
||||
var table = [_]InstrFn{und} ** 0x400;
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < ret.len) : (i += 1) {
|
||||
ret[i] = switch (@as(u3, i >> 7 & 0x7)) {
|
||||
for (&table, 0..) |*handler, i| {
|
||||
handler.* = 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;
|
||||
@@ -231,50 +228,51 @@ pub const thumb = struct {
|
||||
};
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
return table;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cpu_logging = @import("emu.zig").cpu_logging;
|
||||
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,
|
||||
bank: Bank,
|
||||
|
||||
logger: ?Logger,
|
||||
|
||||
pub fn init(sched: *Scheduler, bus: *Bus, log_file: ?std.fs.File) Self {
|
||||
return Self{
|
||||
.r = [_]u32{0x00} ** 16,
|
||||
.sched = sched,
|
||||
.bus = bus,
|
||||
.cpsr = .{ .raw = 0x0000_001F },
|
||||
.spsr = .{ .raw = 0x0000_0000 },
|
||||
.banked_fiq = [_]u32{0x00} ** 10,
|
||||
.banked_r = [_]u32{0x00} ** 12,
|
||||
.banked_spsr = [_]PSR{.{ .raw = 0x0000_0000 }} ** 5,
|
||||
.logger = if (log_file) |file| Logger.init(file) else null,
|
||||
/// Bank of Registers from other CPU Modes
|
||||
const Bank = struct {
|
||||
/// Storage for r13_<mode>, r14_<mode>
|
||||
/// e.g. [r13, r14, r13_svc, r14_svc]
|
||||
r: [2 * 6]u32,
|
||||
|
||||
/// Storage for R8_fiq -> R12_fiq and their normal counterparts
|
||||
/// e.g [r[0 + 8], fiq_r[0 + 8], r[1 + 8], fiq_r[1 + 8]...]
|
||||
fiq: [2 * 5]u32,
|
||||
|
||||
spsr: [5]PSR,
|
||||
|
||||
const Kind = enum(u1) {
|
||||
R13 = 0,
|
||||
R14,
|
||||
};
|
||||
|
||||
pub fn create() Bank {
|
||||
return .{
|
||||
.r = [_]u32{0x00} ** 12,
|
||||
.fiq = [_]u32{0x00} ** 10,
|
||||
.spsr = [_]PSR{.{ .raw = 0x0000_0000 }} ** 5,
|
||||
};
|
||||
}
|
||||
|
||||
inline fn bankedIdx(mode: Mode, kind: BankedKind) usize {
|
||||
inline fn regIdx(mode: Mode, kind: Kind) usize {
|
||||
const idx: usize = switch (mode) {
|
||||
.User, .System => 0,
|
||||
.Supervisor => 1,
|
||||
@@ -287,7 +285,7 @@ pub const Arm7tdmi = struct {
|
||||
return (idx * 2) + if (kind == .R14) @as(usize, 1) else 0;
|
||||
}
|
||||
|
||||
inline fn bankedSpsrIndex(mode: Mode) usize {
|
||||
inline fn spsrIdx(mode: Mode) usize {
|
||||
return switch (mode) {
|
||||
.Supervisor => 0,
|
||||
.Abort => 1,
|
||||
@@ -298,9 +296,31 @@ pub const Arm7tdmi = struct {
|
||||
};
|
||||
}
|
||||
|
||||
inline fn bankedFiqIdx(i: usize, mode: Mode) usize {
|
||||
inline fn fiqIdx(i: usize, mode: Mode) usize {
|
||||
return (i * 2) + if (mode == .Fiq) @as(usize, 1) else 0;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(sched: *Scheduler, bus: *Bus, log_file: ?std.fs.File) Self {
|
||||
return Self{
|
||||
.r = [_]u32{0x00} ** 16,
|
||||
.pipe = Pipeline.init(),
|
||||
.sched = sched,
|
||||
.bus = bus,
|
||||
.cpsr = .{ .raw = 0x0000_001F },
|
||||
.spsr = .{ .raw = 0x0000_0000 },
|
||||
.bank = Bank.create(),
|
||||
.logger = if (log_file) |file| Logger.init(file) else null,
|
||||
};
|
||||
}
|
||||
|
||||
// FIXME: Resetting disables logging (if enabled)
|
||||
pub fn reset(self: *Self) void {
|
||||
const bus_ptr = self.bus;
|
||||
const scheduler_ptr = self.sched;
|
||||
|
||||
self.* = Self.init(scheduler_ptr, bus_ptr, null);
|
||||
}
|
||||
|
||||
pub inline fn hasSPSR(self: *const Self) bool {
|
||||
const mode = getModeChecked(self, self.cpsr.mode.read());
|
||||
@@ -337,14 +357,14 @@ pub const Arm7tdmi = struct {
|
||||
switch (idx) {
|
||||
8...12 => {
|
||||
if (current == .Fiq) {
|
||||
self.banked_fiq[bankedFiqIdx(idx - 8, .User)] = value;
|
||||
self.bank.fiq[Bank.fiqIdx(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;
|
||||
const kind = std.meta.intToEnum(Bank.Kind, idx - 13) catch unreachable;
|
||||
self.bank.r[Bank.regIdx(.User, kind)] = value;
|
||||
},
|
||||
},
|
||||
else => self.r[idx] = value, // R0 -> R7 and R15
|
||||
@@ -355,12 +375,12 @@ pub const Arm7tdmi = struct {
|
||||
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],
|
||||
8...12 => if (current == .Fiq) self.bank.fiq[Bank.fiqIdx(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)];
|
||||
const kind = std.meta.intToEnum(Bank.Kind, idx - 13) catch unreachable;
|
||||
break :blk self.bank.r[Bank.regIdx(.User, kind)];
|
||||
},
|
||||
},
|
||||
else => self.r[idx], // R0 -> R7 and R15
|
||||
@@ -371,69 +391,81 @@ pub const Arm7tdmi = struct {
|
||||
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];
|
||||
for (0..5) |i| {
|
||||
self.bank.fiq[Bank.fiqIdx(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];
|
||||
self.bank.r[Bank.regIdx(now, .R13)] = self.r[13];
|
||||
self.bank.r[Bank.regIdx(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;
|
||||
self.bank.r[Bank.regIdx(now, .R13)] = self.r[13];
|
||||
self.bank.r[Bank.regIdx(now, .R14)] = self.r[14];
|
||||
self.bank.spsr[Bank.spsrIdx(now)] = self.spsr;
|
||||
},
|
||||
}
|
||||
|
||||
// Grab R8 -> R12
|
||||
i = 0;
|
||||
while (i < 5) : (i += 1) {
|
||||
self.r[8 + i] = self.banked_fiq[bankedFiqIdx(i, next)];
|
||||
for (0..5) |i| {
|
||||
self.r[8 + i] = self.bank.fiq[Bank.fiqIdx(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)];
|
||||
self.r[13] = self.bank.r[Bank.regIdx(next, .R13)];
|
||||
self.r[14] = self.bank.r[Bank.regIdx(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.r[13] = self.bank.r[Bank.regIdx(next, .R13)];
|
||||
self.r[14] = self.bank.r[Bank.regIdx(next, .R14)];
|
||||
self.spsr = self.bank.spsr[Bank.spsrIdx(next)];
|
||||
},
|
||||
}
|
||||
|
||||
self.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[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.bank.r[Bank.regIdx(.Irq, .R13)] = 0x0300_7FA0;
|
||||
self.bank.r[Bank.regIdx(.Supervisor, .R13)] = 0x0300_7FE0;
|
||||
|
||||
self.cpsr.raw = 0x6000001F;
|
||||
// self.cpsr.raw = 0x6000001F;
|
||||
self.cpsr.raw = 0x0000_001F;
|
||||
|
||||
self.bus.bios.addr_latch = 0x0000_00DC + 8;
|
||||
}
|
||||
|
||||
pub fn step(self: *Self) void {
|
||||
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 = self.fetch(u16);
|
||||
if (cpu_logging) self.logger.?.mgbaLog(self, opcode);
|
||||
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.fetch(u32);
|
||||
if (cpu_logging) self.logger.?.mgbaLog(self, opcode);
|
||||
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);
|
||||
@@ -442,29 +474,11 @@ pub const Arm7tdmi = struct {
|
||||
}
|
||||
|
||||
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);
|
||||
inline for (0..4) |i| {
|
||||
if (self.bus.dma[i].in_progress) {
|
||||
self.bus.dma[i].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;
|
||||
@@ -473,42 +487,41 @@ pub const Arm7tdmi = struct {
|
||||
pub fn handleInterrupt(self: *Self) void {
|
||||
const should_handle = self.bus.io.ie.raw & self.bus.io.irq.raw;
|
||||
|
||||
if (should_handle != 0) {
|
||||
// Return if IME is disabled, CPSR I is set or there is nothing to handle
|
||||
if (!self.bus.io.ime or self.cpsr.i.read() or should_handle == 0) return;
|
||||
|
||||
// If Pipeline isn't full, we have a bug
|
||||
std.debug.assert(self.pipe.isFull());
|
||||
|
||||
// log.debug("Handling Interrupt!", .{});
|
||||
self.bus.io.haltcnt = .Execute;
|
||||
// log.debug("An Interrupt was Fired!", .{});
|
||||
|
||||
// Either IME is not true or I in CPSR is true
|
||||
// Don't handle interrupts
|
||||
if (!self.bus.io.ime or self.cpsr.i.read()) return;
|
||||
// log.debug("An interrupt was Handled!", .{});
|
||||
|
||||
// retAddr.gba says r15 on it's own is off by -04h in both ARM and THUMB mode
|
||||
const r15 = self.r[15] + 4;
|
||||
const cpsr = self.cpsr.raw;
|
||||
// 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] = r15;
|
||||
self.spsr.raw = cpsr;
|
||||
self.r[15] = 0x000_0018;
|
||||
}
|
||||
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) T {
|
||||
inline fn fetch(self: *Self, comptime T: type, address: u32) T {
|
||||
comptime std.debug.assert(T == u32 or T == u16); // Opcode may be 32-bit (ARM) or 16-bit (THUMB)
|
||||
defer self.r[15] += if (T == u32) 4 else 2;
|
||||
|
||||
// FIXME: You better hope this is optimized out
|
||||
// Bus.read will advance the scheduler. There are different timings for CPU fetches,
|
||||
// so we want to undo what Bus.read will apply. We can do this by caching the current tick
|
||||
// This is very dumb.
|
||||
//
|
||||
// FIXME: Please rework this
|
||||
const tick_cache = self.sched.tick;
|
||||
defer self.sched.tick = tick_cache + Bus.fetch_timings[@boolToInt(T == u32)][@truncate(u4, self.r[15] >> 24)];
|
||||
defer self.sched.tick = tick_cache + Bus.fetch_timings[@boolToInt(T == u32)][@truncate(u4, address >> 24)];
|
||||
|
||||
return self.bus.read(T, self.r[15]);
|
||||
}
|
||||
|
||||
pub fn fakePC(self: *const Self) u32 {
|
||||
return self.r[15] + 4;
|
||||
return self.bus.read(T, address);
|
||||
}
|
||||
|
||||
pub fn panic(self: *const Self, comptime format: []const u8, args: anytype) noreturn {
|
||||
@@ -520,10 +533,12 @@ pub const Arm7tdmi = struct {
|
||||
std.debug.print("R{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\n", .{ i, self.r[i], i_1, self.r[i_1], i_2, self.r[i_2], i_3, self.r[i_3] });
|
||||
}
|
||||
std.debug.print("cpsr: 0x{X:0>8} ", .{self.cpsr.raw});
|
||||
prettyPrintPsr(&self.cpsr);
|
||||
self.cpsr.toString();
|
||||
|
||||
std.debug.print("spsr: 0x{X:0>8} ", .{self.spsr.raw});
|
||||
prettyPrintPsr(&self.spsr);
|
||||
self.spsr.toString();
|
||||
|
||||
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);
|
||||
@@ -539,99 +554,74 @@ pub const Arm7tdmi = struct {
|
||||
|
||||
std.debug.panic(format, args);
|
||||
}
|
||||
|
||||
fn prettyPrintPsr(psr: *const PSR) void {
|
||||
std.debug.print("[", .{});
|
||||
|
||||
if (psr.n.read()) std.debug.print("N", .{}) else std.debug.print("-", .{});
|
||||
if (psr.z.read()) std.debug.print("Z", .{}) else std.debug.print("-", .{});
|
||||
if (psr.c.read()) std.debug.print("C", .{}) else std.debug.print("-", .{});
|
||||
if (psr.v.read()) std.debug.print("V", .{}) else std.debug.print("-", .{});
|
||||
if (psr.i.read()) std.debug.print("I", .{}) else std.debug.print("-", .{});
|
||||
if (psr.f.read()) std.debug.print("F", .{}) else std.debug.print("-", .{});
|
||||
if (psr.t.read()) std.debug.print("T", .{}) else std.debug.print("-", .{});
|
||||
std.debug.print("|", .{});
|
||||
if (getMode(psr.mode.read())) |mode| std.debug.print("{s}", .{modeString(mode)}) else std.debug.print("---", .{});
|
||||
|
||||
std.debug.print("]\n", .{});
|
||||
}
|
||||
|
||||
fn modeString(mode: Mode) []const u8 {
|
||||
return switch (mode) {
|
||||
.User => "usr",
|
||||
.Fiq => "fiq",
|
||||
.Irq => "irq",
|
||||
.Supervisor => "svc",
|
||||
.Abort => "abt",
|
||||
.Undefined => "und",
|
||||
.System => "sys",
|
||||
};
|
||||
}
|
||||
|
||||
fn mgbaLog(self: *const Self, file: *const File, opcode: u32) !void {
|
||||
const thumb_fmt = "{X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} cpsr: {X:0>8} | {X:0>4}:\n";
|
||||
const arm_fmt = "{X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} cpsr: {X:0>8} | {X:0>8}:\n";
|
||||
var buf: [0x100]u8 = [_]u8{0x00} ** 0x100; // this is larger than it needs to be
|
||||
|
||||
const r0 = self.r[0];
|
||||
const r1 = self.r[1];
|
||||
const r2 = self.r[2];
|
||||
const r3 = self.r[3];
|
||||
const r4 = self.r[4];
|
||||
const r5 = self.r[5];
|
||||
const r6 = self.r[6];
|
||||
const r7 = self.r[7];
|
||||
const r8 = self.r[8];
|
||||
const r9 = self.r[9];
|
||||
const r10 = self.r[10];
|
||||
const r11 = self.r[11];
|
||||
const r12 = self.r[12];
|
||||
const r13 = self.r[13];
|
||||
const r14 = self.r[14];
|
||||
const r15 = self.r[15];
|
||||
|
||||
const c_psr = self.cpsr.raw;
|
||||
|
||||
var log_str: []u8 = undefined;
|
||||
if (self.cpsr.t.read()) {
|
||||
if (opcode >> 11 == 0x1E) {
|
||||
// Instruction 1 of a BL Opcode, print in ARM mode
|
||||
const other_half = self.bus.dbgRead(u16, self.r[15]);
|
||||
const bl_opcode = @as(u32, opcode) << 16 | other_half;
|
||||
|
||||
log_str = try std.fmt.bufPrint(&buf, arm_fmt, .{ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, c_psr, bl_opcode });
|
||||
} else {
|
||||
log_str = try std.fmt.bufPrint(&buf, thumb_fmt, .{ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, c_psr, opcode });
|
||||
}
|
||||
} else {
|
||||
log_str = try std.fmt.bufPrint(&buf, arm_fmt, .{ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, c_psr, opcode });
|
||||
}
|
||||
|
||||
_ = try file.writeAll(log_str);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn checkCond(cpsr: PSR, cond: u4) bool {
|
||||
return switch (cond) {
|
||||
0x0 => cpsr.z.read(), // EQ - Equal
|
||||
0x1 => !cpsr.z.read(), // NE - Not equal
|
||||
0x2 => cpsr.c.read(), // CS - Unsigned higher or same
|
||||
0x3 => !cpsr.c.read(), // CC - Unsigned lower
|
||||
0x4 => cpsr.n.read(), // MI - Negative
|
||||
0x5 => !cpsr.n.read(), // PL - Positive or zero
|
||||
0x6 => cpsr.v.read(), // VS - Overflow
|
||||
0x7 => !cpsr.v.read(), // VC - No overflow
|
||||
0x8 => cpsr.c.read() and !cpsr.z.read(), // HI - unsigned higher
|
||||
0x9 => !cpsr.c.read() or cpsr.z.read(), // LS - unsigned lower or same
|
||||
0xA => cpsr.n.read() == cpsr.v.read(), // GE - Greater or equal
|
||||
0xB => cpsr.n.read() != cpsr.v.read(), // LT - Less than
|
||||
0xC => !cpsr.z.read() and (cpsr.n.read() == cpsr.v.read()), // GT - Greater than
|
||||
0xD => cpsr.z.read() or (cpsr.n.read() != cpsr.v.read()), // LE - Less than or equal
|
||||
0xE => true, // AL - Always
|
||||
0xF => false, // NV - Never (reserved in ARMv3 and up, but seems to have not changed?)
|
||||
};
|
||||
const condition_lut = [_]u16{
|
||||
0xF0F0, // EQ - Equal
|
||||
0x0F0F, // NE - Not Equal
|
||||
0xCCCC, // CS - Unsigned higher or same
|
||||
0x3333, // CC - Unsigned lower
|
||||
0xFF00, // MI - Negative
|
||||
0x00FF, // PL - Positive or Zero
|
||||
0xAAAA, // VS - Overflow
|
||||
0x5555, // VC - No Overflow
|
||||
0x0C0C, // HI - unsigned hierh
|
||||
0xF3F3, // LS - unsigned lower or same
|
||||
0xAA55, // GE - greater or equal
|
||||
0x55AA, // LT - less than
|
||||
0x0A05, // GT - greater than
|
||||
0xF5FA, // LE - less than or equal
|
||||
0xFFFF, // AL - always
|
||||
0x0000, // NV - never
|
||||
};
|
||||
|
||||
pub inline fn checkCond(cpsr: PSR, cond: u4) bool {
|
||||
const flags = @truncate(u4, cpsr.raw >> 28);
|
||||
|
||||
return condition_lut[cond] & (@as(u16, 1) << flags) != 0;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const 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),
|
||||
@@ -642,9 +632,25 @@ pub const PSR = extern union {
|
||||
z: Bit(u32, 30),
|
||||
n: Bit(u32, 31),
|
||||
raw: u32,
|
||||
|
||||
fn toString(self: PSR) void {
|
||||
std.debug.print("[", .{});
|
||||
|
||||
if (self.n.read()) std.debug.print("N", .{}) else std.debug.print("-", .{});
|
||||
if (self.z.read()) std.debug.print("Z", .{}) else std.debug.print("-", .{});
|
||||
if (self.c.read()) std.debug.print("C", .{}) else std.debug.print("-", .{});
|
||||
if (self.v.read()) std.debug.print("V", .{}) else std.debug.print("-", .{});
|
||||
if (self.i.read()) std.debug.print("I", .{}) else std.debug.print("-", .{});
|
||||
if (self.f.read()) std.debug.print("F", .{}) else std.debug.print("-", .{});
|
||||
if (self.t.read()) std.debug.print("T", .{}) else std.debug.print("-", .{});
|
||||
std.debug.print("|", .{});
|
||||
if (getMode(self.mode.read())) |m| std.debug.print("{s}", .{m.toString()}) else std.debug.print("---", .{});
|
||||
|
||||
std.debug.print("]\n", .{});
|
||||
}
|
||||
};
|
||||
|
||||
const Mode = enum(u5) {
|
||||
pub const Mode = enum(u5) {
|
||||
User = 0b10000,
|
||||
Fiq = 0b10001,
|
||||
Irq = 0b10010,
|
||||
@@ -652,11 +658,18 @@ const Mode = enum(u5) {
|
||||
Abort = 0b10111,
|
||||
Undefined = 0b11011,
|
||||
System = 0b11111,
|
||||
};
|
||||
|
||||
const BankedKind = enum(u1) {
|
||||
R13 = 0,
|
||||
R14,
|
||||
pub fn toString(self: Mode) []const u8 {
|
||||
return switch (self) {
|
||||
.User => "usr",
|
||||
.Fiq => "fiq",
|
||||
.Irq => "irq",
|
||||
.Supervisor => "svc",
|
||||
.Abort => "abt",
|
||||
.Undefined => "und",
|
||||
.System => "sys",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
fn getMode(bits: u5) ?Mode {
|
||||
|
@@ -55,8 +55,9 @@ pub fn blockDataTransfer(comptime P: bool, comptime U: bool, comptime S: bool, c
|
||||
|
||||
if (L) {
|
||||
cpu.r[15] = bus.read(u32, und_addr);
|
||||
cpu.pipe.reload(cpu);
|
||||
} else {
|
||||
bus.write(u32, und_addr, cpu.r[15] + 8);
|
||||
bus.write(u32, und_addr, cpu.r[15] + 4);
|
||||
}
|
||||
|
||||
cpu.r[rn] = if (U) cpu.r[rn] + 0x40 else cpu.r[rn] - 0x40;
|
||||
@@ -86,17 +87,23 @@ pub fn blockDataTransfer(comptime P: bool, comptime U: bool, comptime S: bool, c
|
||||
cpu.setUserModeRegister(i, bus.read(u32, address));
|
||||
} else {
|
||||
const value = bus.read(u32, address);
|
||||
cpu.r[i] = if (i == 0xF) value & 0xFFFF_FFFC else value;
|
||||
if (S and i == 0xF) cpu.setCpsr(cpu.spsr.raw);
|
||||
|
||||
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) 8 else @as(u32, 0)); // PC is already 4 ahead to make 12
|
||||
bus.write(u32, address, value + if (i == 0xF) 4 else @as(u32, 0)); // PC is already 8 ahead to make 12
|
||||
} else {
|
||||
bus.write(u32, address, cpu.r[i] + if (i == 0xF) 8 else @as(u32, 0));
|
||||
bus.write(u32, address, cpu.r[i] + if (i == 0xF) 4 else @as(u32, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,22 +1,26 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Bus = @import("../../Bus.zig");
|
||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||
|
||||
const sext = @import("../../../util.zig").sext;
|
||||
const sext = @import("zba-util").sext;
|
||||
|
||||
pub fn branch(comptime L: bool) InstrFn {
|
||||
return struct {
|
||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
||||
if (L) cpu.r[14] = cpu.r[15];
|
||||
cpu.r[15] = cpu.fakePC() +% (sext(u32, u24, opcode) << 2);
|
||||
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;
|
||||
cpu.cpsr.t.write(cpu.r[rn] & 1 == 1);
|
||||
cpu.r[15] = cpu.r[rn] & 0xFFFF_FFFE;
|
||||
|
||||
const thumb = cpu.r[rn] & 1 == 1;
|
||||
cpu.r[15] = cpu.r[rn] & if (thumb) ~@as(u32, 1) else ~@as(u32, 3);
|
||||
|
||||
cpu.cpsr.t.write(thumb);
|
||||
cpu.pipe.reload(cpu);
|
||||
}
|
||||
|
@@ -2,10 +2,10 @@ const Bus = @import("../../Bus.zig");
|
||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||
|
||||
const rotateRight = @import("../barrel_shifter.zig").rotateRight;
|
||||
const execute = @import("../barrel_shifter.zig").execute;
|
||||
const exec = @import("../barrel_shifter.zig").exec;
|
||||
const ror = @import("../barrel_shifter.zig").ror;
|
||||
|
||||
pub fn dataProcessing(comptime I: bool, comptime S: bool, comptime instrKind: u4) InstrFn {
|
||||
pub fn dataProcessing(comptime I: bool, comptime S: bool, comptime kind: u4) InstrFn {
|
||||
return struct {
|
||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
||||
const rd = @truncate(u4, opcode >> 12 & 0xF);
|
||||
@@ -13,269 +13,170 @@ pub fn dataProcessing(comptime I: bool, comptime S: bool, comptime instrKind: u4
|
||||
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 op1 = if (rn == 0xF) cpu.fakePC() else cpu.r[rn];
|
||||
|
||||
var op2: u32 = undefined;
|
||||
if (I) {
|
||||
const amount = @truncate(u8, (opcode >> 8 & 0xF) << 1);
|
||||
op2 = rotateRight(S, &cpu.cpsr, opcode & 0xFF, amount);
|
||||
} else {
|
||||
op2 = execute(S, cpu, opcode);
|
||||
}
|
||||
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;
|
||||
|
||||
switch (instrKind) {
|
||||
0x0 => {
|
||||
// AND
|
||||
const result = op1 & op2;
|
||||
cpu.r[rd] = result;
|
||||
setArmLogicOpFlags(S, cpu, rd, result);
|
||||
},
|
||||
0x1 => {
|
||||
// EOR
|
||||
const result = op1 ^ op2;
|
||||
cpu.r[rd] = result;
|
||||
setArmLogicOpFlags(S, cpu, rd, result);
|
||||
},
|
||||
0x2 => {
|
||||
// SUB
|
||||
cpu.r[rd] = armSub(S, cpu, rd, op1, op2);
|
||||
},
|
||||
0x3 => {
|
||||
// RSB
|
||||
cpu.r[rd] = armSub(S, cpu, rd, op2, op1);
|
||||
},
|
||||
0x4 => {
|
||||
// ADD
|
||||
cpu.r[rd] = armAdd(S, cpu, rd, op1, op2);
|
||||
},
|
||||
0x5 => {
|
||||
// ADC
|
||||
cpu.r[rd] = armAdc(S, cpu, rd, op1, op2, old_carry);
|
||||
},
|
||||
0x6 => {
|
||||
// SBC
|
||||
cpu.r[rd] = armSbc(S, cpu, rd, op1, op2, old_carry);
|
||||
},
|
||||
0x7 => {
|
||||
// RSC
|
||||
cpu.r[rd] = armSbc(S, cpu, rd, op2, op1, old_carry);
|
||||
},
|
||||
var result: u32 = undefined;
|
||||
var overflow: u1 = 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) {
|
||||
undefinedTestBehaviour(cpu);
|
||||
return;
|
||||
}
|
||||
if (rd == 0xF)
|
||||
return undefinedTestBehaviour(cpu);
|
||||
|
||||
const result = op1 & op2;
|
||||
setTestOpFlags(S, cpu, opcode, result);
|
||||
result = op1 & op2;
|
||||
},
|
||||
0x9 => {
|
||||
// TEQ
|
||||
if (rd == 0xF) {
|
||||
undefinedTestBehaviour(cpu);
|
||||
return;
|
||||
}
|
||||
if (rd == 0xF)
|
||||
return undefinedTestBehaviour(cpu);
|
||||
|
||||
const result = op1 ^ op2;
|
||||
setTestOpFlags(S, cpu, opcode, result);
|
||||
result = op1 ^ op2;
|
||||
},
|
||||
0xA => {
|
||||
// CMP
|
||||
if (rd == 0xF) {
|
||||
undefinedTestBehaviour(cpu);
|
||||
return;
|
||||
}
|
||||
if (rd == 0xF)
|
||||
return undefinedTestBehaviour(cpu);
|
||||
|
||||
cmp(cpu, op1, op2);
|
||||
result = op1 -% op2;
|
||||
},
|
||||
0xB => {
|
||||
// CMN
|
||||
if (rd == 0xF) {
|
||||
undefinedTestBehaviour(cpu);
|
||||
return;
|
||||
if (rd == 0xF)
|
||||
return undefinedTestBehaviour(cpu);
|
||||
|
||||
const tmp = @addWithOverflow(op1, op2);
|
||||
result = tmp[0];
|
||||
overflow = tmp[1];
|
||||
},
|
||||
0xC => result = op1 | op2, // ORR
|
||||
0xD => result = op2, // MOV
|
||||
0xE => result = op1 & ~op2, // BIC
|
||||
0xF => result = ~op2, // MVN
|
||||
}
|
||||
|
||||
cmn(cpu, op1, op2);
|
||||
},
|
||||
0xC => {
|
||||
// ORR
|
||||
const result = op1 | op2;
|
||||
// Write to Destination Register
|
||||
switch (kind) {
|
||||
0x8, 0x9, 0xA, 0xB => {}, // Test Operations
|
||||
else => {
|
||||
cpu.r[rd] = result;
|
||||
setArmLogicOpFlags(S, cpu, rd, result);
|
||||
if (rd == 0xF) {
|
||||
if (S) cpu.setCpsr(cpu.spsr.raw);
|
||||
cpu.pipe.reload(cpu);
|
||||
}
|
||||
},
|
||||
0xD => {
|
||||
// MOV
|
||||
cpu.r[rd] = op2;
|
||||
setArmLogicOpFlags(S, cpu, rd, op2);
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
},
|
||||
0xE => {
|
||||
// BIC
|
||||
const result = op1 & ~op2;
|
||||
cpu.r[rd] = result;
|
||||
setArmLogicOpFlags(S, cpu, rd, result);
|
||||
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);
|
||||
}
|
||||
},
|
||||
0xF => {
|
||||
// MVN
|
||||
const result = ~op2;
|
||||
cpu.r[rd] = result;
|
||||
setArmLogicOpFlags(S, cpu, rd, result);
|
||||
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 == 0b1);
|
||||
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 == 0b1);
|
||||
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;
|
||||
}
|
||||
|
||||
fn armSbc(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32, old_carry: u1) u32 {
|
||||
var result: u32 = undefined;
|
||||
if (S and rd == 0xF) {
|
||||
result = sbc(false, cpu, left, right, old_carry);
|
||||
cpu.setCpsr(cpu.spsr.raw);
|
||||
} else {
|
||||
result = sbc(S, cpu, left, right, old_carry);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn sbc(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32, old_carry: u1) u32 {
|
||||
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 result = @truncate(u32, left -% subtrahend);
|
||||
const ret = @truncate(u32, left -% subtrahend);
|
||||
|
||||
if (S) {
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
cpu.cpsr.c.write(subtrahend <= left);
|
||||
cpu.cpsr.v.write(((left ^ result) & (~right ^ result)) >> 31 & 1 == 1);
|
||||
}
|
||||
|
||||
return result;
|
||||
return ret;
|
||||
}
|
||||
|
||||
fn armSub(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32) u32 {
|
||||
var result: u32 = undefined;
|
||||
if (S and rd == 0xF) {
|
||||
result = sub(false, cpu, left, right);
|
||||
cpu.setCpsr(cpu.spsr.raw);
|
||||
} else {
|
||||
result = sub(S, cpu, left, right);
|
||||
}
|
||||
pub fn add(overflow: *u1, left: u32, right: u32) u32 {
|
||||
const ret = @addWithOverflow(left, right);
|
||||
overflow.* = ret[1];
|
||||
|
||||
return result;
|
||||
return ret[0];
|
||||
}
|
||||
|
||||
pub fn sub(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32) u32 {
|
||||
const result = left -% right;
|
||||
pub fn adc(overflow: *u1, left: u32, right: u32, old_carry: u1) u32 {
|
||||
const tmp = @addWithOverflow(left, right);
|
||||
const ret = @addWithOverflow(tmp[0], old_carry);
|
||||
overflow.* = tmp[1] | ret[1];
|
||||
|
||||
if (S) {
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
cpu.cpsr.c.write(right <= left);
|
||||
cpu.cpsr.v.write(((left ^ result) & (~right ^ result)) >> 31 & 1 == 1);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fn armAdd(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32) u32 {
|
||||
var result: u32 = undefined;
|
||||
if (S and rd == 0xF) {
|
||||
result = add(false, cpu, left, right);
|
||||
cpu.setCpsr(cpu.spsr.raw);
|
||||
} else {
|
||||
result = add(S, cpu, left, right);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn add(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32) u32 {
|
||||
var result: u32 = undefined;
|
||||
const didOverflow = @addWithOverflow(u32, left, right, &result);
|
||||
|
||||
if (S) {
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
cpu.cpsr.c.write(didOverflow);
|
||||
cpu.cpsr.v.write(((left ^ result) & (right ^ result)) >> 31 & 1 == 1);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fn armAdc(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32, old_carry: u1) u32 {
|
||||
var result: u32 = undefined;
|
||||
if (S and rd == 0xF) {
|
||||
result = adc(false, cpu, left, right, old_carry);
|
||||
cpu.setCpsr(cpu.spsr.raw);
|
||||
} else {
|
||||
result = adc(S, cpu, left, right, old_carry);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn adc(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32, old_carry: u1) u32 {
|
||||
var result: u32 = undefined;
|
||||
const did = @addWithOverflow(u32, left, right, &result);
|
||||
const overflow = @addWithOverflow(u32, result, old_carry, &result);
|
||||
|
||||
if (S) {
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
cpu.cpsr.c.write(did or overflow);
|
||||
cpu.cpsr.v.write(((left ^ result) & (right ^ result)) >> 31 & 1 == 1);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn cmp(cpu: *Arm7tdmi, left: u32, right: u32) void {
|
||||
const result = left -% right;
|
||||
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
cpu.cpsr.c.write(right <= left);
|
||||
cpu.cpsr.v.write(((left ^ result) & (~right ^ result)) >> 31 & 1 == 1);
|
||||
}
|
||||
|
||||
pub fn cmn(cpu: *Arm7tdmi, left: u32, right: u32) void {
|
||||
var result: u32 = undefined;
|
||||
const didOverflow = @addWithOverflow(u32, left, right, &result);
|
||||
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
cpu.cpsr.c.write(didOverflow);
|
||||
cpu.cpsr.v.write(((left ^ result) & (right ^ result)) >> 31 & 1 == 1);
|
||||
}
|
||||
|
||||
fn setArmLogicOpFlags(comptime S: bool, cpu: *Arm7tdmi, rd: u4, result: u32) void {
|
||||
if (S and rd == 0xF) {
|
||||
cpu.setCpsr(cpu.spsr.raw);
|
||||
} else {
|
||||
setLogicOpFlags(S, cpu, result);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setLogicOpFlags(comptime S: bool, cpu: *Arm7tdmi, result: u32) void {
|
||||
if (S) {
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
// C set by Barrel Shifter, V is unaffected
|
||||
}
|
||||
}
|
||||
|
||||
fn setTestOpFlags(comptime S: bool, cpu: *Arm7tdmi, opcode: u32, result: u32) void {
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
// Barrel Shifter should always calc CPSR C in TST
|
||||
if (!S) _ = execute(true, cpu, opcode);
|
||||
return ret[0];
|
||||
}
|
||||
|
||||
fn undefinedTestBehaviour(cpu: *Arm7tdmi) void {
|
||||
|
@@ -1,11 +1,9 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Bus = @import("../../Bus.zig");
|
||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||
|
||||
const sext = @import("../../../util.zig").sext;
|
||||
const rotr = @import("../../../util.zig").rotr;
|
||||
const sext = @import("zba-util").sext;
|
||||
const rotr = @import("zba-util").rotr;
|
||||
|
||||
pub fn halfAndSignedDataTransfer(comptime P: bool, comptime U: bool, comptime I: bool, comptime W: bool, comptime L: bool) InstrFn {
|
||||
return struct {
|
||||
@@ -15,20 +13,8 @@ pub fn halfAndSignedDataTransfer(comptime P: bool, comptime U: bool, comptime I:
|
||||
const rm = opcode & 0xF;
|
||||
const imm_offset_high = opcode >> 8 & 0xF;
|
||||
|
||||
var base: u32 = undefined;
|
||||
if (rn == 0xF) {
|
||||
base = cpu.fakePC();
|
||||
if (!L) base += 4;
|
||||
} else {
|
||||
base = cpu.r[rn];
|
||||
}
|
||||
|
||||
var offset: u32 = undefined;
|
||||
if (I) {
|
||||
offset = imm_offset_high << 4 | rm;
|
||||
} else {
|
||||
offset = cpu.r[rm];
|
||||
}
|
||||
const 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;
|
||||
@@ -47,11 +33,8 @@ pub fn halfAndSignedDataTransfer(comptime P: bool, comptime U: bool, comptime I:
|
||||
},
|
||||
0b11 => {
|
||||
// LDRSH
|
||||
result = if (address & 1 == 1) blk: {
|
||||
break :blk sext(u32, u8, bus.read(u8, address));
|
||||
} else blk: {
|
||||
break :blk sext(u32, u16, bus.read(u16, address));
|
||||
};
|
||||
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
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ const PSR = @import("../../cpu.zig").PSR;
|
||||
|
||||
const log = std.log.scoped(.PsrTransfer);
|
||||
|
||||
const rotr = @import("../../../util.zig").rotr;
|
||||
const rotr = @import("zba-util").rotr;
|
||||
|
||||
pub fn psrTransfer(comptime I: bool, comptime R: bool, comptime kind: u2) InstrFn {
|
||||
return struct {
|
||||
|
@@ -1,10 +1,8 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Bus = @import("../../Bus.zig");
|
||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||
|
||||
const rotr = @import("../../../util.zig").rotr;
|
||||
const rotr = @import("zba-util").rotr;
|
||||
|
||||
pub fn singleDataSwap(comptime B: bool) InstrFn {
|
||||
return struct {
|
||||
|
@@ -1,12 +1,9 @@
|
||||
const std = @import("std");
|
||||
const util = @import("../../../util.zig");
|
||||
|
||||
const shifter = @import("../barrel_shifter.zig");
|
||||
const Bus = @import("../../Bus.zig");
|
||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||
|
||||
const rotr = @import("../../../util.zig").rotr;
|
||||
const rotr = @import("zba-util").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 {
|
||||
@@ -14,15 +11,8 @@ pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool,
|
||||
const rn = opcode >> 16 & 0xF;
|
||||
const rd = opcode >> 12 & 0xF;
|
||||
|
||||
var base: u32 = undefined;
|
||||
if (rn == 0xF) {
|
||||
base = cpu.fakePC();
|
||||
if (!L) base += 4; // Offset of 12
|
||||
} else {
|
||||
base = cpu.r[rn];
|
||||
}
|
||||
|
||||
const offset = if (I) shifter.immShift(false, cpu, opcode) else opcode & 0xFFF;
|
||||
const base = cpu.r[rn];
|
||||
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;
|
||||
@@ -40,18 +30,26 @@ pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool,
|
||||
} else {
|
||||
if (B) {
|
||||
// STRB
|
||||
const value = if (rd == 0xF) cpu.r[rd] + 8 else cpu.r[rd];
|
||||
const value = cpu.r[rd] + if (rd == 0xF) 4 else @as(u32, 0); // PC is 12 ahead
|
||||
bus.write(u8, address, @truncate(u8, value));
|
||||
} else {
|
||||
// STR
|
||||
const value = if (rd == 0xF) cpu.r[rd] + 8 else cpu.r[rd];
|
||||
const value = cpu.r[rd] + if (rd == 0xF) 4 else @as(u32, 0);
|
||||
bus.write(u32, address, value);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ pub fn armSoftwareInterrupt() InstrFn {
|
||||
return struct {
|
||||
fn inner(cpu: *Arm7tdmi, _: *Bus, _: u32) void {
|
||||
// Copy Values from Current Mode
|
||||
const r15 = cpu.r[15];
|
||||
const ret_addr = cpu.r[15] - 4;
|
||||
const cpsr = cpu.cpsr.raw;
|
||||
|
||||
// Switch Mode
|
||||
@@ -14,9 +14,10 @@ pub fn armSoftwareInterrupt() InstrFn {
|
||||
cpu.cpsr.t.write(false); // Force ARM Mode
|
||||
cpu.cpsr.i.write(true); // Disable normal interrupts
|
||||
|
||||
cpu.r[14] = r15; // Resume Execution
|
||||
cpu.r[14] = ret_addr; // Resume Execution
|
||||
cpu.spsr.raw = cpsr; // Previous mode CPSR
|
||||
cpu.r[15] = 0x0000_0008;
|
||||
cpu.pipe.reload(cpu);
|
||||
}
|
||||
}.inner;
|
||||
}
|
||||
|
@@ -1,41 +1,35 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||
const CPSR = @import("../cpu.zig").PSR;
|
||||
|
||||
const rotr = @import("../../util.zig").rotr;
|
||||
const rotr = @import("zba-util").rotr;
|
||||
|
||||
pub fn execute(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
||||
pub fn exec(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
||||
var result: u32 = undefined;
|
||||
if (opcode >> 4 & 1 == 1) {
|
||||
result = registerShift(S, cpu, opcode);
|
||||
result = register(S, cpu, opcode);
|
||||
} else {
|
||||
result = immShift(S, cpu, opcode);
|
||||
result = immediate(S, cpu, opcode);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fn registerShift(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
||||
fn register(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
||||
const rs_idx = opcode >> 8 & 0xF;
|
||||
const rm = cpu.r[opcode & 0xF];
|
||||
const rs = @truncate(u8, cpu.r[rs_idx]);
|
||||
|
||||
const rm_idx = opcode & 0xF;
|
||||
const rm = if (rm_idx == 0xF) cpu.fakePC() else cpu.r[rm_idx];
|
||||
|
||||
return switch (@truncate(u2, opcode >> 5)) {
|
||||
0b00 => logicalLeft(S, &cpu.cpsr, rm, rs),
|
||||
0b01 => logicalRight(S, &cpu.cpsr, rm, rs),
|
||||
0b10 => arithmeticRight(S, &cpu.cpsr, rm, rs),
|
||||
0b11 => rotateRight(S, &cpu.cpsr, rm, rs),
|
||||
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 immShift(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
||||
pub fn immediate(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
||||
const amount = @truncate(u8, opcode >> 7 & 0x1F);
|
||||
|
||||
const rm_idx = opcode & 0xF;
|
||||
const rm = if (rm_idx == 0xF) cpu.fakePC() else cpu.r[rm_idx];
|
||||
const rm = cpu.r[opcode & 0xF];
|
||||
|
||||
var result: u32 = undefined;
|
||||
if (amount == 0) {
|
||||
@@ -64,17 +58,17 @@ pub fn immShift(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
||||
}
|
||||
} else {
|
||||
switch (@truncate(u2, opcode >> 5)) {
|
||||
0b00 => result = logicalLeft(S, &cpu.cpsr, rm, amount),
|
||||
0b01 => result = logicalRight(S, &cpu.cpsr, rm, amount),
|
||||
0b10 => result = arithmeticRight(S, &cpu.cpsr, rm, amount),
|
||||
0b11 => result = rotateRight(S, &cpu.cpsr, rm, amount),
|
||||
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 logicalLeft(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
||||
pub fn lsl(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
||||
const amount = @truncate(u5, total_amount);
|
||||
const bit_count: u8 = @typeInfo(u32).Int.bits;
|
||||
|
||||
@@ -101,7 +95,7 @@ pub fn logicalLeft(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn logicalRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u32) u32 {
|
||||
pub fn lsr(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u32) u32 {
|
||||
const amount = @truncate(u5, total_amount);
|
||||
const bit_count: u8 = @typeInfo(u32).Int.bits;
|
||||
|
||||
@@ -125,7 +119,7 @@ pub fn logicalRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u32) u
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn arithmeticRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
||||
pub fn asr(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
||||
const amount = @truncate(u5, total_amount);
|
||||
const bit_count: u8 = @typeInfo(u32).Int.bits;
|
||||
|
||||
@@ -142,7 +136,7 @@ pub fn arithmeticRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8)
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn rotateRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
||||
pub fn ror(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
||||
const result = rotr(u32, rm, total_amount);
|
||||
|
||||
if (S and total_amount != 0) {
|
||||
|
@@ -4,16 +4,11 @@ const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
||||
|
||||
const adc = @import("../arm/data_processing.zig").adc;
|
||||
const sbc = @import("../arm/data_processing.zig").sbc;
|
||||
const sub = @import("../arm/data_processing.zig").sub;
|
||||
const cmp = @import("../arm/data_processing.zig").cmp;
|
||||
const cmn = @import("../arm/data_processing.zig").cmn;
|
||||
const setTestOpFlags = @import("../arm/data_processing.zig").setTestOpFlags;
|
||||
const setLogicOpFlags = @import("../arm/data_processing.zig").setLogicOpFlags;
|
||||
|
||||
const logicalLeft = @import("../barrel_shifter.zig").logicalLeft;
|
||||
const logicalRight = @import("../barrel_shifter.zig").logicalRight;
|
||||
const arithmeticRight = @import("../barrel_shifter.zig").arithmeticRight;
|
||||
const rotateRight = @import("../barrel_shifter.zig").rotateRight;
|
||||
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 {
|
||||
@@ -22,96 +17,91 @@ pub fn fmt4(comptime op: u4) InstrFn {
|
||||
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: u1 = undefined;
|
||||
|
||||
switch (op) {
|
||||
0x0 => {
|
||||
// AND
|
||||
const result = cpu.r[rd] & cpu.r[rs];
|
||||
cpu.r[rd] = result;
|
||||
setLogicOpFlags(true, cpu, result);
|
||||
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 => {
|
||||
// CMN
|
||||
const tmp = @addWithOverflow(op1, op2);
|
||||
result = tmp[0];
|
||||
overflow = tmp[1];
|
||||
},
|
||||
0x1 => {
|
||||
// EOR
|
||||
const result = cpu.r[rd] ^ cpu.r[rs];
|
||||
cpu.r[rd] = result;
|
||||
setLogicOpFlags(true, cpu, result);
|
||||
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
|
||||
},
|
||||
0x2 => {
|
||||
// LSL
|
||||
const result = logicalLeft(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
|
||||
cpu.r[rd] = result;
|
||||
setLogicOpFlags(true, cpu, result);
|
||||
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);
|
||||
}
|
||||
},
|
||||
0x3 => {
|
||||
// LSR
|
||||
const result = logicalRight(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
|
||||
cpu.r[rd] = result;
|
||||
setLogicOpFlags(true, cpu, result);
|
||||
},
|
||||
0x4 => {
|
||||
// ASR
|
||||
const result = arithmeticRight(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
|
||||
cpu.r[rd] = result;
|
||||
setLogicOpFlags(true, cpu, result);
|
||||
},
|
||||
0x5 => {
|
||||
// ADC
|
||||
cpu.r[rd] = adc(true, cpu, cpu.r[rd], cpu.r[rs], carry);
|
||||
0x5, 0xB => {
|
||||
// ADC, CMN
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
cpu.cpsr.c.write(overflow == 0b1);
|
||||
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
|
||||
},
|
||||
0x6 => {
|
||||
// SBC
|
||||
cpu.r[rd] = sbc(true, cpu, cpu.r[rd], cpu.r[rs], carry);
|
||||
},
|
||||
0x7 => {
|
||||
// ROR
|
||||
const result = rotateRight(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
|
||||
cpu.r[rd] = result;
|
||||
setLogicOpFlags(true, cpu, result);
|
||||
},
|
||||
0x8 => {
|
||||
// TST
|
||||
const result = cpu.r[rd] & cpu.r[rs];
|
||||
setLogicOpFlags(true, cpu, result);
|
||||
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.r[rd] = sub(true, cpu, 0, cpu.r[rs]);
|
||||
},
|
||||
0xA => {
|
||||
// CMP
|
||||
cmp(cpu, cpu.r[rd], cpu.r[rs]);
|
||||
},
|
||||
0xB => {
|
||||
// CMN
|
||||
cmn(cpu, cpu.r[rd], cpu.r[rs]);
|
||||
},
|
||||
0xC => {
|
||||
// ORR
|
||||
const result = cpu.r[rd] | cpu.r[rs];
|
||||
cpu.r[rd] = result;
|
||||
setLogicOpFlags(true, cpu, result);
|
||||
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 => {
|
||||
// MUL
|
||||
const temp = @as(u64, cpu.r[rs]) * @as(u64, cpu.r[rd]);
|
||||
const result = @truncate(u32, temp);
|
||||
cpu.r[rd] = result;
|
||||
|
||||
// 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
|
||||
},
|
||||
0xE => {
|
||||
// BIC
|
||||
const result = cpu.r[rd] & ~cpu.r[rs];
|
||||
cpu.r[rd] = result;
|
||||
setLogicOpFlags(true, cpu, result);
|
||||
},
|
||||
0xF => {
|
||||
// MVN
|
||||
const result = ~cpu.r[rs];
|
||||
cpu.r[rd] = result;
|
||||
setLogicOpFlags(true, cpu, result);
|
||||
},
|
||||
}
|
||||
}
|
||||
}.inner;
|
||||
|
@@ -33,7 +33,8 @@ pub fn fmt14(comptime L: bool, comptime R: bool) InstrFn {
|
||||
if (R) {
|
||||
if (L) {
|
||||
const value = bus.read(u32, address);
|
||||
cpu.r[15] = value & 0xFFFF_FFFE;
|
||||
cpu.r[15] = value & ~@as(u32, 1);
|
||||
cpu.pipe.reload(cpu);
|
||||
} else {
|
||||
bus.write(u32, address, cpu.r[14]);
|
||||
}
|
||||
@@ -52,7 +53,13 @@ pub fn fmt15(comptime L: bool, comptime rb: u3) InstrFn {
|
||||
const end_address = cpu.r[rb] + 4 * countRlist(opcode);
|
||||
|
||||
if (opcode & 0xFF == 0) {
|
||||
if (L) cpu.r[15] = bus.read(u32, address) else bus.write(u32, address, cpu.r[15] + 4);
|
||||
if (L) {
|
||||
cpu.r[15] = bus.read(u32, address);
|
||||
cpu.pipe.reload(cpu);
|
||||
} else {
|
||||
bus.write(u32, address, cpu.r[15] + 2);
|
||||
}
|
||||
|
||||
cpu.r[rb] += 0x40;
|
||||
return;
|
||||
}
|
||||
@@ -85,8 +92,7 @@ pub fn fmt15(comptime L: bool, comptime rb: u3) InstrFn {
|
||||
inline fn countRlist(opcode: u16) u32 {
|
||||
var count: u32 = 0;
|
||||
|
||||
comptime var i: u4 = 0;
|
||||
inline while (i < 8) : (i += 1) {
|
||||
inline for (0..8) |i| {
|
||||
if (opcode >> (7 - i) & 1 == 1) count += 1;
|
||||
}
|
||||
|
||||
|
@@ -3,22 +3,19 @@ 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;
|
||||
const sext = @import("zba-util").sext;
|
||||
|
||||
pub fn fmt16(comptime cond: u4) InstrFn {
|
||||
return struct {
|
||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||
// B
|
||||
const offset = sext(u32, u8, opcode & 0xFF) << 1;
|
||||
if (cond == 0xE or cond == 0xF)
|
||||
cpu.panic("[CPU/THUMB.16] Undefined conditional branch with condition {}", .{cond});
|
||||
|
||||
const should_execute = switch (cond) {
|
||||
0xE, 0xF => cpu.panic("[CPU/THUMB.16] Undefined conditional branch with condition {}", .{cond}),
|
||||
else => checkCond(cpu.cpsr, cond),
|
||||
};
|
||||
if (!checkCond(cpu.cpsr, cond)) return;
|
||||
|
||||
if (should_execute) {
|
||||
cpu.r[15] = (cpu.r[15] + 2) +% offset;
|
||||
}
|
||||
cpu.r[15] +%= sext(u32, u8, opcode & 0xFF) << 1;
|
||||
cpu.pipe.reload(cpu);
|
||||
}
|
||||
}.inner;
|
||||
}
|
||||
@@ -27,8 +24,8 @@ pub fn fmt18() InstrFn {
|
||||
return struct {
|
||||
// B but conditional
|
||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||
const offset = sext(u32, u11, opcode & 0x7FF) << 1;
|
||||
cpu.r[15] = (cpu.r[15] + 2) +% offset;
|
||||
cpu.r[15] +%= sext(u32, u11, opcode & 0x7FF) << 1;
|
||||
cpu.pipe.reload(cpu);
|
||||
}
|
||||
}.inner;
|
||||
}
|
||||
@@ -41,13 +38,16 @@ pub fn fmt19(comptime is_low: bool) InstrFn {
|
||||
|
||||
if (is_low) {
|
||||
// Instruction 2
|
||||
const old_pc = cpu.r[15];
|
||||
const next_opcode = cpu.r[15] - 2;
|
||||
|
||||
cpu.r[15] = cpu.r[14] +% (offset << 1);
|
||||
cpu.r[14] = old_pc | 1;
|
||||
cpu.r[14] = next_opcode | 1;
|
||||
|
||||
cpu.pipe.reload(cpu);
|
||||
} else {
|
||||
// Instruction 1
|
||||
cpu.r[14] = (cpu.r[15] + 2) +% (sext(u32, u11, offset) << 12);
|
||||
const lr_offset = sext(u32, u11, offset) << 12;
|
||||
cpu.r[14] = (cpu.r[15] +% lr_offset) & ~@as(u32, 1);
|
||||
}
|
||||
}
|
||||
}.inner;
|
||||
|
@@ -1,16 +1,12 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Bus = @import("../../Bus.zig");
|
||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
||||
const shifter = @import("../barrel_shifter.zig");
|
||||
|
||||
const add = @import("../arm/data_processing.zig").add;
|
||||
const sub = @import("../arm/data_processing.zig").sub;
|
||||
const cmp = @import("../arm/data_processing.zig").cmp;
|
||||
const setLogicOpFlags = @import("../arm/data_processing.zig").setLogicOpFlags;
|
||||
|
||||
const log = std.log.scoped(.Thumb1);
|
||||
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 {
|
||||
@@ -24,7 +20,7 @@ pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
|
||||
if (offset == 0) {
|
||||
break :blk cpu.r[rs];
|
||||
} else {
|
||||
break :blk shifter.logicalLeft(true, &cpu.cpsr, cpu.r[rs], offset);
|
||||
break :blk lsl(true, &cpu.cpsr, cpu.r[rs], offset);
|
||||
}
|
||||
},
|
||||
0b01 => blk: {
|
||||
@@ -33,7 +29,7 @@ pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
|
||||
cpu.cpsr.c.write(cpu.r[rs] >> 31 & 1 == 1);
|
||||
break :blk @as(u32, 0);
|
||||
} else {
|
||||
break :blk shifter.logicalRight(true, &cpu.cpsr, cpu.r[rs], offset);
|
||||
break :blk lsr(true, &cpu.cpsr, cpu.r[rs], offset);
|
||||
}
|
||||
},
|
||||
0b10 => blk: {
|
||||
@@ -42,7 +38,7 @@ pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
|
||||
cpu.cpsr.c.write(cpu.r[rs] >> 31 & 1 == 1);
|
||||
break :blk @bitCast(u32, @bitCast(i32, cpu.r[rs]) >> 31);
|
||||
} else {
|
||||
break :blk shifter.arithmeticRight(true, &cpu.cpsr, cpu.r[rs], offset);
|
||||
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}),
|
||||
@@ -50,7 +46,10 @@ pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
|
||||
|
||||
// Equivalent to an ARM MOVS
|
||||
cpu.r[rd] = result;
|
||||
setLogicOpFlags(true, cpu, result);
|
||||
|
||||
// Write Flags
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
}
|
||||
}.inner;
|
||||
}
|
||||
@@ -58,28 +57,51 @@ pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
|
||||
pub fn fmt5(comptime op: u2, comptime h1: u1, comptime h2: u1) InstrFn {
|
||||
return struct {
|
||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||
const src_idx = @as(u4, h2) << 3 | (opcode >> 3 & 0x7);
|
||||
const dst_idx = @as(u4, h1) << 3 | (opcode & 0x7);
|
||||
const rs = @as(u4, h2) << 3 | (opcode >> 3 & 0x7);
|
||||
const rd = @as(u4, h1) << 3 | (opcode & 0x7);
|
||||
|
||||
const src = if (src_idx == 0xF) (cpu.r[src_idx] + 2) & 0xFFFF_FFFE else cpu.r[src_idx];
|
||||
const dst = if (dst_idx == 0xF) (cpu.r[dst_idx] + 2) & 0xFFFF_FFFE else cpu.r[dst_idx];
|
||||
const op1 = cpu.r[rd];
|
||||
const op2 = cpu.r[rs];
|
||||
|
||||
var result: u32 = undefined;
|
||||
var overflow: u1 = undefined;
|
||||
switch (op) {
|
||||
0b00 => {
|
||||
// ADD
|
||||
const sum = add(false, cpu, dst, src);
|
||||
cpu.r[dst_idx] = if (dst_idx == 0xF) sum & 0xFFFF_FFFE else sum;
|
||||
},
|
||||
0b01 => cmp(cpu, dst, src), // CMP
|
||||
0b10 => {
|
||||
// MOV
|
||||
cpu.r[dst_idx] = if (dst_idx == 0xF) src & 0xFFFF_FFFE else src;
|
||||
},
|
||||
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
|
||||
cpu.cpsr.t.write(src & 1 == 1);
|
||||
cpu.r[15] = src & 0xFFFF_FFFE;
|
||||
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;
|
||||
@@ -90,21 +112,28 @@ pub fn fmt2(comptime I: bool, is_sub: bool, rn: u3) InstrFn {
|
||||
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
|
||||
cpu.r[rd] = if (I) blk: {
|
||||
break :blk sub(true, cpu, cpu.r[rs], rn);
|
||||
} else blk: {
|
||||
break :blk sub(true, cpu, cpu.r[rs], cpu.r[rn]);
|
||||
};
|
||||
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
|
||||
cpu.r[rd] = if (I) blk: {
|
||||
break :blk add(true, cpu, cpu.r[rs], rn);
|
||||
} else blk: {
|
||||
break :blk add(true, cpu, cpu.r[rs], cpu.r[rn]);
|
||||
};
|
||||
var overflow: u1 = undefined;
|
||||
const result = add(&overflow, op1, op2);
|
||||
cpu.r[rd] = result;
|
||||
|
||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||
cpu.cpsr.z.write(result == 0);
|
||||
cpu.cpsr.c.write(overflow == 0b1);
|
||||
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
|
||||
}
|
||||
}
|
||||
}.inner;
|
||||
@@ -113,17 +142,36 @@ pub fn fmt2(comptime I: bool, is_sub: bool, rn: u3) InstrFn {
|
||||
pub fn fmt3(comptime op: u2, comptime rd: u3) InstrFn {
|
||||
return struct {
|
||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||
const offset = @truncate(u8, opcode);
|
||||
const op1 = cpu.r[rd];
|
||||
const op2: u32 = opcode & 0xFF; // Offset
|
||||
|
||||
var overflow: u1 = 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
|
||||
cpu.r[rd] = offset;
|
||||
setLogicOpFlags(true, cpu, offset);
|
||||
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 == 0b1);
|
||||
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
|
||||
},
|
||||
0b01 => cmp(cpu, cpu.r[rd], offset), // CMP
|
||||
0b10 => cpu.r[rd] = add(true, cpu, cpu.r[rd], offset), // ADD
|
||||
0b11 => cpu.r[rd] = sub(true, cpu, cpu.r[rd], offset), // SUB
|
||||
}
|
||||
}
|
||||
}.inner;
|
||||
@@ -133,10 +181,9 @@ pub fn fmt12(comptime isSP: bool, comptime rd: u3) InstrFn {
|
||||
return struct {
|
||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||
// ADD
|
||||
const left = if (isSP) cpu.r[13] else (cpu.r[15] + 2) & 0xFFFF_FFFD;
|
||||
const left = if (isSP) cpu.r[13] else cpu.r[15] & ~@as(u32, 2);
|
||||
const right = (opcode & 0xFF) << 2;
|
||||
const result = left + right;
|
||||
cpu.r[rd] = result;
|
||||
cpu.r[rd] = left + right;
|
||||
}
|
||||
}.inner;
|
||||
}
|
||||
|
@@ -1,18 +1,18 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Bus = @import("../../Bus.zig");
|
||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
||||
|
||||
const rotr = @import("../../../util.zig").rotr;
|
||||
const sext = @import("../../../util.zig").sext;
|
||||
const rotr = @import("zba-util").rotr;
|
||||
const sext = @import("zba-util").sext;
|
||||
|
||||
pub fn fmt6(comptime rd: u3) InstrFn {
|
||||
return struct {
|
||||
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
|
||||
// LDR
|
||||
const offset = (opcode & 0xFF) << 2;
|
||||
cpu.r[rd] = bus.read(u32, (cpu.r[15] + 2 & 0xFFFF_FFFD) + offset);
|
||||
|
||||
// Bit 1 of the PC intentionally ignored
|
||||
cpu.r[rd] = bus.read(u32, (cpu.r[15] & ~@as(u32, 2)) + offset);
|
||||
}
|
||||
}.inner;
|
||||
}
|
||||
@@ -44,11 +44,8 @@ pub fn fmt78(comptime op: u2, comptime T: bool) InstrFn {
|
||||
},
|
||||
0b11 => {
|
||||
// LDRSH
|
||||
cpu.r[rd] = if (address & 1 == 1) blk: {
|
||||
break :blk sext(u32, u8, bus.read(u8, address));
|
||||
} else blk: {
|
||||
break :blk sext(u32, u16, bus.read(u16, address));
|
||||
};
|
||||
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 {
|
||||
|
@@ -6,7 +6,7 @@ pub fn fmt17() InstrFn {
|
||||
return struct {
|
||||
fn inner(cpu: *Arm7tdmi, _: *Bus, _: u16) void {
|
||||
// Copy Values from Current Mode
|
||||
const r15 = cpu.r[15];
|
||||
const ret_addr = cpu.r[15] - 2;
|
||||
const cpsr = cpu.cpsr.raw;
|
||||
|
||||
// Switch Mode
|
||||
@@ -14,9 +14,10 @@ pub fn fmt17() InstrFn {
|
||||
cpu.cpsr.t.write(false); // Force ARM Mode
|
||||
cpu.cpsr.i.write(true); // Disable normal interrupts
|
||||
|
||||
cpu.r[14] = r15; // Resume Execution
|
||||
cpu.r[14] = ret_addr; // Resume Execution
|
||||
cpu.spsr.raw = cpsr; // Previous mode CPSR
|
||||
cpu.r[15] = 0x0000_0008;
|
||||
cpu.pipe.reload(cpu);
|
||||
}
|
||||
}.inner;
|
||||
}
|
||||
|
292
src/core/emu.zig
292
src/core/emu.zig
@@ -1,37 +1,30 @@
|
||||
const std = @import("std");
|
||||
const SDL = @import("sdl2");
|
||||
const config = @import("../config.zig");
|
||||
|
||||
const Bus = @import("Bus.zig");
|
||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
||||
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
||||
const FpsTracker = @import("../util.zig").FpsTracker;
|
||||
const FilePaths = @import("../util.zig").FilePaths;
|
||||
const Tracker = @import("../util.zig").FpsTracker;
|
||||
const TwoWayChannel = @import("zba-util").TwoWayChannel;
|
||||
|
||||
const Timer = std.time.Timer;
|
||||
const Thread = std.Thread;
|
||||
const Atomic = std.atomic.Atomic;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
// TODO: Move these to a TOML File
|
||||
const sync_audio = false; // Enable Audio Sync
|
||||
const sync_video: RunKind = .LimitedFPS; // Configure Video Sync
|
||||
pub const win_scale = 4; // 1x, 2x, 3x, etc. Window Scaling
|
||||
pub const cpu_logging = false; // Enable detailed CPU logging
|
||||
pub const allow_unhandled_io = true; // Only relevant in Debug Builds
|
||||
pub const force_rtc = false;
|
||||
/// 4 Cycles in 1 dot
|
||||
const cycles_per_dot = 4;
|
||||
|
||||
// 228 Lines which consist of 308 dots (which are 4 cycles long)
|
||||
const cycles_per_frame: u64 = 228 * (308 * 4); //280896
|
||||
const clock_rate: u64 = 1 << 24; // 16.78MHz
|
||||
/// The GBA draws 228 Horizontal which each consist 308 dots
|
||||
/// (note: not all lines are visible)
|
||||
const cycles_per_frame = 228 * (308 * cycles_per_dot); //280896
|
||||
|
||||
// TODO: Don't truncate this, be more accurate w/ timing
|
||||
// 59.6046447754ns (truncated to just 59ns)
|
||||
const clock_period: u64 = std.time.ns_per_s / clock_rate;
|
||||
const frame_period = (clock_period * cycles_per_frame);
|
||||
/// The GBA ARM7TDMI runs at 2^24 Hz
|
||||
const clock_rate = 1 << 24; // 16.78MHz
|
||||
|
||||
// 59.7275005696Hz
|
||||
pub const frame_rate = @intToFloat(f64, std.time.ns_per_s) /
|
||||
((@intToFloat(f64, std.time.ns_per_s) / @intToFloat(f64, clock_rate)) * @intToFloat(f64, cycles_per_frame));
|
||||
/// The # of nanoseconds a frame should take
|
||||
const frame_period = (std.time.ns_per_s * cycles_per_frame) / clock_rate;
|
||||
|
||||
/// Exact Value: 59.7275005696Hz
|
||||
/// The inverse of the frame period
|
||||
pub const frame_rate: f64 = @intToFloat(f64, clock_rate) / cycles_per_frame;
|
||||
|
||||
const log = std.log.scoped(.Emulation);
|
||||
|
||||
@@ -40,18 +33,81 @@ const RunKind = enum {
|
||||
UnlimitedFPS,
|
||||
Limited,
|
||||
LimitedFPS,
|
||||
LimitedBusy,
|
||||
};
|
||||
|
||||
pub fn run(quit: *Atomic(bool), fps: *FpsTracker, sched: *Scheduler, cpu: *Arm7tdmi) void {
|
||||
if (sync_audio) log.info("Audio sync enabled", .{});
|
||||
pub fn run(cpu: *Arm7tdmi, scheduler: *Scheduler, tracker: *Tracker, channel: *TwoWayChannel) void {
|
||||
const audio_sync = config.config().guest.audio_sync and !config.config().host.mute;
|
||||
if (audio_sync) log.info("Audio sync enabled", .{});
|
||||
|
||||
switch (sync_video) {
|
||||
.Unlimited => runUnsynchronized(quit, sched, cpu, null),
|
||||
.Limited => runSynchronized(quit, sched, cpu, null),
|
||||
.UnlimitedFPS => runUnsynchronized(quit, sched, cpu, fps),
|
||||
.LimitedFPS => runSynchronized(quit, sched, cpu, fps),
|
||||
.LimitedBusy => runBusyLoop(quit, sched, cpu),
|
||||
if (config.config().guest.video_sync) {
|
||||
inner(.LimitedFPS, audio_sync, cpu, scheduler, tracker, channel);
|
||||
} else {
|
||||
inner(.UnlimitedFPS, audio_sync, cpu, scheduler, tracker, channel);
|
||||
}
|
||||
}
|
||||
|
||||
fn inner(comptime kind: RunKind, audio_sync: bool, cpu: *Arm7tdmi, scheduler: *Scheduler, tracker: ?*Tracker, channel: *TwoWayChannel) void {
|
||||
if (kind == .UnlimitedFPS or kind == .LimitedFPS) {
|
||||
std.debug.assert(tracker != null);
|
||||
log.info("FPS tracking enabled", .{});
|
||||
}
|
||||
|
||||
var paused: bool = false;
|
||||
|
||||
switch (kind) {
|
||||
.Unlimited, .UnlimitedFPS => {
|
||||
log.info("Emulation w/out video sync", .{});
|
||||
|
||||
while (true) {
|
||||
if (channel.emu.pop()) |e| switch (e) {
|
||||
.Quit => break,
|
||||
.Resume => paused = false,
|
||||
.Pause => {
|
||||
paused = true;
|
||||
channel.gui.push(.Paused);
|
||||
},
|
||||
};
|
||||
|
||||
if (paused) continue;
|
||||
|
||||
runFrame(scheduler, cpu);
|
||||
audioSync(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
|
||||
|
||||
if (kind == .UnlimitedFPS) tracker.?.tick();
|
||||
}
|
||||
},
|
||||
.Limited, .LimitedFPS => {
|
||||
log.info("Emulation w/ video sync", .{});
|
||||
var timer = Timer.start() catch @panic("failed to initalize std.timer.Timer");
|
||||
var wake_time: u64 = frame_period;
|
||||
|
||||
while (true) {
|
||||
if (channel.emu.pop()) |e| switch (e) {
|
||||
.Quit => break,
|
||||
.Resume => paused = false,
|
||||
.Pause => {
|
||||
paused = true;
|
||||
channel.gui.push(.Paused);
|
||||
},
|
||||
};
|
||||
|
||||
if (paused) continue;
|
||||
|
||||
runFrame(scheduler, cpu);
|
||||
const new_wake_time = videoSync(&timer, wake_time);
|
||||
|
||||
// Spin to make up the difference of OS scheduler innacuracies
|
||||
// If we happen to also be syncing to audio, we choose to spin on
|
||||
// the amount of time needed for audio to catch up rather than
|
||||
// our expected wake-up time
|
||||
|
||||
audioSync(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
|
||||
if (!audio_sync) spinLoop(&timer, wake_time);
|
||||
wake_time = new_wake_time;
|
||||
|
||||
if (kind == .LimitedFPS) tracker.?.tick();
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +118,7 @@ pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
|
||||
if (!cpu.stepDmaTransfer()) {
|
||||
if (cpu.isHalted()) {
|
||||
// Fast-forward to next Event
|
||||
sched.tick = sched.queue.peek().?.tick;
|
||||
sched.tick = sched.nextTimestamp();
|
||||
} else {
|
||||
cpu.step();
|
||||
}
|
||||
@@ -72,7 +128,8 @@ pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
|
||||
}
|
||||
}
|
||||
|
||||
fn syncToAudio(stream: *SDL.SDL_AudioStream, is_buffer_full: *bool) void {
|
||||
fn audioSync(audio_sync: bool, stream: *SDL.SDL_AudioStream, is_buffer_full: *bool) void {
|
||||
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;
|
||||
|
||||
@@ -85,96 +142,25 @@ fn syncToAudio(stream: *SDL.SDL_AudioStream, is_buffer_full: *bool) void {
|
||||
|
||||
while (true) {
|
||||
still_full = SDL.SDL_AudioStreamAvailable(stream) > sample_size * max_buf_size >> 1;
|
||||
if (!sync_audio or !still_full) break;
|
||||
if (!audio_sync or !still_full) break;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn runUnsynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*FpsTracker) void {
|
||||
log.info("Emulation thread w/out video sync", .{});
|
||||
|
||||
if (fps) |tracker| {
|
||||
log.info("FPS Tracking Enabled", .{});
|
||||
|
||||
while (!quit.load(.SeqCst)) {
|
||||
runFrame(sched, cpu);
|
||||
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
|
||||
|
||||
tracker.tick();
|
||||
}
|
||||
} else {
|
||||
while (!quit.load(.SeqCst)) {
|
||||
runFrame(sched, cpu);
|
||||
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn runSynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*FpsTracker) void {
|
||||
log.info("Emulation thread w/ video sync", .{});
|
||||
var timer = Timer.start() catch std.debug.panic("Failed to initialize std.timer.Timer", .{});
|
||||
var wake_time: u64 = frame_period;
|
||||
|
||||
if (fps) |tracker| {
|
||||
log.info("FPS Tracking Enabled", .{});
|
||||
|
||||
while (!quit.load(.SeqCst)) {
|
||||
runFrame(sched, cpu);
|
||||
const new_wake_time = blockOnVideo(&timer, wake_time);
|
||||
|
||||
// Spin to make up the difference of OS scheduler innacuracies
|
||||
// If we happen to also be syncing to audio, we choose to spin on
|
||||
// the amount of time needed for audio to catch up rather than
|
||||
// our expected wake-up time
|
||||
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
|
||||
if (!sync_audio) spinLoop(&timer, wake_time);
|
||||
wake_time = new_wake_time;
|
||||
|
||||
tracker.tick();
|
||||
}
|
||||
} else {
|
||||
while (!quit.load(.SeqCst)) {
|
||||
runFrame(sched, cpu);
|
||||
const new_wake_time = blockOnVideo(&timer, wake_time);
|
||||
|
||||
// see above comment
|
||||
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
|
||||
if (!sync_audio) spinLoop(&timer, wake_time);
|
||||
wake_time = new_wake_time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fn blockOnVideo(timer: *Timer, wake_time: u64) u64 {
|
||||
fn videoSync(timer: *Timer, wake_time: u64) u64 {
|
||||
// Use the OS scheduler to put the emulation thread to sleep
|
||||
const maybe_recalc_wake_time = sleep(timer, wake_time);
|
||||
const recalculated = sleep(timer, wake_time);
|
||||
|
||||
// If sleep() determined we need to adjust our wake up time, do so
|
||||
// otherwise predict our next wake up time according to the frame period
|
||||
return if (maybe_recalc_wake_time) |recalc| recalc else wake_time + frame_period;
|
||||
}
|
||||
|
||||
pub fn runBusyLoop(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi) void {
|
||||
log.info("Emulation thread with video sync using busy loop", .{});
|
||||
var timer = Timer.start() catch unreachable;
|
||||
var wake_time: u64 = frame_period;
|
||||
|
||||
while (!quit.load(.SeqCst)) {
|
||||
runFrame(sched, cpu);
|
||||
spinLoop(&timer, wake_time);
|
||||
|
||||
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
|
||||
|
||||
// Update to the new wake time
|
||||
wake_time += frame_period;
|
||||
}
|
||||
return recalculated orelse wake_time + frame_period;
|
||||
}
|
||||
|
||||
// TODO: Better sleep impl?
|
||||
fn sleep(timer: *Timer, wake_time: u64) ?u64 {
|
||||
// const step = std.time.ns_per_ms * 10; // 10ms
|
||||
const timestamp = timer.read();
|
||||
|
||||
// ns_late is non zero if we are late.
|
||||
const ns_late = timestamp -| wake_time;
|
||||
var ns_late = timestamp -| wake_time;
|
||||
|
||||
// If we're more than a frame late, skip the rest of this loop
|
||||
// Recalculate what our new wake time should be so that we can
|
||||
@@ -182,15 +168,17 @@ fn sleep(timer: *Timer, wake_time: u64) ?u64 {
|
||||
if (ns_late > frame_period) return timestamp + frame_period;
|
||||
const sleep_for = frame_period - ns_late;
|
||||
|
||||
// // Employ several sleep calls in periods of 10ms
|
||||
// // By doing this the behaviour should average out to be
|
||||
// // more consistent
|
||||
// const loop_count = sleep_for / step; // How many groups of 10ms
|
||||
const step = 2 * std.time.ns_per_ms; // Granularity of 2ms
|
||||
const times = sleep_for / step;
|
||||
|
||||
// var i: usize = 0;
|
||||
// while (i < loop_count) : (i += 1) std.time.sleep(step);
|
||||
for (0..times) |_| {
|
||||
std.time.sleep(step);
|
||||
|
||||
std.time.sleep(sleep_for);
|
||||
// Upon wakeup, check to see if this particular sleep was longer than expected
|
||||
// if so we should exit early, but probably not skip a whole frame period
|
||||
ns_late = timer.read() -| wake_time;
|
||||
if (ns_late > frame_period) return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -198,3 +186,71 @@ fn sleep(timer: *Timer, wake_time: u64) ?u64 {
|
||||
fn spinLoop(timer: *Timer, wake_time: u64) void {
|
||||
while (true) if (timer.read() > wake_time) break;
|
||||
}
|
||||
|
||||
pub const EmuThing = struct {
|
||||
const Self = @This();
|
||||
const Interface = @import("gdbstub").Emulator;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
cpu: *Arm7tdmi,
|
||||
scheduler: *Scheduler,
|
||||
|
||||
pub fn init(cpu: *Arm7tdmi, scheduler: *Scheduler) Self {
|
||||
return .{ .cpu = cpu, .scheduler = scheduler };
|
||||
}
|
||||
|
||||
pub fn interface(self: *Self, allocator: Allocator) Interface {
|
||||
return Interface.init(allocator, self);
|
||||
}
|
||||
|
||||
pub fn read(self: *const Self, addr: u32) u8 {
|
||||
return self.cpu.bus.dbgRead(u8, addr);
|
||||
}
|
||||
|
||||
pub fn write(self: *Self, addr: u32, value: u8) void {
|
||||
self.cpu.bus.dbgWrite(u8, addr, value);
|
||||
}
|
||||
|
||||
pub fn registers(self: *const Self) *[16]u32 {
|
||||
return &self.cpu.r;
|
||||
}
|
||||
|
||||
pub fn cpsr(self: *const Self) u32 {
|
||||
return self.cpu.cpsr.raw;
|
||||
}
|
||||
|
||||
pub fn step(self: *Self) void {
|
||||
const cpu = self.cpu;
|
||||
const sched = self.scheduler;
|
||||
|
||||
// Is true when we have executed one (1) instruction
|
||||
var did_step: bool = false;
|
||||
|
||||
// TODO: How can I make it easier to keep this in lock-step with runFrame?
|
||||
while (!did_step) {
|
||||
if (!cpu.stepDmaTransfer()) {
|
||||
if (cpu.isHalted()) {
|
||||
// Fast-forward to next Event
|
||||
sched.tick = sched.queue.peek().?.tick;
|
||||
} else {
|
||||
cpu.step();
|
||||
did_step = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (sched.tick >= sched.nextTimestamp()) sched.handleEvent(cpu);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub 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();
|
||||
}
|
||||
|
||||
pub fn replaceGamepak(cpu: *Arm7tdmi, file_path: []const u8) !void {
|
||||
try cpu.bus.replaceGamepak(file_path);
|
||||
reset(cpu);
|
||||
}
|
||||
|
1217
src/core/ppu.zig
1217
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.readIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)]),
|
||||
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.writeIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)], value),
|
||||
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);
|
||||
std.mem.set(u8, buf, 0);
|
||||
|
||||
return Self{ .buf = buf, .allocator = allocator };
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
std.mem.set(u8, 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.readIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)]),
|
||||
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.writeIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)], value),
|
||||
u8 => {
|
||||
const align_addr = addr & ~@as(u32, 1); // Aligned to Halfword boundary
|
||||
std.mem.writeIntSliceLittle(u16, self.buf[align_addr..][0..@sizeOf(u16)], @as(u16, value) * 0x101);
|
||||
},
|
||||
else => @compileError("PALRAM: Unsupported write width"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(allocator: Allocator) !Self {
|
||||
const buf = try allocator.alloc(u8, buf_len);
|
||||
std.mem.set(u8, buf, 0);
|
||||
|
||||
return Self{ .buf = buf, .allocator = allocator };
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
std.mem.set(u8, 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.readIntNative(u16, self.buf[0..2]);
|
||||
}
|
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.readIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)]),
|
||||
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.writeIntSliceLittle(T, self.buf[idx..][0..@sizeOf(T)], value),
|
||||
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.writeIntSliceLittle(u16, self.buf[align_idx..][0..@sizeOf(u16)], @as(u16, value) * 0x101);
|
||||
},
|
||||
else => @compileError("VRAM: Unsupported write width"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(allocator: Allocator) !Self {
|
||||
const buf = try allocator.alloc(u8, buf_len);
|
||||
std.mem.set(u8, buf, 0);
|
||||
|
||||
return Self{ .buf = buf, .allocator = allocator };
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
std.mem.set(u8, 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,5 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Bus = @import("Bus.zig");
|
||||
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
||||
const Clock = @import("bus/gpio.zig").Clock;
|
||||
|
||||
@@ -12,11 +11,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;
|
||||
@@ -27,12 +26,23 @@ 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;
|
||||
|
||||
switch (event.kind) {
|
||||
@@ -47,10 +57,7 @@ pub const Scheduler = struct {
|
||||
},
|
||||
.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| cpu.bus.tim[idx].onTimerExpire(cpu, late),
|
||||
}
|
||||
},
|
||||
.ApuChannel => |id| {
|
||||
@@ -74,22 +81,16 @@ pub const Scheduler = struct {
|
||||
.VBlank => cpu.bus.ppu.onHdrawEnd(cpu, late), // The end of a VBlank
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the **first** scheduled event of type `needle`
|
||||
pub fn removeScheduledEvent(self: *Self, needle: EventKind) void {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
307
src/imgui.zig
Normal file
307
src/imgui.zig
Normal file
@@ -0,0 +1,307 @@
|
||||
//! 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("core/cpu.zig").Arm7tdmi;
|
||||
const RingBuffer = @import("zba-util").RingBuffer;
|
||||
|
||||
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,
|
||||
|
||||
/// 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);
|
||||
|
||||
const title: [12:0]u8 = if (title_opt) |t| t.* ++ [_:0]u8{} else "[No Title]\x00\x00".*;
|
||||
return .{ .title = title, .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, tex_id: GLuint, cpu: *Arm7tdmi) void {
|
||||
const win_scale = config.config().host.win_scale;
|
||||
|
||||
{
|
||||
_ = 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 maybe_path = nfd.openFileDialog("gba", null) catch |e| {
|
||||
log.err("failed to open file dialog: {}", .{e});
|
||||
break :blk;
|
||||
};
|
||||
|
||||
const file_path = maybe_path orelse {
|
||||
log.warn("did not receive a file path", .{});
|
||||
break :blk;
|
||||
};
|
||||
defer nfd.freePath(file_path);
|
||||
|
||||
log.info("user chose: \"{s}\"", .{file_path});
|
||||
emu.replaceGamepak(cpu, file_path) catch |e| {
|
||||
log.err("failed to replace GamePak: {}", .{e});
|
||||
break :blk;
|
||||
};
|
||||
|
||||
state.title = cpu.bus.pak.title ++ [_:0]u8{};
|
||||
}
|
||||
}
|
||||
|
||||
if (zgui.beginMenu("Emulation", true)) {
|
||||
defer zgui.endMenu();
|
||||
|
||||
if (zgui.menuItem("Restart", .{})) {
|
||||
emu.reset(cpu);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const w = @intToFloat(f32, gba_width * win_scale);
|
||||
const h = @intToFloat(f32, gba_height * win_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(@intToPtr(*anyopaque, tex_id), .{ .w = w, .h = h, .uv0 = .{ 0, 1 }, .uv1 = .{ 1, 0 } });
|
||||
}
|
||||
|
||||
{
|
||||
_ = zgui.begin("Information", .{});
|
||||
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", cpu.bus.io.ie);
|
||||
widgets.interrupts("IRQ", cpu.bus.io.irq);
|
||||
}
|
||||
|
||||
{
|
||||
_ = zgui.begin("Performance", .{});
|
||||
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;
|
||||
|
||||
std.mem.copy(u32, buf[0..len], values[0..len]);
|
||||
std.sort.sort(u32, buf[0..len], {}, std.sort.asc(u32));
|
||||
|
||||
break :blk buf;
|
||||
};
|
||||
|
||||
const y_max = 2 * if (len != 0) @intToFloat(f64, sorted[len - 1]) else emu.frame_rate;
|
||||
const x_max = @intToFloat(f64, 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 = average: {
|
||||
var sum: u32 = 0;
|
||||
for (sorted[0..len]) |value| sum += value;
|
||||
|
||||
break :average @intCast(u32, 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]});
|
||||
}
|
||||
|
||||
{
|
||||
_ = zgui.begin("Scheduler", .{});
|
||||
defer zgui.end();
|
||||
|
||||
const scheduler = cpu.sched;
|
||||
|
||||
zgui.text("tick: {X:0>16}", .{scheduler.tick});
|
||||
zgui.separator();
|
||||
|
||||
const Event = std.meta.Child(@TypeOf(scheduler.queue.items));
|
||||
|
||||
var items: [20]Event = undefined;
|
||||
const len = scheduler.queue.len;
|
||||
|
||||
std.mem.copy(Event, &items, scheduler.queue.items);
|
||||
std.sort.sort(Event, items[0..len], {}, widgets.eventDesc(Event));
|
||||
|
||||
for (items[0..len]) |event| {
|
||||
zgui.text("{X:0>16} | {?}", .{ event.tick, event.kind });
|
||||
}
|
||||
}
|
||||
|
||||
// {
|
||||
// zgui.showDemoWindow(null);
|
||||
// }
|
||||
}
|
||||
|
||||
const widgets = struct {
|
||||
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("core/cpu.zig").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;
|
||||
}
|
||||
};
|
204
src/main.zig
204
src/main.zig
@@ -1,46 +1,79 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const known_folders = @import("known_folders");
|
||||
const clap = @import("clap");
|
||||
|
||||
const config = @import("config.zig");
|
||||
const emu = @import("core/emu.zig");
|
||||
|
||||
const TwoWayChannel = @import("zba-util").TwoWayChannel;
|
||||
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 log = std.log.scoped(.Cli);
|
||||
const width = @import("core/ppu.zig").width;
|
||||
const height = @import("core/ppu.zig").height;
|
||||
const cpu_logging = @import("core/emu.zig").cpu_logging;
|
||||
pub const log_level = if (builtin.mode != .Debug) .info else std.log.default_level;
|
||||
|
||||
// TODO: Reimpl Logging
|
||||
const log = std.log.scoped(.Cli);
|
||||
pub const log_level = if (builtin.mode != .Debug) .info else std.log.default_level;
|
||||
|
||||
// CLI Arguments + Help Text
|
||||
const params = clap.parseParamsComptime(
|
||||
\\-h, --help Display this help and exit.
|
||||
\\-s, --skip Skip BIOS.
|
||||
\\-b, --bios <str> Optional path to a GBA BIOS ROM.
|
||||
\\<str> Path to the GBA GamePak 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());
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
// Handle CLI Input
|
||||
const result = try clap.parse(clap.Help, ¶ms, clap.parsers.default, .{});
|
||||
// Determine the Data Directory (stores saves)
|
||||
const data_path = blk: {
|
||||
const result = known_folders.getPath(allocator, .data);
|
||||
const option = result catch |e| exitln("interrupted while determining the data folder: {}", .{e});
|
||||
const path = option orelse exitln("no valid data folder found", .{});
|
||||
ensureDataDirsExist(path) catch |e| exitln("failed to create folders under \"{s}\": {}", .{ path, e });
|
||||
|
||||
break :blk path;
|
||||
};
|
||||
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});
|
||||
defer result.deinit();
|
||||
|
||||
const paths = try handleArguments(allocator, &result);
|
||||
// TODO: Move config file to XDG Config directory?
|
||||
const cfg_file_path = configFilePath(allocator, config_path) catch |e| exitln("failed to ready config file for access: {}", .{e});
|
||||
defer allocator.free(cfg_file_path);
|
||||
|
||||
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);
|
||||
|
||||
const log_file: ?std.fs.File = if (cpu_logging) try std.fs.cwd().createFile("zba.log", .{}) else null;
|
||||
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
|
||||
@@ -49,54 +82,125 @@ pub fn main() anyerror!void {
|
||||
|
||||
var bus: Bus = undefined;
|
||||
var cpu = Arm7tdmi.init(&scheduler, &bus, log_file);
|
||||
if (paths.bios == null) cpu.fastBoot();
|
||||
|
||||
try bus.init(allocator, &scheduler, &cpu, paths);
|
||||
bus.init(allocator, &scheduler, &cpu, paths) catch |e| exitln("failed to init zba bus: {}", .{e});
|
||||
defer bus.deinit();
|
||||
|
||||
var gui = Gui.init(&bus.pak.title, &bus.apu, width, height);
|
||||
defer gui.deinit();
|
||||
|
||||
try gui.run(&cpu, &scheduler);
|
||||
}
|
||||
|
||||
fn getSavePath(allocator: Allocator) !?[]const u8 {
|
||||
const save_subpath = "zba" ++ [_]u8{std.fs.path.sep} ++ "save";
|
||||
|
||||
const maybe_data_path = try known_folders.getPath(allocator, .data);
|
||||
defer if (maybe_data_path) |path| allocator.free(path);
|
||||
|
||||
const save_path = if (maybe_data_path) |base| try std.fs.path.join(allocator, &[_][]const u8{ base, "zba", "save" }) else null;
|
||||
|
||||
if (save_path) |_| {
|
||||
// If we've determined what our save path should be, ensure the prereq directories
|
||||
// are present so that we can successfully write to the path when necessary
|
||||
const maybe_data_dir = try known_folders.open(allocator, .data, .{});
|
||||
if (maybe_data_dir) |data_dir| try data_dir.makePath(save_subpath);
|
||||
if (config.config().guest.skip_bios or result.args.skip or paths.bios == null) {
|
||||
cpu.fastBoot();
|
||||
}
|
||||
|
||||
return save_path;
|
||||
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();
|
||||
|
||||
var quit = std.atomic.Atomic(bool).init(false);
|
||||
|
||||
var items: [0x100]u8 = undefined;
|
||||
var channel = TwoWayChannel.init(&items);
|
||||
|
||||
if (result.args.gdb) {
|
||||
const Server = @import("gdbstub").Server;
|
||||
const EmuThing = @import("core/emu.zig").EmuThing;
|
||||
|
||||
var wrapper = EmuThing.init(&cpu, &scheduler);
|
||||
var emulator = wrapper.interface(allocator);
|
||||
defer emulator.deinit();
|
||||
|
||||
log.info("Ready to connect", .{});
|
||||
|
||||
var server = Server.init(emulator) catch |e| exitln("failed to init gdb server: {}", .{e});
|
||||
defer server.deinit(allocator);
|
||||
|
||||
log.info("Starting GDB Server Thread", .{});
|
||||
|
||||
const thread = std.Thread.spawn(.{}, Server.run, .{ &server, allocator, &quit }) catch |e| exitln("gdb server thread crashed: {}", .{e});
|
||||
defer thread.join();
|
||||
|
||||
gui.run(.{
|
||||
.cpu = &cpu,
|
||||
.scheduler = &scheduler,
|
||||
.channel = &channel,
|
||||
}) catch |e| exitln("main thread panicked: {}", .{e});
|
||||
} else {
|
||||
var tracker = FpsTracker.init();
|
||||
|
||||
const thread = std.Thread.spawn(.{}, emu.run, .{ &cpu, &scheduler, &tracker, &channel }) catch |e| exitln("emu thread panicked: {}", .{e});
|
||||
defer thread.join();
|
||||
|
||||
gui.run(.{
|
||||
.cpu = &cpu,
|
||||
.scheduler = &scheduler,
|
||||
.channel = &channel,
|
||||
.tracker = &tracker,
|
||||
}) catch |e| exitln("main thread panicked: {}", .{e});
|
||||
}
|
||||
}
|
||||
|
||||
fn getRomPath(result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) ![]const u8 {
|
||||
return switch (result.positionals.len) {
|
||||
1 => result.positionals[0],
|
||||
0 => std.debug.panic("ZBA requires a positional path to a GamePak ROM.\n", .{}),
|
||||
else => std.debug.panic("ZBA received too many arguments.\n", .{}),
|
||||
};
|
||||
}
|
||||
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});
|
||||
|
||||
pub fn handleArguments(allocator: Allocator, result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) !FilePaths {
|
||||
const rom_path = try getRomPath(result);
|
||||
log.info("ROM path: {s}", .{rom_path});
|
||||
const bios_path = result.args.bios;
|
||||
if (bios_path) |path| log.info("BIOS path: {s}", .{path}) else log.info("No BIOS provided", .{});
|
||||
const save_path = try getSavePath(allocator);
|
||||
if (save_path) |path| log.info("Save path: {s}", .{path});
|
||||
if (bios_path) |path| log.info("BIOS path: {s}", .{path}) else log.warn("No BIOS provided", .{});
|
||||
|
||||
return FilePaths{
|
||||
const save_path = try std.fs.path.join(allocator, &[_][]const u8{ data_path, "zba", "save" });
|
||||
log.info("Save path: {s}", .{save_path});
|
||||
|
||||
return .{
|
||||
.rom = rom_path,
|
||||
.bios = bios_path,
|
||||
.save = save_path,
|
||||
};
|
||||
}
|
||||
|
||||
fn configFilePath(allocator: Allocator, config_path: []const u8) ![]const u8 {
|
||||
const path = try std.fs.path.join(allocator, &[_][]const u8{ config_path, "zba", "config.toml" });
|
||||
errdefer allocator.free(path);
|
||||
|
||||
// We try to create the file exclusively, meaning that we err out if the file already exists.
|
||||
// All we care about is a file being there so we can just ignore that error in particular and
|
||||
// continue down the happy pathj
|
||||
std.fs.accessAbsolute(path, .{}) catch |e| {
|
||||
if (e != error.FileNotFound) return e;
|
||||
|
||||
const config_file = std.fs.createFileAbsolute(path, .{}) catch |err| exitln("failed to create \"{s}\": {}", .{ path, err });
|
||||
defer config_file.close();
|
||||
|
||||
try config_file.writeAll(@embedFile("../example.toml"));
|
||||
};
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
fn ensureDataDirsExist(data_path: []const u8) !void {
|
||||
var dir = try std.fs.openDirAbsolute(data_path, .{});
|
||||
defer dir.close();
|
||||
|
||||
// Will recursively create directories
|
||||
try dir.makePath("zba" ++ std.fs.path.sep_str ++ "save");
|
||||
}
|
||||
|
||||
fn ensureConfigDirExists(config_path: []const u8) !void {
|
||||
var dir = try std.fs.openDirAbsolute(config_path, .{});
|
||||
defer dir.close();
|
||||
|
||||
try dir.makePath("zba");
|
||||
}
|
||||
|
||||
fn romPath(result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) ?[]const u8 {
|
||||
return switch (result.positionals.len) {
|
||||
0 => null,
|
||||
1 => result.positionals[0],
|
||||
else => exitln("ZBA received too many positional arguments.", .{}),
|
||||
};
|
||||
}
|
||||
|
||||
fn exitln(comptime format: []const u8, args: anytype) noreturn {
|
||||
const stderr = std.io.getStdErr().writer();
|
||||
stderr.print(format, args) catch {}; // Just exit already...
|
||||
stderr.writeByte('\n') catch {};
|
||||
std.os.exit(1);
|
||||
}
|
||||
|
477
src/platform.zig
477
src/platform.zig
@@ -1,153 +1,380 @@
|
||||
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 Scheduler = @import("core/scheduler.zig").Scheduler;
|
||||
const FpsTracker = @import("util.zig").FpsTracker;
|
||||
const TwoWayChannel = @import("zba-util").TwoWayChannel;
|
||||
|
||||
const span = @import("util.zig").span;
|
||||
const gba_width = @import("core/ppu.zig").width;
|
||||
const gba_height = @import("core/ppu.zig").height;
|
||||
|
||||
const pitch = @import("core/ppu.zig").framebuf_pitch;
|
||||
const scale = @import("core/emu.zig").win_scale;
|
||||
const GLuint = gl.GLuint;
|
||||
const GLsizei = gl.GLsizei;
|
||||
const SDL_GLContext = *anyopaque;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const default_title: []const u8 = "ZBA";
|
||||
const width = 1280;
|
||||
const 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 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,
|
||||
title: []const u8,
|
||||
renderer: *SDL.SDL_Renderer,
|
||||
texture: *SDL.SDL_Texture,
|
||||
ctx: SDL_GLContext,
|
||||
audio: Audio,
|
||||
|
||||
pub fn init(title: *const [12]u8, apu: *Apu, width: i32, height: i32) Self {
|
||||
const ret = SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO | SDL.SDL_INIT_GAMECONTROLLER);
|
||||
if (ret < 0) panic();
|
||||
state: imgui.State,
|
||||
|
||||
allocator: Allocator,
|
||||
program_id: gl.GLuint,
|
||||
|
||||
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 window = SDL.SDL_CreateWindow(
|
||||
default_title.ptr,
|
||||
window_title,
|
||||
SDL.SDL_WINDOWPOS_CENTERED,
|
||||
SDL.SDL_WINDOWPOS_CENTERED,
|
||||
@as(c_int, width * scale),
|
||||
@as(c_int, height * scale),
|
||||
SDL.SDL_WINDOW_SHOWN,
|
||||
width,
|
||||
height,
|
||||
SDL.SDL_WINDOW_OPENGL | SDL.SDL_WINDOW_SHOWN,
|
||||
) orelse panic();
|
||||
|
||||
const renderer = SDL.SDL_CreateRenderer(window, -1, SDL.SDL_RENDERER_ACCELERATED | SDL.SDL_RENDERER_PRESENTVSYNC) orelse panic();
|
||||
const ctx = SDL.SDL_GL_CreateContext(window) orelse panic();
|
||||
if (SDL.SDL_GL_MakeCurrent(window, ctx) < 0) panic();
|
||||
|
||||
const texture = SDL.SDL_CreateTexture(
|
||||
renderer,
|
||||
SDL.SDL_PIXELFORMAT_RGBA8888,
|
||||
SDL.SDL_TEXTUREACCESS_STREAMING,
|
||||
@as(c_int, width),
|
||||
@as(c_int, height),
|
||||
) orelse panic();
|
||||
gl.load(ctx, Self.glGetProcAddress) catch {};
|
||||
if (SDL.SDL_GL_SetSwapInterval(@boolToInt(config.config().host.vsync)) < 0) panic();
|
||||
|
||||
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),
|
||||
.renderer = renderer,
|
||||
.texture = texture,
|
||||
.ctx = ctx,
|
||||
.program_id = try compileShaders(),
|
||||
.audio = Audio.init(apu),
|
||||
|
||||
.allocator = allocator,
|
||||
.state = try imgui.State.init(allocator, title_opt),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn run(self: *Self, cpu: *Arm7tdmi, scheduler: *Scheduler) !void {
|
||||
var quit = std.atomic.Atomic(bool).init(false);
|
||||
var frame_rate = FpsTracker.init();
|
||||
|
||||
const thread = try std.Thread.spawn(.{}, emu.run, .{ &quit, &frame_rate, scheduler, cpu });
|
||||
defer thread.join();
|
||||
|
||||
var title_buf: [0x100]u8 = [_]u8{0} ** 0x100;
|
||||
|
||||
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 => log.err("Sample Count: {}", .{@intCast(u32, SDL.SDL_AudioStreamAvailable(cpu.bus.apu.stream)) / (2 * @sizeOf(u16))}),
|
||||
SDL.SDLK_j => log.err("Scheduler Capacity: {} | Scheduler Event Count: {}", .{ scheduler.queue.capacity(), scheduler.queue.count() }),
|
||||
SDL.SDLK_k => {
|
||||
// Dump IWRAM to file
|
||||
log.info("PC: 0x{X:0>8}", .{cpu.r[15]});
|
||||
log.info("LR: 0x{X:0>8}", .{cpu.r[14]});
|
||||
// const iwram_file = try std.fs.cwd().createFile("iwram.bin", .{});
|
||||
// defer iwram_file.close();
|
||||
|
||||
// try iwram_file.writeAll(cpu.bus.iwram.buf);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
// Emulator has an internal Double Buffer
|
||||
const framebuf = cpu.bus.ppu.framebuf.get(.Renderer);
|
||||
_ = SDL.SDL_UpdateTexture(self.texture, null, framebuf.ptr, pitch);
|
||||
_ = SDL.SDL_RenderCopy(self.renderer, self.texture, null, null);
|
||||
SDL.SDL_RenderPresent(self.renderer);
|
||||
|
||||
const dyn_title = std.fmt.bufPrint(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, frame_rate.value() }) catch unreachable;
|
||||
SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr);
|
||||
}
|
||||
|
||||
quit.store(true, .SeqCst); // Terminate Emulator Thread
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.audio.deinit();
|
||||
SDL.SDL_DestroyTexture(self.texture);
|
||||
SDL.SDL_DestroyRenderer(self.renderer);
|
||||
self.state.deinit(self.allocator);
|
||||
|
||||
zgui.backend.deinit();
|
||||
zgui.plot.deinit();
|
||||
zgui.deinit();
|
||||
|
||||
gl.deleteProgram(self.program_id);
|
||||
SDL.SDL_GL_DeleteContext(self.ctx);
|
||||
SDL.SDL_DestroyWindow(self.window);
|
||||
SDL.SDL_Quit();
|
||||
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
fn drawGbaTexture(self: *const Self, obj_ids: struct { GLuint, GLuint, GLuint }, tex_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, EBO. VBO not bound
|
||||
gl.bindVertexArray(obj_ids[0]); // VAO
|
||||
defer gl.bindVertexArray(0);
|
||||
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, obj_ids[2]); // EBO
|
||||
defer gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, 0);
|
||||
|
||||
// Use compiled frag + vertex shader
|
||||
gl.useProgram(self.program_id);
|
||||
defer gl.useProgram(0);
|
||||
|
||||
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, null);
|
||||
}
|
||||
|
||||
fn compileShaders() !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 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 genBufferObjects() struct { GLuint, GLuint, GLuint } {
|
||||
var vao_id: GLuint = undefined;
|
||||
var vbo_id: GLuint = undefined;
|
||||
var ebo_id: GLuint = undefined;
|
||||
|
||||
gl.genVertexArrays(1, &vao_id);
|
||||
gl.genBuffers(1, &vbo_id);
|
||||
gl.genBuffers(1, &ebo_id);
|
||||
|
||||
gl.bindVertexArray(vao_id);
|
||||
defer gl.bindVertexArray(0);
|
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, vbo_id);
|
||||
defer gl.bindBuffer(gl.ARRAY_BUFFER, 0);
|
||||
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo_id);
|
||||
defer gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, 0);
|
||||
|
||||
gl.bufferData(gl.ARRAY_BUFFER, @sizeOf(@TypeOf(vertices)), &vertices, gl.STATIC_DRAW);
|
||||
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), null); // 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 genGbaTexture(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 genOutTexture() 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 genFrameBufObject(tex_id: c_uint) !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);
|
||||
|
||||
const draw_buffers: [1]GLuint = .{gl.COLOR_ATTACHMENT0};
|
||||
gl.drawBuffers(1, &draw_buffers);
|
||||
|
||||
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE)
|
||||
return error.FrameBufferObejctInitFailed;
|
||||
|
||||
return fbo_id;
|
||||
}
|
||||
|
||||
const RunOptions = struct {
|
||||
channel: *TwoWayChannel,
|
||||
tracker: ?*FpsTracker = null,
|
||||
cpu: *Arm7tdmi,
|
||||
scheduler: *Scheduler,
|
||||
};
|
||||
|
||||
pub fn run(self: *Self, opt: RunOptions) !void {
|
||||
const cpu = opt.cpu;
|
||||
const tracker = opt.tracker;
|
||||
const channel = opt.channel;
|
||||
|
||||
const obj_ids = Self.genBufferObjects();
|
||||
defer gl.deleteBuffers(3, @as(*const [3]c_uint, &obj_ids));
|
||||
|
||||
const emu_tex = Self.genGbaTexture(cpu.bus.ppu.framebuf.get(.Renderer));
|
||||
const out_tex = Self.genOutTexture();
|
||||
defer gl.deleteTextures(2, &[_]c_uint{ emu_tex, out_tex });
|
||||
|
||||
const fbo_id = try Self.genFrameBufObject(out_tex);
|
||||
defer gl.deleteFramebuffers(1, &fbo_id);
|
||||
|
||||
emu_loop: while (true) {
|
||||
// `quit` from RunOptions may be modified by the GDBSTUB thread,
|
||||
// so we want to recognize that it may change to `true` and exit the GUI thread
|
||||
if (channel.gui.pop()) |event| switch (event) {
|
||||
.Quit => break :emu_loop,
|
||||
.Paused => @panic("TODO: We want to peek (and then pop if it's .Quit), not always pop"),
|
||||
};
|
||||
|
||||
// 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) 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 => {
|
||||
const key_code = event.key.keysym.sym;
|
||||
var keyinput = cpu.bus.io.keyinput.load(.Monotonic);
|
||||
|
||||
switch (key_code) {
|
||||
SDL.SDLK_UP => keyinput.up.unset(),
|
||||
SDL.SDLK_DOWN => keyinput.down.unset(),
|
||||
SDL.SDLK_LEFT => keyinput.left.unset(),
|
||||
SDL.SDLK_RIGHT => keyinput.right.unset(),
|
||||
SDL.SDLK_x => keyinput.a.unset(),
|
||||
SDL.SDLK_z => keyinput.b.unset(),
|
||||
SDL.SDLK_a => keyinput.shoulder_l.unset(),
|
||||
SDL.SDLK_s => keyinput.shoulder_r.unset(),
|
||||
SDL.SDLK_RETURN => keyinput.start.unset(),
|
||||
SDL.SDLK_RSHIFT => keyinput.select.unset(),
|
||||
else => {},
|
||||
}
|
||||
|
||||
cpu.bus.io.keyinput.store(keyinput.raw, .Monotonic);
|
||||
},
|
||||
SDL.SDL_KEYUP => {
|
||||
const key_code = event.key.keysym.sym;
|
||||
var keyinput = cpu.bus.io.keyinput.load(.Monotonic);
|
||||
|
||||
switch (key_code) {
|
||||
SDL.SDLK_UP => keyinput.up.set(),
|
||||
SDL.SDLK_DOWN => keyinput.down.set(),
|
||||
SDL.SDLK_LEFT => keyinput.left.set(),
|
||||
SDL.SDLK_RIGHT => keyinput.right.set(),
|
||||
SDL.SDLK_x => keyinput.a.set(),
|
||||
SDL.SDLK_z => keyinput.b.set(),
|
||||
SDL.SDLK_a => keyinput.shoulder_l.set(),
|
||||
SDL.SDLK_s => keyinput.shoulder_r.set(),
|
||||
SDL.SDLK_RETURN => keyinput.start.set(),
|
||||
SDL.SDLK_RSHIFT => keyinput.select.set(),
|
||||
else => {},
|
||||
}
|
||||
|
||||
cpu.bus.io.keyinput.store(keyinput.raw, .Monotonic);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
channel.emu.push(.Pause);
|
||||
defer channel.emu.push(.Resume);
|
||||
|
||||
// Spin Loop until we know that the emu is paused
|
||||
wait: while (true) switch (channel.gui.pop() orelse continue) {
|
||||
.Paused => break :wait,
|
||||
else => |any| std.debug.panic("[Gui/Channel]: Unhandled Event: {}", .{any}),
|
||||
};
|
||||
|
||||
// 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);
|
||||
|
||||
const buf = cpu.bus.ppu.framebuf.get(.Renderer);
|
||||
gl.viewport(0, 0, gba_width, gba_height);
|
||||
self.drawGbaTexture(obj_ids, emu_tex, buf);
|
||||
}
|
||||
|
||||
// Background Colour
|
||||
const size = zgui.io.getDisplaySize();
|
||||
gl.viewport(0, 0, @floatToInt(c_int, size[0]), @floatToInt(c_int, size[1]));
|
||||
gl.clearColor(0, 0, 0, 1.0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
zgui.backend.newFrame(width, height);
|
||||
imgui.draw(&self.state, out_tex, cpu);
|
||||
zgui.backend.draw();
|
||||
}
|
||||
|
||||
SDL.SDL_GL_SwapWindow(self.window);
|
||||
}
|
||||
|
||||
channel.emu.push(.Quit);
|
||||
}
|
||||
|
||||
fn glGetProcAddress(ctx: SDL.SDL_GLContext, proc: [:0]const u8) ?*anyopaque {
|
||||
_ = ctx;
|
||||
return SDL.SDL_GL_GetProcAddress(proc.ptr);
|
||||
}
|
||||
};
|
||||
|
||||
const Audio = struct {
|
||||
const Self = @This();
|
||||
const log = std.log.scoped(.PlatformAudio);
|
||||
const sample_rate = @import("core/apu.zig").host_sample_rate;
|
||||
|
||||
device: SDL.SDL_AudioDeviceID,
|
||||
|
||||
@@ -155,16 +382,22 @@ const Audio = struct {
|
||||
var have: SDL.SDL_AudioSpec = undefined;
|
||||
var want: SDL.SDL_AudioSpec = std.mem.zeroes(SDL.SDL_AudioSpec);
|
||||
want.freq = sample_rate;
|
||||
want.format = SDL.AUDIO_U16;
|
||||
want.format = sample_format;
|
||||
want.channels = 2;
|
||||
want.samples = 0x100;
|
||||
want.callback = Self.callback;
|
||||
want.userdata = apu;
|
||||
|
||||
std.debug.assert(sample_format == SDL.AUDIO_U16);
|
||||
log.info("Host Sample Rate: {}Hz, Host Format: SDL.AUDIO_U16", .{sample_rate});
|
||||
|
||||
const device = SDL.SDL_OpenAudioDevice(null, 0, &want, &have, 0);
|
||||
if (device == 0) panic();
|
||||
|
||||
if (!config.config().host.mute) {
|
||||
SDL.SDL_PauseAudioDevice(device, 0); // Unpause Audio
|
||||
log.info("Unpaused Device", .{});
|
||||
}
|
||||
|
||||
return .{ .device = device };
|
||||
}
|
||||
@@ -175,12 +408,32 @@ const Audio = struct {
|
||||
}
|
||||
|
||||
export fn callback(userdata: ?*anyopaque, stream: [*c]u8, len: c_int) void {
|
||||
const apu = @ptrCast(*Apu, @alignCast(@alignOf(*Apu), userdata));
|
||||
_ = SDL.SDL_AudioStreamGet(apu.stream, stream, len);
|
||||
const T = *Apu;
|
||||
const apu = @ptrCast(T, @alignCast(@alignOf(T), userdata));
|
||||
|
||||
// If we don't write anything, play silence otherwise garbage will be played
|
||||
// FIXME: I don't think this hack to remove DC Offset is acceptable :thinking:
|
||||
// if (written == 0) std.mem.set(u8, stream[0..@intCast(usize, len)], 0x40);
|
||||
_ = SDL.SDL_AudioStreamGet(apu.stream, stream, len);
|
||||
}
|
||||
};
|
||||
|
||||
const shader = struct {
|
||||
const Kind = enum { vertex, fragment };
|
||||
const log = std.log.scoped(.Shader);
|
||||
|
||||
fn didCompile(id: gl.GLuint) bool {
|
||||
var success: gl.GLint = undefined;
|
||||
gl.getShaderiv(id, gl.COMPILE_STATUS, &success);
|
||||
|
||||
if (success == 0) err(id);
|
||||
|
||||
return success == 1;
|
||||
}
|
||||
|
||||
fn err(id: gl.GLuint) void {
|
||||
const buf_len = 512;
|
||||
var error_msg: [buf_len]u8 = undefined;
|
||||
|
||||
gl.getShaderInfoLog(id, buf_len, 0, &error_msg);
|
||||
log.err("{s}", .{std.mem.sliceTo(&error_msg, 0)});
|
||||
}
|
||||
};
|
||||
|
||||
|
25
src/shader/pixelbuf.frag
Normal file
25
src/shader/pixelbuf.frag
Normal file
@@ -0,0 +1,25 @@
|
||||
#version 330 core
|
||||
out vec4 frag_color;
|
||||
|
||||
in vec3 color;
|
||||
in vec2 uv;
|
||||
|
||||
uniform sampler2D screen;
|
||||
|
||||
void main() {
|
||||
// https://near.sh/video/color-emulation
|
||||
// Thanks to Talarubi + Near for the Colour Correction
|
||||
// Thanks to fleur + mattrb for the Shader Impl
|
||||
|
||||
vec4 color = texture(screen, uv);
|
||||
color.rgb = pow(color.rgb, vec3(4.0)); // LCD Gamma
|
||||
|
||||
frag_color = vec4(
|
||||
pow(vec3(
|
||||
0 * color.b + 50 * color.g + 255 * color.r,
|
||||
30 * color.b + 230 * color.g + 10 * color.r,
|
||||
220 * color.b + 10 * color.g + 50 * color.r
|
||||
) / 255, vec3(1.0 / 2.2)), // Out Gamma
|
||||
1.0);
|
||||
}
|
||||
|
13
src/shader/pixelbuf.vert
Normal file
13
src/shader/pixelbuf.vert
Normal file
@@ -0,0 +1,13 @@
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 pos;
|
||||
layout (location = 1) in vec3 in_color;
|
||||
layout (location = 2) in vec2 in_uv;
|
||||
|
||||
out vec3 color;
|
||||
out vec2 uv;
|
||||
|
||||
void main() {
|
||||
color = in_color;
|
||||
uv = in_uv;
|
||||
gl_Position = vec4(pos, 1.0);
|
||||
}
|
204
src/util.zig
204
src/util.zig
@@ -1,30 +1,11 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const config = @import("config.zig");
|
||||
|
||||
const Log2Int = std.math.Log2Int;
|
||||
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
|
||||
|
||||
const allow_unhandled_io = @import("core/emu.zig").allow_unhandled_io;
|
||||
|
||||
// Sign-Extend value of type `T` to type `U`
|
||||
pub fn sext(comptime T: type, comptime U: type, value: T) T {
|
||||
// U must have less bits than T
|
||||
comptime std.debug.assert(@typeInfo(U).Int.bits <= @typeInfo(T).Int.bits);
|
||||
|
||||
const iT = std.meta.Int(.signed, @typeInfo(T).Int.bits);
|
||||
const ExtU = if (@typeInfo(U).Int.signedness == .unsigned) T else iT;
|
||||
const shift = @intCast(Log2Int(T), @typeInfo(T).Int.bits - @typeInfo(U).Int.bits);
|
||||
|
||||
return @bitCast(T, @bitCast(iT, @as(ExtU, @truncate(U, value)) << shift) >> shift);
|
||||
}
|
||||
|
||||
/// See https://godbolt.org/z/W3en9Eche
|
||||
pub inline fn rotr(comptime T: type, x: T, r: anytype) T {
|
||||
if (@typeInfo(T).Int.signedness == .signed)
|
||||
@compileError("cannot rotate signed integer");
|
||||
|
||||
const ar = @intCast(Log2Int(T), @mod(r, @typeInfo(T).Int.bits));
|
||||
return x >> ar | x << (1 +% ~ar);
|
||||
}
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const FpsTracker = struct {
|
||||
const Self = @This();
|
||||
@@ -47,7 +28,7 @@ pub const FpsTracker = struct {
|
||||
|
||||
pub fn value(self: *Self) u32 {
|
||||
if (self.timer.read() >= std.time.ns_per_s) {
|
||||
self.fps = self.count.swap(0, .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,7 +50,7 @@ pub fn escape(title: [12]u8) [12]u8 {
|
||||
}
|
||||
|
||||
pub const FilePaths = struct {
|
||||
rom: []const u8,
|
||||
rom: ?[]const u8,
|
||||
bios: ?[]const u8,
|
||||
save: ?[]const u8,
|
||||
};
|
||||
@@ -143,24 +62,38 @@ pub const io = struct {
|
||||
return 0;
|
||||
}
|
||||
|
||||
pub fn undef(comptime T: type, log: anytype, comptime format: []const u8, args: anytype) ?T {
|
||||
log.warn(format, args);
|
||||
if (builtin.mode == .Debug and !allow_unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{});
|
||||
pub fn undef(comptime T: type, comptime log: anytype, comptime format: []const u8, args: anytype) ?T {
|
||||
@setCold(true);
|
||||
|
||||
const unhandled_io = config.config().debug.unhandled_io;
|
||||
|
||||
log.warn(format, args);
|
||||
if (builtin.mode == .Debug and !unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn err(comptime T: type, comptime log: anytype, comptime format: []const u8, args: anytype) ?T {
|
||||
@setCold(true);
|
||||
|
||||
log.err(format, args);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
pub const write = struct {
|
||||
pub fn undef(log: anytype, comptime format: []const u8, args: anytype) void {
|
||||
const unhandled_io = config.config().debug.unhandled_io;
|
||||
|
||||
log.warn(format, args);
|
||||
if (builtin.mode == .Debug and !allow_unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{});
|
||||
if (builtin.mode == .Debug and !unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
pub 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),
|
||||
|
||||
@@ -172,6 +105,7 @@ pub const Logger = struct {
|
||||
|
||||
pub fn print(self: *Self, comptime format: []const u8, args: anytype) !void {
|
||||
try self.buf.writer().print(format, args);
|
||||
try self.buf.flush(); // FIXME: On panics, whatever is in the buffer isn't written to file
|
||||
}
|
||||
|
||||
pub fn mgbaLog(self: *Self, cpu: *const Arm7tdmi, opcode: u32) void {
|
||||
@@ -182,7 +116,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");
|
||||
@@ -211,15 +145,13 @@ pub const Logger = struct {
|
||||
cpu.r[12],
|
||||
cpu.r[13],
|
||||
cpu.r[14],
|
||||
cpu.r[15],
|
||||
cpu.r[15] - if (cpu.cpsr.t.read()) 2 else @as(u32, 4),
|
||||
cpu.cpsr.raw,
|
||||
opcode,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
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");
|
||||
|
||||
@@ -269,22 +201,37 @@ pub const audio = struct {
|
||||
};
|
||||
};
|
||||
|
||||
/// Sets the high bits of an integer to a value
|
||||
pub inline fn setHi(comptime T: type, left: T, right: HalfInt(T)) T {
|
||||
return switch (T) {
|
||||
u32 => (left & 0xFFFF_0000) | right,
|
||||
u16 => (left & 0xFF00) | right,
|
||||
u8 => (left & 0xF0) | right,
|
||||
else => @compileError("unsupported type"),
|
||||
/// Sets a quarter (8) of the bits of the u32 `left` to the value of u8 `right`
|
||||
pub inline fn setQuart(left: u32, addr: u8, right: u8) u32 {
|
||||
const offset = @truncate(u2, addr);
|
||||
|
||||
return switch (offset) {
|
||||
0b00 => (left & 0xFFFF_FF00) | right,
|
||||
0b01 => (left & 0xFFFF_00FF) | @as(u32, right) << 8,
|
||||
0b10 => (left & 0xFF00_FFFF) | @as(u32, right) << 16,
|
||||
0b11 => (left & 0x00FF_FFFF) | @as(u32, right) << 24,
|
||||
};
|
||||
}
|
||||
|
||||
/// sets the low bits of an integer to a value
|
||||
pub inline fn setLo(comptime T: type, left: T, right: HalfInt(T)) T {
|
||||
/// Calculates the correct shift offset for an aligned/unaligned u8 read
|
||||
///
|
||||
/// TODO: Support u16 reads of u32 values?
|
||||
pub inline fn getHalf(byte: u8) u4 {
|
||||
return @truncate(u4, byte & 1) << 3;
|
||||
}
|
||||
|
||||
pub inline fn setHalf(comptime T: type, left: T, addr: u8, right: HalfInt(T)) T {
|
||||
const offset = @truncate(u1, addr >> if (T == u32) 1 else 0);
|
||||
|
||||
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,
|
||||
u32 => switch (offset) {
|
||||
0b0 => (left & 0xFFFF_0000) | right,
|
||||
0b1 => (left & 0x0000_FFFF) | @as(u32, right) << 16,
|
||||
},
|
||||
u16 => switch (offset) {
|
||||
0b0 => (left & 0xFF00) | right,
|
||||
0b1 => (left & 0x00FF) | @as(u16, right) << 8,
|
||||
},
|
||||
else => @compileError("unsupported type"),
|
||||
};
|
||||
}
|
||||
@@ -297,3 +244,48 @@ 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);
|
||||
std.mem.set(u8, 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 {
|
||||
std.mem.set(u8, 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];
|
||||
}
|
||||
};
|
||||
|
Reference in New Issue
Block a user