142 Commits

Author SHA1 Message Date
9844d657b0 fix: implement Ace Lightning (2002) obscure behaviour 2023-04-01 01:23:18 -05:00
1d8b21d6b4 fix: reimplement grabbing ROM title if provided via cmd arg 2023-03-27 16:22:07 -05:00
b879c76510 chore: update zgui 2023-03-26 23:23:34 -05:00
0dbba2fb9a chore: update to 0.11.0-dev.2168+322ace70f 2023-03-26 23:23:34 -05:00
49b0620c48 style(imgui): use orelse instead of if () || 2023-03-17 11:34:38 -05:00
a6a9e3ac72 chore(imgui): change size of histogram 2023-03-17 11:26:14 -05:00
aeefff86f8 chore: update dependencies 2023-03-16 00:27:08 -05:00
91aa98eef7 chore: copy array instead of calling memcpy 2023-03-11 00:30:34 -06:00
f3b6c4f3fe Merge branch 'ci-suffering' 2023-03-11 00:29:56 -06:00
5aa5ac2a8b ci: update github actions config 2023-03-11 00:09:14 -06:00
b1827ccea0 Merge pull request 'Add a GUI to ZBA' (#7) from imgui into main
Reviewed-on: #7
2023-03-11 03:18:14 +00:00
2629d15e2f feat: don't require path to ROM in CLI 2023-03-10 21:16:06 -06:00
c7b62d3202 chore: dynamically update window title on ROM replace 2023-03-10 20:41:49 -06:00
85ec9a84c4 chore: add screenshot to README.md 2023-03-10 19:37:28 -06:00
5adbc354d6 feat: replace Gamepak 2023-03-10 02:50:31 -06:00
f8477714ae feat: implement resetting 2023-03-10 02:28:03 -06:00
bd872ee1c0 fix: drop select atomics in favour of a thread-safe channel 2023-03-10 02:02:34 -06:00
11eae091db chore: introduce zba-util
In an effort to reuse code between zba and zba-gdbstub, move common util
code (like the SPSC Channel I implemented in this commit) in a new lib
2023-03-10 00:05:31 -06:00
72b702cb21 fix: handle null GBA ROM titles when passing to imgui 2023-03-04 18:02:12 -06:00
d985eac0fc tmp: implement mechanisms for a emu reset fn (currently crashes) 2023-02-23 23:49:56 -06:00
3fff4fd742 chore: move imgui-specific code to its own file 2023-02-23 18:25:05 -06:00
e90d5a17ba fix: ensure code builds + works
the gdbstub branch got merged into main, rebasing on top of main led to
a bunch of merge conflicts that had to be resolved. Unfortunately some
things got missed, and this commit covers the immediate problems that
the rebase caused
2023-02-23 17:27:42 -06:00
54143332ab chore: update for loop in RingBuffer impl 2023-02-23 17:27:42 -06:00
baa3fb7905 chore: update gui libs to latest zig master 2023-02-23 17:27:42 -06:00
57c7437f77 chore: add gui deps to README.md 2023-02-23 17:27:42 -06:00
eef5a238a0 chore: update nfd-zig
respond to build.zig changes in zig master
2023-02-23 17:27:42 -06:00
6048458f9b feat: implement menu bar + add file picker dep 2023-02-23 17:27:42 -06:00
ff609c85ba feat: show game title as imgui screen title 2023-02-23 17:27:42 -06:00
3e98f4053a chore: update zgui 2023-02-23 17:27:42 -06:00
1d601dba39 feat: add scheduler ui 2023-02-23 17:27:42 -06:00
a8fac5f3c6 feat: pause emu when UI reads emu state 2023-02-23 17:27:42 -06:00
ae78588b80 feat: implement ui for register, interrupt 2023-02-23 17:27:42 -06:00
fe6fc0e517 feat: add system information window 2023-02-23 17:27:42 -06:00
3dcc4cb385 fix: update zgui to work with sdl2 vcpkg package 2023-02-23 17:27:42 -06:00
5e94cbfbea feat: add imgui support using zgui 2023-02-23 17:27:42 -06:00
3b13102abb ci: ensure that submodules are updated recursively 2023-02-23 17:26:59 -06:00
7234ecab37 Merge pull request 'Implement a GDBSTUB Server' (#6) from gdbstub into main
Reviewed-on: #6
2023-02-23 22:18:26 +00:00
ddf4599162 chore: update dependencies 2023-02-23 02:45:59 -06:00
01f5410180 feat: allow gui and gdbstub to run in parallel 2023-02-23 02:40:24 -06:00
49706842af fix: run more than just the CPU when stepping via gdb 2023-02-23 02:40:24 -06:00
2798a90d83 chore: update zba-gdbstub to zig master 2023-02-23 02:40:24 -06:00
518b868249 feat: respond to API changes for software bkpts 2023-02-23 02:40:24 -06:00
755115660b feat: allow gdb writes to certain mem regions 2023-02-23 02:40:24 -06:00
6709f8c551 chore: update gdbstub lib 2023-02-23 02:40:24 -06:00
1f3cdd9513 feat: add gdb support to zba 2023-02-23 02:40:24 -06:00
65af6aa499 feat: add gdbstub library 2023-02-23 02:40:23 -06:00
024151a5c1 chore: update to latest zig master 2023-02-22 14:46:46 -06:00
e380af7056 chore: use a more efficient decimal->bcd algorithm
This will not improve perf in any way because this code only gets run
one time a second orz
2023-02-21 23:22:42 -06:00
e654abfd1d ci: don't assume any cpu features 2023-02-18 23:52:51 -06:00
3510a6cff8 chore: drop macOS support
CI is currently broken and I don't have the $$$ for macOS
2023-02-18 23:34:59 -06:00
3fb351e762 chore: update SDL.zig 2023-02-17 00:05:42 -06:00
a11b96b84e chore: update minimum zig version 2023-02-07 17:52:16 -06:00
c3be1c0a67 chore: update to latest zig build system
I feel like I'm misusing addAnonymousModule
2023-02-07 16:00:06 -06:00
fdf7399e52 chore: update README.md 2023-02-04 19:30:05 -06:00
ed8155139a chore: update CI 2023-02-04 18:22:50 -06:00
8112b1aab2 chore: update zig to latest master 2023-02-04 18:15:10 -06:00
c0e583d20d fix: resolve off-by-one error in str addr when r15 is involved
I seem to have made up this rule (I was thinking about when r15 was
a source register). `rn` is the destination register.... whoops
2023-01-29 08:58:41 -06:00
3f72367aaf chore: remove .vscode folder 2023-01-21 19:01:44 -06:00
c27f487bf0 chore: update dependencies 2023-01-16 02:57:50 -06:00
ae3bb94036 fix(ppu): draw file select sprites in amazing mirror 2023-01-08 01:36:58 -06:00
ddc54e2977 fix: ignore missing opengl proc addresses
is this really a fix? the error never happens with mach-glfw
2023-01-01 15:56:18 -06:00
ed49d7c460 chore: update lib/gl.zig 2023-01-01 13:41:53 -06:00
59baa14bde Merge branch 'main' of ssh://musuka.dev:2222/paoda/zba 2022-12-30 19:47:24 -06:00
6bf1c44961 chore: refactor sprite rendering code 2022-12-30 19:47:01 -06:00
94702b9b51 chore: update min zig version 2022-12-28 16:26:51 -06:00
0f148507e4 fix: respond to @addWithOverflow changes in latest zig 2022-12-28 15:20:44 -06:00
0cec779545 chore: misc style changes 2022-12-28 07:29:07 -06:00
1ecbbc7d29 chore: cleanup BIOS struct init code 2022-12-27 06:42:06 -06:00
caaa60d1a8 fix: rotate unaligned reads on BIOS open-bus 2022-12-27 06:25:12 -06:00
39d50466c9 chore: update min zig version 2022-12-22 13:21:59 -06:00
5a452d85c1 feat: update dependencies 2022-12-21 00:24:55 -06:00
4326ae7a0a fix: resolve broken affine bg in mario kart 2022-12-18 08:59:19 -04:00
905c4448d0 feat: kind-of account for 1/4th of obj mode 2022-12-18 08:35:14 -04:00
0de44835e5 fix: properly implement black/white blending for sprites
There's unique rules to handle for BLDY w/r/t sprites, I didn't know
about them (shown in bld_demo.gba). I'm sure I haven't ironed out every
rule but bld_demo.gba now *actually* passes
2022-12-18 07:44:01 -04:00
5aac04faf5 tmp: disable buggy window emulation
I'd like to merge my affine sprite impl into main, which will require
merging a lot of the rewrites I did in this branch. My plan is to
merge the buggy ppu window impl to main, but keep it disabled.

This is technically a regression but the current impl barely worked
anyways so....
2022-12-17 09:58:15 -04:00
f98a1700e0 feat: implement affine sprites 2022-12-17 09:47:10 -04:00
acdb270793 chore: reimplement alpha blending 2022-12-16 22:16:37 -04:00
4ceed382ed chore(ppu): use @ptrCast in drawTextMode 2022-12-16 22:16:37 -04:00
52ce4f3d20 chore(ppu): reimplement modes 3, 4, and 5 2022-12-16 22:16:37 -04:00
c1c8cac6e4 style(ppu): move text mode drawing to unique fn 2022-12-16 22:16:37 -04:00
be7a34f719 fix(window): proper inRange impl for window
window wrap now works (it's pretty slow though?)
2022-12-16 22:16:37 -04:00
f7a94634f9 chore: improve readability of sprite drawing code a bit 2022-12-16 22:16:37 -04:00
7d4ab6db2c style: remove unused imports 2022-12-16 22:16:37 -04:00
0a78587d8e chore: dont allocate not-small ?Sprite array on stack
use memset like most other allocations in this emu
2022-12-16 22:16:37 -04:00
b753ceef8e chore: move FrameBuffer struct to util.zig 2022-12-16 22:16:37 -04:00
8963fe205b chore: move OAM, PALRAM and VRAM structs to separate files 2022-12-16 22:16:37 -04:00
e906506e16 fix: 8-bit writes to WIN PPU registers
Advance Wars depends on these registers similar to Mario Kart's 8-bit
writes to Affine Background registers:
2022-12-16 22:16:37 -04:00
3195a45e3d chore: refactor window 2022-12-16 22:16:37 -04:00
6aad911985 chore: crude background window impl (no affine) 2022-12-16 22:16:37 -04:00
e3b45ef794 chore: rename function (misspelt until now somehow) 2022-12-16 22:16:37 -04:00
8e1a539e70 chore: debug read takes advantage of fastmem
deduplicate slowmem backup read handler
2022-12-15 23:18:54 -04:00
63fa972afa chore: update dependencies
in response to zig master deprecations
2022-12-14 22:57:51 -04:00
bf95eee3f1 fix(apu): resolve bug in NR10 obscure behaviour 2022-12-05 11:08:04 -04:00
240fbcb1df chore: update dependencies 2022-12-01 13:23:09 -04:00
26db340077 fix(input): implement atomic for KeyInput 2022-11-30 00:42:20 -04:00
20f611b7b5 chore: be more intentional in atomic ordering use 2022-11-30 00:21:02 -04:00
f9aefedf60 chore: cal glDeleteTextures on program exit 2022-11-29 23:35:13 -04:00
d7e3d34726 fix(platform): ensure that title char* is null terminated 2022-11-29 23:21:57 -04:00
2294dc8832 chore: add minimum zig version 2022-11-29 23:10:29 -04:00
4af86e1cb3 style: replace meta.Tuple calls with new tuple syntax 2022-11-29 23:01:06 -04:00
9fcbbe7d57 chore: cleanup OpenGL vertex array + buffers 2022-11-29 22:53:37 -04:00
c3f67e38a1 chore: exit early on shader compile failure 2022-11-29 22:25:04 -04:00
46e29245b7 fix(apu): disable APU writes when APU is disabled 2022-11-26 12:20:42 -04:00
002e33b48b fix: properly render table in README 2022-11-24 08:22:58 -04:00
5bb25fe214 chore: update dependencies 2022-11-23 21:57:53 -04:00
66db2e6049 Revert "chore: refactor flash impl"
This reverts commit 96a9ae2ca5.
2022-11-20 21:46:40 -04:00
c5cf471912 fix(timer): removing cascade when TIM aleady enabled shouldn't reset counter 2022-11-20 19:13:49 -04:00
4ed4f8e143 fix(dma): implement obscure behaviour for DMAs from ROM 2022-11-20 17:49:26 -04:00
f31699d921 fix(log): logged improper second opcode for THUMB BL 2022-11-20 15:36:40 -04:00
96a9ae2ca5 chore: refactor flash impl 2022-11-17 10:47:19 -04:00
ee1c0bb313 chore: update README 2022-11-16 10:55:33 -04:00
558c03b12b style: changes to cpu.zig 2022-11-16 10:21:40 -04:00
7d8fbbb086 fix(bus): resolve off-by-one error 2022-11-14 01:59:43 -04:00
9fd405a896 chore(ci): update CI dependency 2022-11-11 13:25:56 -04:00
5d7cf3a8a2 chore: remove util fn for stdlib equivalent 2022-11-11 13:02:51 -04:00
1230aa1e91 fix(cpu): remove miscompilation workaround 2022-11-11 03:56:49 -04:00
accecb3350 chore(ci): rename CI workflow 2022-11-10 11:58:47 -04:00
1e0ade8f55 chore: update depdendencies 2022-11-07 00:54:35 -04:00
429676ad43 feat(config): write config.toml to config dir, not data dir 2022-11-03 09:45:57 -03:00
ef39d9a7b8 chore(ci): only run for .zig files, name workflow
Also enabled workflow dispatch
2022-11-03 08:56:14 -03:00
986bc9448e fix(bus): account for read_table being the first table when freeing 2022-11-03 07:50:12 -03:00
d34893ba72 fix(bus): fix confusion about which fastmem write table is for which write type 2022-11-02 08:21:59 -03:00
b8a5fb95c1 fix(io): account for read-only bit in WAITCNT 2022-11-02 08:06:19 -03:00
102b2c946b fix(io): respect read-only bits in DISPSTAT
Superstar Saga now renders correctly
2022-11-02 07:54:06 -03:00
505b1b9608 fix(bus): resolve simple oversights 2022-11-01 09:00:25 -03:00
2851c140ea fix(cpu): use LUT for ARM condition codes 2022-11-01 08:29:42 -03:00
637d81ce44 chore(bus): only perform one allocation for fastmem tables 2022-11-01 07:04:42 -03:00
bc52461f0f fix(bus): replace write table with two tables for u32/u8 and u8 writes 2022-11-01 07:00:07 -03:00
c395c04a6e feat(bus): implement fastmem
+100 fps in Pokemon Emerald lol
2022-11-01 06:18:12 -03:00
9eb4f8f191 chore: reccomend stable Zig v0.10.0 2022-11-01 01:01:48 -03:00
f774256c42 chore: update README.md 2022-10-31 09:14:42 -03:00
5c15d039e1 chore(ci): update actions/checkout to v3
supresses deprecation warning for node12
2022-10-31 08:16:45 -03:00
28e9342c25 ci: add github actions config file 2022-10-31 08:04:21 -03:00
af8ec4db5b chore: go through TODOs and FIXMEs
mainly deleting / rewording those that no longer apply
2022-10-31 06:17:09 -03:00
5d47e5d167 fix(io): force-align all i/o reads
Of course, backups being the exception due to flash or sram quirks,
I don't remember lol
2022-10-31 05:50:27 -03:00
5101fbd809 feat(io): pass all suite.gba i/o read tests 2022-10-31 05:22:11 -03:00
472457b9f3 chore: make use of comptime control flow when working with tuples 2022-10-31 05:14:20 -03:00
2ef4bb7dcc revert(apu): switch from f32 44.1kHz to u16 32.768kHz 2022-10-31 05:14:20 -03:00
9a732ea6f8 chore(i/o): ensure interrupt i/o exists 2022-10-31 05:14:20 -03:00
f80799a593 fix(util): resolve bug in setHalf function
introduced in 472215b4c2
2022-10-30 04:12:58 -03:00
ca67ca3183 fix(apu): only enable dma sound fifo after manual write 2022-10-30 03:48:12 -03:00
47fc49deb6 fix(audio): add asserts where I assume audio format 2022-10-30 03:25:49 -03:00
65 changed files with 4916 additions and 3468 deletions

59
.github/workflows/main.yml vendored Normal file
View 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

4
.gitignore vendored
View File

@@ -12,3 +12,7 @@
# Any Custom Scripts for Debugging purposes # Any Custom Scripts for Debugging purposes
*.sh *.sh
# Dear ImGui
**/imgui.ini

12
.gitmodules vendored
View File

@@ -13,3 +13,15 @@
[submodule "lib/zig-toml"] [submodule "lib/zig-toml"]
path = lib/zig-toml path = lib/zig-toml
url = https://github.com/aeronavery/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

View File

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

135
README.md
View File

@@ -1,80 +1,113 @@
# ZBA (working title) # ZBA (working title)
A Game Boy Advance Emulator written in Zig ⚡! A Game Boy Advance Emulator written in Zig ⚡!
## Scope ![ZBA running リズム天国](assets/screenshot.png)
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 ## Scope
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:
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 ### TODO
- [ ] Affine Sprites
- [x] Affine Sprites
- [ ] Windowing (see [this branch](https://git.musuka.dev/paoda/zba/src/branch/window)) - [ ] Windowing (see [this branch](https://git.musuka.dev/paoda/zba/src/branch/window))
- [ ] Audio Resampler (Having issues with SDL2's) - [ ] Audio Resampler (Having issues with SDL2's)
- [ ] Immediate Mode GUI
- [ ] Refactoring for easy-ish perf boosts - [ ] 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 ## Tests
- [x] [jsmolka's GBA Test Collection](https://github.com/jsmolka/gba-tests)
- [x] `arm.gba` and `thumb.gba` GBA Tests | [jsmolka](https://github.com/jsmolka/)
- [x] `flash64.gba`, `flash128.gba`, `none.gba`, and `sram.gba` --- | ---
- [x] `hello.gba`, `shades.gba`, and `stripes.gba` `arm.gba`, `thumb.gba` | PASS
- [x] `memory.gba` `memory.gba`, `bios.gba` | PASS
- [x] `bios.gba` `flash64.gba`, `flash128.gba` | PASS
- [x] `nes.gba` `sram.gba` | PASS
- [ ] [DenSinH's GBA ROMs](https://github.com/DenSinH/GBARoms) `none.gba` | PASS
- [x] `eeprom-test` and `flash-test` `hello.gba`, `shades.gba`, `stripes.gba` | PASS
- [x] `midikey2freq` `nes.gba` | PASS
- [ ] `swi-tests-random`
- [ ] [destoer's GBA Tests](https://github.com/destoer/gba_tests) GBARoms | [DenSinH](https://github.com/DenSinH/)
- [x] `cond_invalid.gba` --- | ---
- [x] `dma_priority.gba` `eeprom-test`, `flash-test` | PASS
- [x] `hello_world.gba` `midikey2freq` | PASS
- [x] `if_ack.gba` `swi-tests-random` | FAIL
- [ ] `line_timing.gba`
- [ ] `lyc_midline.gba` gba_tests | [destoer](https://github.com/destoer/)
- [ ] `window_midframe.gba` --- | ---
- [x] [ladystarbreeze's GBA Test Collection](https://github.com/ladystarbreeze/GBA-Test-Collection) `cond_invalid.gba` | PASS
- [x] `retAddr.gba` `dma_priority.gba` | PASS
- [x] `helloWorld.gba` `hello_world.gba` | PASS
- [x] `helloAudio.gba` `if_ack.gba` | PASS
- [x] [`armwrestler-gba-fixed.gba`](https://github.com/destoer/armwrestler-gba-fixed) `line_timing.gba` | FAIL
- [x] [FuzzARM](https://github.com/DenSinH/FuzzARM) `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 ## Resources
* [GBATEK](https://problemkaputt.de/gbatek.htm)
* [TONC](https://coranac.com/tonc/text/toc.htm) - [GBATEK](https://problemkaputt.de/gbatek.htm)
* [ARM Architecture Reference Manual](https://www.intel.com/content/dam/www/programmable/us/en/pdfs/literature/third-party/ddi0100e_arm_arm.pdf) - [TONC](https://coranac.com/tonc/text/toc.htm)
* [ARM7TDMI Data Sheet](https://www.dca.fee.unicamp.br/cursos/EA871/references/ARM/ARM7TDMIDataSheet.pdf) - [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 ## Compiling
Most recently built on Zig [0.10.0-dev.4474+b41b35f57](https://github.com/ziglang/zig/tree/b41b35f57)
Most recently built on Zig [v0.11.0-dev.2168+322ace70f](https://github.com/ziglang/zig/tree/322ace70f)
### Dependencies ### Dependencies
* [SDL.zig](https://github.com/MasterQ32/SDL.zig)
* [SDL2](https://www.libsdl.org/download-2.0.php)
* [zig-clap](https://github.com/Hejsil/zig-clap)
* [known-folders](https://github.com/ziglibs/known-folders)
* [zig-toml](https://github.com/aeronavery/zig-toml)
* [zig-datetime](https://github.com/frmdstryr/zig-datetime)
* [`bitfields.zig`](https://github.com/FlorenceOS/Florence/blob/aaa5a9e568/lib/util/bitfields.zig)
`bitfields.zig` from [FlorenceOS](https://github.com/FlorenceOS) is included under `lib/util/bitfield.zig`. Dependency | Source
--- | ---
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`, `known-folders`, `zig-toml` and `zig-datetime` Use `git submodule update --init` from the project root to pull the git relevant git submodules
Be sure to provide SDL2 using: Be sure to provide SDL2 using:
* Linux: Your distro's package manager
* MacOS: ¯\\\_(ツ)_/¯ - Linux: Your distro's package manager
* Windows: [`vcpkg`](https://github.com/Microsoft/vcpkg) (install `sdl2:x64-windows`) - macOS: ¯\\\_(ツ)_/¯ (try [this formula](https://formulae.brew.sh/formula/sdl2)?)
- Windows: [`vcpkg`](https://github.com/Microsoft/vcpkg) (install `sdl2:x64-windows`)
`SDL.zig` will provide a helpful compile error if the zig compiler is unable to find SDL2. `SDL.zig` will provide a helpful compile error if the zig compiler is unable to find SDL2.
Once you've got all the dependencies, execute `zig build -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 ## Controls
Key | Button Key | Button
--- | --- --- | ---
<kbd>X</kbd> | A <kbd>X</kbd> | A

BIN
assets/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -1,46 +1,70 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin");
const Sdk = @import("lib/SDL.zig/Sdk.zig"); 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 target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Standard release options allow the person running `zig build` to select const exe = b.addExecutable(.{
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. .name = "zba",
const mode = b.standardReleaseOptions(); .root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
const exe = b.addExecutable("zba", "src/main.zig");
exe.setMainPkgPath("."); // Necessary so that src/main.zig can embed example.toml exe.setMainPkgPath("."); // Necessary so that src/main.zig can embed example.toml
exe.setTarget(target);
// Known Folders (%APPDATA%, XDG, etc.) // Known Folders (%APPDATA%, XDG, etc.)
exe.addPackagePath("known_folders", "lib/known-folders/known-folders.zig"); exe.addAnonymousModule("known_folders", .{ .source_file = .{ .path = "lib/known-folders/known-folders.zig" } });
// DateTime Library // 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/ // Bitfield type from FlorenceOS: https://github.com/FlorenceOS/
// exe.addPackage(.{ .name = "bitfield", .path = .{ .path = "lib/util/bitfield.zig" } }); exe.addAnonymousModule("bitfield", .{ .source_file = .{ .path = "lib/bitfield.zig" } });
exe.addPackagePath("bitfield", "lib/util/bitfield.zig");
// Argument Parsing Library // Argument Parsing Library
exe.addPackagePath("clap", "lib/zig-clap/clap.zig"); exe.addAnonymousModule("clap", .{ .source_file = .{ .path = "lib/zig-clap/clap.zig" } });
// TOML Library // TOML Library
exe.addPackagePath("toml", "lib/zig-toml/src/toml.zig"); exe.addAnonymousModule("toml", .{ .source_file = .{ .path = "lib/zig-toml/src/toml.zig" } });
// OpenGL 3.3 Bindings // OpenGL 3.3 Bindings
exe.addPackagePath("gl", "lib/gl.zig"); 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 // Zig SDL Bindings: https://github.com/MasterQ32/SDL.zig
const sdk = Sdk.init(b); const sdk = Sdk.init(b, null);
sdk.link(exe, .dynamic); 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(); exe.install();
const run_cmd = exe.run(); const run_cmd = exe.run();
@@ -52,9 +76,11 @@ pub fn build(b: *std.build.Builder) void {
const run_step = b.step("run", "Run the app"); const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step); run_step.dependOn(&run_cmd.step);
const exe_tests = b.addTest("src/main.zig"); const exe_tests = b.addTest(.{
exe_tests.setTarget(target); .root_source_file = .{ .path = "src/main.zig" },
exe_tests.setBuildMode(mode); .target = target,
.optimize = optimize,
});
const test_step = b.step("test", "Run unit tests"); const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&exe_tests.step); test_step.dependOn(&exe_tests.step);

2785
lib/gl.zig

File diff suppressed because it is too large Load Diff

1
lib/nfd-zig Submodule

Submodule lib/nfd-zig added at 5e5098bcaf

1
lib/zba-gdbstub Submodule

Submodule lib/zba-gdbstub added at 215e053b9a

1
lib/zba-util Submodule

Submodule lib/zba-util added at d5e66caf21

1
lib/zgui Submodule

Submodule lib/zgui added at 5b2b64a9de

View File

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

View File

@@ -19,7 +19,7 @@ const log = std.log.scoped(.Bus);
const createDmaTuple = @import("bus/dma.zig").create; const createDmaTuple = @import("bus/dma.zig").create;
const createTimerTuple = @import("bus/timer.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{ const timings: [2][0x10]u8 = [_][0x10]u8{
// BIOS, Unused, EWRAM, IWRAM, I/0, PALRAM, VRAM, OAM, ROM0, ROM0, ROM1, ROM1, ROM2, ROM2, SRAM, Unused // BIOS, Unused, EWRAM, IWRAM, I/0, PALRAM, VRAM, OAM, ROM0, ROM0, ROM1, ROM1, ROM2, ROM2, SRAM, Unused
@@ -33,6 +33,11 @@ pub const fetch_timings: [2][0x10]u8 = [_][0x10]u8{
[_]u8{ 1, 1, 6, 1, 1, 2, 2, 1, 4, 4, 4, 4, 4, 4, 8, 8 }, // 32-bit [_]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(); const Self = @This();
pak: GamePak, pak: GamePak,
@@ -48,7 +53,16 @@ io: Io,
cpu: *Arm7tdmi, cpu: *Arm7tdmi,
sched: *Scheduler, 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 { 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.* = .{ self.* = .{
.pak = try GamePak.init(allocator, cpu, paths.rom, paths.save), .pak = try GamePak.init(allocator, cpu, paths.rom, paths.save),
.bios = try Bios.init(allocator, paths.bios), .bios = try Bios.init(allocator, paths.bios),
@@ -61,7 +75,17 @@ pub fn init(self: *Self, allocator: Allocator, sched: *Scheduler, cpu: *Arm7tdmi
.io = Io.init(), .io = Io.init(),
.cpu = cpu, .cpu = cpu,
.sched = sched, .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 { pub fn deinit(self: *Self) void {
@@ -70,59 +94,167 @@ pub fn deinit(self: *Self) void {
self.pak.deinit(); self.pak.deinit();
self.bios.deinit(); self.bios.deinit();
self.ppu.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; self.* = undefined;
} }
pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T { pub fn reset(self: *Self) void {
const page = @truncate(u8, address >> 24); self.bios.reset();
const aligned_addr = forceAlign(T, address); self.ppu.reset();
self.apu.reset();
self.iwram.reset();
self.ewram.reset();
return switch (page) { // https://github.com/ziglang/zig/issues/14705
// General Internal Memory {
0x00 => blk: { comptime var i: usize = 0;
if (address < Bios.size) inline while (i < self.dma.len) : (i += 1) {
break :blk self.bios.dbgRead(T, self.cpu.r[15], aligned_addr); self.dma[0].reset();
}
break :blk self.openBus(T, address);
},
0x02 => self.ewram.read(T, aligned_addr),
0x03 => self.iwram.read(T, aligned_addr),
0x04 => self.readIo(T, address),
// Internal Display Memory
0x05 => self.ppu.palette.read(T, aligned_addr),
0x06 => self.ppu.vram.read(T, aligned_addr),
0x07 => self.ppu.oam.read(T, aligned_addr),
// External Memory (Game Pak)
0x08...0x0D => self.pak.dbgRead(T, aligned_addr),
0x0E...0x0F => blk: {
const value = self.pak.backup.read(address);
const multiplier = switch (T) {
u32 => 0x01010101,
u16 => 0x0101,
u8 => 1,
else => @compileError("Backup: Unsupported read width"),
};
break :blk @as(T, value) * multiplier;
},
else => self.openBus(T, address),
};
} }
/// TODO: Should open bus read addresses be force-aligned? // https://github.com/ziglang/zig/issues/14705
fn readIo(self: *const Self, comptime T: type, unaligned_address: u32) T { {
const maybe_value = io.read(self, T, forceAlign(T, unaligned_address)); comptime var i: usize = 0;
return if (maybe_value) |value| value else self.openBus(T, unaligned_address); 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
0x0000_0000...0x0000_3FFF => null, // BIOS has it's own checks
0x0200_0000...0x02FF_FFFF => &self.ewram.buf[addr & 0x3FFFF],
0x0300_0000...0x03FF_FFFF => &self.iwram.buf[addr & 0x7FFF],
0x0400_0000...0x0400_03FF => null, // I/O
// Internal Display Memory
0x0500_0000...0x05FF_FFFF => &self.ppu.palette.buf[addr & 0x3FF],
0x0600_0000...0x06FF_FFFF => &self.ppu.vram.buf[vramMirror(addr)],
0x0700_0000...0x07FF_FFFF => &self.ppu.oam.buf[addr & 0x3FF],
// External Memory (Game Pak)
0x0800_0000...0x0DFF_FFFF => self.fillReadTableExternal(addr),
0x0E00_0000...0x0FFF_FFFF => null, // SRAM
else => null,
};
}
}
fn fillWriteTable(self: *Self, comptime T: type, table: *[table_len]?*const anyopaque) void {
comptime std.debug.assert(T == u32 or T == u16 or T == u8);
const vramMirror = @import("ppu/Vram.zig").mirror;
for (table, 0..) |*ptr, i| {
const addr = @intCast(u32, page_size * i);
ptr.* = switch (addr) {
// General Internal Memory
0x0000_0000...0x0000_3FFF => null, // BIOS has it's own checks
0x0200_0000...0x02FF_FFFF => &self.ewram.buf[addr & 0x3FFFF],
0x0300_0000...0x03FF_FFFF => &self.iwram.buf[addr & 0x7FFF],
0x0400_0000...0x0400_03FF => null, // I/O
// Internal Display Memory
0x0500_0000...0x05FF_FFFF => if (T != u8) &self.ppu.palette.buf[addr & 0x3FF] else null,
0x0600_0000...0x06FF_FFFF => if (T != u8) &self.ppu.vram.buf[vramMirror(addr)] else null,
0x0700_0000...0x07FF_FFFF => if (T != u8) &self.ppu.oam.buf[addr & 0x3FF] else null,
// External Memory (Game Pak)
0x0800_0000...0x0DFF_FFFF => null, // ROM
0x0E00_0000...0x0FFF_FFFF => null, // SRAM
else => null,
};
}
}
fn fillReadTableExternal(self: *Self, addr: u32) ?*anyopaque {
// see `GamePak.zig` for more information about what conditions need to be true
// so that a simple pointer dereference isn't possible
std.debug.assert(addr & @as(u32, page_size - 1) == 0); // addr is guaranteed to be page-aligned
const start_addr = addr;
const end_addr = start_addr + page_size;
{
const data = start_addr <= 0x0800_00C4 and 0x0800_00C4 < end_addr; // GPIO Data
const direction = start_addr <= 0x0800_00C6 and 0x0800_00C6 < end_addr; // GPIO Direction
const control = start_addr <= 0x0800_00C8 and 0x0800_00C8 < end_addr; // GPIO Control
const has_gpio = data or direction or control;
const gpio_kind = self.pak.gpio.device.kind;
// There is a GPIO Device, and the current page contains at least one memory-mapped GPIO register
if (gpio_kind != .None and has_gpio) return null;
}
if (self.pak.backup.kind == .Eeprom) {
if (self.pak.buf.len > 0x100_000) {
// We are using a "large" EEPROM which means that if the below check is true
// this page has an address that's reserved for the EEPROM and therefore must
// be handled in slowmem
if (addr & 0x1FF_FFFF > 0x1FF_FEFF) return null;
} else {
// We are using a "small" EEPROM which means that if the below check is true
// (that is, we're in the 0xD address page) then we must handle at least one
// address in this page in slowmem
if (@truncate(u4, addr >> 24) == 0xD) return null;
}
}
// Finally, the GamePak has some unique behaviour for reads past the end of the ROM,
// so those will be handled by slowmem as well
const masked_addr = addr & 0x1FF_FFFF;
if (masked_addr >= self.pak.buf.len) return null;
return &self.pak.buf[masked_addr];
}
fn readIo(self: *const Self, comptime T: type, address: u32) T {
return io.read(self, T, address) orelse self.openBus(T, address);
} }
fn openBus(self: *const Self, comptime T: type, address: u32) T { fn openBus(self: *const Self, comptime T: type, address: u32) T {
@setCold(true);
const r15 = self.cpu.r[15]; const r15 = self.cpu.r[15];
const word = blk: { const word = blk: {
// If Arm, get the most recently fetched instruction (PC + 8) // 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].?; if (!self.cpu.cpsr.t.read()) break :blk self.cpu.pipe.stage[1].?;
const page = @truncate(u8, r15 >> 24); const page = @truncate(u8, r15 >> 24);
@@ -172,33 +304,111 @@ fn openBus(self: *const Self, comptime T: type, address: u32) T {
return @truncate(T, word); return @truncate(T, word);
} }
pub fn read(self: *Self, comptime T: type, address: u32) T { pub fn read(self: *Self, comptime T: type, unaligned_address: u32) T {
const page = @truncate(u8, address >> 24); const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits;
const aligned_addr = forceAlign(T, address); 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) { return switch (page) {
// General Internal Memory // General Internal Memory
0x00 => blk: { 0x00 => blk: {
if (address < Bios.size) if (address < Bios.size)
break :blk self.bios.read(T, self.cpu.r[15], aligned_addr); break :blk self.bios.read(T, self.cpu.r[15], unaligned_address);
break :blk self.openBus(T, address); break :blk self.openBus(T, address);
}, },
0x02 => self.ewram.read(T, aligned_addr), 0x02 => unreachable, // completely handled by fastmeme
0x03 => self.iwram.read(T, aligned_addr), 0x03 => unreachable, // completely handled by fastmeme
0x04 => self.readIo(T, address), 0x04 => self.readIo(T, address),
// Internal Display Memory // Internal Display Memory
0x05 => self.ppu.palette.read(T, aligned_addr), 0x05 => unreachable, // completely handled by fastmeme
0x06 => self.ppu.vram.read(T, aligned_addr), 0x06 => unreachable, // completely handled by fastmeme
0x07 => self.ppu.oam.read(T, aligned_addr), 0x07 => unreachable, // completely handled by fastmeme
// External Memory (Game Pak) // External Memory (Game Pak)
0x08...0x0D => self.pak.read(T, aligned_addr), 0x08...0x0D => self.pak.read(T, address),
0x0E...0x0F => blk: { 0x0E...0x0F => self.readBackup(T, unaligned_address),
const value = self.pak.backup.read(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) { const multiplier = switch (T) {
u32 => 0x01010101, u32 => 0x01010101,
@@ -207,50 +417,124 @@ pub fn read(self: *Self, comptime T: type, address: u32) T {
else => @compileError("Backup: Unsupported read width"), else => @compileError("Backup: Unsupported read width"),
}; };
break :blk @as(T, value) * multiplier; return @as(T, value) * multiplier;
},
else => self.openBus(T, address),
};
} }
pub fn write(self: *Self, comptime T: type, address: u32, value: T) void { pub fn write(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
const page = @truncate(u8, address >> 24); const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits;
const aligned_addr = forceAlign(T, address); 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) { switch (page) {
// General Internal Memory // General Internal Memory
0x00 => self.bios.write(T, aligned_addr, value), 0x00 => self.bios.write(T, address, value),
0x02 => self.ewram.write(T, aligned_addr, value), 0x02 => unreachable, // completely handled by fastmem
0x03 => self.iwram.write(T, aligned_addr, value), 0x03 => unreachable, // completely handled by fastmem
0x04 => io.write(self, T, aligned_addr, value), 0x04 => io.write(self, T, address, value),
// Internal Display Memory // Internal Display Memory
0x05 => self.ppu.palette.write(T, aligned_addr, value), 0x05 => self.ppu.palette.write(T, address, value),
0x06 => self.ppu.vram.write(T, self.ppu.dispcnt, aligned_addr, value), 0x06 => self.ppu.vram.write(T, self.ppu.dispcnt, address, value),
0x07 => self.ppu.oam.write(T, aligned_addr, value), 0x07 => unreachable, // completely handled by fastmem
// External Memory (Game Pak) // External Memory (Game Pak)
0x08...0x0D => self.pak.write(T, self.dma[3].word_count, aligned_addr, value), 0x08...0x0D => self.pak.write(T, self.dma[3].word_count, address, value),
0x0E...0x0F => { 0x0E...0x0F => self.pak.backup.write(unaligned_address, @truncate(u8, rotr(T, value, 8 * rotateBy(T, unaligned_address)))),
const rotate_by = switch (T) {
u32 => address & 3,
u16 => address & 1,
u8 => 0,
else => @compileError("Backup: Unsupported write width"),
};
self.pak.backup.write(address, @truncate(u8, rotr(T, value, 8 * rotate_by)));
},
else => {}, 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) { return switch (T) {
u32 => address & 0xFFFF_FFFC, u32 => address & 3,
u16 => address & 0xFFFF_FFFE, 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, u8 => address,
else => @compileError("Bus: Invalid read/write type"), else => @compileError("Bus: Invalid read/write type"),
}; };

View File

@@ -14,7 +14,6 @@ const SoundFifo = std.fifo.LinearFifo(u8, .{ .Static = 0x20 });
const getHalf = util.getHalf; const getHalf = util.getHalf;
const setHalf = util.setHalf; const setHalf = util.setHalf;
const intToBytes = util.intToBytes;
const log = std.log.scoped(.APU); const log = std.log.scoped(.APU);
@@ -108,8 +107,14 @@ pub fn read(comptime T: type, apu: *const Apu, addr: u32) ?T {
pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void { pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void {
const byte_addr = @truncate(u8, addr); const byte_addr = @truncate(u8, addr);
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return;
switch (T) { switch (T) {
u32 => switch (byte_addr) { 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), 0x60 => apu.ch1.setSound1Cnt(value),
0x64 => apu.ch1.setSound1CntX(&apu.fs, @truncate(u16, value)), 0x64 => apu.ch1.setSound1CntX(&apu.fs, @truncate(u16, value)),
@@ -131,8 +136,12 @@ pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void {
0xA0 => apu.chA.push(value), // FIFO_A 0xA0 => apu.chA.push(value), // FIFO_A
0xA4 => apu.chB.push(value), // FIFO_B 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 }), else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
}
}, },
u16 => switch (byte_addr) { u16 => {
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return;
switch (byte_addr) {
0x60 => apu.ch1.setSound1CntL(@truncate(u8, value)), // SOUND1CNT_L 0x60 => apu.ch1.setSound1CntL(@truncate(u8, value)), // SOUND1CNT_L
0x62 => apu.ch1.setSound1CntH(value), 0x62 => apu.ch1.setSound1CntH(value),
0x64 => apu.ch1.setSound1CntX(&apu.fs, value), 0x64 => apu.ch1.setSound1CntX(&apu.fs, value),
@@ -164,8 +173,12 @@ pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void {
0xA0, 0xA2 => log.err("Tried to write 0x{X:0>4}{} to FIFO_A", .{ value, T }), 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 }), 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 }), else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
}
}, },
u8 => switch (byte_addr) { u8 => {
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return;
switch (byte_addr) {
0x60 => apu.ch1.setSound1CntL(value), 0x60 => apu.ch1.setSound1CntL(value),
0x61 => {}, 0x61 => {},
0x62 => apu.ch1.setNr11(value), 0x62 => apu.ch1.setNr11(value),
@@ -208,6 +221,7 @@ pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void {
0xA0...0xA3 => log.err("Tried to write 0x{X:0>2}{} to FIFO_A", .{ value, T }), 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 }), 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 => 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"), else => @compileError("APU: Unsupported write width"),
} }
@@ -264,26 +278,58 @@ pub const Apu = struct {
.is_buffer_full = false, .is_buffer_full = false,
}; };
sched.push(.SampleAudio, apu.interval()); Self.initEvents(apu.sched, 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);
return apu; 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.ch1.reset();
self.ch2.reset(); self.ch2.reset();
self.ch3.reset(); self.ch3.reset();
self.ch4.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 /// SOUNDCNT
fn setSoundCnt(self: *Self, value: u32) void { fn setSoundCnt(self: *Self, value: u32) void {
self.setSoundCntL(@truncate(u16, value)); if (self.cnt.apu_enable.read()) self.setSoundCntL(@truncate(u16, value));
self.setSoundCntH(@truncate(u16, value >> 16)); self.setSoundCntH(@truncate(u16, value >> 16));
} }
@@ -322,13 +368,16 @@ pub const Apu = struct {
self.fs.step = 0; // Reset Frame Sequencer self.fs.step = 0; // Reset Frame Sequencer
// Reset Square Wave Offsets // Reset Square Wave Offsets
self.ch1.square.pos = 0; self.ch1.square.reset();
self.ch2.square.pos = 0; self.ch2.square.reset();
// Reset Wave Device Offsets // Reset Wave
self.ch3.wave_dev.offset = 0; self.ch3.wave_dev.reset();
// Rest Noise
self.ch4.lfsr.reset();
} else { } else {
self.reset(); self._reset();
} }
} }
@@ -395,8 +444,8 @@ pub const Apu = struct {
right += if (self.dma_cnt.chB_right.read()) chB_sample else 0; right += if (self.dma_cnt.chB_right.read()) chB_sample else 0;
// Add SOUNDBIAS // Add SOUNDBIAS
// FIXME: Is SOUNDBIAS 9-bit or 10-bit? // 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()) << 1; const bias = @as(i16, self.bias.level.read()) << 2;
left += bias; left += bias;
right += bias; right += bias;
@@ -407,7 +456,6 @@ pub const Apu = struct {
const ext_left = (clamped_left << 5) | (clamped_left >> 6); const ext_left = (clamped_left << 5) | (clamped_left >> 6);
const ext_right = (clamped_right << 5) | (clamped_right >> 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(); 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)); _ = SDL.SDL_AudioStreamPut(self.stream, &[2]u16{ ext_left, ext_right }, 2 * @sizeOf(u16));
@@ -473,11 +521,15 @@ pub const Apu = struct {
if (!self.cnt.apu_enable.read()) return; if (!self.cnt.apu_enable.read()) return;
if (@boolToInt(self.dma_cnt.chA_timer.read()) == tim_id) { if (@boolToInt(self.dma_cnt.chA_timer.read()) == tim_id) {
if (!self.chA.enabled) return;
self.chA.updateSample(); self.chA.updateSample();
if (self.chA.len() <= 15) cpu.bus.dma[1].requestAudio(0x0400_00A0); if (self.chA.len() <= 15) cpu.bus.dma[1].requestAudio(0x0400_00A0);
} }
if (@boolToInt(self.dma_cnt.chB_timer.read()) == tim_id) { if (@boolToInt(self.dma_cnt.chB_timer.read()) == tim_id) {
if (!self.chB.enabled) return;
self.chB.updateSample(); self.chB.updateSample();
if (self.chB.len() <= 15) cpu.bus.dma[2].requestAudio(0x0400_00A4); if (self.chB.len() <= 15) cpu.bus.dma[2].requestAudio(0x0400_00A4);
} }
@@ -491,17 +543,31 @@ pub fn DmaSound(comptime kind: DmaSoundKind) type {
fifo: SoundFifo, fifo: SoundFifo,
kind: DmaSoundKind, kind: DmaSoundKind,
sample: i8, sample: i8,
enabled: bool,
fn init() Self { fn init() Self {
return .{ return .{
.fifo = SoundFifo.init(), .fifo = SoundFifo.init(),
.kind = kind, .kind = kind,
.sample = 0, .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 { 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 { pub fn len(self: *const Self) usize {
@@ -527,10 +593,14 @@ pub const FrameSequencer = struct {
const Self = @This(); const Self = @This();
pub const interval = (1 << 24) / 512; pub const interval = (1 << 24) / 512;
step: u3, step: u3 = 0,
pub fn init() Self { pub fn init() Self {
return .{ .step = 0 }; return .{};
}
pub fn reset(self: *Self) void {
self.* = .{};
} }
pub fn tick(self: *Self) void { pub fn tick(self: *Self) void {

View File

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

View File

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

View File

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

View File

@@ -42,10 +42,13 @@ pub fn init(sched: *Scheduler) Self {
} }
pub fn reset(self: *Self) void { pub fn reset(self: *Self) void {
self.select.raw = 0; self.select.raw = 0; // NR30
self.length = 0; self.length = 0; // NR31
self.vol.raw = 0; self.vol.raw = 0; // NR32
self.freq.raw = 0; self.freq.raw = 0; // NR33, NR34
self.len_dev.reset();
self.wave_dev.reset();
self.sample = 0; self.sample = 0;
self.enabled = false; self.enabled = false;

View File

@@ -3,12 +3,16 @@ const io = @import("../../bus/io.zig");
const Self = @This(); const Self = @This();
/// Period Timer /// Period Timer
timer: u3, timer: u3 = 0,
/// Current Volume /// Current Volume
vol: u4, vol: u4 = 0,
pub fn create() Self { 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 { pub fn tick(self: *Self, nrx2: io.Envelope) void {

View File

@@ -1,9 +1,13 @@
const Self = @This(); const Self = @This();
timer: u9, timer: u9 = 0,
pub fn create() Self { 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 { pub fn tick(self: *Self, enabled: bool, ch_enable: *bool) void {

View File

@@ -3,19 +3,18 @@ const ToneSweep = @import("../ToneSweep.zig");
const Self = @This(); const Self = @This();
timer: u8, timer: u8 = 0,
enabled: bool, enabled: bool = false,
shadow: u11, shadow: u11 = 0,
calc_performed: bool, calc_performed: bool = false,
pub fn create() Self { pub fn create() Self {
return .{ return .{};
.timer = 0, }
.enabled = false,
.shadow = 0, pub fn reset(self: *Self) void {
.calc_performed = false, self.* = .{};
};
} }
pub fn tick(self: *Self, ch1: *ToneSweep) void { pub fn tick(self: *Self, ch1: *ToneSweep) void {
@@ -24,7 +23,6 @@ pub fn tick(self: *Self, ch1: *ToneSweep) void {
if (self.timer == 0) { if (self.timer == 0) {
const period = ch1.sweep.period.read(); const period = ch1.sweep.period.read();
self.timer = if (period == 0) 8 else period; self.timer = if (period == 0) 8 else period;
if (!self.calc_performed) self.calc_performed = true;
if (self.enabled and period != 0) { if (self.enabled and period != 0) {
const new_freq = self.calculate(ch1.sweep, &ch1.enabled); 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 shadow_shifted = shadow >> sweep.shift.read();
const decrease = sweep.direction.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; if (freq > 0x7FF) ch_enable.* = false;
return freq; return freq;

View File

@@ -19,6 +19,11 @@ pub fn create(sched: *Scheduler) Self {
}; };
} }
pub fn reset(self: *Self) void {
self.shift = 0;
self.timer = 0;
}
pub fn sample(self: *const Self) i8 { pub fn sample(self: *const Self) i8 {
return if ((~self.shift & 1) == 1) 1 else -1; return if ((~self.shift & 1) == 1) 1 else -1;
} }
@@ -33,7 +38,7 @@ pub fn reload(self: *Self, poly: io.PolyCounter) void {
} }
/// Scheduler Event Handler for LFSR Timer Expire /// 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 { pub fn onLfsrTimerExpire(self: *Self, poly: io.PolyCounter, late: u64) void {
// Obscure: "Using a noise channel clock shift of 14 or 15 // Obscure: "Using a noise channel clock shift of 14 or 15
// results in the LFSR receiving no clocks." // results in the LFSR receiving no clocks."

View File

@@ -20,6 +20,11 @@ pub fn init(sched: *Scheduler) Self {
}; };
} }
pub fn reset(self: *Self) void {
self.timer = 0;
self.pos = 0;
}
/// Scheduler Event Handler for Square Synth Timer Expire /// Scheduler Event Handler for Square Synth Timer Expire
pub fn onSquareTimerExpire(self: *Self, comptime T: type, nrx34: io.Frequency, late: u64) void { pub fn onSquareTimerExpire(self: *Self, comptime T: type, nrx34: io.Frequency, late: u64) void {
comptime std.debug.assert(T == ToneSweep or T == Tone); comptime std.debug.assert(T == ToneSweep or T == Tone);

View File

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

View File

@@ -3,6 +3,9 @@ const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const log = std.log.scoped(.Bios); const log = std.log.scoped(.Bios);
const rotr = @import("zba-util").rotr;
const forceAlign = @import("../Bus.zig").forceAlign;
/// Size of the BIOS in bytes /// Size of the BIOS in bytes
pub const size = 0x4000; pub const size = 0x4000;
const Self = @This(); const Self = @This();
@@ -10,21 +13,37 @@ const Self = @This();
buf: ?[]u8, buf: ?[]u8,
allocator: Allocator, 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) { if (r15 < Self.size) {
const addr = forceAlign(T, address);
self.addr_latch = addr; self.addr_latch = addr;
return self._read(T, addr); return self._read(T, addr);
} }
log.debug("Rejected read since r15=0x{X:0>8}", .{r15}); log.warn("Open Bus! Read from 0x{X:0>8}, but PC was 0x{X:0>8}", .{ address, r15 });
return @truncate(T, self._read(T, self.addr_latch)); 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 { fn rotateBy(comptime T: type, address: u32) u32 {
if (r15 < Self.size) return self._read(T, addr); return switch (T) {
return @truncate(T, self._read(T, self.addr_latch + 8)); 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 /// 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 { 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, .{}); const file = try std.fs.cwd().openFile(path, .{});
defer file.close(); defer file.close();
break :blk try file.readToEndAlloc(allocator, try file.getEndPos()); const file_len = try file.readAll(buf);
} else null; if (file_len != Self.size) log.err("Expected BIOS to be {}B, was {}B", .{ Self.size, file_len });
return Self{ return Self{ .buf = buf, .allocator = allocator };
.buf = buf, }
.allocator = allocator,
.addr_latch = 0, pub fn reset(self: *Self) void {
}; self.addr_latch = 0;
} }
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {

View File

@@ -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 { pub fn deinit(self: *Self) void {
self.allocator.free(self.buf); self.allocator.free(self.buf);
self.* = undefined; self.* = undefined;

View File

@@ -105,14 +105,13 @@ pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
switch (T) { switch (T) {
u32 => switch (address) { 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_00C4 => std.debug.panic("Handle 32-bit GPIO Data/Direction Reads", .{}),
0x0800_00C6 => std.debug.panic("Handle 32-bit GPIO Direction/Control Reads", .{}), 0x0800_00C6 => std.debug.panic("Handle 32-bit GPIO Direction/Control Reads", .{}),
0x0800_00C8 => std.debug.panic("Handle 32-bit GPIO Control Reads", .{}), 0x0800_00C8 => std.debug.panic("Handle 32-bit GPIO Control Reads", .{}),
else => {}, else => {},
}, },
u16 => switch (address) { u16 => switch (address) {
// FIXME: What do 16-bit GPIO Reads look like?
0x0800_00C4 => return self.gpio.read(.Data), 0x0800_00C4 => return self.gpio.read(.Data),
0x0800_00C6 => return self.gpio.read(.Direction), 0x0800_00C6 => return self.gpio.read(.Direction),
0x0800_00C8 => return self.gpio.read(.Control), 0x0800_00C8 => return self.gpio.read(.Control),
@@ -180,23 +179,30 @@ pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value
} }
} }
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, rom_path: []const u8, save_path: ?[]const u8) !Self { pub fn init(allocator: Allocator, cpu: *Arm7tdmi, maybe_rom: ?[]const u8, maybe_save: ?[]const u8) !Self {
const file = try std.fs.cwd().openFile(rom_path, .{}); 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(); defer file.close();
const file_buf = try file.readToEndAlloc(allocator, try file.getEndPos()); const buffer = try file.readToEndAlloc(allocator, try file.getEndPos());
const title = file_buf[0xA0..0xAC].*; const title = buffer[0xA0..0xAC];
const kind = Backup.guess(file_buf); logHeader(buffer, title);
const device = if (config.config().guest.force_rtc) .Rtc else guessDevice(file_buf);
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 .{ return .{
.buf = file_buf, .buf = items[0],
.allocator = allocator, .allocator = allocator,
.title = title, .title = title,
.backup = try Backup.init(allocator, kind, title, save_path), .backup = try Backup.init(allocator, items[2], title, maybe_save),
.gpio = try Gpio.init(allocator, cpu, device), .gpio = try Gpio.init(allocator, cpu, items[3]),
}; };
} }
@@ -214,25 +220,24 @@ fn guessDevice(buf: []const u8) Gpio.Device.Kind {
// Try to Guess if ROM uses RTC // Try to Guess if ROM uses RTC
const needle = "RTC_V"; // I was told SIIRTC_V, though Pokemen Firered (USA) is a false negative 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; var i: usize = 0;
while ((i + needle.len) < buf.len) : (i += 1) { while ((i + needle.len) < buf.len) : (i += 1) {
if (std.mem.eql(u8, needle, buf[i..(i + needle.len)])) return .Rtc; if (std.mem.eql(u8, needle, buf[i..(i + needle.len)])) return .Rtc;
} }
// TODO: Detect other GPIO devices // TODO: Detect other GPIO devices
return .None; return .None;
} }
fn logHeader(buf: []const u8, title: *const [12]u8) void { fn logHeader(buf: []const u8, title: *const [12]u8) void {
const code = buf[0xAC..0xB0];
const maker = buf[0xB0..0xB2];
const version = buf[0xBC]; const version = buf[0xBC];
log.info("Title: {s}", .{title}); log.info("Title: {s}", .{title});
if (version != 0) log.info("Version: {}", .{version}); 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" { test "OOB Access" {

View File

@@ -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 { pub fn deinit(self: *Self) void {
self.allocator.free(self.buf); self.allocator.free(self.buf);
self.* = undefined; self.* = undefined;

View File

@@ -6,7 +6,6 @@ const Eeprom = @import("backup/eeprom.zig").Eeprom;
const Flash = @import("backup/Flash.zig"); const Flash = @import("backup/Flash.zig");
const escape = @import("../../util.zig").escape; const escape = @import("../../util.zig").escape;
const span = @import("../../util.zig").span;
const Needle = struct { str: []const u8, kind: Backup.Kind }; const Needle = struct { str: []const u8, kind: Backup.Kind };
const backup_kinds = [6]Needle{ const backup_kinds = [6]Needle{
@@ -33,7 +32,7 @@ pub const Backup = struct {
flash: Flash, flash: Flash,
eeprom: Eeprom, eeprom: Eeprom,
const Kind = enum { pub const Kind = enum {
Eeprom, Eeprom,
Sram, Sram,
Flash, Flash,
@@ -138,6 +137,7 @@ pub const Backup = struct {
for (backup_kinds) |needle| { for (backup_kinds) |needle| {
const needle_len = needle.str.len; const needle_len = needle.str.len;
// TODO: Use new for loop syntax?
var i: usize = 0; var i: usize = 0;
while ((i + needle_len) < rom.len) : (i += 1) { while ((i + needle_len) < rom.len) : (i += 1) {
if (std.mem.eql(u8, needle.str, rom[i..][0..needle_len])) return needle.kind; 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); const file_path = try self.savePath(allocator, path);
defer allocator.free(file_path); defer allocator.free(file_path);
// FIXME: Don't rely on this lol const expected = "untitled.sav";
if (std.mem.eql(u8, file_path[file_path.len - 12 .. file_path.len], "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", .{}); 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 { 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"; const name = if (title_str.len != 0) title_str else "untitled";
return try std.mem.concat(allocator, u8, &[_][]const u8{ name, ".sav" }); 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); const file_path = try self.savePath(allocator, path);
defer allocator.free(file_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) { switch (self.kind) {
.Sram, .Flash, .Flash1M, .Eeprom => { .Sram, .Flash, .Flash1M, .Eeprom => {
const file = try std.fs.createFileAbsolute(file_path, .{}); const file = try std.fs.createFileAbsolute(file_path, .{});

View File

@@ -63,7 +63,7 @@ pub const Eeprom = struct {
} }
if (self.state == .RequestEnd) { 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; self.state = .Ready;
return; return;
} }

View File

@@ -5,14 +5,14 @@ const DmaControl = @import("io.zig").DmaControl;
const Bus = @import("../Bus.zig"); const Bus = @import("../Bus.zig");
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi; 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 log = std.log.scoped(.DmaTransfer);
const getHalf = util.getHalf; const getHalf = util.getHalf;
const setHalf = util.setHalf; const setHalf = util.setHalf;
const setQuart = util.setQuart; const setQuart = util.setQuart;
const rotr = @import("../../util.zig").rotr; const rotr = @import("zba-util").rotr;
pub fn create() DmaTuple { pub fn create() DmaTuple {
return .{ DmaController(0).init(), DmaController(1).init(), DmaController(2).init(), DmaController(3).init() }; return .{ DmaController(0).init(), DmaController(1).init(), DmaController(2).init(), DmaController(3).init() };
@@ -195,6 +195,10 @@ fn DmaController(comptime id: u2) type {
}; };
} }
pub fn reset(self: *Self) void {
self.* = Self.init();
}
pub fn setDmasad(self: *Self, addr: u32) void { pub fn setDmasad(self: *Self, addr: u32) void {
self.sad = addr & sad_mask; self.sad = addr & sad_mask;
} }
@@ -256,12 +260,16 @@ fn DmaController(comptime id: u2) type {
cpu.bus.write(u16, dad_addr, @truncate(u16, rotr(u32, self.data_latch, 8 * (dad_addr & 3)))); cpu.bus.write(u16, dad_addr, @truncate(u16, rotr(u32, self.data_latch, 8 * (dad_addr & 3))));
} }
switch (sad_adj) { 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, .Increment => self.sad_latch +%= offset,
.Decrement => 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}), .IncrementReload => log.err("{} is a prohibited adjustment on SAD", .{sad_adj}),
.Fixed => {}, .Fixed => {},
},
} }
switch (dad_adj) { switch (dad_adj) {
@@ -334,11 +342,8 @@ fn DmaController(comptime id: u2) type {
}; };
} }
pub fn pollDmaOnBlank(bus: *Bus, comptime kind: DmaKind) void { pub fn onBlanking(bus: *Bus, comptime kind: DmaKind) void {
bus.dma[0].poll(kind); inline for (0..4) |i| bus.dma[i].poll(kind);
bus.dma[1].poll(kind);
bus.dma[2].poll(kind);
bus.dma[3].poll(kind);
} }
const Adjustment = enum(u2) { const Adjustment = enum(u2) {

View File

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

View File

@@ -9,6 +9,9 @@ const Bit = @import("bitfield").Bit;
const Bitfield = @import("bitfield").Bitfield; const Bitfield = @import("bitfield").Bitfield;
const Bus = @import("../Bus.zig"); const Bus = @import("../Bus.zig");
const getHalf = util.getHalf;
const setHalf = util.setHalf;
const log = std.log.scoped(.@"I/O"); const log = std.log.scoped(.@"I/O");
pub const Io = struct { pub const Io = struct {
@@ -19,20 +22,26 @@ pub const Io = struct {
ie: InterruptEnable, ie: InterruptEnable,
irq: InterruptRequest, irq: InterruptRequest,
postflg: PostFlag, postflg: PostFlag,
waitcnt: WaitControl,
haltcnt: HaltControl, haltcnt: HaltControl,
keyinput: KeyInput, keyinput: AtomicKeyInput,
pub fn init() Self { pub fn init() Self {
return .{ return .{
.ime = false, .ime = false,
.ie = .{ .raw = 0x0000 }, .ie = .{ .raw = 0x0000 },
.irq = .{ .raw = 0x0000 }, .irq = .{ .raw = 0x0000 },
.keyinput = .{ .raw = 0x03FF }, .keyinput = AtomicKeyInput.init(.{ .raw = 0x03FF }),
.waitcnt = .{ .raw = 0x0000_0000 }, // Bit 15 == 0 for GBA
.postflg = .FirstBoot, .postflg = .FirstBoot,
.haltcnt = .Execute, .haltcnt = .Execute,
}; };
} }
pub fn reset(self: *Self) void {
self.* = Self.init();
}
fn setIrqs(self: *Io, word: u32) void { fn setIrqs(self: *Io, word: u32) void {
self.ie.raw = @truncate(u16, word); self.ie.raw = @truncate(u16, word);
self.irq.raw &= ~@truncate(u16, word >> 16); self.irq.raw &= ~@truncate(u16, word >> 16);
@@ -64,8 +73,10 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) ?T {
0x0400_0150 => util.io.read.todo(log, "Read {} from JOY_RECV", .{T}), 0x0400_0150 => util.io.read.todo(log, "Read {} from JOY_RECV", .{T}),
// Interrupts // 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_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 }), else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
}, },
u16 => switch (address) { u16 => switch (address) {
@@ -85,16 +96,23 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) ?T {
0x0400_0128 => util.io.read.todo(log, "Read {} from SIOCNT", .{T}), 0x0400_0128 => util.io.read.todo(log, "Read {} from SIOCNT", .{T}),
// Keypad Input // Keypad Input
0x0400_0130 => bus.io.keyinput.raw, 0x0400_0130 => bus.io.keyinput.load(.Monotonic).raw,
// Serial Communication 2 // Serial Communication 2
0x0400_0134 => util.io.read.todo(log, "Read {} from RCNT", .{T}), 0x0400_0134 => util.io.read.todo(log, "Read {} from RCNT", .{T}),
0x0400_0136 => 0x0000,
0x0400_0142 => 0x0000,
0x0400_015A => 0x0000,
// Interrupts // Interrupts
0x0400_0200 => bus.io.ie.raw, 0x0400_0200 => bus.io.ie.raw,
0x0400_0202 => bus.io.irq.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_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 }), else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
}, },
u8 => return switch (address) { u8 => return switch (address) {
@@ -118,10 +136,20 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) ?T {
// Serial Communication 2 // Serial Communication 2
0x0400_0135 => util.io.read.todo(log, "Read {} from RCNT_H", .{T}), 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 // 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_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 => 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"), else => @compileError("I/O: Unsupported read width"),
@@ -168,9 +196,12 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
// Interrupts // Interrupts
0x0400_0200 => bus.io.setIrqs(value), 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_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 }), else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, address }),
}, },
u16 => switch (address) { u16 => switch (address) {
@@ -186,7 +217,7 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
// Timers // Timers
0x0400_0100...0x0400_010E => timer.write(T, &bus.tim, address, value), 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, 0x0400_0110 => {}, // Not Used,
// Serial Communication 1 // Serial Communication 1
@@ -210,9 +241,14 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
// Interrupts // Interrupts
0x0400_0200 => bus.io.ie.raw = value, 0x0400_0200 => bus.io.ie.raw = value,
0x0400_0202 => bus.io.irq.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_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 }), else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, address }),
}, },
u8 => switch (address) { u8 => switch (address) {
@@ -237,9 +273,16 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
0x0400_0140 => log.debug("Wrote 0x{X:0>2} to JOYCNT_L", .{value}), 0x0400_0140 => log.debug("Wrote 0x{X:0>2} to JOYCNT_L", .{value}),
// Interrupts // 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_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_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_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 }), 0x0400_0410 => log.debug("Wrote 0x{X:0>2} to the common yet undocumented 0x{X:0>8}", .{ value, address }),
@@ -278,14 +321,22 @@ pub const DisplayControl = extern union {
/// Read / Write /// Read / Write
pub const DisplayStatus = extern union { pub const DisplayStatus = extern union {
/// read-only
vblank: Bit(u16, 0), vblank: Bit(u16, 0),
/// read-only
hblank: Bit(u16, 1), hblank: Bit(u16, 1),
// read-only
coincidence: Bit(u16, 2), coincidence: Bit(u16, 2),
vblank_irq: Bit(u16, 3), vblank_irq: Bit(u16, 3),
hblank_irq: Bit(u16, 4), hblank_irq: Bit(u16, 4),
vcount_irq: Bit(u16, 5), vcount_irq: Bit(u16, 5),
vcount_trigger: Bitfield(u16, 8, 8), vcount_trigger: Bitfield(u16, 8, 8),
raw: u16, 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 /// Read Only
@@ -299,10 +350,10 @@ const InterruptEnable = extern union {
vblank: Bit(u16, 0), vblank: Bit(u16, 0),
hblank: Bit(u16, 1), hblank: Bit(u16, 1),
coincidence: Bit(u16, 2), coincidence: Bit(u16, 2),
tm0_overflow: Bit(u16, 3), tim0: Bit(u16, 3),
tm1_overflow: Bit(u16, 4), tim1: Bit(u16, 4),
tm2_overflow: Bit(u16, 5), tim2: Bit(u16, 5),
tm3_overflow: Bit(u16, 6), tim3: Bit(u16, 6),
serial: Bit(u16, 7), serial: Bit(u16, 7),
dma0: Bit(u16, 8), dma0: Bit(u16, 8),
dma1: Bit(u16, 9), dma1: Bit(u16, 9),
@@ -329,6 +380,31 @@ const KeyInput = extern union {
raw: u16, 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 // Read / Write
pub const BackgroundControl = extern union { pub const BackgroundControl = extern union {
priority: Bitfield(u16, 0, 2), priority: Bitfield(u16, 0, 2),
@@ -377,6 +453,8 @@ pub const BldY = extern union {
raw: u16, raw: u16,
}; };
const u8WriteKind = enum { Hi, Lo };
/// Write-only /// Write-only
pub const WinH = extern union { pub const WinH = extern union {
x2: Bitfield(u16, 0, 8), x2: Bitfield(u16, 0, 8),
@@ -386,6 +464,8 @@ pub const WinH = extern union {
/// Write-only /// Write-only
pub const WinV = extern union { pub const WinV = extern union {
const Self = @This();
y2: Bitfield(u16, 0, 8), y2: Bitfield(u16, 0, 8),
y1: Bitfield(u16, 8, 8), y1: Bitfield(u16, 8, 8),
raw: u16, raw: u16,
@@ -394,20 +474,20 @@ pub const WinV = extern union {
pub const WinIn = extern union { pub const WinIn = extern union {
w0_bg: Bitfield(u16, 0, 4), w0_bg: Bitfield(u16, 0, 4),
w0_obj: Bit(u16, 4), w0_obj: Bit(u16, 4),
w0_colour: Bit(u16, 5), w0_bld: Bit(u16, 5),
w1_bg: Bitfield(u16, 8, 4), w1_bg: Bitfield(u16, 8, 4),
w1_obj: Bit(u16, 12), w1_obj: Bit(u16, 12),
w1_colour: Bit(u16, 13), w1_bld: Bit(u16, 13),
raw: u16, raw: u16,
}; };
pub const WinOut = extern union { pub const WinOut = extern union {
out_bg: Bitfield(u16, 0, 4), out_bg: Bitfield(u16, 0, 4),
out_obj: Bit(u16, 4), out_obj: Bit(u16, 4),
out_colour: Bit(u16, 5), out_bld: Bit(u16, 5),
obj_bg: Bitfield(u16, 8, 4), obj_bg: Bitfield(u16, 8, 4),
obj_obj: Bit(u16, 12), obj_obj: Bit(u16, 12),
obj_colour: Bit(u16, 13), obj_bld: Bit(u16, 13),
raw: u16, raw: u16,
}; };
@@ -576,3 +656,24 @@ pub const SoundBias = extern union {
sampling_cycle: Bitfield(u16, 14, 2), sampling_cycle: Bitfield(u16, 14, 2),
raw: u16, raw: u16,
}; };
/// Read / Write
pub const WaitControl = extern union {
sram_cnt: Bitfield(u16, 0, 2),
s0_first: Bitfield(u16, 2, 2),
s0_second: Bit(u16, 4),
s1_first: Bitfield(u16, 5, 2),
s1_second: Bit(u16, 7),
s2_first: Bitfield(u16, 8, 2),
s2_second: Bit(u16, 10),
phi_out: Bitfield(u16, 11, 2),
prefetch_enable: Bit(u16, 14),
pak_kind: Bit(u16, 15),
raw: u16,
pub fn set(self: *WaitControl, value: u16) void {
const mask: u16 = 0x8000; // set bits are read-only
self.raw = (self.raw & mask) | (value & ~mask);
}
};

View File

@@ -5,7 +5,7 @@ const TimerControl = @import("io.zig").TimerControl;
const Scheduler = @import("../scheduler.zig").Scheduler; const Scheduler = @import("../scheduler.zig").Scheduler;
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi; 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 log = std.log.scoped(.Timer);
const getHalf = util.getHalf; const getHalf = util.getHalf;
@@ -128,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 /// TIMCNT_L Getter
pub fn timcntL(self: *const Self) u16 { pub fn timcntL(self: *const Self) u16 {
if (self.cnt.cascade.read() or !self.cnt.enabled.read()) return self._counter; if (self.cnt.cascade.read() or !self.cnt.enabled.read()) return self._counter;
@@ -150,20 +156,35 @@ fn Timer(comptime id: u2) type {
pub fn setTimcntH(self: *Self, halfword: u16) void { pub fn setTimcntH(self: *Self, halfword: u16) void {
const new = TimerControl{ .raw = halfword }; const new = TimerControl{ .raw = halfword };
// If Timer happens to be enabled, It will either be resheduled or disabled if (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 }); self.sched.removeScheduledEvent(.{ .TimerOverflow = id });
if (self.cnt.enabled.read() and (new.cascade.read() or !new.enabled.read())) { // Counter should hold the value it stopped at meaning we have to calculate it now
// Either through the cascade bit or the enable bit, the timer has effectively been disabled
// The Counter should hold whatever value it should have been at when it was disabled
self._counter +%= @truncate(u16, (self.sched.now() - self._start_timestamp) / self.frequency()); 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 // the timer has always been enabled, but the cascade bit which was blocking the timer has been unset
if (!self.cnt.enabled.read() and new.enabled.read()) self._counter = self._reload; 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 self.rescheduleTimerExpire(0);
if (new.enabled.read() and !new.cascade.read()) 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; self.cnt.raw = halfword;
} }
@@ -190,23 +211,20 @@ fn Timer(comptime id: u2) type {
// Perform Cascade Behaviour // Perform Cascade Behaviour
switch (id) { switch (id) {
0 => if (cpu.bus.tim[1].cnt.cascade.read()) { inline 0, 1, 2 => |idx| {
cpu.bus.tim[1]._counter +%= 1; const next = idx + 1;
if (cpu.bus.tim[1]._counter == 0) cpu.bus.tim[1].onTimerExpire(cpu, late);
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()) { 3 => {}, // THere is no timer for TIM3 to cascade to
cpu.bus.tim[2]._counter +%= 1;
if (cpu.bus.tim[2]._counter == 0) cpu.bus.tim[2].onTimerExpire(cpu, late);
},
2 => if (cpu.bus.tim[3].cnt.cascade.read()) {
cpu.bus.tim[3]._counter +%= 1;
if (cpu.bus.tim[3]._counter == 0) cpu.bus.tim[3].onTimerExpire(cpu, late);
},
3 => {}, // There is no Timer for TIM3 to "cascade" to,
} }
// Reschedule Timer if we're not cascading // 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._counter = self._reload;
self.rescheduleTimerExpire(late); self.rescheduleTimerExpire(late);
} }

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi; const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn; const InstrFn = @import("../../cpu.zig").arm.InstrFn;
const sext = @import("../../../util.zig").sext; const sext = @import("zba-util").sext;
pub fn branch(comptime L: bool) InstrFn { pub fn branch(comptime L: bool) InstrFn {
return struct { return struct {

View File

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

View File

@@ -2,8 +2,8 @@ const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi; const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn; const InstrFn = @import("../../cpu.zig").arm.InstrFn;
const sext = @import("../../../util.zig").sext; const sext = @import("zba-util").sext;
const rotr = @import("../../../util.zig").rotr; 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 { pub fn halfAndSignedDataTransfer(comptime P: bool, comptime U: bool, comptime I: bool, comptime W: bool, comptime L: bool) InstrFn {
return struct { return struct {

View File

@@ -7,7 +7,7 @@ const PSR = @import("../../cpu.zig").PSR;
const log = std.log.scoped(.PsrTransfer); 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 { pub fn psrTransfer(comptime I: bool, comptime R: bool, comptime kind: u2) InstrFn {
return struct { return struct {

View File

@@ -2,7 +2,7 @@ const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi; const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn; const InstrFn = @import("../../cpu.zig").arm.InstrFn;
const rotr = @import("../../../util.zig").rotr; const rotr = @import("zba-util").rotr;
pub fn singleDataSwap(comptime B: bool) InstrFn { pub fn singleDataSwap(comptime B: bool) InstrFn {
return struct { return struct {

View File

@@ -3,7 +3,7 @@ const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi; const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn; const InstrFn = @import("../../cpu.zig").arm.InstrFn;
const 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 { pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool, comptime B: bool, comptime W: bool, comptime L: bool) InstrFn {
return struct { return struct {
@@ -11,9 +11,7 @@ pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool,
const rn = opcode >> 16 & 0xF; const rn = opcode >> 16 & 0xF;
const rd = opcode >> 12 & 0xF; const rd = opcode >> 12 & 0xF;
// rn is r15 and L is not set, the PC is 12 ahead const base = cpu.r[rn];
const base = cpu.r[rn] + if (!L and rn == 0xF) 4 else @as(u32, 0);
const offset = if (I) shifter.immediate(false, cpu, opcode) else opcode & 0xFFF; const offset = if (I) shifter.immediate(false, cpu, opcode) else opcode & 0xFFF;
const modified_base = if (U) base +% offset else base -% offset; const modified_base = if (U) base +% offset else base -% offset;

View File

@@ -1,7 +1,7 @@
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi; const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
const CPSR = @import("../cpu.zig").PSR; const CPSR = @import("../cpu.zig").PSR;
const rotr = @import("../../util.zig").rotr; const rotr = @import("zba-util").rotr;
pub fn exec(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 { pub fn exec(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
var result: u32 = undefined; var result: u32 = undefined;

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").thumb.InstrFn; const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
const checkCond = @import("../../cpu.zig").checkCond; 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 { pub fn fmt16(comptime cond: u4) InstrFn {
return struct { return struct {

View File

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

View File

@@ -2,8 +2,8 @@ const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi; const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").thumb.InstrFn; const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
const rotr = @import("../../../util.zig").rotr; const rotr = @import("zba-util").rotr;
const sext = @import("../../../util.zig").sext; const sext = @import("zba-util").sext;
pub fn fmt6(comptime rd: u3) InstrFn { pub fn fmt6(comptime rd: u3) InstrFn {
return struct { return struct {

View File

@@ -4,10 +4,10 @@ const config = @import("../config.zig");
const Scheduler = @import("scheduler.zig").Scheduler; const Scheduler = @import("scheduler.zig").Scheduler;
const Arm7tdmi = @import("cpu.zig").Arm7tdmi; const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
const FpsTracker = @import("../util.zig").FpsTracker; const Tracker = @import("../util.zig").FpsTracker;
const TwoWayChannel = @import("zba-util").TwoWayChannel;
const Timer = std.time.Timer; const Timer = std.time.Timer;
const Atomic = std.atomic.Atomic;
/// 4 Cycles in 1 dot /// 4 Cycles in 1 dot
const cycles_per_dot = 4; const cycles_per_dot = 4;
@@ -35,28 +35,41 @@ const RunKind = enum {
LimitedFPS, LimitedFPS,
}; };
pub fn run(quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: *FpsTracker) void { 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; const audio_sync = config.config().guest.audio_sync and !config.config().host.mute;
if (audio_sync) log.info("Audio sync enabled", .{}); if (audio_sync) log.info("Audio sync enabled", .{});
if (config.config().guest.video_sync) { if (config.config().guest.video_sync) {
inner(.LimitedFPS, audio_sync, quit, scheduler, cpu, tracker); inner(.LimitedFPS, audio_sync, cpu, scheduler, tracker, channel);
} else { } else {
inner(.UnlimitedFPS, audio_sync, quit, scheduler, cpu, tracker); inner(.UnlimitedFPS, audio_sync, cpu, scheduler, tracker, channel);
} }
} }
fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: ?*FpsTracker) void { fn inner(comptime kind: RunKind, audio_sync: bool, cpu: *Arm7tdmi, scheduler: *Scheduler, tracker: ?*Tracker, channel: *TwoWayChannel) void {
if (kind == .UnlimitedFPS or kind == .LimitedFPS) { if (kind == .UnlimitedFPS or kind == .LimitedFPS) {
std.debug.assert(tracker != null); std.debug.assert(tracker != null);
log.info("FPS tracking enabled", .{}); log.info("FPS tracking enabled", .{});
} }
var paused: bool = false;
switch (kind) { switch (kind) {
.Unlimited, .UnlimitedFPS => { .Unlimited, .UnlimitedFPS => {
log.info("Emulation w/out video sync", .{}); log.info("Emulation w/out video sync", .{});
while (!quit.load(.SeqCst)) { 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); runFrame(scheduler, cpu);
audioSync(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full); audioSync(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
@@ -68,7 +81,18 @@ fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), schedule
var timer = Timer.start() catch @panic("failed to initalize std.timer.Timer"); var timer = Timer.start() catch @panic("failed to initalize std.timer.Timer");
var wake_time: u64 = frame_period; var wake_time: u64 = frame_period;
while (!quit.load(.SeqCst)) { 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); runFrame(scheduler, cpu);
const new_wake_time = videoSync(&timer, wake_time); const new_wake_time = videoSync(&timer, wake_time);
@@ -94,7 +118,7 @@ pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
if (!cpu.stepDmaTransfer()) { if (!cpu.stepDmaTransfer()) {
if (cpu.isHalted()) { if (cpu.isHalted()) {
// Fast-forward to next Event // Fast-forward to next Event
sched.tick = sched.queue.peek().?.tick; sched.tick = sched.nextTimestamp();
} else { } else {
cpu.step(); cpu.step();
} }
@@ -105,6 +129,7 @@ pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
} }
fn audioSync(audio_sync: bool, 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 sample_size = 2 * @sizeOf(u16);
const max_buf_size: c_int = 0x400; const max_buf_size: c_int = 0x400;
@@ -145,9 +170,8 @@ fn sleep(timer: *Timer, wake_time: u64) ?u64 {
const step = 2 * std.time.ns_per_ms; // Granularity of 2ms const step = 2 * std.time.ns_per_ms; // Granularity of 2ms
const times = sleep_for / step; const times = sleep_for / step;
var i: usize = 0;
while (i < times) : (i += 1) { for (0..times) |_| {
std.time.sleep(step); std.time.sleep(step);
// Upon wakeup, check to see if this particular sleep was longer than expected // Upon wakeup, check to see if this particular sleep was longer than expected
@@ -162,3 +186,71 @@ fn sleep(timer: *Timer, wake_time: u64) ?u64 {
fn spinLoop(timer: *Timer, wake_time: u64) void { fn spinLoop(timer: *Timer, wake_time: u64) void {
while (true) if (timer.read() > wake_time) break; 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);
}

File diff suppressed because it is too large Load Diff

44
src/core/ppu/Oam.zig Normal file
View 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
View 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
View 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);
}

View File

@@ -11,11 +11,11 @@ const log = std.log.scoped(.Scheduler);
pub const Scheduler = struct { pub const Scheduler = struct {
const Self = @This(); const Self = @This();
tick: u64, tick: u64 = 0,
queue: PriorityQueue(Event, void, lessThan), queue: PriorityQueue(Event, void, lessThan),
pub fn init(allocator: Allocator) Self { 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; sched.queue.add(.{ .kind = .HeatDeath, .tick = std.math.maxInt(u64) }) catch unreachable;
return sched; return sched;
@@ -26,12 +26,23 @@ pub const Scheduler = struct {
self.* = undefined; 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 { pub inline fn now(self: *const Self) u64 {
return self.tick; return self.tick;
} }
pub fn handleEvent(self: *Self, cpu: *Arm7tdmi) void { pub fn handleEvent(self: *Self, cpu: *Arm7tdmi) void {
if (self.queue.removeOrNull()) |event| { const event = self.queue.remove();
const late = self.tick - event.tick; const late = self.tick - event.tick;
switch (event.kind) { switch (event.kind) {
@@ -46,10 +57,7 @@ pub const Scheduler = struct {
}, },
.TimerOverflow => |id| { .TimerOverflow => |id| {
switch (id) { switch (id) {
0 => cpu.bus.tim[0].onTimerExpire(cpu, late), inline 0...3 => |idx| cpu.bus.tim[idx].onTimerExpire(cpu, late),
1 => cpu.bus.tim[1].onTimerExpire(cpu, late),
2 => cpu.bus.tim[2].onTimerExpire(cpu, late),
3 => cpu.bus.tim[3].onTimerExpire(cpu, late),
} }
}, },
.ApuChannel => |id| { .ApuChannel => |id| {
@@ -73,22 +81,16 @@ pub const Scheduler = struct {
.VBlank => cpu.bus.ppu.onHdrawEnd(cpu, late), // The end of a VBlank .VBlank => cpu.bus.ppu.onHdrawEnd(cpu, late), // The end of a VBlank
} }
} }
}
/// Removes the **first** scheduled event of type `needle` /// Removes the **first** scheduled event of type `needle`
pub fn removeScheduledEvent(self: *Self, needle: EventKind) void { pub fn removeScheduledEvent(self: *Self, needle: EventKind) void {
var it = self.queue.iterator(); for (self.queue.items, 0..) |event, i| {
var i: usize = 0;
while (it.next()) |event| : (i += 1) {
if (std.meta.eql(event.kind, needle)) { if (std.meta.eql(event.kind, needle)) {
// This invalidates the iterator // invalidates the slice we're iterating over
_ = self.queue.removeIndex(i); _ = self.queue.removeIndex(i);
// Since removing something from the PQ invalidates the iterator, log.debug("Removed {?}@{}", .{ event.kind, event.tick });
// this implementation can safely only remove the first instance of
// a Scheduled Event. Exit Early
break; break;
} }
} }

307
src/imgui.zig Normal file
View 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;
}
};

View File

@@ -4,17 +4,18 @@ const known_folders = @import("known_folders");
const clap = @import("clap"); const clap = @import("clap");
const config = @import("config.zig"); const config = @import("config.zig");
const emu = @import("core/emu.zig");
const TwoWayChannel = @import("zba-util").TwoWayChannel;
const Gui = @import("platform.zig").Gui; const Gui = @import("platform.zig").Gui;
const Bus = @import("core/Bus.zig"); const Bus = @import("core/Bus.zig");
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi; const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
const Scheduler = @import("core/scheduler.zig").Scheduler; const Scheduler = @import("core/scheduler.zig").Scheduler;
const FilePaths = @import("util.zig").FilePaths; const FilePaths = @import("util.zig").FilePaths;
const FpsTracker = @import("util.zig").FpsTracker;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const log = std.log.scoped(.Cli); const log = std.log.scoped(.Cli);
const width = @import("core/ppu.zig").width;
const height = @import("core/ppu.zig").height;
pub const log_level = if (builtin.mode != .Debug) .info else std.log.default_level; pub const log_level = if (builtin.mode != .Debug) .info else std.log.default_level;
// CLI Arguments + Help Text // CLI Arguments + Help Text
@@ -22,44 +23,57 @@ const params = clap.parseParamsComptime(
\\-h, --help Display this help and exit. \\-h, --help Display this help and exit.
\\-s, --skip Skip BIOS. \\-s, --skip Skip BIOS.
\\-b, --bios <str> Optional path to a GBA BIOS ROM. \\-b, --bios <str> Optional path to a GBA BIOS ROM.
\\ --gdb Run ZBA from the context of a GDB Server
\\<str> Path to the GBA GamePak ROM. \\<str> Path to the GBA GamePak ROM.
\\ \\
); );
pub fn main() anyerror!void { pub fn main() void {
// Main Allocator for ZBA // Main Allocator for ZBA
var gpa = std.heap.GeneralPurposeAllocator(.{}){}; var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer std.debug.assert(!gpa.deinit()); defer std.debug.assert(!gpa.deinit());
const allocator = gpa.allocator(); const allocator = gpa.allocator();
// Determine the Data Directory (stores saves, config file, etc.) // Determine the Data Directory (stores saves)
const data_path = blk: { const data_path = blk: {
const result = known_folders.getPath(allocator, .data); const result = known_folders.getPath(allocator, .data);
const option = result catch |e| exitln("interrupted while attempting to find a data directory: {}", .{e}); const option = result catch |e| exitln("interrupted while determining the data folder: {}", .{e});
const path = option orelse exitln("no valid data directory could be found", .{}); const path = option orelse exitln("no valid data folder found", .{});
ensureDirectoriesExist(path) catch |e| exitln("failed to create directories under \"{s}\": {}", .{ path, e }); ensureDataDirsExist(path) catch |e| exitln("failed to create folders under \"{s}\": {}", .{ path, e });
break :blk path; break :blk path;
}; };
defer allocator.free(data_path); defer allocator.free(data_path);
// Determine the Config Directory
const config_path = blk: {
const result = known_folders.getPath(allocator, .roaming_configuration);
const option = result catch |e| exitln("interreupted while determining the config folder: {}", .{e});
const path = option orelse exitln("no valid config folder found", .{});
ensureConfigDirExists(path) catch |e| exitln("failed to create required folder \"{s}\": {}", .{ path, e });
break :blk path;
};
defer allocator.free(config_path);
// Parse CLI // Parse CLI
const result = clap.parse(clap.Help, &params, clap.parsers.default, .{}) catch |e| exitln("failed to parse cli: {}", .{e}); const result = clap.parse(clap.Help, &params, clap.parsers.default, .{}) catch |e| exitln("failed to parse cli: {}", .{e});
defer result.deinit(); defer result.deinit();
// TODO: Move config file to XDG Config directory? // TODO: Move config file to XDG Config directory?
const config_path = configFilePath(allocator, data_path) catch |e| exitln("failed to determine the config file path for ZBA: {}", .{e}); const cfg_file_path = configFilePath(allocator, config_path) catch |e| exitln("failed to ready config file for access: {}", .{e});
defer allocator.free(config_path); defer allocator.free(cfg_file_path);
config.load(allocator, config_path) catch |e| exitln("failed to read config file: {}", .{e}); config.load(allocator, cfg_file_path) catch |e| exitln("failed to load config file: {}", .{e});
const paths = handleArguments(allocator, data_path, &result) catch |e| exitln("failed to handle cli arguments: {}", .{e}); const paths = handleArguments(allocator, data_path, &result) catch |e| exitln("failed to handle cli arguments: {}", .{e});
defer if (paths.save) |path| allocator.free(path); defer if (paths.save) |path| allocator.free(path);
const log_file = if (config.config().debug.cpu_trace) blk: { const log_file = switch (config.config().debug.cpu_trace) {
break :blk std.fs.cwd().createFile("zba.log", .{}) catch |e| exitln("failed to create trace log file: {}", .{e}); true => std.fs.cwd().createFile("zba.log", .{}) catch |e| exitln("failed to create trace log file: {}", .{e}),
} else null; false => null,
};
defer if (log_file) |file| file.close(); defer if (log_file) |file| file.close();
// TODO: Take Emulator Init Code out of main.zig // TODO: Take Emulator Init Code out of main.zig
@@ -76,15 +90,58 @@ pub fn main() anyerror!void {
cpu.fastBoot(); cpu.fastBoot();
} }
var gui = Gui.init(&bus.pak.title, &bus.apu, width, height); const title_ptr = if (paths.rom != null) &bus.pak.title else null;
// TODO: Just copy the title instead of grabbing a pointer to it
var gui = Gui.init(allocator, &bus.apu, title_ptr) catch |e| exitln("failed to init gui: {}", .{e});
defer gui.deinit(); defer gui.deinit();
gui.run(&cpu, &scheduler) catch |e| exitln("failed to run gui thread: {}", .{e}); 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});
}
} }
pub fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const clap.Result(clap.Help, &params, clap.parsers.default)) !FilePaths { fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const clap.Result(clap.Help, &params, clap.parsers.default)) !FilePaths {
const rom_path = romPath(result); const rom_path = romPath(result);
log.info("ROM path: {s}", .{rom_path}); log.info("ROM path: {?s}", .{rom_path});
const bios_path = result.args.bios; const bios_path = result.args.bios;
if (bios_path) |path| log.info("BIOS path: {s}", .{path}) else log.warn("No BIOS provided", .{}); if (bios_path) |path| log.info("BIOS path: {s}", .{path}) else log.warn("No BIOS provided", .{});
@@ -99,8 +156,8 @@ pub fn handleArguments(allocator: Allocator, data_path: []const u8, result: *con
}; };
} }
fn configFilePath(allocator: Allocator, data_path: []const u8) ![]const u8 { fn configFilePath(allocator: Allocator, config_path: []const u8) ![]const u8 {
const path = try std.fs.path.join(allocator, &[_][]const u8{ data_path, "zba", "config.toml" }); const path = try std.fs.path.join(allocator, &[_][]const u8{ config_path, "zba", "config.toml" });
errdefer allocator.free(path); errdefer allocator.free(path);
// We try to create the file exclusively, meaning that we err out if the file already exists. // We try to create the file exclusively, meaning that we err out if the file already exists.
@@ -109,7 +166,7 @@ fn configFilePath(allocator: Allocator, data_path: []const u8) ![]const u8 {
std.fs.accessAbsolute(path, .{}) catch |e| { std.fs.accessAbsolute(path, .{}) catch |e| {
if (e != error.FileNotFound) return e; if (e != error.FileNotFound) return e;
const config_file = try std.fs.createFileAbsolute(path, .{}); const config_file = std.fs.createFileAbsolute(path, .{}) catch |err| exitln("failed to create \"{s}\": {}", .{ path, err });
defer config_file.close(); defer config_file.close();
try config_file.writeAll(@embedFile("../example.toml")); try config_file.writeAll(@embedFile("../example.toml"));
@@ -118,21 +175,25 @@ fn configFilePath(allocator: Allocator, data_path: []const u8) ![]const u8 {
return path; return path;
} }
fn ensureDirectoriesExist(data_path: []const u8) !void { fn ensureDataDirsExist(data_path: []const u8) !void {
var dir = try std.fs.openDirAbsolute(data_path, .{}); var dir = try std.fs.openDirAbsolute(data_path, .{});
defer dir.close(); defer dir.close();
// We want to make sure: %APPDATA%/zba and %APPDATA%/zba/save exist
// (~/.local/share/zba/save for linux, ??? for macOS)
// Will recursively create directories // Will recursively create directories
try dir.makePath("zba" ++ std.fs.path.sep_str ++ "save"); try dir.makePath("zba" ++ std.fs.path.sep_str ++ "save");
} }
fn romPath(result: *const clap.Result(clap.Help, &params, clap.parsers.default)) []const u8 { fn ensureConfigDirExists(config_path: []const u8) !void {
var dir = try std.fs.openDirAbsolute(config_path, .{});
defer dir.close();
try dir.makePath("zba");
}
fn romPath(result: *const clap.Result(clap.Help, &params, clap.parsers.default)) ?[]const u8 {
return switch (result.positionals.len) { return switch (result.positionals.len) {
0 => null,
1 => result.positionals[0], 1 => result.positionals[0],
0 => exitln("ZBA requires a path to a GamePak ROM", .{}),
else => exitln("ZBA received too many positional arguments.", .{}), else => exitln("ZBA received too many positional arguments.", .{}),
}; };
} }

View File

@@ -1,27 +1,36 @@
const std = @import("std"); const std = @import("std");
const SDL = @import("sdl2"); const SDL = @import("sdl2");
const gl = @import("gl"); const gl = @import("gl");
const zgui = @import("zgui");
const emu = @import("core/emu.zig"); const emu = @import("core/emu.zig");
const config = @import("config.zig"); const config = @import("config.zig");
const imgui = @import("imgui.zig");
const Apu = @import("core/apu.zig").Apu; const Apu = @import("core/apu.zig").Apu;
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi; const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
const Scheduler = @import("core/scheduler.zig").Scheduler; const Scheduler = @import("core/scheduler.zig").Scheduler;
const FpsTracker = @import("util.zig").FpsTracker; 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_width = @import("core/ppu.zig").width;
const gba_height = @import("core/ppu.zig").height; const gba_height = @import("core/ppu.zig").height;
pub const sample_rate = 44100; const GLuint = gl.GLuint;
pub const sample_format = SDL.AUDIO_F32; 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 { pub const Gui = struct {
const Self = @This(); const Self = @This();
const SDL_GLContext = *anyopaque; // SDL.SDL_GLContext is a ?*anyopaque
const log = std.log.scoped(.Gui); const log = std.log.scoped(.Gui);
// zig fmt: off // zig fmt: off
@@ -41,47 +50,88 @@ pub const Gui = struct {
window: *SDL.SDL_Window, window: *SDL.SDL_Window,
ctx: SDL_GLContext, ctx: SDL_GLContext,
title: []const u8,
audio: Audio, audio: Audio,
state: imgui.State,
allocator: Allocator,
program_id: gl.GLuint, program_id: gl.GLuint,
pub fn init(title: *const [12]u8, apu: *Apu, width: i32, height: i32) Self { pub fn init(allocator: Allocator, apu: *Apu, title_opt: ?*const [12]u8) !Self {
if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO) < 0) panic(); if (SDL.SDL_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_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();
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic(); if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic();
const win_scale = @intCast(c_int, config.config().host.win_scale);
const window = SDL.SDL_CreateWindow( const window = SDL.SDL_CreateWindow(
default_title.ptr, window_title,
SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED,
SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED,
@as(c_int, width * win_scale), width,
@as(c_int, height * win_scale), height,
SDL.SDL_WINDOW_OPENGL | SDL.SDL_WINDOW_SHOWN, SDL.SDL_WINDOW_OPENGL | SDL.SDL_WINDOW_SHOWN,
) orelse panic(); ) orelse panic();
const ctx = SDL.SDL_GL_CreateContext(window) orelse panic(); const ctx = SDL.SDL_GL_CreateContext(window) orelse panic();
if (SDL.SDL_GL_MakeCurrent(window, ctx) < 0) panic(); if (SDL.SDL_GL_MakeCurrent(window, ctx) < 0) panic();
gl.load(ctx, Self.glGetProcAddress) catch @panic("gl.load failed"); gl.load(ctx, Self.glGetProcAddress) catch {};
if (SDL.SDL_GL_SetSwapInterval(@boolToInt(config.config().host.vsync)) < 0) panic(); if (SDL.SDL_GL_SetSwapInterval(@boolToInt(config.config().host.vsync)) < 0) panic();
const program_id = compileShaders(); zgui.init(allocator);
zgui.plot.init();
zgui.backend.init(window, ctx, "#version 330 core");
// zgui.io.setIniFilename(null);
return Self{ return Self{
.window = window, .window = window,
.title = span(title),
.ctx = ctx, .ctx = ctx,
.program_id = program_id, .program_id = try compileShaders(),
.audio = Audio.init(apu), .audio = Audio.init(apu),
.allocator = allocator,
.state = try imgui.State.init(allocator, title_opt),
}; };
} }
fn compileShaders() gl.GLuint { pub fn deinit(self: *Self) void {
// TODO: Panic on Shader Compiler Failure + Error Message self.audio.deinit();
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 vert_shader = @embedFile("shader/pixelbuf.vert");
const frag_shader = @embedFile("shader/pixelbuf.frag"); const frag_shader = @embedFile("shader/pixelbuf.frag");
@@ -91,12 +141,16 @@ pub const Gui = struct {
gl.shaderSource(vs, 1, &[_][*c]const u8{vert_shader}, 0); gl.shaderSource(vs, 1, &[_][*c]const u8{vert_shader}, 0);
gl.compileShader(vs); gl.compileShader(vs);
if (!shader.didCompile(vs)) return error.VertexCompileError;
const fs = gl.createShader(gl.FRAGMENT_SHADER); const fs = gl.createShader(gl.FRAGMENT_SHADER);
defer gl.deleteShader(fs); defer gl.deleteShader(fs);
gl.shaderSource(fs, 1, &[_][*c]const u8{frag_shader}, 0); gl.shaderSource(fs, 1, &[_][*c]const u8{frag_shader}, 0);
gl.compileShader(fs); gl.compileShader(fs);
if (!shader.didCompile(fs)) return error.FragmentCompileError;
const program = gl.createProgram(); const program = gl.createProgram();
gl.attachShader(program, vs); gl.attachShader(program, vs);
gl.attachShader(program, fs); gl.attachShader(program, fs);
@@ -106,24 +160,29 @@ pub const Gui = struct {
} }
// Returns the VAO ID since it's used in run() // Returns the VAO ID since it's used in run()
fn generateBuffers() [3]c_uint { fn genBufferObjects() struct { GLuint, GLuint, GLuint } {
var vao_id: c_uint = undefined; var vao_id: GLuint = undefined;
var vbo_id: c_uint = undefined; var vbo_id: GLuint = undefined;
var ebo_id: c_uint = undefined; var ebo_id: GLuint = undefined;
gl.genVertexArrays(1, &vao_id); gl.genVertexArrays(1, &vao_id);
gl.genBuffers(1, &vbo_id); gl.genBuffers(1, &vbo_id);
gl.genBuffers(1, &ebo_id); gl.genBuffers(1, &ebo_id);
gl.bindVertexArray(vao_id); gl.bindVertexArray(vao_id);
defer gl.bindVertexArray(0);
gl.bindBuffer(gl.ARRAY_BUFFER, vbo_id); gl.bindBuffer(gl.ARRAY_BUFFER, vbo_id);
gl.bufferData(gl.ARRAY_BUFFER, @sizeOf(@TypeOf(vertices)), &vertices, gl.STATIC_DRAW); defer gl.bindBuffer(gl.ARRAY_BUFFER, 0);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo_id); 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); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, @sizeOf(@TypeOf(indices)), &indices, gl.STATIC_DRAW);
// Position // Position
gl.vertexAttribPointer(0, 3, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, 0)); // lmao gl.vertexAttribPointer(0, 3, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), null); // lmao
gl.enableVertexAttribArray(0); gl.enableVertexAttribArray(0);
// Colour // Colour
gl.vertexAttribPointer(1, 3, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, (3 * @sizeOf(f32)))); gl.vertexAttribPointer(1, 3, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, (3 * @sizeOf(f32))));
@@ -135,115 +194,176 @@ pub const Gui = struct {
return .{ vao_id, vbo_id, ebo_id }; return .{ vao_id, vbo_id, ebo_id };
} }
fn generateTexture(buf: []const u8) c_uint { fn genGbaTexture(buf: []const u8) GLuint {
var tex_id: c_uint = undefined; var tex_id: GLuint = undefined;
gl.genTextures(1, &tex_id); gl.genTextures(1, &tex_id);
gl.bindTexture(gl.TEXTURE_2D, tex_id);
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.bindTexture(gl.TEXTURE_2D, tex_id);
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 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_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gba_width, gba_height, 0, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, buf.ptr); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gba_width, gba_height, 0, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, buf.ptr);
// gl.generateMipmap(gl.TEXTURE_2D); // TODO: Remove?
return tex_id; return tex_id;
} }
pub fn run(self: *Self, cpu: *Arm7tdmi, scheduler: *Scheduler) !void { fn genOutTexture() GLuint {
var quit = std.atomic.Atomic(bool).init(false); var tex_id: GLuint = undefined;
var tracker = FpsTracker.init(); gl.genTextures(1, &tex_id);
const thread = try std.Thread.spawn(.{}, emu.run, .{ &quit, scheduler, cpu, &tracker }); gl.bindTexture(gl.TEXTURE_2D, tex_id);
defer thread.join(); defer gl.bindTexture(gl.TEXTURE_2D, 0);
var title_buf: [0x100]u8 = [_]u8{0} ** 0x100; gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
const vao_id = Self.generateBuffers()[0]; gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gba_width, gba_height, 0, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, null);
_ = Self.generateTexture(cpu.bus.ppu.framebuf.get(.Renderer));
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) { 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; var event: SDL.SDL_Event = undefined;
while (SDL.SDL_PollEvent(&event) != 0) { while (SDL.SDL_PollEvent(&event) != 0) {
_ = zgui.backend.processEvent(&event);
switch (event.type) { switch (event.type) {
SDL.SDL_QUIT => break :emu_loop, SDL.SDL_QUIT => break :emu_loop,
SDL.SDL_KEYDOWN => { SDL.SDL_KEYDOWN => {
const io = &cpu.bus.io;
const key_code = event.key.keysym.sym; const key_code = event.key.keysym.sym;
var keyinput = cpu.bus.io.keyinput.load(.Monotonic);
switch (key_code) { switch (key_code) {
SDL.SDLK_UP => io.keyinput.up.unset(), SDL.SDLK_UP => keyinput.up.unset(),
SDL.SDLK_DOWN => io.keyinput.down.unset(), SDL.SDLK_DOWN => keyinput.down.unset(),
SDL.SDLK_LEFT => io.keyinput.left.unset(), SDL.SDLK_LEFT => keyinput.left.unset(),
SDL.SDLK_RIGHT => io.keyinput.right.unset(), SDL.SDLK_RIGHT => keyinput.right.unset(),
SDL.SDLK_x => io.keyinput.a.unset(), SDL.SDLK_x => keyinput.a.unset(),
SDL.SDLK_z => io.keyinput.b.unset(), SDL.SDLK_z => keyinput.b.unset(),
SDL.SDLK_a => io.keyinput.shoulder_l.unset(), SDL.SDLK_a => keyinput.shoulder_l.unset(),
SDL.SDLK_s => io.keyinput.shoulder_r.unset(), SDL.SDLK_s => keyinput.shoulder_r.unset(),
SDL.SDLK_RETURN => io.keyinput.start.unset(), SDL.SDLK_RETURN => keyinput.start.unset(),
SDL.SDLK_RSHIFT => io.keyinput.select.unset(), SDL.SDLK_RSHIFT => keyinput.select.unset(),
else => {}, else => {},
} }
cpu.bus.io.keyinput.store(keyinput.raw, .Monotonic);
}, },
SDL.SDL_KEYUP => { SDL.SDL_KEYUP => {
const io = &cpu.bus.io;
const key_code = event.key.keysym.sym; const key_code = event.key.keysym.sym;
var keyinput = cpu.bus.io.keyinput.load(.Monotonic);
switch (key_code) { switch (key_code) {
SDL.SDLK_UP => io.keyinput.up.set(), SDL.SDLK_UP => keyinput.up.set(),
SDL.SDLK_DOWN => io.keyinput.down.set(), SDL.SDLK_DOWN => keyinput.down.set(),
SDL.SDLK_LEFT => io.keyinput.left.set(), SDL.SDLK_LEFT => keyinput.left.set(),
SDL.SDLK_RIGHT => io.keyinput.right.set(), SDL.SDLK_RIGHT => keyinput.right.set(),
SDL.SDLK_x => io.keyinput.a.set(), SDL.SDLK_x => keyinput.a.set(),
SDL.SDLK_z => io.keyinput.b.set(), SDL.SDLK_z => keyinput.b.set(),
SDL.SDLK_a => io.keyinput.shoulder_l.set(), SDL.SDLK_a => keyinput.shoulder_l.set(),
SDL.SDLK_s => io.keyinput.shoulder_r.set(), SDL.SDLK_s => keyinput.shoulder_r.set(),
SDL.SDLK_RETURN => io.keyinput.start.set(), SDL.SDLK_RETURN => keyinput.start.set(),
SDL.SDLK_RSHIFT => io.keyinput.select.set(), SDL.SDLK_RSHIFT => 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 => {},
} }
cpu.bus.io.keyinput.store(keyinput.raw, .Monotonic);
}, },
else => {}, else => {},
} }
} }
// Emulator has an internal Double Buffer {
const framebuf = cpu.bus.ppu.framebuf.get(.Renderer); channel.emu.push(.Pause);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gba_width, gba_height, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, framebuf.ptr); 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();
}
gl.useProgram(self.program_id);
gl.bindVertexArray(vao_id);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, null);
SDL.SDL_GL_SwapWindow(self.window); SDL.SDL_GL_SwapWindow(self.window);
const dyn_title = std.fmt.bufPrint(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, tracker.value() }) catch unreachable;
SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr);
} }
quit.store(true, .SeqCst); // Terminate Emulator Thread channel.emu.push(.Quit);
}
pub fn deinit(self: *Self) void {
self.audio.deinit();
// TODO: Buffer deletions
gl.deleteProgram(self.program_id);
SDL.SDL_GL_DeleteContext(self.ctx);
SDL.SDL_DestroyWindow(self.window);
SDL.SDL_Quit();
self.* = undefined;
} }
fn glGetProcAddress(ctx: SDL.SDL_GLContext, proc: [:0]const u8) ?*anyopaque { fn glGetProcAddress(ctx: SDL.SDL_GLContext, proc: [:0]const u8) ?*anyopaque {
@@ -268,8 +388,8 @@ const Audio = struct {
want.callback = Self.callback; want.callback = Self.callback;
want.userdata = apu; want.userdata = apu;
std.debug.assert(sample_format == SDL.AUDIO_F32); std.debug.assert(sample_format == SDL.AUDIO_U16);
log.info("Host Sample Rate: {}Hz, Host Format: SDL.AUDIO_F32", .{sample_rate}); log.info("Host Sample Rate: {}Hz, Host Format: SDL.AUDIO_U16", .{sample_rate});
const device = SDL.SDL_OpenAudioDevice(null, 0, &want, &have, 0); const device = SDL.SDL_OpenAudioDevice(null, 0, &want, &have, 0);
if (device == 0) panic(); if (device == 0) panic();
@@ -295,6 +415,28 @@ const Audio = struct {
} }
}; };
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)});
}
};
fn panic() noreturn { fn panic() noreturn {
const str = @as(?[*:0]const u8, SDL.SDL_GetError()) orelse "unknown error"; const str = @as(?[*:0]const u8, SDL.SDL_GetError()) orelse "unknown error";
@panic(std.mem.sliceTo(str, 0)); @panic(std.mem.sliceTo(str, 0));

View File

@@ -5,26 +5,7 @@ const config = @import("config.zig");
const Log2Int = std.math.Log2Int; const Log2Int = std.math.Log2Int;
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi; const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
// Sign-Extend value of type `T` to type `U` const Allocator = std.mem.Allocator;
pub fn sext(comptime T: type, comptime U: type, value: T) T {
// U must have less bits than T
comptime std.debug.assert(@typeInfo(U).Int.bits <= @typeInfo(T).Int.bits);
const iT = std.meta.Int(.signed, @typeInfo(T).Int.bits);
const ExtU = if (@typeInfo(U).Int.signedness == .unsigned) T else iT;
const shift_amt = @intCast(Log2Int(T), @typeInfo(T).Int.bits - @typeInfo(U).Int.bits);
return @bitCast(T, @bitCast(iT, @as(ExtU, @truncate(U, value)) << shift_amt) >> shift_amt);
}
/// See https://godbolt.org/z/W3en9Eche
pub inline fn rotr(comptime T: type, x: T, r: anytype) T {
if (@typeInfo(T).Int.signedness == .signed)
@compileError("cannot rotate signed integer");
const ar = @intCast(Log2Int(T), @mod(r, @typeInfo(T).Int.bits));
return x >> ar | x << (1 +% ~ar);
}
pub const FpsTracker = struct { pub const FpsTracker = struct {
const Self = @This(); const Self = @This();
@@ -47,7 +28,7 @@ pub const FpsTracker = struct {
pub fn value(self: *Self) u32 { pub fn value(self: *Self) u32 {
if (self.timer.read() >= std.time.ns_per_s) { 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(); 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 /// Creates a copy of a title with all Filesystem-invalid characters replaced
/// ///
/// e.g. POKEPIN R/S to POKEPIN R_S /// 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 { pub const FilePaths = struct {
rom: []const u8, rom: ?[]const u8,
bios: ?[]const u8, bios: ?[]const u8,
save: ?[]const u8, save: ?[]const u8,
}; };
@@ -174,6 +93,7 @@ pub const io = struct {
pub const Logger = struct { pub const Logger = struct {
const Self = @This(); 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), buf: std.io.BufferedWriter(4096 << 2, std.fs.File.Writer),
@@ -196,7 +116,7 @@ pub const Logger = struct {
if (cpu.cpsr.t.read()) { if (cpu.cpsr.t.read()) {
if (opcode >> 11 == 0x1E) { if (opcode >> 11 == 0x1E) {
// Instruction 1 of a BL Opcode, print in ARM mode // Instruction 1 of a BL Opcode, print in ARM mode
const 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; 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"); self.print(arm_fmt, Self.fmtArgs(cpu, bl_opcode)) catch @panic("failed to write to log file");
@@ -232,8 +152,6 @@ pub const Logger = struct {
} }
}; };
const FmtArgTuple = std.meta.Tuple(&.{ u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32 });
pub const audio = struct { pub const audio = struct {
const _io = @import("core/bus/io.zig"); const _io = @import("core/bus/io.zig");
@@ -302,8 +220,6 @@ pub inline fn getHalf(byte: u8) u4 {
return @truncate(u4, byte & 1) << 3; return @truncate(u4, byte & 1) << 3;
} }
// TODO: Maybe combine SetLo and SetHi, use addr alignment to deduplicate code
pub inline fn setHalf(comptime T: type, left: T, addr: u8, right: HalfInt(T)) T { pub inline fn setHalf(comptime T: type, left: T, addr: u8, right: HalfInt(T)) T {
const offset = @truncate(u1, addr >> if (T == u32) 1 else 0); const offset = @truncate(u1, addr >> if (T == u32) 1 else 0);
@@ -314,32 +230,12 @@ pub inline fn setHalf(comptime T: type, left: T, addr: u8, right: HalfInt(T)) T
}, },
u16 => switch (offset) { u16 => switch (offset) {
0b0 => (left & 0xFF00) | right, 0b0 => (left & 0xFF00) | right,
0b1 => (right & 0x00FF) | @as(u16, right) << 8, 0b1 => (left & 0x00FF) | @as(u16, right) << 8,
}, },
else => @compileError("unsupported type"), else => @compileError("unsupported type"),
}; };
} }
/// Sets the high bits of an integer to a value
pub inline fn setLo(comptime T: type, left: T, right: HalfInt(T)) T {
return switch (T) {
u32 => (left & 0xFFFF_0000) | right,
u16 => (left & 0xFF00) | right,
u8 => (left & 0xF0) | right,
else => @compileError("unsupported type"),
};
}
/// sets the low bits of an integer to a value
pub inline fn setHi(comptime T: type, left: T, right: HalfInt(T)) T {
return switch (T) {
u32 => (left & 0x0000_FFFF) | @as(u32, right) << 16,
u16 => (left & 0x00FF) | @as(u16, right) << 8,
u8 => (left & 0x0F) | @as(u8, right) << 4,
else => @compileError("unsupported type"),
};
}
/// The Integer type which corresponds to T with exactly half the amount of bits /// The Integer type which corresponds to T with exactly half the amount of bits
fn HalfInt(comptime T: type) type { fn HalfInt(comptime T: type) type {
const type_info = @typeInfo(T); const type_info = @typeInfo(T);
@@ -348,3 +244,48 @@ fn HalfInt(comptime T: type) type {
return std.meta.Int(type_info.Int.signedness, type_info.Int.bits >> 1); 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];
}
};