144 Commits

Author SHA1 Message Date
2f2c03b96d chore: add test for RingBuffer 2022-12-10 01:02:59 -04:00
9a2b7a48c0 tmp: removed audio resampler 2022-12-09 22:16:51 -04:00
fe908a6ea9 feat(util): implement RingBuffer 2022-12-09 22:16: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
472215b4c2 feat(ppu): implement all i/o writes 2022-10-30 03:11:04 -03:00
c9a423d094 fix(ppu): resolve mistakes in ppu i/o reads 2022-10-30 02:15:26 -03:00
1d163fa56f feat(apu): implement all apu i/o writes 2022-10-30 02:02:23 -03:00
13710a3236 feat(timer): implement all timer i/o writes 2022-10-30 01:18:46 -03:00
6154585e77 feat(dma): implement all dma i/o writes 2022-10-30 01:04:22 -03:00
7debdc490d fix(io): resovle off-by-one errors in i/o register ranges 2022-10-29 05:23:05 -03:00
58375795bf fix(ppu): apply proper masks to ppu i/o
Refactor Window, and bldcnt, bldalpha, bldy
2022-10-29 05:18:53 -03:00
f0dca29836 fix(dma): apply proper masks to dma i/o 2022-10-29 04:53:21 -03:00
c75682dbd4 fix(apu): some invalid i/o registers should read 0x0000 2022-10-29 04:29:44 -03:00
36832ba1fb feat(apu): impelement all apu i/o reads 2022-10-29 04:24:06 -03:00
647bd83224 chore(io): rewrite certain error messages
We can do this now that we know that it won't be because of any
unimplemented feature in some circumstances
2022-10-29 02:37:54 -03:00
c831f67d1a feat(timer): implemeant all timer i/o reads 2022-10-29 01:37:28 -03:00
268961262d feat(dma): implement all dma i/o reads 2022-10-29 01:30:12 -03:00
3e62feacba feat(ppu): implement all ppu i/o reads 2022-10-29 01:29:27 -03:00
d859cee365 style: get rid of unnecessary type coersion 2022-10-29 00:06:08 -03:00
371cf4cc12 style(i/o, ppu): refactor ppu i/o 2022-10-28 23:45:54 -03:00
10aec67ee0 emu: implement thread sleep in granular steps 2022-10-28 21:58:55 -03:00
4eb715a138 doc(emu): properly document + simply constants 2022-10-28 21:57:30 -03:00
14b24787ab style: remove unnecessary imports 2022-10-28 21:56:55 -03:00
eb7ffa29f4 fix(apu): pause device on mute instead of writing silence 2022-10-27 09:11:08 -03:00
4b8ed3cebb fix(io): resolve embarrasingly simple regression
introduced in 21eddac31e
2022-10-23 04:39:31 -03:00
928ce674d9 fix(cpu): fix obscure LDRSH behaviour 2022-10-22 22:12:41 -03:00
945dbec013 fix(open-bus): don't rotate result
Rotating misaligned reads is the responsibility of the CPU
2022-10-22 21:32:36 -03:00
dd98066a34 Merge pull request 'feat(dma): Implement DMA Latch' (#5) from dma-latch into main
Reviewed-on: #5
2022-10-22 23:53:21 +00:00
a2868dfe9e feat(dma): Implement DMA Latch 2022-10-22 20:52:02 -03:00
22979d9450 fix(bios): fix regression
was reading addr_latch + 8, which is a remnant from when I was faking
the pipeline
2022-10-22 15:33:36 -03:00
712c58391d chore(config): change defaults in config.toml 2022-10-21 06:01:22 -03:00
407774d798 chore(gitignore): update .gitignore 2022-10-21 04:40:55 -03:00
16f8f4c953 feat: write default config.toml if it doesn't exist
also resolves panic on missing /zba or /zba/save directory by ensuring
those directories exist as soon as we know the data directory
2022-10-21 04:39:16 -03:00
143ffd95f7 chore: update README 2022-10-21 02:59:43 -03:00
250ff25ed7 Merge pull request 'Configure SDL2 to use OpenGL' (#4) from opengl into main
Reviewed-on: #4
2022-10-20 01:41:50 +00:00
eff52ac1bb fix(opengl): properly control whether vsync is enabled 2022-10-17 20:31:42 -03:00
e60b556f72 chore(ppu): remove BGR555 -> RGBA888 LUT
LUT probably couldn't fit in CPU cache anyways.

TODO: Consider whether LUTs for separate channels (size 32 * 3 * 3
instead of std.math.maxInt(u15))
2022-10-17 20:31:42 -03:00
3a3e6acc6a chore: replace OpenGL 4.5 bindings with OpenGL 3.3 2022-10-17 20:31:42 -03:00
4b4bc7f894 chore: remove unnecessary ptr cast 2022-10-17 20:31:42 -03:00
325208d460 feat: implement better Colour Emulation 2022-10-17 20:31:42 -03:00
f44a1a49fd fix: lower required OpenGL version + resolve offset bug 2022-10-17 20:31:42 -03:00
1575f517a9 feat: use opengl
TODO:
- Texture isn't scaling properly
- I need to reverse the colours in the frag shader
2022-10-17 20:31:42 -03:00
26dba16789 chore(gpio): add missing errdefer 2022-10-17 20:01:50 -03:00
b133880064 chore(main): report errors slightly better 2022-10-17 18:30:40 -03:00
2474daa3ae chore(config): add log message 2022-10-17 17:39:02 -03:00
fc53a40b3c feat(config): add option to skip BIOS 2022-10-17 17:31:07 -03:00
7097e21361 feat(cli): Add option to skip BIOS 2022-10-17 17:25:04 -03:00
a9fe24b1b4 chore: Update README.md 2022-10-17 17:00:54 -03:00
f38c840d32 Merge pull request 'Draft: Implement Instruction Pipeline' (#3) from pipeline into main
Reviewed-on: #3
2022-10-17 19:42:42 +00:00
19e70c39d1 feat(config): add config option to mute ZBA 2022-10-13 00:54:15 -03:00
5a72a8e7f3 chore(config): add example config file 2022-10-13 00:46:18 -03:00
7b146ad7ca fix(bios): set addr_latch even if bios is skipped 2022-10-13 00:35:22 -03:00
822eed1f3a fix(bus): make open bus impl aware of CPU pipeline 2022-10-13 00:35:22 -03:00
b37a14900c style(bus): cpu ptr doesn't need to be optional 2022-10-13 00:35:22 -03:00
f5bd20bc2a style: code cleanup 2022-10-13 00:35:22 -03:00
d3514b14f3 fix: resolve timing regressions
make sure to use fetch timings when fetching instructions
2022-10-13 00:35:20 -03:00
06c60dad74 fix: rename Pipline to Pipeline 2022-10-13 00:34:18 -03:00
870e991862 feat: working pipeline implementation 2022-10-13 00:34:18 -03:00
5bb5bdf389 chore: refactor ARM/THUMB data processing instructions 2022-10-13 00:34:18 -03:00
a3996cbc58 fix: don't flush pipeline when reloading CPSR in ARM Data Processing 2022-10-13 00:34:18 -03:00
a948c6f900 chore: don't write to CPSR + swap with SPSR at the same time 2022-10-13 00:34:18 -03:00
014180cbd0 chore: update README.md 2022-10-13 00:33:13 -03:00
e4451738b5 fix: advance r15, even when the pipeline is reloaded from the scheduler
The PC would fall behind whenever an IRQ was called because the pipeline
was reloaded (+8 to PC), however that was never actually done by any code

Now, the PC is always incremented when the pipeline is reloaded
2022-10-13 00:33:13 -03:00
48b81c8e7a chore: dump pipeline state on cpu panic 2022-10-13 00:33:13 -03:00
3cf1bf54e9 fix: reimpl THUMB.5 instructions
pipeline branch now passes arm.gba and thumb.gba again

(TODO: Stop rewriting my commits away)
2022-10-13 00:33:13 -03:00
1f9eeedfe8 fix: impl workaround for stage2 miscompilation 2022-10-13 00:33:13 -03:00
72a63eeb98 chore: instantly refill the pipeline on flush
I believe this to be necessary in order to get hardware interrupts
working.

thumb.gba test 108 fails but I'm committing anyways (despite the
regression) because this is kind of rebase/merge hell and I have
something that at least sort of works rn
2022-10-13 00:33:13 -03:00
2799c3f202 fix: reimpl handleInterrupt code 2022-10-13 00:33:13 -03:00
b3ada64e64 feat: implement basic pipeline
passes arm.gba, thumb.gb and armwrestler, fails in actual games
TODO: run FuzzARM debug specific titles
2022-10-13 00:33:11 -03:00
62162ba492 feat: resolve off-by-{word, halfword} errors when printing debug info 2022-10-13 00:31:47 -03:00
aa100de581 feat: reimplement cpu logging 2022-10-13 00:31:47 -03:00
7142831284 Merge pull request 'Add TOML Support' (#2) from toml into main
Reviewed-on: #2
2022-10-13 03:30:26 +00:00
97f48c730e chore(emu): refactor code 2022-10-13 00:29:51 -03:00
293fbd9f55 feat(config): add support for (and read from) TOML config file 2022-10-13 00:29:48 -03:00
622f479e07 feat: parse config.toml in data folder
Also took the chance to rework parts of the logic that determines
ZBA's save path
2022-10-13 00:27:18 -03:00
0204eb6f94 chore: add zig-toml dependency 2022-10-13 00:27:18 -03:00
86d2224cfc chore: update dependencies 2022-10-13 00:23:58 -03:00
21eddac31e style: improve code quality 2022-10-13 00:23:58 -03:00
785135a074 feat: rewrite device ticks 2022-10-13 00:23:58 -03:00
fd38fd6506 style(scheduler): rename scheduler event handlers 2022-10-13 00:23:58 -03:00
bcacac64df style: code refactoring 2022-10-13 00:23:58 -03:00
dc7cad9691 style(apu): split apu.zig into multiple files + refactor 2022-10-13 00:23:58 -03:00
b5d8a65e69 style(backup): refactor code 2022-10-10 12:01:49 -03:00
8028394105 style(flash): move flash code into it's own file 2022-10-10 12:01:49 -03:00
cb0eb67e4b style(eeprom): move eeprom code to it's own file 2022-10-10 12:00:45 -03:00
13f6ee8ec4 style(bus): refactor several hardware abstractions 2022-10-10 11:57:57 -03:00
c71e954748 chore: SDL2.zig expects target to be set before link() is called 2022-09-25 18:59:55 -03:00
c697dec716 chore: update dependencies 2022-09-23 07:21:46 -03:00
92cfc763c0 chore: move util.zig 2022-09-19 16:07:19 -03:00
e192c6712f chore: disable audio sync by default
forgot SDL2 AudioStream doesn't work well for my use-case
2022-09-18 09:20:01 -03:00
3466bf6c0a chore: change default settings 2022-09-18 06:30:39 -03:00
fbe3de0eb3 chore: reimpl util.escape
should make use of stdlib when I can
2022-09-18 06:23:30 -03:00
4af144fca2 fix: Detect FRAM ROMs 2022-09-18 06:19:05 -03:00
9a8aaba1ab chore: improve util and Gui API 2022-09-18 05:55:15 -03:00
fa3b9c21b9 chore: move Gpio and Clock structs to separate file 2022-09-18 00:37:45 -03:00
61 changed files with 10141 additions and 3930 deletions

58
.github/workflows/main.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
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]
runs-on: ${{matrix.os}}
steps:
- uses: goto-bus-stop/setup-zig@v2
with:
version: master
- name: prepare-linux
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install libsdl2-dev
- name: prepare-windows
if: runner.os == 'Windows'
run: |
vcpkg integrate install
vcpkg install sdl2:x64-windows
git config --global core.autocrlf false
- name: prepare-macos
if: runner.os == 'macOS'
run: |
brew install sdl2
- uses: actions/checkout@v3
with:
submodules: true
- name: build
run: zig build -Drelease-safe
- 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: true
- uses: goto-bus-stop/setup-zig@v2
with:
version: master
- run: zig fmt src/**/*.zig

4
.gitignore vendored
View File

@@ -1,7 +1,7 @@
/.vscode
/bin
/zig-cache
/zig-out
**/zig-cache
**/zig-out
/docs
**/*.log
**/*.bin

3
.gitmodules vendored
View File

@@ -10,3 +10,6 @@
[submodule "lib/zig-datetime"]
path = lib/zig-datetime
url = https://github.com/frmdstryr/zig-datetime
[submodule "lib/zig-toml"]
path = lib/zig-toml
url = https://github.com/aeronavery/zig-toml

129
README.md
View File

@@ -1,63 +1,110 @@
# ZBA (working title)
An in-progress Game Boy Advance Emulator written in Zig ⚡!
A Game Boy Advance Emulator written in Zig ⚡!
## Scope
I'm hardly the first to write a Game Boy Advance Emulator nor will I be the last. This project isn't going to compete with the GOATs like [mGBA](https://github.com/mgba-emu) or [NanoBoyAdvance](https://github.com/nba-emu/NanoBoyAdvance). There aren't any interesting ideas either like in [DSHBA](https://github.com/DenSinH/DSHBA).
This is a simple (read: incomplete) for-fun long-term project. I hope to get "mostly there", which to me means that I'm not missing any major hardware features and the set of possible improvements would be in memory timing or in UI/UX. With respect to that goal, here's what's outstanding:
### TODO
- [ ] Affine Sprites
- [ ] Windowing (see [this branch](https://git.musuka.dev/paoda/zba/src/branch/window))
- [ ] Audio Resampler (Having issues with SDL2's)
- [ ] Immediate Mode GUI
- [ ] Refactoring for easy-ish perf boosts
## Usage
As it currently exists, ZBA is run from the terminal. In your console of choice, type `./zba --help` to see what you can do.
I typically find myself typing `./zba -b ./bin/bios.bin ./bin/test/suite.gba` to see how badly my "cool new feature" broke everything else.
Need a BIOS? Why not try using the open-source [Cult-Of-GBA BIOS](https://github.com/Cult-of-GBA/BIOS) written by [fleroviux](https://github.com/fleroviux) and [DenSinH](https://github.com/DenSinH)?
Finally it's worth noting that ZBA uses a TOML config file it'll store in your OS's data directory. See `example.toml` to learn about the defaults and what exactly you can mess around with.
## Tests
- [ ] [jsmolka's GBA Test Collection](https://github.com/jsmolka/gba-tests)
- [x] `arm.gba` and `thumb.gba`
- [x] `flash64.gba`, `flash128.gba`, `none.gba`, and `sram.gba`
- [x] `hello.gba`, `shades.gba`, and `stripes.gba`
- [x] `memory.gba`
- [x] `bios.gba`
- [ ] `nes.gba`
- [ ] [DenSinH's GBA ROMs](https://github.com/DenSinH/GBARoms)
- [x] `eeprom-test` and `flash-test`
- [x] `midikey2freq`
- [ ] `swi-tests-random`
- [ ] [destoer's GBA Tests](https://github.com/destoer/gba_tests)
- [x] `cond_invalid.gba`
- [x] `dma_priority.gba`
- [x] `hello_world.gba`
- [x] `if_ack.gba`
- [ ] `line_timing.gba`
- [ ] `lyc_midline.gba`
- [ ] `window_midframe.gba`
- [x] [ladystarbreeze's GBA Test Collection](https://github.com/ladystarbreeze/GBA-Test-Collection)
- [x] `retAddr.gba`
- [x] `helloWorld.gba`
- [x] `helloAudio.gba`
- [x] [`armwrestler-gba-fixed.gba`](https://github.com/destoer/armwrestler-gba-fixed)
- [x] [FuzzARM](https://github.com/DenSinH/FuzzARM)
GBA Tests | [jsmolka](https://github.com/jsmolka/)
--- | ---
`arm.gba`, `thumb.gba` | PASS
`memory.gba`, `bios.gba` | PASS
`flash64.gba`, `flash128.gba` | PASS
`sram.gba` | PASS
`none.gba` | PASS
`hello.gba`, `shades.gba`, `stripes.gba` | PASS
`nes.gba` | PASS
GBARoms | [DenSinH](https://github.com/DenSinH/)
--- | ---
`eeprom-test`, `flash-test` | PASS
`midikey2freq` | PASS
`swi-tests-random` | FAIL
gba_tests | [destoer](https://github.com/destoer/)
--- | ---
`cond_invalid.gba` | PASS
`dma_priority.gba` | PASS
`hello_world.gba` | PASS
`if_ack.gba` | PASS
`line_timing.gba` | FAIL
`lyc_midline.gba` | FAIL
`window_midframe.gba` | FAIL
GBA Test Collection | [ladystarbreeze](https://github.com/ladystarbreeze)
--- | ---
`retAddr.gba` | PASS
`helloWorld.gba` | PASS
`helloAudio.gba` | PASS
FuzzARM | [DenSinH](https://github.com/DenSinH/)
--- | ---
`main.gba` | PASS
arm7wrestler GBA Fixed | [destoer](https://github.com/destoer)
--- | ---
`armwrestler-gba-fixed.gba` | PASS
## Resources
* [GBATEK](https://problemkaputt.de/gbatek.htm)
* [TONC](https://coranac.com/tonc/text/toc.htm)
* [ARM Architecture Reference Manual](https://www.intel.com/content/dam/www/programmable/us/en/pdfs/literature/third-party/ddi0100e_arm_arm.pdf)
* [ARM7TDMI Data Sheet](https://www.dca.fee.unicamp.br/cursos/EA871/references/ARM/ARM7TDMIDataSheet.pdf)
- [GBATEK](https://problemkaputt.de/gbatek.htm)
- [TONC](https://coranac.com/tonc/text/toc.htm)
- [ARM Architecture Reference Manual](https://www.intel.com/content/dam/www/programmable/us/en/pdfs/literature/third-party/ddi0100e_arm_arm.pdf)
- [ARM7TDMI Data Sheet](https://www.dca.fee.unicamp.br/cursos/EA871/references/ARM/ARM7TDMIDataSheet.pdf)
## Compiling
Most recently built on Zig [0.10.0-dev.3900+ab4b26d8a](https://github.com/ziglang/zig/tree/ab4b26d8a)
Most recently built on Zig [0.11.0-dev.368+1829b6eab](https://github.com/ziglang/zig/tree/1829b6eab)
### Dependencies
* [SDL.zig](https://github.com/MasterQ32/SDL.zig)
* [SDL2](https://www.libsdl.org/download-2.0.php)
* [zig-clap](https://github.com/Hejsil/zig-clap)
* [known-folders](https://github.com/ziglibs/known-folders)
* [`bitfields.zig`](https://github.com/FlorenceOS/Florence/blob/aaa5a9e568197ad24780ec9adb421217530d4466/lib/util/bitfields.zig)
`bitfields.zig` from [FlorenceOS](https://github.com/FlorenceOS) is included under `lib/util/bitfield.zig`.
Dependency | Source
--- | ---
SDL.zig | <https://github.com/MasterQ32/SDL.zig>
zig-clap | <https://github.com/Hejsil/zig-clap>
known-folders | <https://github.com/ziglibs/known-folders>
zig-toml | <https://github.com/aeronavery/zig-toml>
zig-datetime | <https://github.com/frmdstryr/zig-datetime>
`bitfields.zig` | [https://github.com/FlorenceOS/Florence](https://github.com/FlorenceOS/Florence/blob/aaa5a9e568/lib/util/bitfields.zig)
`gl.zig` | <https://github.com/MasterQ32/zig-opengl>
Use `git submodule update --init` from the project root to pull the git submodules `SDL.zig`, `zig-clap`, and `known-folders`
Use `git submodule update --init` from the project root to pull the git submodules `SDL.zig`, `zig-clap`, `known-folders`, `zig-toml` and `zig-datetime`
Be sure to provide SDL2 using:
* Linux: Your distro's package manager
* MacOS: ¯\\\_(ツ)_/¯
* Windows: [`vcpkg`](https://github.com/Microsoft/vcpkg) (install `sdl2:x64-windows`)
- Linux: Your distro's package manager
- MacOS: ¯\\\_(ツ)_/¯
- Windows: [`vcpkg`](https://github.com/Microsoft/vcpkg) (install `sdl2:x64-windows`)
`SDL.zig` will provide a helpful compile error if the zig compiler is unable to find SDL2.
Once you've got all the dependencies, execute `zig build -Drelease-fast`. The executable is located at `zig-out/bin/`.
## Controls
Key | Button
--- | ---
<kbd>X</kbd> | A

View File

@@ -1,7 +1,15 @@
const std = @import("std");
const builtin = @import("builtin");
const Sdk = @import("lib/SDL.zig/Sdk.zig");
pub fn build(b: *std.build.Builder) void {
// Minimum Zig Version
const min_ver = std.SemanticVersion.parse("0.11.0-dev.323+30eb2a175") catch return; // https://github.com/ziglang/zig/commit/30eb2a175
if (builtin.zig_version.order(min_ver).compare(.lt)) {
std.log.err("{s}", .{b.fmt("Zig v{} does not meet the minimum version requirement. (Zig v{})", .{ builtin.zig_version, min_ver })});
std.os.exit(1);
}
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
@@ -13,6 +21,9 @@ pub fn build(b: *std.build.Builder) void {
const mode = b.standardReleaseOptions();
const exe = b.addExecutable("zba", "src/main.zig");
exe.setMainPkgPath("."); // Necessary so that src/main.zig can embed example.toml
exe.setTarget(target);
// Known Folders (%APPDATA%, XDG, etc.)
exe.addPackagePath("known_folders", "lib/known-folders/known-folders.zig");
@@ -26,13 +37,17 @@ pub fn build(b: *std.build.Builder) void {
// Argument Parsing Library
exe.addPackagePath("clap", "lib/zig-clap/clap.zig");
// TOML Library
exe.addPackagePath("toml", "lib/zig-toml/src/toml.zig");
// OpenGL 3.3 Bindings
exe.addPackagePath("gl", "lib/gl.zig");
// Zig SDL Bindings: https://github.com/MasterQ32/SDL.zig
const sdk = Sdk.init(b);
sdk.link(exe, .dynamic);
exe.addPackage(sdk.getNativePackage("sdl2"));
exe.setTarget(target);
exe.setBuildMode(mode);
exe.install();

25
example.toml Normal file
View File

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

5028
lib/gl.zig Normal file

File diff suppressed because it is too large Load Diff

1
lib/zig-toml Submodule

Submodule lib/zig-toml added at 016b8bcf98

View File

@@ -1,194 +0,0 @@
const std = @import("std");
const SDL = @import("sdl2");
const Self = @This();
const Apu = @import("core/apu.zig").Apu;
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
const Scheduler = @import("core/scheduler.zig").Scheduler;
const FpsTracker = @import("core/util.zig").FpsTracker;
const pitch = @import("core/ppu.zig").framebuf_pitch;
const scale = @import("core/emu.zig").win_scale;
const emu = @import("core/emu.zig");
const log = std.log.scoped(.GUI);
const default_title: []const u8 = "ZBA";
window: *SDL.SDL_Window,
base_title: [12]u8,
renderer: *SDL.SDL_Renderer,
texture: *SDL.SDL_Texture,
audio: ?Audio,
pub fn init(title: [12]u8, width: i32, height: i32) Self {
const ret = SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO | SDL.SDL_INIT_GAMECONTROLLER);
if (ret < 0) panic();
const window = SDL.SDL_CreateWindow(
default_title.ptr,
SDL.SDL_WINDOWPOS_CENTERED,
SDL.SDL_WINDOWPOS_CENTERED,
@as(c_int, width * scale),
@as(c_int, height * scale),
SDL.SDL_WINDOW_SHOWN,
) orelse panic();
const renderer = SDL.SDL_CreateRenderer(window, -1, SDL.SDL_RENDERER_ACCELERATED | SDL.SDL_RENDERER_PRESENTVSYNC) orelse panic();
const texture = SDL.SDL_CreateTexture(
renderer,
SDL.SDL_PIXELFORMAT_RGBA8888,
SDL.SDL_TEXTUREACCESS_STREAMING,
@as(c_int, width),
@as(c_int, height),
) orelse panic();
return Self{
.window = window,
.base_title = title,
.renderer = renderer,
.texture = texture,
.audio = null,
};
}
pub fn run(self: *Self, cpu: *Arm7tdmi, scheduler: *Scheduler) !void {
var quit = std.atomic.Atomic(bool).init(false);
var frame_rate = FpsTracker.init();
const thread = try std.Thread.spawn(.{}, emu.run, .{ &quit, &frame_rate, scheduler, cpu });
defer thread.join();
var title_buf: [0x100]u8 = [_]u8{0} ** 0x100;
emu_loop: while (true) {
var event: SDL.SDL_Event = undefined;
while (SDL.SDL_PollEvent(&event) != 0) {
switch (event.type) {
SDL.SDL_QUIT => break :emu_loop,
SDL.SDL_KEYDOWN => {
const io = &cpu.bus.io;
const key_code = event.key.keysym.sym;
switch (key_code) {
SDL.SDLK_UP => io.keyinput.up.unset(),
SDL.SDLK_DOWN => io.keyinput.down.unset(),
SDL.SDLK_LEFT => io.keyinput.left.unset(),
SDL.SDLK_RIGHT => io.keyinput.right.unset(),
SDL.SDLK_x => io.keyinput.a.unset(),
SDL.SDLK_z => io.keyinput.b.unset(),
SDL.SDLK_a => io.keyinput.shoulder_l.unset(),
SDL.SDLK_s => io.keyinput.shoulder_r.unset(),
SDL.SDLK_RETURN => io.keyinput.start.unset(),
SDL.SDLK_RSHIFT => io.keyinput.select.unset(),
else => {},
}
},
SDL.SDL_KEYUP => {
const io = &cpu.bus.io;
const key_code = event.key.keysym.sym;
switch (key_code) {
SDL.SDLK_UP => io.keyinput.up.set(),
SDL.SDLK_DOWN => io.keyinput.down.set(),
SDL.SDLK_LEFT => io.keyinput.left.set(),
SDL.SDLK_RIGHT => io.keyinput.right.set(),
SDL.SDLK_x => io.keyinput.a.set(),
SDL.SDLK_z => io.keyinput.b.set(),
SDL.SDLK_a => io.keyinput.shoulder_l.set(),
SDL.SDLK_s => io.keyinput.shoulder_r.set(),
SDL.SDLK_RETURN => io.keyinput.start.set(),
SDL.SDLK_RSHIFT => io.keyinput.select.set(),
SDL.SDLK_i => log.err("Sample Count: {}", .{@intCast(u32, SDL.SDL_AudioStreamAvailable(cpu.bus.apu.stream)) / (2 * @sizeOf(u16))}),
SDL.SDLK_j => log.err("Scheduler Capacity: {} | Scheduler Event Count: {}", .{ scheduler.queue.capacity(), scheduler.queue.count() }),
SDL.SDLK_k => {
// Dump IWRAM to file
log.info("PC: 0x{X:0>8}", .{cpu.r[15]});
log.info("LR: 0x{X:0>8}", .{cpu.r[14]});
// const iwram_file = try std.fs.cwd().createFile("iwram.bin", .{});
// defer iwram_file.close();
// try iwram_file.writeAll(cpu.bus.iwram.buf);
},
else => {},
}
},
else => {},
}
}
// Emulator has an internal Double Buffer
const framebuf = cpu.bus.ppu.framebuf.get(.Renderer);
_ = SDL.SDL_UpdateTexture(self.texture, null, framebuf.ptr, pitch);
_ = SDL.SDL_RenderCopy(self.renderer, self.texture, null, null);
SDL.SDL_RenderPresent(self.renderer);
const title = std.fmt.bufPrint(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.base_title, frame_rate.value() }) catch unreachable;
SDL.SDL_SetWindowTitle(self.window, title.ptr);
}
quit.store(true, .SeqCst); // Terminate Emulator Thread
}
pub fn initAudio(self: *Self, apu: *Apu) void {
self.audio = Audio.init(apu);
self.audio.?.play();
}
pub fn deinit(self: *Self) void {
if (self.audio) |*aud| aud.deinit();
SDL.SDL_DestroyTexture(self.texture);
SDL.SDL_DestroyRenderer(self.renderer);
SDL.SDL_DestroyWindow(self.window);
SDL.SDL_Quit();
self.* = undefined;
}
const Audio = struct {
const This = @This();
const sample_rate = @import("core/apu.zig").host_sample_rate;
device: SDL.SDL_AudioDeviceID,
fn init(apu: *Apu) This {
var have: SDL.SDL_AudioSpec = undefined;
var want: SDL.SDL_AudioSpec = std.mem.zeroes(SDL.SDL_AudioSpec);
want.freq = sample_rate;
want.format = SDL.AUDIO_U16;
want.channels = 2;
want.samples = 0x100;
want.callback = This.callback;
want.userdata = apu;
const device = SDL.SDL_OpenAudioDevice(null, 0, &want, &have, 0);
if (device == 0) panic();
return .{
.device = device,
};
}
fn deinit(self: *This) void {
SDL.SDL_CloseAudioDevice(self.device);
self.* = undefined;
}
pub fn play(self: *This) void {
SDL.SDL_PauseAudioDevice(self.device, 0);
}
export fn callback(userdata: ?*anyopaque, stream: [*c]u8, len: c_int) void {
const apu = @ptrCast(*Apu, @alignCast(@alignOf(*Apu), userdata));
_ = SDL.SDL_AudioStreamGet(apu.stream, stream, len);
// If we don't write anything, play silence otherwise garbage will be played
// FIXME: I don't think this hack to remove DC Offset is acceptable :thinking:
// if (written == 0) std.mem.set(u8, stream[0..@intCast(usize, len)], 0x40);
}
};
fn panic() noreturn {
const str = @as(?[*:0]const u8, SDL.SDL_GetError()) orelse "unknown error";
@panic(std.mem.sliceTo(str, 0));
}

86
src/config.zig Normal file
View File

@@ -0,0 +1,86 @@
const std = @import("std");
const toml = @import("toml");
const Allocator = std.mem.Allocator;
const log = std.log.scoped(.Config);
var state: Config = .{};
const Config = struct {
host: Host = .{},
guest: Guest = .{},
debug: Debug = .{},
/// Settings related to the Computer the Emulator is being run on
const Host = struct {
/// Using Nearest-Neighbor, multiply the resolution of the GBA Window
win_scale: i64 = 3,
/// Enable Vsync
///
/// Note: This does not affect whether Emulation is synced to 59Hz
vsync: bool = true,
/// Mute ZBA
mute: bool = false,
};
// Settings realted to the emulation itself
const Guest = struct {
/// Whether Emulation thread to sync to Audio Callbacks
audio_sync: bool = true,
/// Whether Emulation thread should sync to 59Hz
video_sync: bool = true,
/// Whether RTC I/O should always be enabled
force_rtc: bool = false,
/// Skip BIOS
skip_bios: bool = false,
};
/// Settings related to debugging ZBA
const Debug = struct {
/// Enable CPU Trace logs
cpu_trace: bool = false,
/// If false and ZBA is built in debug mode, ZBA will panic on unhandled I/O
unhandled_io: bool = true,
};
};
pub fn config() *const Config {
return &state;
}
/// Reads a config file and then loads it into the global state
pub fn load(allocator: Allocator, file_path: []const u8) !void {
var config_file = try std.fs.cwd().openFile(file_path, .{});
defer config_file.close();
log.info("loaded from {s}", .{file_path});
const contents = try config_file.readToEndAlloc(allocator, try config_file.getEndPos());
defer allocator.free(contents);
var parser = try toml.parseFile(allocator, file_path);
defer parser.deinit();
const table = try parser.parse();
defer table.deinit();
// TODO: Report unknown config options
if (table.keys.get("Host")) |host| {
if (host.Table.keys.get("win_scale")) |scale| state.host.win_scale = scale.Integer;
if (host.Table.keys.get("vsync")) |vsync| state.host.vsync = vsync.Boolean;
if (host.Table.keys.get("mute")) |mute| state.host.mute = mute.Boolean;
}
if (table.keys.get("Guest")) |guest| {
if (guest.Table.keys.get("audio_sync")) |sync| state.guest.audio_sync = sync.Boolean;
if (guest.Table.keys.get("video_sync")) |sync| state.guest.video_sync = sync.Boolean;
if (guest.Table.keys.get("force_rtc")) |forced| state.guest.force_rtc = forced.Boolean;
if (guest.Table.keys.get("skip_bios")) |skip| state.guest.skip_bios = skip.Boolean;
}
if (table.keys.get("Debug")) |debug| {
if (debug.Table.keys.get("cpu_trace")) |trace| state.debug.cpu_trace = trace.Boolean;
if (debug.Table.keys.get("unhandled_io")) |unhandled| state.debug.unhandled_io = unhandled.Boolean;
}
}

View File

@@ -1,6 +1,5 @@
const std = @import("std");
const AudioDeviceId = @import("sdl2").SDL_AudioDeviceID;
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
const Bios = @import("bus/Bios.zig");
const Ewram = @import("bus/Ewram.zig");
@@ -12,7 +11,7 @@ const Apu = @import("apu.zig").Apu;
const DmaTuple = @import("bus/dma.zig").DmaTuple;
const TimerTuple = @import("bus/timer.zig").TimerTuple;
const Scheduler = @import("scheduler.zig").Scheduler;
const FilePaths = @import("util.zig").FilePaths;
const FilePaths = @import("../util.zig").FilePaths;
const io = @import("bus/io.zig");
const Allocator = std.mem.Allocator;
@@ -20,7 +19,7 @@ const log = std.log.scoped(.Bus);
const createDmaTuple = @import("bus/dma.zig").create;
const createTimerTuple = @import("bus/timer.zig").create;
const rotr = @import("util.zig").rotr;
const rotr = @import("../util.zig").rotr;
const timings: [2][0x10]u8 = [_][0x10]u8{
// BIOS, Unused, EWRAM, IWRAM, I/0, PALRAM, VRAM, OAM, ROM0, ROM0, ROM1, ROM1, ROM2, ROM2, SRAM, Unused
@@ -34,6 +33,11 @@ pub const fetch_timings: [2][0x10]u8 = [_][0x10]u8{
[_]u8{ 1, 1, 6, 1, 1, 2, 2, 1, 4, 4, 4, 4, 4, 4, 8, 8 }, // 32-bit
};
// Fastmem Related
const page_size = 1 * 0x400; // 1KiB
const address_space_size = 0x1000_0000;
const table_len = address_space_size / page_size;
const Self = @This();
pak: GamePak,
@@ -46,10 +50,20 @@ iwram: Iwram,
ewram: Ewram,
io: Io,
cpu: ?*Arm7tdmi,
cpu: *Arm7tdmi,
sched: *Scheduler,
read_table: *const [table_len]?*const anyopaque,
write_tables: [2]*const [table_len]?*anyopaque,
allocator: Allocator,
pub fn init(self: *Self, allocator: Allocator, sched: *Scheduler, cpu: *Arm7tdmi, paths: FilePaths) !void {
const tables = try allocator.alloc(?*anyopaque, 3 * table_len); // Allocate all tables
const read_table: *[table_len]?*const anyopaque = tables[0..table_len];
const left_write: *[table_len]?*anyopaque = tables[table_len .. 2 * table_len];
const right_write: *[table_len]?*anyopaque = tables[2 * table_len .. 3 * table_len];
self.* = .{
.pak = try GamePak.init(allocator, cpu, paths.rom, paths.save),
.bios = try Bios.init(allocator, paths.bios),
@@ -62,7 +76,20 @@ pub fn init(self: *Self, allocator: Allocator, sched: *Scheduler, cpu: *Arm7tdmi
.io = Io.init(),
.cpu = cpu,
.sched = sched,
.read_table = read_table,
.write_tables = .{ left_write, right_write },
.allocator = allocator,
};
// read_table, write_tables, and *Self are not restricted to the lifetime
// of this init function so we can initialize our tables here
fillReadTable(self, read_table);
// Internal Display Memory behavious unusually on 8-bit reads
// so we have two different tables depending on whether there's an 8-bit read or not
fillWriteTable(u32, self, left_write);
fillWriteTable(u8, self, right_write);
}
pub fn deinit(self: *Self) void {
@@ -71,34 +98,131 @@ pub fn deinit(self: *Self) void {
self.pak.deinit();
self.bios.deinit();
self.ppu.deinit();
// This is so I can deallocate the original `allocator.alloc`. I have to re-make the type
// since I'm not keeping it around, This is very jank and bad though
// FIXME: please figure out another way
self.allocator.free(@ptrCast([*]const ?*anyopaque, self.read_table[0..])[0 .. 3 * table_len]);
self.* = undefined;
}
pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
const page = @truncate(u8, address >> 24);
const aligned_addr = forceAlign(T, address);
fn fillReadTable(bus: *Self, table: *[table_len]?*const anyopaque) void {
const vramMirror = @import("ppu.zig").Vram.mirror;
for (table) |*ptr, i| {
const addr = page_size * i;
ptr.* = switch (addr) {
// General Internal Memory
0x0000_0000...0x0000_3FFF => null, // BIOS has it's own checks
0x0200_0000...0x02FF_FFFF => &bus.ewram.buf[addr & 0x3FFFF],
0x0300_0000...0x03FF_FFFF => &bus.iwram.buf[addr & 0x7FFF],
0x0400_0000...0x0400_03FF => null, // I/O
// Internal Display Memory
0x0500_0000...0x05FF_FFFF => &bus.ppu.palette.buf[addr & 0x3FF],
0x0600_0000...0x06FF_FFFF => &bus.ppu.vram.buf[vramMirror(addr)],
0x0700_0000...0x07FF_FFFF => &bus.ppu.oam.buf[addr & 0x3FF],
// External Memory (Game Pak)
0x0800_0000...0x0DFF_FFFF => fillTableExternalMemory(bus, addr),
0x0E00_0000...0x0FFF_FFFF => null, // SRAM
else => null,
};
}
}
fn fillWriteTable(comptime T: type, bus: *Self, table: *[table_len]?*const anyopaque) void {
comptime std.debug.assert(T == u32 or T == u16 or T == u8);
const vramMirror = @import("ppu.zig").Vram.mirror;
for (table) |*ptr, i| {
const addr = page_size * i;
ptr.* = switch (addr) {
// General Internal Memory
0x0000_0000...0x0000_3FFF => null, // BIOS has it's own checks
0x0200_0000...0x02FF_FFFF => &bus.ewram.buf[addr & 0x3FFFF],
0x0300_0000...0x03FF_FFFF => &bus.iwram.buf[addr & 0x7FFF],
0x0400_0000...0x0400_03FF => null, // I/O
// Internal Display Memory
0x0500_0000...0x05FF_FFFF => if (T != u8) &bus.ppu.palette.buf[addr & 0x3FF] else null,
0x0600_0000...0x06FF_FFFF => if (T != u8) &bus.ppu.vram.buf[vramMirror(addr)] else null,
0x0700_0000...0x07FF_FFFF => if (T != u8) &bus.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 fillTableExternalMemory(bus: *Self, addr: usize) ?*anyopaque {
// see `GamePak.zig` for more information about what conditions need to be true
// so that a simple pointer dereference isn't possible
const start_addr = addr;
const end_addr = addr + page_size;
const gpio_data = start_addr <= 0x0800_00C4 and 0x0800_00C4 < end_addr;
const gpio_direction = start_addr <= 0x0800_00C6 and 0x0800_00C6 < end_addr;
const gpio_control = start_addr <= 0x0800_00C8 and 0x0800_00C8 < end_addr;
if (bus.pak.gpio.device.kind != .None and (gpio_data or gpio_direction or gpio_control)) {
// We found a GPIO device, and this page a GPIO register. We want to handle this in slowmem
return null;
}
if (bus.pak.backup.kind == .Eeprom) {
if (bus.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 >= bus.pak.buf.len) return null;
return &bus.pak.buf[masked_addr];
}
// TODO: Take advantage of fastmem here too?
pub fn dbgRead(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], aligned_addr);
break :blk self.bios.dbgRead(T, self.cpu.r[15], address);
break :blk self.readOpenBus(T, address);
break :blk self.openBus(T, address);
},
0x02 => self.ewram.read(T, aligned_addr),
0x03 => self.iwram.read(T, aligned_addr),
0x02 => self.ewram.read(T, address),
0x03 => self.iwram.read(T, address),
0x04 => self.readIo(T, address),
// Internal Display Memory
0x05 => self.ppu.palette.read(T, aligned_addr),
0x06 => self.ppu.vram.read(T, aligned_addr),
0x07 => self.ppu.oam.read(T, aligned_addr),
0x05 => self.ppu.palette.read(T, address),
0x06 => self.ppu.vram.read(T, address),
0x07 => self.ppu.oam.read(T, address),
// External Memory (Game Pak)
0x08...0x0D => self.pak.dbgRead(T, aligned_addr),
0x08...0x0D => self.pak.dbgRead(T, address),
0x0E...0x0F => blk: {
const value = self.pak.backup.read(address);
const value = self.pak.backup.read(unaligned_address);
const multiplier = switch (T) {
u32 => 0x01010101,
@@ -109,81 +233,127 @@ pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
break :blk @as(T, value) * multiplier;
},
else => self.readOpenBus(T, address),
else => self.openBus(T, address),
};
}
fn readIo(self: *const Self, comptime T: type, unaligned_address: u32) T {
const maybe_value = io.read(self, T, forceAlign(T, unaligned_address));
return if (maybe_value) |value| value else self.readOpenBus(T, unaligned_address);
fn readIo(self: *const Self, comptime T: type, address: u32) T {
return io.read(self, T, address) orelse self.openBus(T, address);
}
fn readOpenBus(self: *const Self, comptime T: type, address: u32) T {
const r15 = self.cpu.?.r[15];
fn openBus(self: *const Self, comptime T: type, address: u32) T {
@setCold(true);
const r15 = self.cpu.r[15];
const word = blk: {
// If u32 Open Bus, read recently fetched opcode (PC + 8)
if (!self.cpu.?.cpsr.t.read()) break :blk self.dbgRead(u32, r15 + 4);
// If Arm, get the most recently fetched instruction (PC + 8)
//
// FIXME: This is most likely a faulty assumption.
// I think what *actually* happens is that the Bus has a latch for the most
// recently fetched piece of data, which is then returned during Open Bus (also DMA open bus?)
// I can "get away" with this because it's very statistically likely that the most recently latched value is
// the most recently fetched instruction by the pipeline
if (!self.cpu.cpsr.t.read()) break :blk self.cpu.pipe.stage[1].?;
const page = @truncate(u8, r15 >> 24);
// PC + 2 = stage[0]
// PC + 4 = stage[1]
// PC + 6 = Need a Debug Read for this?
switch (page) {
// EWRAM, PALRAM, VRAM, and Game ROM (16-bit)
0x02, 0x05, 0x06, 0x08...0x0D => {
// (PC + 4)
const halfword = self.dbgRead(u16, r15 + 2);
break :blk @as(u32, halfword) << 16 | halfword;
const halfword: u32 = @truncate(u16, self.cpu.pipe.stage[1].?);
break :blk halfword << 16 | halfword;
},
// BIOS or OAM (32-bit)
0x00, 0x07 => {
// Aligned: (PC + 6) | (PC + 4)
// Unaligned: (PC + 4) | (PC + 2)
const offset: u32 = if (address & 3 == 0b00) 2 else 0;
const aligned = address & 3 == 0b00;
break :blk @as(u32, self.dbgRead(u16, r15 + 2 + offset)) << 16 | self.dbgRead(u16, r15 + offset);
// TODO: What to do on PC + 6?
const high: u32 = if (aligned) self.dbgRead(u16, r15 + 4) else @truncate(u16, self.cpu.pipe.stage[1].?);
const low: u32 = @truncate(u16, self.cpu.pipe.stage[@boolToInt(aligned)].?);
break :blk high << 16 | low;
},
// IWRAM (16-bit but special)
0x03 => {
// Aligned: (PC + 2) | (PC + 4)
// Unaligned: (PC + 4) | (PC + 2)
const offset: u32 = if (address & 3 == 0b00) 2 else 0;
const aligned = address & 3 == 0b00;
break :blk @as(u32, self.dbgRead(u16, r15 + 2 - offset)) << 16 | self.dbgRead(u16, r15 + offset);
const high: u32 = @truncate(u16, self.cpu.pipe.stage[1 - @boolToInt(aligned)].?);
const low: u32 = @truncate(u16, self.cpu.pipe.stage[@boolToInt(aligned)].?);
break :blk high << 16 | low;
},
else => {
log.err("THUMB open bus read from 0x{X:0>2} page @0x{X:0>8}", .{ page, address });
@panic("invariant most-likely broken");
},
else => unreachable,
}
};
return @truncate(T, rotr(u32, word, 8 * (address & 3)));
return @truncate(T, word);
}
pub fn read(self: *Self, comptime T: type, address: u32) T {
const page = @truncate(u8, address >> 24);
const aligned_addr = forceAlign(T, address);
pub fn read(self: *Self, comptime T: type, unaligned_address: u32) T {
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits;
const page = unaligned_address >> bits;
const offset = unaligned_address & (page_size - 1);
self.sched.tick += timings[@boolToInt(T == u32)][@truncate(u4, page)];
// whether or not we do this in slowmem or fastmem, we should advance the scheduler
self.sched.tick += timings[@boolToInt(T == u32)][@truncate(u4, unaligned_address >> 24)];
// We're doing some serious out-of-bounds open-bus reads
if (page >= table_len) return self.openBus(T, unaligned_address);
if (self.read_table[page]) |some_ptr| {
// We have a pointer to a page, cast the pointer to it's underlying type
const Ptr = [*]const T;
const alignment = @alignOf(std.meta.Child(Ptr));
const ptr = @ptrCast(Ptr, @alignCast(alignment, 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);
}
fn slowRead(self: *Self, comptime T: type, unaligned_address: u32) T {
@setCold(true);
const page = @truncate(u8, unaligned_address >> 24);
const address = forceAlign(T, unaligned_address);
return switch (page) {
// General Internal Memory
0x00 => blk: {
if (address < Bios.size)
break :blk self.bios.read(T, self.cpu.?.r[15], aligned_addr);
break :blk self.bios.read(T, self.cpu.r[15], address);
break :blk self.readOpenBus(T, address);
break :blk self.openBus(T, address);
},
0x02 => self.ewram.read(T, aligned_addr),
0x03 => self.iwram.read(T, aligned_addr),
0x02 => unreachable, // completely handled by fastmeme
0x03 => unreachable, // completely handled by fastmeme
0x04 => self.readIo(T, address),
// Internal Display Memory
0x05 => self.ppu.palette.read(T, aligned_addr),
0x06 => self.ppu.vram.read(T, aligned_addr),
0x07 => self.ppu.oam.read(T, aligned_addr),
0x05 => unreachable, // completely handled by fastmeme
0x06 => unreachable, // completely handled by fastmeme
0x07 => unreachable, // completely handled by fastmeme
// External Memory (Game Pak)
0x08...0x0D => self.pak.read(T, aligned_addr),
0x08...0x0D => self.pak.read(T, address),
0x0E...0x0F => blk: {
const value = self.pak.backup.read(address);
const value = self.pak.backup.read(unaligned_address);
const multiplier = switch (T) {
u32 => 0x01010101,
@@ -194,48 +364,75 @@ pub fn read(self: *Self, comptime T: type, address: u32) T {
break :blk @as(T, value) * multiplier;
},
else => self.readOpenBus(T, address),
else => self.openBus(T, address),
};
}
pub fn write(self: *Self, comptime T: type, address: u32, value: T) void {
const page = @truncate(u8, address >> 24);
const aligned_addr = forceAlign(T, address);
pub fn write(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits;
const page = unaligned_address >> bits;
const offset = unaligned_address & (page_size - 1);
self.sched.tick += timings[@boolToInt(T == u32)][@truncate(u4, page)];
// whether or not we do this in slowmem or fastmem, we should advance the scheduler
self.sched.tick += timings[@boolToInt(T == u32)][@truncate(u4, unaligned_address >> 24)];
// We're doing some serious out-of-bounds open-bus writes, they do nothing though
if (page >= table_len) return;
if (self.write_tables[@boolToInt(T == u8)][page]) |some_ptr| {
// We have a pointer to a page, cast the pointer to it's underlying type
const Ptr = [*]T;
const alignment = @alignOf(std.meta.Child(Ptr));
const ptr = @ptrCast(Ptr, @alignCast(alignment, 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);
}
}
pub fn slowWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
// @setCold(true);
const page = @truncate(u8, unaligned_address >> 24);
const address = forceAlign(T, unaligned_address);
switch (page) {
// General Internal Memory
0x00 => self.bios.write(T, aligned_addr, value),
0x02 => self.ewram.write(T, aligned_addr, value),
0x03 => self.iwram.write(T, aligned_addr, value),
0x04 => io.write(self, T, aligned_addr, value),
0x00 => self.bios.write(T, address, value),
0x02 => unreachable, // completely handled by fastmem
0x03 => unreachable, // completely handled by fastmem
0x04 => io.write(self, T, address, value),
// Internal Display Memory
0x05 => self.ppu.palette.write(T, aligned_addr, value),
0x06 => self.ppu.vram.write(T, self.ppu.dispcnt, aligned_addr, value),
0x07 => self.ppu.oam.write(T, aligned_addr, value),
0x05 => self.ppu.palette.write(T, address, value),
0x06 => self.ppu.vram.write(T, self.ppu.dispcnt, address, value),
0x07 => unreachable, // completely handled by fastmem
// External Memory (Game Pak)
0x08...0x0D => self.pak.write(T, self.dma[3].word_count, aligned_addr, value),
0x0E...0x0F => {
const rotate_by = switch (T) {
0x08...0x0D => self.pak.write(T, self.dma[3].word_count, address, value),
0x0E...0x0F => self.pak.backup.write(unaligned_address, @truncate(u8, rotr(T, value, 8 * rotateBy(T, unaligned_address)))),
else => {},
}
}
inline fn rotateBy(comptime T: type, address: u32) u32 {
return switch (T) {
u32 => address & 3,
u16 => address & 1,
u8 => 0,
else => @compileError("Backup: Unsupported write width"),
};
self.pak.backup.write(address, @truncate(u8, rotr(T, value, 8 * rotate_by)));
},
else => {},
}
}
fn forceAlign(comptime T: type, address: u32) u32 {
inline fn forceAlign(comptime T: type, address: u32) u32 {
return switch (T) {
u32 => address & 0xFFFF_FFFC,
u16 => address & 0xFFFF_FFFE,
u32 => address & ~@as(u32, 3),
u16 => address & ~@as(u32, 1),
u8 => address,
else => @compileError("Bus: Invalid read/write type"),
};

File diff suppressed because it is too large Load Diff

145
src/core/apu/Noise.zig Normal file
View File

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

141
src/core/apu/Tone.zig Normal file
View File

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

185
src/core/apu/ToneSweep.zig Normal file
View File

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

145
src/core/apu/Wave.zig Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,36 @@ allocator: Allocator,
addr_latch: u32,
pub fn read(self: *Self, comptime T: type, r15: u32, addr: u32) T {
if (r15 < Self.size) {
self.addr_latch = addr;
return self._read(T, addr);
}
log.debug("Rejected read since r15=0x{X:0>8}", .{r15});
return @truncate(T, self._read(T, self.addr_latch));
}
pub fn dbgRead(self: *const Self, comptime T: type, r15: u32, addr: u32) T {
if (r15 < Self.size) return self._read(T, addr);
return @truncate(T, self._read(T, self.addr_latch + 8));
}
/// Read without the GBA safety checks
fn _read(self: *const Self, comptime T: type, addr: u32) T {
const buf = self.buf orelse std.debug.panic("[BIOS] ZBA tried to read {} from 0x{X:0>8} but not BIOS was present", .{ T, addr });
return switch (T) {
u32, u16, u8 => std.mem.readIntSliceLittle(T, buf[addr..][0..@sizeOf(T)]),
else => @compileError("BIOS: Unsupported read width"),
};
}
pub fn write(_: *Self, comptime T: type, addr: u32, value: T) void {
@setCold(true);
log.debug("Tried to write {} 0x{X:} to 0x{X:0>8} ", .{ T, value, addr });
}
pub fn init(allocator: Allocator, maybe_path: ?[]const u8) !Self {
const buf: ?[]u8 = if (maybe_path) |path| blk: {
const file = try std.fs.cwd().openFile(path, .{});
@@ -31,34 +61,3 @@ pub fn deinit(self: *Self) void {
if (self.buf) |buf| self.allocator.free(buf);
self.* = undefined;
}
pub fn read(self: *Self, comptime T: type, r15: u32, addr: u32) T {
if (r15 < Self.size) {
self.addr_latch = addr;
return self.uncheckedRead(T, addr);
}
log.debug("Rejected read since r15=0x{X:0>8}", .{r15});
return @truncate(T, self.uncheckedRead(T, self.addr_latch + 8));
}
pub fn dbgRead(self: *const Self, comptime T: type, r15: u32, addr: u32) T {
if (r15 < Self.size) return self.uncheckedRead(T, addr);
return @truncate(T, self.uncheckedRead(T, self.addr_latch + 8));
}
fn uncheckedRead(self: *const Self, comptime T: type, addr: u32) T {
if (self.buf) |buf| {
return switch (T) {
u32, u16, u8 => std.mem.readIntSliceLittle(T, buf[addr..][0..@sizeOf(T)]),
else => @compileError("BIOS: Unsupported read width"),
};
}
std.debug.panic("[BIOS] ZBA tried to read {} from 0x{X:0>8} but not BIOS was present", .{ T, addr });
}
pub fn write(_: *Self, comptime T: type, addr: u32, value: T) void {
@setCold(true);
log.debug("Tried to write {} 0x{X:} to 0x{X:0>8} ", .{ T, value, addr });
}

View File

@@ -7,21 +7,6 @@ const Self = @This();
buf: []u8,
allocator: Allocator,
pub fn init(allocator: Allocator) !Self {
const buf = try allocator.alloc(u8, ewram_size);
std.mem.set(u8, buf, 0);
return Self{
.buf = buf,
.allocator = allocator,
};
}
pub fn deinit(self: *Self) void {
self.allocator.free(self.buf);
self.* = undefined;
}
pub fn read(self: *const Self, comptime T: type, address: usize) T {
const addr = address & 0x3FFFF;
@@ -39,3 +24,18 @@ pub fn write(self: *const Self, comptime T: type, address: usize, value: T) void
else => @compileError("EWRAM: Unsupported write width"),
};
}
pub fn init(allocator: Allocator) !Self {
const buf = try allocator.alloc(u8, ewram_size);
std.mem.set(u8, buf, 0);
return Self{
.buf = buf,
.allocator = allocator,
};
}
pub fn deinit(self: *Self) void {
self.allocator.free(self.buf);
self.* = undefined;
}

View File

@@ -1,13 +1,11 @@
const std = @import("std");
const DateTime = @import("datetime").datetime.Datetime;
const config = @import("../../config.zig");
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
const Bit = @import("bitfield").Bit;
const Bitfield = @import("bitfield").Bitfield;
const Backup = @import("backup.zig").Backup;
const Gpio = @import("gpio.zig").Gpio;
const Allocator = std.mem.Allocator;
const force_rtc = @import("../emu.zig").force_rtc;
const log = std.log.scoped(.GamePak);
const Self = @This();
@@ -18,78 +16,11 @@ allocator: Allocator,
backup: Backup,
gpio: *Gpio,
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, rom_path: []const u8, save_path: ?[]const u8) !Self {
const file = try std.fs.cwd().openFile(rom_path, .{});
defer file.close();
const file_buf = try file.readToEndAlloc(allocator, try file.getEndPos());
const title = file_buf[0xA0..0xAC].*;
const kind = Backup.guessKind(file_buf);
const device = if (force_rtc) .Rtc else guessDevice(file_buf);
logHeader(file_buf, &title);
return .{
.buf = file_buf,
.allocator = allocator,
.title = title,
.backup = try Backup.init(allocator, kind, title, save_path),
.gpio = try Gpio.init(allocator, cpu, device),
};
}
/// Searches the ROM to see if it can determine whether the ROM it's searching uses
/// any GPIO device, like a RTC for example.
fn guessDevice(buf: []const u8) Gpio.Device.Kind {
// Try to Guess if ROM uses RTC
const needle = "RTC_V"; // I was told SIIRTC_V, though Pokemen Firered (USA) is a false negative
var i: usize = 0;
while ((i + needle.len) < buf.len) : (i += 1) {
if (std.mem.eql(u8, needle, buf[i..(i + needle.len)])) return .Rtc;
}
// TODO: Detect other GPIO devices
return .None;
}
fn logHeader(buf: []const u8, title: *const [12]u8) void {
const code = buf[0xAC..0xB0];
const maker = buf[0xB0..0xB2];
const version = buf[0xBC];
log.info("Title: {s}", .{title});
if (version != 0) log.info("Version: {}", .{version});
log.info("Game Code: {s}", .{code});
if (lookupMaker(maker)) |c| log.info("Maker: {s}", .{c}) else log.info("Maker Code: {s}", .{maker});
}
fn lookupMaker(slice: *const [2]u8) ?[]const u8 {
const id = @as(u16, slice[1]) << 8 | @as(u16, slice[0]);
return switch (id) {
0x3130 => "Nintendo",
else => null,
};
}
inline fn isLarge(self: *const Self) bool {
return self.buf.len > 0x100_0000;
}
pub fn deinit(self: *Self) void {
self.backup.deinit();
self.gpio.deinit(self.allocator);
self.allocator.destroy(self.gpio);
self.allocator.free(self.buf);
self.* = undefined;
}
pub fn read(self: *Self, comptime T: type, address: u32) T {
const addr = address & 0x1FF_FFFF;
if (self.backup.kind == .Eeprom) {
if (self.isLarge()) {
if (self.buf.len > 0x100_0000) { // Large
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
// * Backup type is EEPROM
// * Large ROM (Size is greater than 16MB)
@@ -141,11 +72,19 @@ pub fn read(self: *Self, comptime T: type, address: u32) T {
};
}
inline fn get(self: *const Self, i: u32) u8 {
@setRuntimeSafety(false);
if (i < self.buf.len) return self.buf[i];
const lhs = i >> 1 & 0xFFFF;
return @truncate(u8, lhs >> 8 * @truncate(u5, i & 1));
}
pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
const addr = address & 0x1FF_FFFF;
if (self.backup.kind == .Eeprom) {
if (self.isLarge()) {
if (self.buf.len > 0x100_0000) { // Large
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
// * Backup type is EEPROM
// * Large ROM (Size is greater than 16MB)
@@ -160,6 +99,34 @@ pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
}
}
if (self.gpio.cnt == 1) {
// GPIO Can be read from
// We assume that this will only be true when a ROM actually does want something from GPIO
switch (T) {
u32 => switch (address) {
// FIXME: Do I even need to implement these?
0x0800_00C4 => std.debug.panic("Handle 32-bit GPIO Data/Direction Reads", .{}),
0x0800_00C6 => std.debug.panic("Handle 32-bit GPIO Direction/Control Reads", .{}),
0x0800_00C8 => std.debug.panic("Handle 32-bit GPIO Control Reads", .{}),
else => {},
},
u16 => switch (address) {
0x0800_00C4 => return self.gpio.read(.Data),
0x0800_00C6 => return self.gpio.read(.Direction),
0x0800_00C8 => return self.gpio.read(.Control),
else => {},
},
u8 => switch (address) {
0x0800_00C4 => return self.gpio.read(.Data),
0x0800_00C6 => return self.gpio.read(.Direction),
0x0800_00C8 => return self.gpio.read(.Control),
else => {},
},
else => @compileError("GamePak[GPIO]: Unsupported read width"),
}
}
return switch (T) {
u32 => (@as(T, self.get(addr + 3)) << 24) | (@as(T, self.get(addr + 2)) << 16) | (@as(T, self.get(addr + 1)) << 8) | (@as(T, self.get(addr))),
u16 => (@as(T, self.get(addr + 1)) << 8) | @as(T, self.get(addr)),
@@ -174,7 +141,7 @@ pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value
if (self.backup.kind == .Eeprom) {
const bit = @truncate(u1, value);
if (self.isLarge()) {
if (self.buf.len > 0x100_0000) { // Large
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
// * Backup type is EEPROM
// * Large ROM (Size is greater than 16MB)
@@ -212,12 +179,59 @@ pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value
}
}
fn get(self: *const Self, i: u32) u8 {
@setRuntimeSafety(false);
if (i < self.buf.len) return self.buf[i];
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, rom_path: []const u8, save_path: ?[]const u8) !Self {
const file = try std.fs.cwd().openFile(rom_path, .{});
defer file.close();
const lhs = i >> 1 & 0xFFFF;
return @truncate(u8, lhs >> 8 * @truncate(u5, i & 1));
const file_buf = try file.readToEndAlloc(allocator, try file.getEndPos());
const title = file_buf[0xA0..0xAC].*;
const kind = Backup.guess(file_buf);
const device = if (config.config().guest.force_rtc) .Rtc else guessDevice(file_buf);
logHeader(file_buf, &title);
return .{
.buf = file_buf,
.allocator = allocator,
.title = title,
.backup = try Backup.init(allocator, kind, title, save_path),
.gpio = try Gpio.init(allocator, cpu, device),
};
}
pub fn deinit(self: *Self) void {
self.backup.deinit();
self.gpio.deinit(self.allocator);
self.allocator.destroy(self.gpio);
self.allocator.free(self.buf);
self.* = undefined;
}
/// Searches the ROM to see if it can determine whether the ROM it's searching uses
/// any GPIO device, like a RTC for example.
fn guessDevice(buf: []const u8) Gpio.Device.Kind {
// Try to Guess if ROM uses RTC
const needle = "RTC_V"; // I was told SIIRTC_V, though Pokemen Firered (USA) is a false negative
var i: usize = 0;
while ((i + needle.len) < buf.len) : (i += 1) {
if (std.mem.eql(u8, needle, buf[i..(i + needle.len)])) return .Rtc;
}
// TODO: Detect other GPIO devices
return .None;
}
fn logHeader(buf: []const u8, title: *const [12]u8) void {
const code = buf[0xAC..0xB0];
const maker = buf[0xB0..0xB2];
const version = buf[0xBC];
log.info("Title: {s}", .{title});
if (version != 0) log.info("Version: {}", .{version});
log.info("Game Code: {s}", .{code});
log.info("Maker Code: {s}", .{maker});
}
test "OOB Access" {
@@ -239,463 +253,3 @@ test "OOB Access" {
std.debug.assert(pak.get(4) == 0x02); // 0x0002
std.debug.assert(pak.get(5) == 0x00);
}
/// GPIO Register Implementation
const Gpio = struct {
const This = @This();
data: u4,
direction: u4,
cnt: u1,
device: Device,
const Device = struct {
ptr: ?*anyopaque,
kind: Kind, // TODO: Make comptime known?
const Kind = enum { Rtc, None };
fn step(self: *Device, value: u4) u4 {
return switch (self.kind) {
.Rtc => blk: {
const clock = @ptrCast(*Clock, @alignCast(@alignOf(*Clock), self.ptr.?));
break :blk clock.step(Clock.Data{ .raw = value });
},
.None => value,
};
}
fn init(kind: Kind, ptr: ?*anyopaque) Device {
return .{ .kind = kind, .ptr = ptr };
}
};
const Register = enum {
Data,
Direction,
Control,
};
fn init(allocator: Allocator, cpu: *Arm7tdmi, kind: Device.Kind) !*This {
log.info("Device: {}", .{kind});
const self = try allocator.create(This);
self.* = .{
.data = 0b0000,
.direction = 0b1111, // TODO: What is GPIO DIrection set to by default?
.cnt = 0b0,
.device = switch (kind) {
.Rtc => blk: {
const clock = try allocator.create(Clock);
clock.init(cpu, self);
break :blk Device{ .kind = kind, .ptr = clock };
},
.None => Device{ .kind = kind, .ptr = null },
},
};
return self;
}
fn deinit(self: *This, allocator: Allocator) void {
switch (self.device.kind) {
.Rtc => {
allocator.destroy(@ptrCast(*Clock, @alignCast(@alignOf(*Clock), self.device.ptr.?)));
},
.None => {},
}
self.* = undefined;
}
fn write(self: *This, comptime reg: Register, value: if (reg == .Control) u1 else u4) void {
switch (reg) {
.Data => {
const masked_value = value & self.direction;
// The value which is actually stored in the GPIO register
// might be modified by the device implementing the GPIO interface e.g. RTC reads
self.data = self.device.step(masked_value);
},
.Direction => self.direction = value,
.Control => self.cnt = value,
}
}
fn read(self: *const This, comptime reg: Register) if (reg == .Control) u1 else u4 {
if (self.cnt == 0) return 0;
return switch (reg) {
.Data => self.data & ~self.direction,
.Direction => self.direction,
.Control => self.cnt,
};
}
};
/// GBA Real Time Clock
pub const Clock = struct {
const This = @This();
writer: Writer,
reader: Reader,
state: State,
cnt: Control,
year: u8,
month: u5,
day: u6,
weekday: u3,
hour: u6,
minute: u7,
second: u7,
cpu: *Arm7tdmi,
gpio: *const Gpio,
const Register = enum {
Control,
DateTime,
Time,
};
const State = union(enum) {
Idle,
Command,
Write: Register,
Read: Register,
};
const Reader = struct {
i: u4,
count: u8,
/// Reads a bit from RTC registers. Which bit it reads is dependent on
///
/// 1. The RTC State Machine, whitch tells us which register we're accessing
/// 2. A `count`, which keeps track of which byte is currently being read
/// 3. An index, which keeps track of which bit of the byte determined by `count` is being read
fn read(self: *Reader, clock: *const Clock, register: Register) u1 {
const idx = @intCast(u3, self.i);
defer self.i += 1;
// FIXME: What do I do about the unused bits?
return switch (register) {
.Control => @truncate(u1, switch (self.count) {
0 => clock.cnt.raw >> idx,
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 1 byte)", .{ self.count, register }),
}),
.DateTime => @truncate(u1, switch (self.count) {
// Date
0 => clock.year >> idx,
1 => @as(u8, clock.month) >> idx,
2 => @as(u8, clock.day) >> idx,
3 => @as(u8, clock.weekday) >> idx,
// Time
4 => @as(u8, clock.hour) >> idx,
5 => @as(u8, clock.minute) >> idx,
6 => @as(u8, clock.second) >> idx,
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 7 bytes)", .{ self.count, register }),
}),
.Time => @truncate(u1, switch (self.count) {
0 => @as(u8, clock.hour) >> idx,
1 => @as(u8, clock.minute) >> idx,
2 => @as(u8, clock.second) >> idx,
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 3 bytes)", .{ self.count, register }),
}),
};
}
/// Is true when a Reader has read a u8's worth of bits
fn finished(self: *const Reader) bool {
return self.i >= 8;
}
/// Resets the index used to shift bits out of RTC registers
/// and `count`, which is used to keep track of which byte we're reading
/// is incremeneted
fn lap(self: *Reader) void {
self.i = 0;
self.count += 1;
}
/// Resets the state of a `Reader` in preparation for a future
/// read command
fn reset(self: *Reader) void {
self.i = 0;
self.count = 0;
}
};
const Writer = struct {
buf: u8,
i: u4,
/// The Number of bytes written since last reset
count: u8,
/// Append a bit to the internal bit buffer (aka an integer)
fn push(self: *Writer, value: u1) void {
const idx = @intCast(u3, self.i);
self.buf = (self.buf & ~(@as(u8, 1) << idx)) | @as(u8, value) << idx;
self.i += 1;
}
/// Takes the contents of the internal buffer and writes it to an RTC register
/// Where it writes to is dependent on:
///
/// 1. The RTC State Machine, whitch tells us which register we're accessing
/// 2. A `count`, which keeps track of which byte is currently being read
fn write(self: *const Writer, clock: *Clock, register: Register) void {
// FIXME: What do do about unused bits?
switch (register) {
.Control => switch (self.count) {
0 => clock.cnt.raw = (clock.cnt.raw & 0x80) | (self.buf & 0x7F), // Bit 7 read-only
else => std.debug.panic("Tried to write to byte #{} of {} (hint: there's only 1 byte)", .{ self.count, register }),
},
.DateTime, .Time => log.debug("RTC: Ignoring {} write", .{register}),
}
}
/// Is true when 8 bits have been shifted into the internal buffer
fn finished(self: *const Writer) bool {
return self.i >= 8;
}
/// Resets the internal buffer
/// resets the index used to shift bits into the internal buffer
/// increments `count` (which keeps track of byte offsets) by one
fn lap(self: *Writer) void {
self.buf = 0;
self.i = 0;
self.count += 1;
}
/// Resets `Writer` to a clean state in preparation for a future write command
fn reset(self: *Writer) void {
self.buf = 0;
self.i = 0;
self.count = 0;
}
};
const Data = extern union {
sck: Bit(u8, 0),
sio: Bit(u8, 1),
cs: Bit(u8, 2),
raw: u8,
};
const Control = extern union {
/// Unknown, value should be preserved though
unk: Bit(u8, 1),
/// Per-minute IRQ
/// If set, fire a Gamepak IRQ every 30s,
irq: Bit(u8, 3),
/// 12/24 Hour Bit
/// If set, 12h mode
/// If cleared, 24h mode
mode: Bit(u8, 6),
/// Read-Only, bit cleared on read
/// If is set, means that there has been a failure / time has been lost
off: Bit(u8, 7),
raw: u8,
};
fn init(ptr: *This, cpu: *Arm7tdmi, gpio: *const Gpio) void {
ptr.* = .{
.writer = .{ .buf = 0, .i = 0, .count = 0 },
.reader = .{ .i = 0, .count = 0 },
.state = .Idle,
.cnt = .{ .raw = 0 },
.year = 0x01,
.month = 0x6,
.day = 0x13,
.weekday = 0x3,
.hour = 0x23,
.minute = 0x59,
.second = 0x59,
.cpu = cpu,
.gpio = gpio, // Can't use Arm7tdmi ptr b/c not initialized yet
};
cpu.sched.push(.RealTimeClock, 1 << 24); // Every Second
}
pub fn updateTime(self: *This, late: u64) void {
self.cpu.sched.push(.RealTimeClock, (1 << 24) -| late); // Reschedule
const now = DateTime.now();
self.year = toBcd(u8, @intCast(u8, now.date.year - 2000));
self.month = toBcd(u5, now.date.month);
self.day = toBcd(u6, now.date.day);
self.weekday = toBcd(u3, (now.date.weekday() + 1) % 7); // API is Monday = 0, Sunday = 6. We want Sunday = 0, Saturday = 6
self.hour = toBcd(u6, now.time.hour);
self.minute = toBcd(u7, now.time.minute);
self.second = toBcd(u7, now.time.second);
}
fn step(self: *This, value: Data) u4 {
const cache: Data = .{ .raw = self.gpio.data };
return switch (self.state) {
.Idle => blk: {
// FIXME: Maybe check incoming value to see if SCK is also high?
if (cache.sck.read()) {
if (!cache.cs.read() and value.cs.read()) {
log.debug("RTC: Entering Command Mode", .{});
self.state = .Command;
}
}
break :blk @truncate(u4, value.raw);
},
.Command => blk: {
if (!value.cs.read()) log.err("RTC: Expected CS to be set during {}, however CS was cleared", .{self.state});
// If SCK rises, sample SIO
if (!cache.sck.read() and value.sck.read()) {
self.writer.push(@boolToInt(value.sio.read()));
if (self.writer.finished()) {
self.state = self.processCommand(self.writer.buf);
self.writer.reset();
log.debug("RTC: Switching to {}", .{self.state});
}
}
break :blk @truncate(u4, value.raw);
},
.Write => |register| blk: {
if (!value.cs.read()) log.err("RTC: Expected CS to be set during {}, however CS was cleared", .{self.state});
// If SCK rises, sample SIO
if (!cache.sck.read() and value.sck.read()) {
self.writer.push(@boolToInt(value.sio.read()));
const register_width: u32 = switch (register) {
.Control => 1,
.DateTime => 7,
.Time => 3,
};
if (self.writer.finished()) {
self.writer.write(self, register); // write inner buffer to RTC register
self.writer.lap();
if (self.writer.count == register_width) {
self.writer.reset();
self.state = .Idle;
}
}
}
break :blk @truncate(u4, value.raw);
},
.Read => |register| blk: {
if (!value.cs.read()) log.err("RTC: Expected CS to be set during {}, however CS was cleared", .{self.state});
var ret = value;
// if SCK rises, sample SIO
if (!cache.sck.read() and value.sck.read()) {
ret.sio.write(self.reader.read(self, register) == 0b1);
const register_width: u32 = switch (register) {
.Control => 1,
.DateTime => 7,
.Time => 3,
};
if (self.reader.finished()) {
self.reader.lap();
if (self.reader.count == register_width) {
self.reader.reset();
self.state = .Idle;
}
}
}
break :blk @truncate(u4, ret.raw);
},
};
}
fn reset(self: *This) void {
// mGBA and NBA only zero the control register. We will do the same
log.debug("RTC: Reset (control register was zeroed)", .{});
self.cnt.raw = 0;
}
fn irq(self: *This) void {
// TODO: Confirm that this is the right behaviour
log.debug("RTC: Force GamePak IRQ", .{});
self.cpu.bus.io.irq.game_pak.set();
self.cpu.handleInterrupt();
}
fn processCommand(self: *This, raw_command: u8) State {
const command = blk: {
// If High Nybble is 0x6, no need to switch the endianness
if (raw_command >> 4 & 0xF == 0x6) break :blk raw_command;
// Turns out reversing the order of bits isn't trivial at all
// https://stackoverflow.com/questions/2602823/in-c-c-whats-the-simplest-way-to-reverse-the-order-of-bits-in-a-byte
var ret = raw_command;
ret = (ret & 0xF0) >> 4 | (ret & 0x0F) << 4;
ret = (ret & 0xCC) >> 2 | (ret & 0x33) << 2;
ret = (ret & 0xAA) >> 1 | (ret & 0x55) << 1;
break :blk ret;
};
log.debug("RTC: Handling Command 0x{X:0>2} [0b{b:0>8}]", .{ command, command });
const is_write = command & 1 == 0;
const rtc_register = @truncate(u3, command >> 1 & 0x7);
if (is_write) {
return switch (rtc_register) {
0 => blk: {
self.reset();
break :blk .Idle;
},
1 => .{ .Write = .Control },
2 => .{ .Write = .DateTime },
3 => .{ .Write = .Time },
6 => blk: {
self.irq();
break :blk .Idle;
},
4, 5, 7 => .Idle,
};
} else {
return switch (rtc_register) {
1 => .{ .Read = .Control },
2 => .{ .Read = .DateTime },
3 => .{ .Read = .Time },
0, 4, 5, 6, 7 => .Idle, // Do Nothing
};
}
}
};
fn toBcd(comptime T: type, value: u8) T {
var input = value;
var ret: u8 = 0;
var shift: u3 = 0;
while (input > 0) {
ret |= (input % 10) << (shift << 2);
shift += 1;
input /= 10;
}
return @truncate(T, ret);
}

View File

@@ -7,21 +7,6 @@ const Self = @This();
buf: []u8,
allocator: Allocator,
pub fn init(allocator: Allocator) !Self {
const buf = try allocator.alloc(u8, iwram_size);
std.mem.set(u8, buf, 0);
return Self{
.buf = buf,
.allocator = allocator,
};
}
pub fn deinit(self: *Self) void {
self.allocator.free(self.buf);
self.* = undefined;
}
pub fn read(self: *const Self, comptime T: type, address: usize) T {
const addr = address & 0x7FFF;
@@ -39,3 +24,18 @@ pub fn write(self: *const Self, comptime T: type, address: usize, value: T) void
else => @compileError("IWRAM: Unsupported write width"),
};
}
pub fn init(allocator: Allocator) !Self {
const buf = try allocator.alloc(u8, iwram_size);
std.mem.set(u8, buf, 0);
return Self{
.buf = buf,
.allocator = allocator,
};
}
pub fn deinit(self: *Self) void {
self.allocator.free(self.buf);
self.* = undefined;
}

View File

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

View File

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

View File

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

View File

@@ -1,90 +1,144 @@
const std = @import("std");
const util = @import("../util.zig");
const util = @import("../../util.zig");
const DmaControl = @import("io.zig").DmaControl;
const Bus = @import("../Bus.zig");
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
pub const DmaTuple = std.meta.Tuple(&[_]type{ DmaController(0), DmaController(1), DmaController(2), DmaController(3) });
pub const DmaTuple = struct { DmaController(0), DmaController(1), DmaController(2), DmaController(3) };
const log = std.log.scoped(.DmaTransfer);
const getHalf = util.getHalf;
const setHalf = util.setHalf;
const setQuart = util.setQuart;
const rotr = @import("../../util.zig").rotr;
pub fn create() DmaTuple {
return .{ DmaController(0).init(), DmaController(1).init(), DmaController(2).init(), DmaController(3).init() };
}
pub fn read(comptime T: type, dma: *const DmaTuple, addr: u32) ?T {
const byte = @truncate(u8, addr);
const byte_addr = @truncate(u8, addr);
return switch (T) {
u32 => switch (byte) {
0xB8 => @as(T, dma.*[0].cnt.raw) << 16,
0xC4 => @as(T, dma.*[1].cnt.raw) << 16,
0xD0 => @as(T, dma.*[2].cnt.raw) << 16,
0xDC => @as(T, dma.*[3].cnt.raw) << 16,
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
u32 => switch (byte_addr) {
0xB0, 0xB4 => null, // DMA0SAD, DMA0DAD,
0xB8 => @as(T, dma.*[0].dmacntH()) << 16, // DMA0CNT_L is write-only
0xBC, 0xC0 => null, // DMA1SAD, DMA1DAD
0xC4 => @as(T, dma.*[1].dmacntH()) << 16, // DMA1CNT_L is write-only
0xC8, 0xCC => null, // DMA2SAD, DMA2DAD
0xD0 => @as(T, dma.*[2].dmacntH()) << 16, // DMA2CNT_L is write-only
0xD4, 0xD8 => null, // DMA3SAD, DMA3DAD
0xDC => @as(T, dma.*[3].dmacntH()) << 16, // DMA3CNT_L is write-only
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
},
u16 => switch (byte) {
0xBA => dma.*[0].cnt.raw,
0xC6 => dma.*[1].cnt.raw,
0xD2 => dma.*[2].cnt.raw,
0xDE => dma.*[3].cnt.raw,
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
u16 => switch (byte_addr) {
0xB0, 0xB2, 0xB4, 0xB6 => null, // DMA0SAD, DMA0DAD
0xB8 => 0x0000, // DMA0CNT_L, suite.gba expects 0x0000 instead of 0xDEAD
0xBA => dma.*[0].dmacntH(),
0xBC, 0xBE, 0xC0, 0xC2 => null, // DMA1SAD, DMA1DAD
0xC4 => 0x0000, // DMA1CNT_L
0xC6 => dma.*[1].dmacntH(),
0xC8, 0xCA, 0xCC, 0xCE => null, // DMA2SAD, DMA2DAD
0xD0 => 0x0000, // DMA2CNT_L
0xD2 => dma.*[2].dmacntH(),
0xD4, 0xD6, 0xD8, 0xDA => null, // DMA3SAD, DMA3DAD
0xDC => 0x0000, // DMA3CNT_L
0xDE => dma.*[3].dmacntH(),
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
},
u8 => switch (byte_addr) {
0xB0...0xB7 => null, // DMA0SAD, DMA0DAD
0xB8, 0xB9 => 0x00, // DMA0CNT_L
0xBA, 0xBB => @truncate(T, dma.*[0].dmacntH() >> getHalf(byte_addr)),
0xBC...0xC3 => null, // DMA1SAD, DMA1DAD
0xC4, 0xC5 => 0x00, // DMA1CNT_L
0xC6, 0xC7 => @truncate(T, dma.*[1].dmacntH() >> getHalf(byte_addr)),
0xC8...0xCF => null, // DMA2SAD, DMA2DAD
0xD0, 0xD1 => 0x00, // DMA2CNT_L
0xD2, 0xD3 => @truncate(T, dma.*[2].dmacntH() >> getHalf(byte_addr)),
0xD4...0xDB => null, // DMA3SAD, DMA3DAD
0xDC, 0xDD => 0x00, // DMA3CNT_L
0xDE, 0xDF => @truncate(T, dma.*[3].dmacntH() >> getHalf(byte_addr)),
else => util.io.read.err(T, log, "unexpected {} read from 0x{X:0>8}", .{ T, addr }),
},
u8 => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
else => @compileError("DMA: Unsupported read width"),
};
}
pub fn write(comptime T: type, dma: *DmaTuple, addr: u32, value: T) void {
const byte = @truncate(u8, addr);
const byte_addr = @truncate(u8, addr);
switch (T) {
u32 => switch (byte) {
0xB0 => dma.*[0].setSad(value),
0xB4 => dma.*[0].setDad(value),
0xB8 => dma.*[0].setCnt(value),
0xBC => dma.*[1].setSad(value),
0xC0 => dma.*[1].setDad(value),
0xC4 => dma.*[1].setCnt(value),
0xC8 => dma.*[2].setSad(value),
0xCC => dma.*[2].setDad(value),
0xD0 => dma.*[2].setCnt(value),
0xD4 => dma.*[3].setSad(value),
0xD8 => dma.*[3].setDad(value),
0xDC => dma.*[3].setCnt(value),
u32 => switch (byte_addr) {
0xB0 => dma.*[0].setDmasad(value),
0xB4 => dma.*[0].setDmadad(value),
0xB8 => dma.*[0].setDmacnt(value),
0xBC => dma.*[1].setDmasad(value),
0xC0 => dma.*[1].setDmadad(value),
0xC4 => dma.*[1].setDmacnt(value),
0xC8 => dma.*[2].setDmasad(value),
0xCC => dma.*[2].setDmadad(value),
0xD0 => dma.*[2].setDmacnt(value),
0xD4 => dma.*[3].setDmasad(value),
0xD8 => dma.*[3].setDmadad(value),
0xDC => dma.*[3].setDmacnt(value),
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
},
u16 => switch (byte) {
0xB0 => dma.*[0].setSad(setU32L(dma.*[0].sad, value)),
0xB2 => dma.*[0].setSad(setU32H(dma.*[0].sad, value)),
0xB4 => dma.*[0].setDad(setU32L(dma.*[0].dad, value)),
0xB6 => dma.*[0].setDad(setU32H(dma.*[0].dad, value)),
0xB8 => dma.*[0].setCntL(value),
0xBA => dma.*[0].setCntH(value),
u16 => switch (byte_addr) {
0xB0, 0xB2 => dma.*[0].setDmasad(setHalf(u32, dma.*[0].sad, byte_addr, value)),
0xB4, 0xB6 => dma.*[0].setDmadad(setHalf(u32, dma.*[0].dad, byte_addr, value)),
0xB8 => dma.*[0].setDmacntL(value),
0xBA => dma.*[0].setDmacntH(value),
0xBC => dma.*[1].setSad(setU32L(dma.*[1].sad, value)),
0xBE => dma.*[1].setSad(setU32H(dma.*[1].sad, value)),
0xC0 => dma.*[1].setDad(setU32L(dma.*[1].dad, value)),
0xC2 => dma.*[1].setDad(setU32H(dma.*[1].dad, value)),
0xC4 => dma.*[1].setCntL(value),
0xC6 => dma.*[1].setCntH(value),
0xBC, 0xBE => dma.*[1].setDmasad(setHalf(u32, dma.*[1].sad, byte_addr, value)),
0xC0, 0xC2 => dma.*[1].setDmadad(setHalf(u32, dma.*[1].dad, byte_addr, value)),
0xC4 => dma.*[1].setDmacntL(value),
0xC6 => dma.*[1].setDmacntH(value),
0xC8 => dma.*[2].setSad(setU32L(dma.*[2].sad, value)),
0xCA => dma.*[2].setSad(setU32H(dma.*[2].sad, value)),
0xCC => dma.*[2].setDad(setU32L(dma.*[2].dad, value)),
0xCE => dma.*[2].setDad(setU32H(dma.*[2].dad, value)),
0xD0 => dma.*[2].setCntL(value),
0xD2 => dma.*[2].setCntH(value),
0xC8, 0xCA => dma.*[2].setDmasad(setHalf(u32, dma.*[2].sad, byte_addr, value)),
0xCC, 0xCE => dma.*[2].setDmadad(setHalf(u32, dma.*[2].dad, byte_addr, value)),
0xD0 => dma.*[2].setDmacntL(value),
0xD2 => dma.*[2].setDmacntH(value),
0xD4 => dma.*[3].setSad(setU32L(dma.*[3].sad, value)),
0xD6 => dma.*[3].setSad(setU32H(dma.*[3].sad, value)),
0xD8 => dma.*[3].setDad(setU32L(dma.*[3].dad, value)),
0xDA => dma.*[3].setDad(setU32H(dma.*[3].dad, value)),
0xDC => dma.*[3].setCntL(value),
0xDE => dma.*[3].setCntH(value),
0xD4, 0xD6 => dma.*[3].setDmasad(setHalf(u32, dma.*[3].sad, byte_addr, value)),
0xD8, 0xDA => dma.*[3].setDmadad(setHalf(u32, dma.*[3].dad, byte_addr, value)),
0xDC => dma.*[3].setDmacntL(value),
0xDE => dma.*[3].setDmacntH(value),
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
},
u8 => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
u8 => switch (byte_addr) {
0xB0, 0xB1, 0xB2, 0xB3 => dma.*[0].setDmasad(setQuart(dma.*[0].sad, byte_addr, value)),
0xB4, 0xB5, 0xB6, 0xB7 => dma.*[0].setDmadad(setQuart(dma.*[0].dad, byte_addr, value)),
0xB8, 0xB9 => dma.*[0].setDmacntL(setHalf(u16, dma.*[0].word_count, byte_addr, value)),
0xBA, 0xBB => dma.*[0].setDmacntH(setHalf(u16, dma.*[0].cnt.raw, byte_addr, value)),
0xBC, 0xBD, 0xBE, 0xBF => dma.*[1].setDmasad(setQuart(dma.*[1].sad, byte_addr, value)),
0xC0, 0xC1, 0xC2, 0xC3 => dma.*[1].setDmadad(setQuart(dma.*[1].dad, byte_addr, value)),
0xC4, 0xC5 => dma.*[1].setDmacntL(setHalf(u16, dma.*[1].word_count, byte_addr, value)),
0xC6, 0xC7 => dma.*[1].setDmacntH(setHalf(u16, dma.*[1].cnt.raw, byte_addr, value)),
0xC8, 0xC9, 0xCA, 0xCB => dma.*[2].setDmasad(setQuart(dma.*[2].sad, byte_addr, value)),
0xCC, 0xCD, 0xCE, 0xCF => dma.*[2].setDmadad(setQuart(dma.*[2].dad, byte_addr, value)),
0xD0, 0xD1 => dma.*[2].setDmacntL(setHalf(u16, dma.*[2].word_count, byte_addr, value)),
0xD2, 0xD3 => dma.*[2].setDmacntH(setHalf(u16, dma.*[2].cnt.raw, byte_addr, value)),
0xD4, 0xD5, 0xD6, 0xD7 => dma.*[3].setDmasad(setQuart(dma.*[3].sad, byte_addr, value)),
0xD8, 0xD9, 0xDA, 0xDB => dma.*[3].setDmadad(setQuart(dma.*[3].dad, byte_addr, value)),
0xDC, 0xDD => dma.*[3].setDmacntL(setHalf(u16, dma.*[3].word_count, byte_addr, value)),
0xDE, 0xDF => dma.*[3].setDmacntH(setHalf(u16, dma.*[3].cnt.raw, byte_addr, value)),
else => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
},
else => @compileError("DMA: Unsupported write width"),
}
}
@@ -96,6 +150,7 @@ fn DmaController(comptime id: u2) type {
const sad_mask: u32 = if (id == 0) 0x07FF_FFFF else 0x0FFF_FFFF;
const dad_mask: u32 = if (id != 3) 0x07FF_FFFF else 0x0FFF_FFFF;
const WordCount = if (id == 3) u16 else u14;
/// Write-only. The first address in a DMA transfer. (DMASAD)
/// Note: use writeSrc instead of manipulating src_addr directly
@@ -104,20 +159,19 @@ fn DmaController(comptime id: u2) type {
/// Note: Use writeDst instead of manipulatig dst_addr directly
dad: u32,
/// Write-only. The Word Count for the DMA Transfer (DMACNT_L)
word_count: if (id == 3) u16 else u14,
word_count: WordCount,
/// Read / Write. DMACNT_H
/// Note: Use writeControl instead of manipulating cnt directly.
cnt: DmaControl,
/// Internal. The last successfully read value
data_latch: u32,
/// Internal. Currrent Source Address
_sad: u32,
sad_latch: u32,
/// Internal. Current Destination Address
_dad: u32,
dad_latch: u32,
/// Internal. Word Count
_word_count: if (id == 3) u16 else u14,
// Internal. FIFO Word Count
_fifo_word_count: u8,
_word_count: WordCount,
/// Some DMA Transfers are enabled during Hblank / VBlank and / or
/// have delays. Thefore bit 15 of DMACNT isn't actually something
@@ -132,34 +186,39 @@ fn DmaController(comptime id: u2) type {
.cnt = .{ .raw = 0x000 },
// Internals
._sad = 0,
._dad = 0,
.sad_latch = 0,
.dad_latch = 0,
.data_latch = 0,
._word_count = 0,
._fifo_word_count = 4,
.in_progress = false,
};
}
pub fn setSad(self: *Self, addr: u32) void {
pub fn setDmasad(self: *Self, addr: u32) void {
self.sad = addr & sad_mask;
}
pub fn setDad(self: *Self, addr: u32) void {
pub fn setDmadad(self: *Self, addr: u32) void {
self.dad = addr & dad_mask;
}
pub fn setCntL(self: *Self, halfword: u16) void {
pub fn setDmacntL(self: *Self, halfword: u16) void {
self.word_count = @truncate(@TypeOf(self.word_count), halfword);
}
pub fn setCntH(self: *Self, halfword: u16) void {
pub fn dmacntH(self: *const Self) u16 {
return self.cnt.raw & if (id == 3) 0xFFE0 else 0xF7E0;
}
pub fn setDmacntH(self: *Self, halfword: u16) void {
const new = DmaControl{ .raw = halfword };
if (!self.cnt.enabled.read() and new.enabled.read()) {
// Reload Internals on Rising Edge.
self._sad = self.sad;
self._dad = self.dad;
self._word_count = if (self.word_count == 0) std.math.maxInt(@TypeOf(self._word_count)) else self.word_count;
self.sad_latch = self.sad;
self.dad_latch = self.dad;
self._word_count = if (self.word_count == 0) std.math.maxInt(WordCount) else self.word_count;
// Only a Start Timing of 00 has a DMA Transfer immediately begin
self.in_progress = new.start_timing.read() == 0b00;
@@ -168,38 +227,50 @@ fn DmaController(comptime id: u2) type {
self.cnt.raw = halfword;
}
pub fn setCnt(self: *Self, word: u32) void {
self.setCntL(@truncate(u16, word));
self.setCntH(@truncate(u16, word >> 16));
pub fn setDmacnt(self: *Self, word: u32) void {
self.setDmacntL(@truncate(u16, word));
self.setDmacntH(@truncate(u16, word >> 16));
}
pub fn step(self: *Self, cpu: *Arm7tdmi) void {
const is_fifo = (id == 1 or id == 2) and self.cnt.start_timing.read() == 0b11;
const sad_adj = Self.adjustment(self.cnt.sad_adj.read());
const dad_adj = if (is_fifo) .Fixed else Self.adjustment(self.cnt.dad_adj.read());
const sad_adj = @intToEnum(Adjustment, self.cnt.sad_adj.read());
const dad_adj = if (is_fifo) .Fixed else @intToEnum(Adjustment, self.cnt.dad_adj.read());
const transfer_type = is_fifo or self.cnt.transfer_type.read();
const offset: u32 = if (transfer_type) @sizeOf(u32) else @sizeOf(u16);
const mask = if (transfer_type) ~@as(u32, 3) else ~@as(u32, 1);
const sad_addr = self.sad_latch & mask;
const dad_addr = self.dad_latch & mask;
if (transfer_type) {
cpu.bus.write(u32, self._dad & mask, cpu.bus.read(u32, self._sad & mask));
if (sad_addr >= 0x0200_0000) self.data_latch = cpu.bus.read(u32, sad_addr);
cpu.bus.write(u32, dad_addr, self.data_latch);
} else {
cpu.bus.write(u16, self._dad & mask, cpu.bus.read(u16, self._sad & mask));
if (sad_addr >= 0x0200_0000) {
const value: u32 = cpu.bus.read(u16, sad_addr);
self.data_latch = value << 16 | value;
}
switch (sad_adj) {
.Increment => self._sad +%= offset,
.Decrement => self._sad -%= offset,
// TODO: Is just ignoring this ok?
cpu.bus.write(u16, dad_addr, @truncate(u16, rotr(u32, self.data_latch, 8 * (dad_addr & 3))));
}
switch (@truncate(u8, sad_addr >> 24)) {
// according to fleroviux, DMAs with a source address in ROM misbehave
// the resultant behaviour is that the source address will increment despite what DMAXCNT says
0x08...0x0D => self.sad_latch +%= offset, // obscure behaviour
else => switch (sad_adj) {
.Increment => self.sad_latch +%= offset,
.Decrement => self.sad_latch -%= offset,
.IncrementReload => log.err("{} is a prohibited adjustment on SAD", .{sad_adj}),
.Fixed => {},
},
}
switch (dad_adj) {
.Increment, .IncrementReload => self._dad +%= offset,
.Decrement => self._dad -%= offset,
.Increment, .IncrementReload => self.dad_latch +%= offset,
.Decrement => self.dad_latch -%= offset,
.Fixed => {},
}
@@ -227,7 +298,7 @@ fn DmaController(comptime id: u2) type {
}
}
pub fn pollBlankingDma(self: *Self, comptime kind: DmaKind) void {
fn poll(self: *Self, comptime kind: DmaKind) void {
if (self.in_progress) return; // If there's an ongoing DMA Transfer, exit early
// No ongoing DMA Transfer, We want to check if we should repeat an existing one
@@ -243,11 +314,11 @@ fn DmaController(comptime id: u2) type {
// Reload internal DAD latch if we are in IncrementRelaod
if (self.in_progress) {
self._word_count = if (self.word_count == 0) std.math.maxInt(@TypeOf(self._word_count)) else self.word_count;
if (Self.adjustment(self.cnt.dad_adj.read()) == .IncrementReload) self._dad = self.dad;
if (@intToEnum(Adjustment, self.cnt.dad_adj.read()) == .IncrementReload) self.dad_latch = self.dad;
}
}
pub fn requestSoundDma(self: *Self, _: u32) void {
pub fn requestAudio(self: *Self, _: u32) void {
comptime std.debug.assert(id == 1 or id == 2);
if (self.in_progress) return; // APU must wait their turn
@@ -259,23 +330,19 @@ fn DmaController(comptime id: u2) type {
// We Assume DMACNT_L is set to 4
// FIXME: Safe to just assume whatever DAD is set to is the FIFO Address?
// self._dad = fifo_addr;
// self.dad_latch = fifo_addr;
self.cnt.repeat.set();
self._word_count = 4;
self.in_progress = true;
}
fn adjustment(idx: u2) Adjustment {
return std.meta.intToEnum(Adjustment, idx) catch unreachable;
}
};
}
pub fn pollBlankingDma(bus: *Bus, comptime kind: DmaKind) void {
bus.dma[0].pollBlankingDma(kind);
bus.dma[1].pollBlankingDma(kind);
bus.dma[2].pollBlankingDma(kind);
bus.dma[3].pollBlankingDma(kind);
pub fn pollDmaOnBlank(bus: *Bus, comptime kind: DmaKind) void {
comptime var i: usize = 0;
inline while (i < 4) : (i += 1) {
bus.dma[i].poll(kind);
}
}
const Adjustment = enum(u2) {
@@ -291,11 +358,3 @@ const DmaKind = enum(u2) {
VBlank,
Special,
};
fn setU32L(left: u32, right: u16) u32 {
return (left & 0xFFFF_0000) | right;
}
fn setU32H(left: u32, right: u16) u32 {
return (left & 0x0000_FFFF) | (@as(u32, right) << 16);
}

464
src/core/bus/gpio.zig Normal file
View File

@@ -0,0 +1,464 @@
const std = @import("std");
const Bit = @import("bitfield").Bit;
const DateTime = @import("datetime").datetime.Datetime;
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
const Allocator = std.mem.Allocator;
/// GPIO Register Implementation
pub const Gpio = struct {
const Self = @This();
const log = std.log.scoped(.Gpio);
data: u4,
direction: u4,
cnt: u1,
device: Device,
const Register = enum { Data, Direction, Control };
pub const Device = struct {
ptr: ?*anyopaque,
kind: Kind, // TODO: Make comptime known?
pub const Kind = enum { Rtc, None };
fn step(self: *Device, value: u4) u4 {
return switch (self.kind) {
.Rtc => blk: {
const clock = @ptrCast(*Clock, @alignCast(@alignOf(*Clock), self.ptr.?));
break :blk clock.step(Clock.Data{ .raw = value });
},
.None => value,
};
}
fn init(kind: Kind, ptr: ?*anyopaque) Device {
return .{ .kind = kind, .ptr = ptr };
}
};
pub fn write(self: *Self, comptime reg: Register, value: if (reg == .Control) u1 else u4) void {
switch (reg) {
.Data => {
const masked_value = value & self.direction;
// The value which is actually stored in the GPIO register
// might be modified by the device implementing the GPIO interface e.g. RTC reads
self.data = self.device.step(masked_value);
},
.Direction => self.direction = value,
.Control => self.cnt = value,
}
}
pub fn read(self: *const Self, comptime reg: Register) if (reg == .Control) u1 else u4 {
if (self.cnt == 0) return 0;
return switch (reg) {
.Data => self.data & ~self.direction,
.Direction => self.direction,
.Control => self.cnt,
};
}
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, kind: Device.Kind) !*Self {
log.info("Device: {}", .{kind});
const self = try allocator.create(Self);
errdefer allocator.destroy(self);
self.* = .{
.data = 0b0000,
.direction = 0b1111, // TODO: What is GPIO Direction set to by default?
.cnt = 0b0,
.device = switch (kind) {
.Rtc => blk: {
const clock = try allocator.create(Clock);
clock.init(cpu, self);
break :blk Device{ .kind = kind, .ptr = clock };
},
.None => Device{ .kind = kind, .ptr = null },
},
};
return self;
}
pub fn deinit(self: *Self, allocator: Allocator) void {
switch (self.device.kind) {
.Rtc => allocator.destroy(@ptrCast(*Clock, @alignCast(@alignOf(*Clock), self.device.ptr.?))),
.None => {},
}
self.* = undefined;
}
};
/// GBA Real Time Clock
pub const Clock = struct {
const Self = @This();
const log = std.log.scoped(.Rtc);
writer: Writer,
reader: Reader,
state: State,
cnt: Control,
year: u8,
month: u5,
day: u6,
weekday: u3,
hour: u6,
minute: u7,
second: u7,
cpu: *Arm7tdmi,
gpio: *const Gpio,
const Register = enum {
Control,
DateTime,
Time,
};
const State = union(enum) {
Idle,
Command,
Write: Register,
Read: Register,
};
const Reader = struct {
i: u4,
count: u8,
/// Reads a bit from RTC registers. Which bit it reads is dependent on
///
/// 1. The RTC State Machine, whitch tells us which register we're accessing
/// 2. A `count`, which keeps track of which byte is currently being read
/// 3. An index, which keeps track of which bit of the byte determined by `count` is being read
fn read(self: *Reader, clock: *const Clock, register: Register) u1 {
const idx = @intCast(u3, self.i);
defer self.i += 1;
// FIXME: What do I do about the unused bits?
return switch (register) {
.Control => @truncate(u1, switch (self.count) {
0 => clock.cnt.raw >> idx,
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 1 byte)", .{ self.count, register }),
}),
.DateTime => @truncate(u1, switch (self.count) {
// Date
0 => clock.year >> idx,
1 => @as(u8, clock.month) >> idx,
2 => @as(u8, clock.day) >> idx,
3 => @as(u8, clock.weekday) >> idx,
// Time
4 => @as(u8, clock.hour) >> idx,
5 => @as(u8, clock.minute) >> idx,
6 => @as(u8, clock.second) >> idx,
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 7 bytes)", .{ self.count, register }),
}),
.Time => @truncate(u1, switch (self.count) {
0 => @as(u8, clock.hour) >> idx,
1 => @as(u8, clock.minute) >> idx,
2 => @as(u8, clock.second) >> idx,
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 3 bytes)", .{ self.count, register }),
}),
};
}
/// Is true when a Reader has read a u8's worth of bits
fn finished(self: *const Reader) bool {
return self.i >= 8;
}
/// Resets the index used to shift bits out of RTC registers
/// and `count`, which is used to keep track of which byte we're reading
/// is incremeneted
fn lap(self: *Reader) void {
self.i = 0;
self.count += 1;
}
/// Resets the state of a `Reader` in preparation for a future
/// read command
fn reset(self: *Reader) void {
self.i = 0;
self.count = 0;
}
};
const Writer = struct {
buf: u8,
i: u4,
/// The Number of bytes written since last reset
count: u8,
/// Append a bit to the internal bit buffer (aka an integer)
fn push(self: *Writer, value: u1) void {
const idx = @intCast(u3, self.i);
self.buf = (self.buf & ~(@as(u8, 1) << idx)) | @as(u8, value) << idx;
self.i += 1;
}
/// Takes the contents of the internal buffer and writes it to an RTC register
/// Where it writes to is dependent on:
///
/// 1. The RTC State Machine, whitch tells us which register we're accessing
/// 2. A `count`, which keeps track of which byte is currently being read
fn write(self: *const Writer, clock: *Clock, register: Register) void {
// FIXME: What do do about unused bits?
switch (register) {
.Control => switch (self.count) {
0 => clock.cnt.raw = (clock.cnt.raw & 0x80) | (self.buf & 0x7F), // Bit 7 read-only
else => std.debug.panic("Tried to write to byte #{} of {} (hint: there's only 1 byte)", .{ self.count, register }),
},
.DateTime, .Time => log.debug("Ignoring {} write", .{register}),
}
}
/// Is true when 8 bits have been shifted into the internal buffer
fn finished(self: *const Writer) bool {
return self.i >= 8;
}
/// Resets the internal buffer
/// resets the index used to shift bits into the internal buffer
/// increments `count` (which keeps track of byte offsets) by one
fn lap(self: *Writer) void {
self.buf = 0;
self.i = 0;
self.count += 1;
}
/// Resets `Writer` to a clean state in preparation for a future write command
fn reset(self: *Writer) void {
self.buf = 0;
self.i = 0;
self.count = 0;
}
};
const Data = extern union {
sck: Bit(u8, 0),
sio: Bit(u8, 1),
cs: Bit(u8, 2),
raw: u8,
};
const Control = extern union {
/// Unknown, value should be preserved though
unk: Bit(u8, 1),
/// Per-minute IRQ
/// If set, fire a Gamepak IRQ every 30s,
irq: Bit(u8, 3),
/// 12/24 Hour Bit
/// If set, 12h mode
/// If cleared, 24h mode
mode: Bit(u8, 6),
/// Read-Only, bit cleared on read
/// If is set, means that there has been a failure / time has been lost
off: Bit(u8, 7),
raw: u8,
};
fn init(ptr: *Self, cpu: *Arm7tdmi, gpio: *const Gpio) void {
ptr.* = .{
.writer = .{ .buf = 0, .i = 0, .count = 0 },
.reader = .{ .i = 0, .count = 0 },
.state = .Idle,
.cnt = .{ .raw = 0 },
.year = 0x01,
.month = 0x6,
.day = 0x13,
.weekday = 0x3,
.hour = 0x23,
.minute = 0x59,
.second = 0x59,
.cpu = cpu,
.gpio = gpio, // Can't use Arm7tdmi ptr b/c not initialized yet
};
cpu.sched.push(.RealTimeClock, 1 << 24); // Every Second
}
pub fn onClockUpdate(self: *Self, late: u64) void {
self.cpu.sched.push(.RealTimeClock, (1 << 24) -| late); // Reschedule
const now = DateTime.now();
self.year = bcd(u8, @intCast(u8, now.date.year - 2000));
self.month = bcd(u5, now.date.month);
self.day = bcd(u6, now.date.day);
self.weekday = bcd(u3, (now.date.weekday() + 1) % 7); // API is Monday = 0, Sunday = 6. We want Sunday = 0, Saturday = 6
self.hour = bcd(u6, now.time.hour);
self.minute = bcd(u7, now.time.minute);
self.second = bcd(u7, now.time.second);
}
fn step(self: *Self, value: Data) u4 {
const cache: Data = .{ .raw = self.gpio.data };
return switch (self.state) {
.Idle => blk: {
// FIXME: Maybe check incoming value to see if SCK is also high?
if (cache.sck.read()) {
if (!cache.cs.read() and value.cs.read()) {
log.debug("Entering Command Mode", .{});
self.state = .Command;
}
}
break :blk @truncate(u4, value.raw);
},
.Command => blk: {
if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state});
// If SCK rises, sample SIO
if (!cache.sck.read() and value.sck.read()) {
self.writer.push(@boolToInt(value.sio.read()));
if (self.writer.finished()) {
self.state = self.processCommand(self.writer.buf);
self.writer.reset();
log.debug("Switching to {}", .{self.state});
}
}
break :blk @truncate(u4, value.raw);
},
.Write => |register| blk: {
if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state});
// If SCK rises, sample SIO
if (!cache.sck.read() and value.sck.read()) {
self.writer.push(@boolToInt(value.sio.read()));
const register_width: u32 = switch (register) {
.Control => 1,
.DateTime => 7,
.Time => 3,
};
if (self.writer.finished()) {
self.writer.write(self, register); // write inner buffer to RTC register
self.writer.lap();
if (self.writer.count == register_width) {
self.writer.reset();
self.state = .Idle;
}
}
}
break :blk @truncate(u4, value.raw);
},
.Read => |register| blk: {
if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state});
var ret = value;
// if SCK rises, sample SIO
if (!cache.sck.read() and value.sck.read()) {
ret.sio.write(self.reader.read(self, register) == 0b1);
const register_width: u32 = switch (register) {
.Control => 1,
.DateTime => 7,
.Time => 3,
};
if (self.reader.finished()) {
self.reader.lap();
if (self.reader.count == register_width) {
self.reader.reset();
self.state = .Idle;
}
}
}
break :blk @truncate(u4, ret.raw);
},
};
}
fn reset(self: *Self) void {
// mGBA and NBA only zero the control register. We will do the same
log.debug("Reset (control register was zeroed)", .{});
self.cnt.raw = 0;
}
fn irq(self: *Self) void {
// TODO: Confirm that this is the right behaviour
log.debug("Force GamePak IRQ", .{});
self.cpu.bus.io.irq.game_pak.set();
self.cpu.handleInterrupt();
}
fn processCommand(self: *Self, raw_command: u8) State {
const command = blk: {
// If High Nybble is 0x6, no need to switch the endianness
if (raw_command >> 4 & 0xF == 0x6) break :blk raw_command;
// Turns out reversing the order of bits isn't trivial at all
// https://stackoverflow.com/questions/2602823/in-c-c-whats-the-simplest-way-to-reverse-the-order-of-bits-in-a-byte
var ret = raw_command;
ret = (ret & 0xF0) >> 4 | (ret & 0x0F) << 4;
ret = (ret & 0xCC) >> 2 | (ret & 0x33) << 2;
ret = (ret & 0xAA) >> 1 | (ret & 0x55) << 1;
break :blk ret;
};
log.debug("Handling Command 0x{X:0>2} [0b{b:0>8}]", .{ command, command });
const is_write = command & 1 == 0;
const rtc_register = @truncate(u3, command >> 1 & 0x7);
if (is_write) {
return switch (rtc_register) {
0 => blk: {
self.reset();
break :blk .Idle;
},
1 => .{ .Write = .Control },
2 => .{ .Write = .DateTime },
3 => .{ .Write = .Time },
6 => blk: {
self.irq();
break :blk .Idle;
},
4, 5, 7 => .Idle,
};
} else {
return switch (rtc_register) {
1 => .{ .Read = .Control },
2 => .{ .Read = .DateTime },
3 => .{ .Read = .Time },
0, 4, 5, 6, 7 => .Idle, // Do Nothing
};
}
}
};
fn bcd(comptime T: type, value: u8) T {
var input = value;
var ret: u8 = 0;
var shift: u3 = 0;
while (input > 0) {
ret |= (input % 10) << (shift << 2);
shift += 1;
input /= 10;
}
return @truncate(T, ret);
}

View File

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

View File

@@ -1,69 +1,100 @@
const std = @import("std");
const util = @import("../util.zig");
const util = @import("../../util.zig");
const TimerControl = @import("io.zig").TimerControl;
const Io = @import("io.zig").Io;
const Scheduler = @import("../scheduler.zig").Scheduler;
const Event = @import("../scheduler.zig").Event;
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
pub const TimerTuple = std.meta.Tuple(&[_]type{ Timer(0), Timer(1), Timer(2), Timer(3) });
pub const TimerTuple = struct { Timer(0), Timer(1), Timer(2), Timer(3) };
const log = std.log.scoped(.Timer);
const getHalf = util.getHalf;
const setHalf = util.setHalf;
pub fn create(sched: *Scheduler) TimerTuple {
return .{ Timer(0).init(sched), Timer(1).init(sched), Timer(2).init(sched), Timer(3).init(sched) };
}
pub fn read(comptime T: type, tim: *const TimerTuple, addr: u32) ?T {
const nybble = @truncate(u4, addr);
const nybble_addr = @truncate(u4, addr);
return switch (T) {
u32 => switch (nybble) {
0x0 => @as(T, tim.*[0].cnt.raw) << 16 | tim.*[0].getCntL(),
0x4 => @as(T, tim.*[1].cnt.raw) << 16 | tim.*[1].getCntL(),
0x8 => @as(T, tim.*[2].cnt.raw) << 16 | tim.*[2].getCntL(),
0xC => @as(T, tim.*[3].cnt.raw) << 16 | tim.*[3].getCntL(),
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
u32 => switch (nybble_addr) {
0x0 => @as(T, tim.*[0].cnt.raw) << 16 | tim.*[0].timcntL(),
0x4 => @as(T, tim.*[1].cnt.raw) << 16 | tim.*[1].timcntL(),
0x8 => @as(T, tim.*[2].cnt.raw) << 16 | tim.*[2].timcntL(),
0xC => @as(T, tim.*[3].cnt.raw) << 16 | tim.*[3].timcntL(),
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
},
u16 => switch (nybble) {
0x0 => tim.*[0].getCntL(),
u16 => switch (nybble_addr) {
0x0 => tim.*[0].timcntL(),
0x2 => tim.*[0].cnt.raw,
0x4 => tim.*[1].getCntL(),
0x4 => tim.*[1].timcntL(),
0x6 => tim.*[1].cnt.raw,
0x8 => tim.*[2].getCntL(),
0x8 => tim.*[2].timcntL(),
0xA => tim.*[2].cnt.raw,
0xC => tim.*[3].getCntL(),
0xC => tim.*[3].timcntL(),
0xE => tim.*[3].cnt.raw,
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
},
u8 => switch (nybble_addr) {
0x0, 0x1 => @truncate(T, tim.*[0].timcntL() >> getHalf(nybble_addr)),
0x2, 0x3 => @truncate(T, tim.*[0].cnt.raw >> getHalf(nybble_addr)),
0x4, 0x5 => @truncate(T, tim.*[1].timcntL() >> getHalf(nybble_addr)),
0x6, 0x7 => @truncate(T, tim.*[1].cnt.raw >> getHalf(nybble_addr)),
0x8, 0x9 => @truncate(T, tim.*[2].timcntL() >> getHalf(nybble_addr)),
0xA, 0xB => @truncate(T, tim.*[2].cnt.raw >> getHalf(nybble_addr)),
0xC, 0xD => @truncate(T, tim.*[3].timcntL() >> getHalf(nybble_addr)),
0xE, 0xF => @truncate(T, tim.*[3].cnt.raw >> getHalf(nybble_addr)),
},
u8 => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
else => @compileError("TIM: Unsupported read width"),
};
}
pub fn write(comptime T: type, tim: *TimerTuple, addr: u32, value: T) void {
const nybble = @truncate(u4, addr);
const nybble_addr = @truncate(u4, addr);
return switch (T) {
u32 => switch (nybble) {
0x0 => tim.*[0].setCnt(value),
0x4 => tim.*[1].setCnt(value),
0x8 => tim.*[2].setCnt(value),
0xC => tim.*[3].setCnt(value),
u32 => switch (nybble_addr) {
0x0 => tim.*[0].setTimcnt(value),
0x4 => tim.*[1].setTimcnt(value),
0x8 => tim.*[2].setTimcnt(value),
0xC => tim.*[3].setTimcnt(value),
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
},
u16 => switch (nybble) {
0x0 => tim.*[0].setCntL(value),
0x2 => tim.*[0].setCntH(value),
0x4 => tim.*[1].setCntL(value),
0x6 => tim.*[1].setCntH(value),
0x8 => tim.*[2].setCntL(value),
0xA => tim.*[2].setCntH(value),
0xC => tim.*[3].setCntL(value),
0xE => tim.*[3].setCntH(value),
u16 => switch (nybble_addr) {
0x0 => tim.*[0].setTimcntL(value),
0x2 => tim.*[0].setTimcntH(value),
0x4 => tim.*[1].setTimcntL(value),
0x6 => tim.*[1].setTimcntH(value),
0x8 => tim.*[2].setTimcntL(value),
0xA => tim.*[2].setTimcntH(value),
0xC => tim.*[3].setTimcntL(value),
0xE => tim.*[3].setTimcntH(value),
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
},
u8 => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
u8 => switch (nybble_addr) {
0x0, 0x1 => tim.*[0].setTimcntL(setHalf(u16, tim.*[0]._reload, nybble_addr, value)),
0x2, 0x3 => tim.*[0].setTimcntH(setHalf(u16, tim.*[0].cnt.raw, nybble_addr, value)),
0x4, 0x5 => tim.*[1].setTimcntL(setHalf(u16, tim.*[1]._reload, nybble_addr, value)),
0x6, 0x7 => tim.*[1].setTimcntH(setHalf(u16, tim.*[1].cnt.raw, nybble_addr, value)),
0x8, 0x9 => tim.*[2].setTimcntL(setHalf(u16, tim.*[2]._reload, nybble_addr, value)),
0xA, 0xB => tim.*[2].setTimcntH(setHalf(u16, tim.*[2].cnt.raw, nybble_addr, value)),
0xC, 0xD => tim.*[3].setTimcntL(setHalf(u16, tim.*[3]._reload, nybble_addr, value)),
0xE, 0xF => tim.*[3].setTimcntH(setHalf(u16, tim.*[3].cnt.raw, nybble_addr, value)),
},
else => @compileError("TIM: Unsupported write width"),
};
}
@@ -72,13 +103,13 @@ fn Timer(comptime id: u2) type {
return struct {
const Self = @This();
/// Read Only, Internal. Please use self.getCntL()
/// Read Only, Internal. Please use self.timcntL()
_counter: u16,
/// Write Only, Internal. Please use self.setCntL()
/// Write Only, Internal. Please use self.setTimcntL()
_reload: u16,
/// Write Only, Internal. Please use self.setCntH()
/// Write Only, Internal. Please use self.setTimcntH()
cnt: TimerControl,
/// Internal.
@@ -97,47 +128,62 @@ fn Timer(comptime id: u2) type {
};
}
/// TIMCNT_L
pub fn getCntL(self: *const Self) u16 {
/// TIMCNT_L Getter
pub fn timcntL(self: *const Self) u16 {
if (self.cnt.cascade.read() or !self.cnt.enabled.read()) return self._counter;
return self._counter +% @truncate(u16, (self.sched.now() - self._start_timestamp) / self.frequency());
}
/// TIMCNT_L
pub fn setCntL(self: *Self, halfword: u16) void {
/// TIMCNT_L Setter
pub fn setTimcntL(self: *Self, halfword: u16) void {
self._reload = halfword;
}
/// TIMCNT_L & TIMCNT_H
pub fn setCnt(self: *Self, word: u32) void {
self.setCntL(@truncate(u16, word));
self.setCntH(@truncate(u16, word >> 16));
pub fn setTimcnt(self: *Self, word: u32) void {
self.setTimcntL(@truncate(u16, word));
self.setTimcntH(@truncate(u16, word >> 16));
}
/// TIMCNT_H
pub fn setCntH(self: *Self, halfword: u16) void {
pub fn setTimcntH(self: *Self, halfword: u16) void {
const new = TimerControl{ .raw = halfword };
// If Timer happens to be enabled, It will either be resheduled or disabled
if (self.cnt.enabled.read()) {
// timer was already enabled
// If enabled falling edge or cascade falling edge, timer is paused
if (!new.enabled.read() or (!self.cnt.cascade.read() and new.cascade.read())) {
self.sched.removeScheduledEvent(.{ .TimerOverflow = id });
if (self.cnt.enabled.read() and (new.cascade.read() or !new.enabled.read())) {
// Either through the cascade bit or the enable bit, the timer has effectively been disabled
// The Counter should hold whatever value it should have been at when it was disabled
// Counter should hold the value it stopped at meaning we have to calculate it now
self._counter +%= @truncate(u16, (self.sched.now() - self._start_timestamp) / self.frequency());
}
// The counter is only reloaded on the rising edge of the enable bit
if (!self.cnt.enabled.read() and new.enabled.read()) self._counter = self._reload;
// the timer has always been enabled, but the cascade bit which was blocking the timer has been unset
if (new.enabled.read() and (self.cnt.cascade.read() and !new.cascade.read())) {
// we want to reschedule the timer event, however we won't reload the counter.
// the invariant here is that self._counter holds the already calculated paused value
// If Timer is enabled and we're not cascading, we need to schedule an overflow event
if (new.enabled.read() and !new.cascade.read()) self.scheduleOverflow(0);
self.rescheduleTimerExpire(0);
}
} else {
// the timer was previously disabeld
if (new.enabled.read()) {
// timer should start counting (with a reloaded counter value)
self._counter = self._reload;
// if cascade happens to be set, the timer doesn't actually do anything though
if (!new.cascade.read()) self.rescheduleTimerExpire(0);
}
}
self.cnt.raw = halfword;
}
pub fn handleOverflow(self: *Self, cpu: *Arm7tdmi, late: u64) void {
pub fn onTimerExpire(self: *Self, cpu: *Arm7tdmi, late: u64) void {
// Fire IRQ if enabled
const io = &cpu.bus.io;
@@ -154,34 +200,31 @@ fn Timer(comptime id: u2) type {
// DMA Sound Things
if (id == 0 or id == 1) {
cpu.bus.apu.handleTimerOverflow(cpu, id);
cpu.bus.apu.onDmaAudioSampleRequest(cpu, id);
}
// Perform Cascade Behaviour
switch (id) {
0 => if (cpu.bus.tim[1].cnt.cascade.read()) {
cpu.bus.tim[1]._counter +%= 1;
if (cpu.bus.tim[1]._counter == 0) cpu.bus.tim[1].handleOverflow(cpu, late);
inline 0, 1, 2 => |idx| {
const next = idx + 1;
if (cpu.bus.tim[next].cnt.cascade.read()) {
cpu.bus.tim[next]._counter +%= 1;
if (cpu.bus.tim[next]._counter == 0) cpu.bus.tim[next].onTimerExpire(cpu, late);
}
},
1 => if (cpu.bus.tim[2].cnt.cascade.read()) {
cpu.bus.tim[2]._counter +%= 1;
if (cpu.bus.tim[2]._counter == 0) cpu.bus.tim[2].handleOverflow(cpu, late);
},
2 => if (cpu.bus.tim[3].cnt.cascade.read()) {
cpu.bus.tim[3]._counter +%= 1;
if (cpu.bus.tim[3]._counter == 0) cpu.bus.tim[3].handleOverflow(cpu, late);
},
3 => {}, // There is no Timer for TIM3 to "cascade" to,
3 => {}, // THere is no timer for TIM3 to cascade to
}
// Reschedule Timer if we're not cascading
if (!self.cnt.cascade.read()) {
// TIM0 cascade value is N/A
if (id == 0 or !self.cnt.cascade.read()) {
self._counter = self._reload;
self.scheduleOverflow(late);
self.rescheduleTimerExpire(late);
}
}
fn scheduleOverflow(self: *Self, late: u64) void {
fn rescheduleTimerExpire(self: *Self, late: u64) void {
const when = (@as(u64, 0x10000) - self._counter) * self.frequency();
self._start_timestamp = self.sched.now();

View File

@@ -1,14 +1,13 @@
const std = @import("std");
const util = @import("util.zig");
const Bus = @import("Bus.zig");
const Bit = @import("bitfield").Bit;
const Bitfield = @import("bitfield").Bitfield;
const Scheduler = @import("scheduler.zig").Scheduler;
const FilePaths = @import("util.zig").FilePaths;
const Logger = @import("util.zig").Logger;
const Logger = @import("../util.zig").Logger;
const File = std.fs.File;
const log = std.log.scoped(.Arm7Tdmi);
// ARM Instructions
pub const arm = struct {
@@ -236,45 +235,46 @@ pub const thumb = struct {
}
};
const cpu_logging = @import("emu.zig").cpu_logging;
const log = std.log.scoped(.Arm7Tdmi);
pub const Arm7tdmi = struct {
const Self = @This();
r: [16]u32,
pipe: Pipeline,
sched: *Scheduler,
bus: *Bus,
cpsr: PSR,
spsr: PSR,
/// Storage for R8_fiq -> R12_fiq and their normal counterparts
/// e.g [r[0 + 8], fiq_r[0 + 8], r[1 + 8], fiq_r[1 + 8]...]
banked_fiq: [2 * 5]u32,
/// Storage for r13_<mode>, r14_<mode>
/// e.g. [r13, r14, r13_svc, r14_svc]
banked_r: [2 * 6]u32,
banked_spsr: [5]PSR,
bank: Bank,
logger: ?Logger,
pub fn init(sched: *Scheduler, bus: *Bus, log_file: ?std.fs.File) Self {
return Self{
.r = [_]u32{0x00} ** 16,
.sched = sched,
.bus = bus,
.cpsr = .{ .raw = 0x0000_001F },
.spsr = .{ .raw = 0x0000_0000 },
.banked_fiq = [_]u32{0x00} ** 10,
.banked_r = [_]u32{0x00} ** 12,
.banked_spsr = [_]PSR{.{ .raw = 0x0000_0000 }} ** 5,
.logger = if (log_file) |file| Logger.init(file) else null,
/// Bank of Registers from other CPU Modes
const Bank = struct {
/// Storage for r13_<mode>, r14_<mode>
/// e.g. [r13, r14, r13_svc, r14_svc]
r: [2 * 6]u32,
/// Storage for R8_fiq -> R12_fiq and their normal counterparts
/// e.g [r[0 + 8], fiq_r[0 + 8], r[1 + 8], fiq_r[1 + 8]...]
fiq: [2 * 5]u32,
spsr: [5]PSR,
const Kind = enum(u1) {
R13 = 0,
R14,
};
pub fn create() Bank {
return .{
.r = [_]u32{0x00} ** 12,
.fiq = [_]u32{0x00} ** 10,
.spsr = [_]PSR{.{ .raw = 0x0000_0000 }} ** 5,
};
}
inline fn bankedIdx(mode: Mode, kind: BankedKind) usize {
inline fn regIdx(mode: Mode, kind: Kind) usize {
const idx: usize = switch (mode) {
.User, .System => 0,
.Supervisor => 1,
@@ -287,7 +287,7 @@ pub const Arm7tdmi = struct {
return (idx * 2) + if (kind == .R14) @as(usize, 1) else 0;
}
inline fn bankedSpsrIndex(mode: Mode) usize {
inline fn spsrIdx(mode: Mode) usize {
return switch (mode) {
.Supervisor => 0,
.Abort => 1,
@@ -298,9 +298,23 @@ pub const Arm7tdmi = struct {
};
}
inline fn bankedFiqIdx(i: usize, mode: Mode) usize {
inline fn fiqIdx(i: usize, mode: Mode) usize {
return (i * 2) + if (mode == .Fiq) @as(usize, 1) else 0;
}
};
pub fn init(sched: *Scheduler, bus: *Bus, log_file: ?std.fs.File) Self {
return Self{
.r = [_]u32{0x00} ** 16,
.pipe = Pipeline.init(),
.sched = sched,
.bus = bus,
.cpsr = .{ .raw = 0x0000_001F },
.spsr = .{ .raw = 0x0000_0000 },
.bank = Bank.create(),
.logger = if (log_file) |file| Logger.init(file) else null,
};
}
pub inline fn hasSPSR(self: *const Self) bool {
const mode = getModeChecked(self, self.cpsr.mode.read());
@@ -337,14 +351,14 @@ pub const Arm7tdmi = struct {
switch (idx) {
8...12 => {
if (current == .Fiq) {
self.banked_fiq[bankedFiqIdx(idx - 8, .User)] = value;
self.bank.fiq[Bank.fiqIdx(idx - 8, .User)] = value;
} else self.r[idx] = value;
},
13, 14 => switch (current) {
.User, .System => self.r[idx] = value,
else => {
const kind = std.meta.intToEnum(BankedKind, idx - 13) catch unreachable;
self.banked_r[bankedIdx(.User, kind)] = value;
const kind = std.meta.intToEnum(Bank.Kind, idx - 13) catch unreachable;
self.bank.r[Bank.regIdx(.User, kind)] = value;
},
},
else => self.r[idx] = value, // R0 -> R7 and R15
@@ -355,12 +369,12 @@ pub const Arm7tdmi = struct {
const current = getModeChecked(self, self.cpsr.mode.read());
return switch (idx) {
8...12 => if (current == .Fiq) self.banked_fiq[bankedFiqIdx(idx - 8, .User)] else self.r[idx],
8...12 => if (current == .Fiq) self.bank.fiq[Bank.fiqIdx(idx - 8, .User)] else self.r[idx],
13, 14 => switch (current) {
.User, .System => self.r[idx],
else => blk: {
const kind = std.meta.intToEnum(BankedKind, idx - 13) catch unreachable;
break :blk self.banked_r[bankedIdx(.User, kind)];
const kind = std.meta.intToEnum(Bank.Kind, idx - 13) catch unreachable;
break :blk self.bank.r[Bank.regIdx(.User, kind)];
},
},
else => self.r[idx], // R0 -> R7 and R15
@@ -373,67 +387,81 @@ pub const Arm7tdmi = struct {
// Bank R8 -> r12
var i: usize = 0;
while (i < 5) : (i += 1) {
self.banked_fiq[bankedFiqIdx(i, now)] = self.r[8 + i];
self.bank.fiq[Bank.fiqIdx(i, now)] = self.r[8 + i];
}
// Bank r13, r14, SPSR
switch (now) {
.User, .System => {
self.banked_r[bankedIdx(now, .R13)] = self.r[13];
self.banked_r[bankedIdx(now, .R14)] = self.r[14];
self.bank.r[Bank.regIdx(now, .R13)] = self.r[13];
self.bank.r[Bank.regIdx(now, .R14)] = self.r[14];
},
else => {
self.banked_r[bankedIdx(now, .R13)] = self.r[13];
self.banked_r[bankedIdx(now, .R14)] = self.r[14];
self.banked_spsr[bankedSpsrIndex(now)] = self.spsr;
self.bank.r[Bank.regIdx(now, .R13)] = self.r[13];
self.bank.r[Bank.regIdx(now, .R14)] = self.r[14];
self.bank.spsr[Bank.spsrIdx(now)] = self.spsr;
},
}
// Grab R8 -> R12
i = 0;
while (i < 5) : (i += 1) {
self.r[8 + i] = self.banked_fiq[bankedFiqIdx(i, next)];
self.r[8 + i] = self.bank.fiq[Bank.fiqIdx(i, next)];
}
// Grab r13, r14, SPSR
switch (next) {
.User, .System => {
self.r[13] = self.banked_r[bankedIdx(next, .R13)];
self.r[14] = self.banked_r[bankedIdx(next, .R14)];
self.r[13] = self.bank.r[Bank.regIdx(next, .R13)];
self.r[14] = self.bank.r[Bank.regIdx(next, .R14)];
},
else => {
self.r[13] = self.banked_r[bankedIdx(next, .R13)];
self.r[14] = self.banked_r[bankedIdx(next, .R14)];
self.spsr = self.banked_spsr[bankedSpsrIndex(next)];
self.r[13] = self.bank.r[Bank.regIdx(next, .R13)];
self.r[14] = self.bank.r[Bank.regIdx(next, .R14)];
self.spsr = self.bank.spsr[Bank.spsrIdx(next)];
},
}
self.cpsr.mode.write(@enumToInt(next));
}
/// Advances state so that the BIOS is skipped
///
/// Note: This accesses the CPU's bus ptr so it only may be called
/// once the Bus has been properly initialized
///
/// TODO: Make above notice impossible to do in code
pub fn fastBoot(self: *Self) void {
self.r = std.mem.zeroes([16]u32);
self.r[0] = 0x08000000;
self.r[1] = 0x000000EA;
// self.r[0] = 0x08000000;
// self.r[1] = 0x000000EA;
self.r[13] = 0x0300_7F00;
self.r[15] = 0x0800_0000;
self.banked_r[bankedIdx(.Irq, .R13)] = 0x0300_7FA0;
self.banked_r[bankedIdx(.Supervisor, .R13)] = 0x0300_7FE0;
self.bank.r[Bank.regIdx(.Irq, .R13)] = 0x0300_7FA0;
self.bank.r[Bank.regIdx(.Supervisor, .R13)] = 0x0300_7FE0;
self.cpsr.raw = 0x6000001F;
// self.cpsr.raw = 0x6000001F;
self.cpsr.raw = 0x0000_001F;
self.bus.bios.addr_latch = 0x0000_00DC + 8;
}
pub fn step(self: *Self) void {
defer {
if (!self.pipe.flushed) self.r[15] += if (self.cpsr.t.read()) 2 else @as(u32, 4);
self.pipe.flushed = false;
}
if (self.cpsr.t.read()) {
const opcode = self.fetch(u16);
if (cpu_logging) self.logger.?.mgbaLog(self, opcode);
const opcode = @truncate(u16, self.pipe.step(self, u16) orelse return);
if (self.logger) |*trace| trace.mgbaLog(self, opcode);
thumb.lut[thumb.idx(opcode)](self, self.bus, opcode);
} else {
const opcode = self.fetch(u32);
if (cpu_logging) self.logger.?.mgbaLog(self, opcode);
const opcode = self.pipe.step(self, u32) orelse return;
if (self.logger) |*trace| trace.mgbaLog(self, opcode);
if (checkCond(self.cpsr, @truncate(u4, opcode >> 28))) {
arm.lut[arm.idx(opcode)](self, self.bus, opcode);
@@ -442,29 +470,12 @@ pub const Arm7tdmi = struct {
}
pub fn stepDmaTransfer(self: *Self) bool {
const dma0 = &self.bus.dma[0];
const dma1 = &self.bus.dma[1];
const dma2 = &self.bus.dma[2];
const dma3 = &self.bus.dma[3];
if (dma0.in_progress) {
dma0.step(self);
comptime var i: usize = 0;
inline while (i < 4) : (i += 1) {
if (self.bus.dma[i].in_progress) {
self.bus.dma[i].step(self);
return true;
}
if (dma1.in_progress) {
dma1.step(self);
return true;
}
if (dma2.in_progress) {
dma2.step(self);
return true;
}
if (dma3.in_progress) {
dma3.step(self);
return true;
}
return false;
@@ -473,42 +484,41 @@ pub const Arm7tdmi = struct {
pub fn handleInterrupt(self: *Self) void {
const should_handle = self.bus.io.ie.raw & self.bus.io.irq.raw;
if (should_handle != 0) {
// Return if IME is disabled, CPSR I is set or there is nothing to handle
if (!self.bus.io.ime or self.cpsr.i.read() or should_handle == 0) return;
// If Pipeline isn't full, we have a bug
std.debug.assert(self.pipe.isFull());
// log.debug("Handling Interrupt!", .{});
self.bus.io.haltcnt = .Execute;
// log.debug("An Interrupt was Fired!", .{});
// Either IME is not true or I in CPSR is true
// Don't handle interrupts
if (!self.bus.io.ime or self.cpsr.i.read()) return;
// log.debug("An interrupt was Handled!", .{});
// retAddr.gba says r15 on it's own is off by -04h in both ARM and THUMB mode
const r15 = self.r[15] + 4;
const cpsr = self.cpsr.raw;
// FIXME: This seems weird, but retAddr.gba suggests I need to make these changes
const ret_addr = self.r[15] - if (self.cpsr.t.read()) 0 else @as(u32, 4);
const new_spsr = self.cpsr.raw;
self.changeMode(.Irq);
self.cpsr.t.write(false);
self.cpsr.i.write(true);
self.r[14] = r15;
self.spsr.raw = cpsr;
self.r[15] = 0x000_0018;
}
self.r[14] = ret_addr;
self.spsr.raw = new_spsr;
self.r[15] = 0x0000_0018;
self.pipe.reload(self);
}
inline fn fetch(self: *Self, comptime T: type) T {
inline fn fetch(self: *Self, comptime T: type, address: u32) T {
comptime std.debug.assert(T == u32 or T == u16); // Opcode may be 32-bit (ARM) or 16-bit (THUMB)
defer self.r[15] += if (T == u32) 4 else 2;
// FIXME: You better hope this is optimized out
// Bus.read will advance the scheduler. There are different timings for CPU fetches,
// so we want to undo what Bus.read will apply. We can do this by caching the current tick
// This is very dumb.
//
// FIXME: Please rework this
const tick_cache = self.sched.tick;
defer self.sched.tick = tick_cache + Bus.fetch_timings[@boolToInt(T == u32)][@truncate(u4, self.r[15] >> 24)];
defer self.sched.tick = tick_cache + Bus.fetch_timings[@boolToInt(T == u32)][@truncate(u4, address >> 24)];
return self.bus.read(T, self.r[15]);
}
pub fn fakePC(self: *const Self) u32 {
return self.r[15] + 4;
return self.bus.read(T, address);
}
pub fn panic(self: *const Self, comptime format: []const u8, args: anytype) noreturn {
@@ -520,10 +530,12 @@ pub const Arm7tdmi = struct {
std.debug.print("R{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\n", .{ i, self.r[i], i_1, self.r[i_1], i_2, self.r[i_2], i_3, self.r[i_3] });
}
std.debug.print("cpsr: 0x{X:0>8} ", .{self.cpsr.raw});
prettyPrintPsr(&self.cpsr);
self.cpsr.toString();
std.debug.print("spsr: 0x{X:0>8} ", .{self.spsr.raw});
prettyPrintPsr(&self.spsr);
self.spsr.toString();
std.debug.print("pipeline: {??X:0>8}\n", .{self.pipe.stage});
if (self.cpsr.t.read()) {
const opcode = self.bus.dbgRead(u16, self.r[15] - 4);
@@ -539,99 +551,74 @@ pub const Arm7tdmi = struct {
std.debug.panic(format, args);
}
fn prettyPrintPsr(psr: *const PSR) void {
std.debug.print("[", .{});
if (psr.n.read()) std.debug.print("N", .{}) else std.debug.print("-", .{});
if (psr.z.read()) std.debug.print("Z", .{}) else std.debug.print("-", .{});
if (psr.c.read()) std.debug.print("C", .{}) else std.debug.print("-", .{});
if (psr.v.read()) std.debug.print("V", .{}) else std.debug.print("-", .{});
if (psr.i.read()) std.debug.print("I", .{}) else std.debug.print("-", .{});
if (psr.f.read()) std.debug.print("F", .{}) else std.debug.print("-", .{});
if (psr.t.read()) std.debug.print("T", .{}) else std.debug.print("-", .{});
std.debug.print("|", .{});
if (getMode(psr.mode.read())) |mode| std.debug.print("{s}", .{modeString(mode)}) else std.debug.print("---", .{});
std.debug.print("]\n", .{});
}
fn modeString(mode: Mode) []const u8 {
return switch (mode) {
.User => "usr",
.Fiq => "fiq",
.Irq => "irq",
.Supervisor => "svc",
.Abort => "abt",
.Undefined => "und",
.System => "sys",
};
}
fn mgbaLog(self: *const Self, file: *const File, opcode: u32) !void {
const thumb_fmt = "{X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} cpsr: {X:0>8} | {X:0>4}:\n";
const arm_fmt = "{X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} cpsr: {X:0>8} | {X:0>8}:\n";
var buf: [0x100]u8 = [_]u8{0x00} ** 0x100; // this is larger than it needs to be
const r0 = self.r[0];
const r1 = self.r[1];
const r2 = self.r[2];
const r3 = self.r[3];
const r4 = self.r[4];
const r5 = self.r[5];
const r6 = self.r[6];
const r7 = self.r[7];
const r8 = self.r[8];
const r9 = self.r[9];
const r10 = self.r[10];
const r11 = self.r[11];
const r12 = self.r[12];
const r13 = self.r[13];
const r14 = self.r[14];
const r15 = self.r[15];
const c_psr = self.cpsr.raw;
var log_str: []u8 = undefined;
if (self.cpsr.t.read()) {
if (opcode >> 11 == 0x1E) {
// Instruction 1 of a BL Opcode, print in ARM mode
const other_half = self.bus.dbgRead(u16, self.r[15]);
const bl_opcode = @as(u32, opcode) << 16 | other_half;
log_str = try std.fmt.bufPrint(&buf, arm_fmt, .{ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, c_psr, bl_opcode });
} else {
log_str = try std.fmt.bufPrint(&buf, thumb_fmt, .{ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, c_psr, opcode });
}
} else {
log_str = try std.fmt.bufPrint(&buf, arm_fmt, .{ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, c_psr, opcode });
}
_ = try file.writeAll(log_str);
}
};
pub fn checkCond(cpsr: PSR, cond: u4) bool {
return switch (cond) {
0x0 => cpsr.z.read(), // EQ - Equal
0x1 => !cpsr.z.read(), // NE - Not equal
0x2 => cpsr.c.read(), // CS - Unsigned higher or same
0x3 => !cpsr.c.read(), // CC - Unsigned lower
0x4 => cpsr.n.read(), // MI - Negative
0x5 => !cpsr.n.read(), // PL - Positive or zero
0x6 => cpsr.v.read(), // VS - Overflow
0x7 => !cpsr.v.read(), // VC - No overflow
0x8 => cpsr.c.read() and !cpsr.z.read(), // HI - unsigned higher
0x9 => !cpsr.c.read() or cpsr.z.read(), // LS - unsigned lower or same
0xA => cpsr.n.read() == cpsr.v.read(), // GE - Greater or equal
0xB => cpsr.n.read() != cpsr.v.read(), // LT - Less than
0xC => !cpsr.z.read() and (cpsr.n.read() == cpsr.v.read()), // GT - Greater than
0xD => cpsr.z.read() or (cpsr.n.read() != cpsr.v.read()), // LE - Less than or equal
0xE => true, // AL - Always
0xF => false, // NV - Never (reserved in ARMv3 and up, but seems to have not changed?)
};
const condition_lut = [_]u16{
0xF0F0, // EQ - Equal
0x0F0F, // NE - Not Equal
0xCCCC, // CS - Unsigned higher or same
0x3333, // CC - Unsigned lower
0xFF00, // MI - Negative
0x00FF, // PL - Positive or Zero
0xAAAA, // VS - Overflow
0x5555, // VC - No Overflow
0x0C0C, // HI - unsigned hierh
0xF3F3, // LS - unsigned lower or same
0xAA55, // GE - greater or equal
0x55AA, // LT - less than
0x0A05, // GT - greater than
0xF5FA, // LE - less than or equal
0xFFFF, // AL - always
0x0000, // NV - never
};
pub inline fn checkCond(cpsr: PSR, cond: u4) bool {
const flags = @truncate(u4, cpsr.raw >> 28);
return condition_lut[cond] & (@as(u16, 1) << flags) != 0;
}
const Pipeline = struct {
const Self = @This();
stage: [2]?u32,
flushed: bool,
fn init() Self {
return .{
.stage = [_]?u32{null} ** 2,
.flushed = false,
};
}
pub fn isFull(self: *const Self) bool {
return self.stage[0] != null and self.stage[1] != null;
}
pub fn step(self: *Self, cpu: *Arm7tdmi, comptime T: type) ?u32 {
comptime std.debug.assert(T == u32 or T == u16);
const opcode = self.stage[0];
self.stage[0] = self.stage[1];
self.stage[1] = cpu.fetch(T, cpu.r[15]);
return opcode;
}
pub fn reload(self: *Self, cpu: *Arm7tdmi) void {
if (cpu.cpsr.t.read()) {
self.stage[0] = cpu.fetch(u16, cpu.r[15]);
self.stage[1] = cpu.fetch(u16, cpu.r[15] + 2);
cpu.r[15] += 4;
} else {
self.stage[0] = cpu.fetch(u32, cpu.r[15]);
self.stage[1] = cpu.fetch(u32, cpu.r[15] + 4);
cpu.r[15] += 8;
}
self.flushed = true;
}
};
pub const PSR = extern union {
mode: Bitfield(u32, 0, 5),
t: Bit(u32, 5),
@@ -642,6 +629,22 @@ pub const PSR = extern union {
z: Bit(u32, 30),
n: Bit(u32, 31),
raw: u32,
fn toString(self: PSR) void {
std.debug.print("[", .{});
if (self.n.read()) std.debug.print("N", .{}) else std.debug.print("-", .{});
if (self.z.read()) std.debug.print("Z", .{}) else std.debug.print("-", .{});
if (self.c.read()) std.debug.print("C", .{}) else std.debug.print("-", .{});
if (self.v.read()) std.debug.print("V", .{}) else std.debug.print("-", .{});
if (self.i.read()) std.debug.print("I", .{}) else std.debug.print("-", .{});
if (self.f.read()) std.debug.print("F", .{}) else std.debug.print("-", .{});
if (self.t.read()) std.debug.print("T", .{}) else std.debug.print("-", .{});
std.debug.print("|", .{});
if (getMode(self.mode.read())) |m| std.debug.print("{s}", .{m.toString()}) else std.debug.print("---", .{});
std.debug.print("]\n", .{});
}
};
const Mode = enum(u5) {
@@ -652,11 +655,18 @@ const Mode = enum(u5) {
Abort = 0b10111,
Undefined = 0b11011,
System = 0b11111,
};
const BankedKind = enum(u1) {
R13 = 0,
R14,
fn toString(self: Mode) []const u8 {
return switch (self) {
.User => "usr",
.Fiq => "fiq",
.Irq => "irq",
.Supervisor => "svc",
.Abort => "abt",
.Undefined => "und",
.System => "sys",
};
}
};
fn getMode(bits: u5) ?Mode {

View File

@@ -55,8 +55,9 @@ pub fn blockDataTransfer(comptime P: bool, comptime U: bool, comptime S: bool, c
if (L) {
cpu.r[15] = bus.read(u32, und_addr);
cpu.pipe.reload(cpu);
} else {
bus.write(u32, und_addr, cpu.r[15] + 8);
bus.write(u32, und_addr, cpu.r[15] + 4);
}
cpu.r[rn] = if (U) cpu.r[rn] + 0x40 else cpu.r[rn] - 0x40;
@@ -86,17 +87,23 @@ pub fn blockDataTransfer(comptime P: bool, comptime U: bool, comptime S: bool, c
cpu.setUserModeRegister(i, bus.read(u32, address));
} else {
const value = bus.read(u32, address);
cpu.r[i] = if (i == 0xF) value & 0xFFFF_FFFC else value;
if (S and i == 0xF) cpu.setCpsr(cpu.spsr.raw);
cpu.r[i] = value;
if (i == 0xF) {
cpu.r[i] &= ~@as(u32, 3); // Align r15
cpu.pipe.reload(cpu);
if (S) cpu.setCpsr(cpu.spsr.raw);
}
}
} else {
if (S) {
// Always Transfer User mode Registers
// This happens regardless if r15 is in the list
const value = cpu.getUserModeRegister(i);
bus.write(u32, address, value + if (i == 0xF) 8 else @as(u32, 0)); // PC is already 4 ahead to make 12
bus.write(u32, address, value + if (i == 0xF) 4 else @as(u32, 0)); // PC is already 8 ahead to make 12
} else {
bus.write(u32, address, cpu.r[i] + if (i == 0xF) 8 else @as(u32, 0));
bus.write(u32, address, cpu.r[i] + if (i == 0xF) 4 else @as(u32, 0));
}
}
}

View File

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

View File

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

View File

@@ -1,11 +1,9 @@
const std = @import("std");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
const sext = @import("../../util.zig").sext;
const rotr = @import("../../util.zig").rotr;
const sext = @import("../../../util.zig").sext;
const rotr = @import("../../../util.zig").rotr;
pub fn halfAndSignedDataTransfer(comptime P: bool, comptime U: bool, comptime I: bool, comptime W: bool, comptime L: bool) InstrFn {
return struct {
@@ -15,20 +13,8 @@ pub fn halfAndSignedDataTransfer(comptime P: bool, comptime U: bool, comptime I:
const rm = opcode & 0xF;
const imm_offset_high = opcode >> 8 & 0xF;
var base: u32 = undefined;
if (rn == 0xF) {
base = cpu.fakePC();
if (!L) base += 4;
} else {
base = cpu.r[rn];
}
var offset: u32 = undefined;
if (I) {
offset = imm_offset_high << 4 | rm;
} else {
offset = cpu.r[rm];
}
const base = cpu.r[rn] + if (!L and rn == 0xF) 4 else @as(u32, 0);
const offset = if (I) imm_offset_high << 4 | rm else cpu.r[rm];
const modified_base = if (U) base +% offset else base -% offset;
var address = if (P) modified_base else base;
@@ -47,11 +33,8 @@ pub fn halfAndSignedDataTransfer(comptime P: bool, comptime U: bool, comptime I:
},
0b11 => {
// LDRSH
result = if (address & 1 == 1) blk: {
break :blk sext(u32, u8, bus.read(u8, address));
} else blk: {
break :blk sext(u32, u16, bus.read(u16, address));
};
const value = bus.read(u16, address);
result = if (address & 1 == 1) sext(u32, u8, @truncate(u8, value >> 8)) else sext(u32, u16, value);
},
0b00 => unreachable, // SWP
}

View File

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

View File

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

View File

@@ -1,12 +1,9 @@
const std = @import("std");
const util = @import("../../util.zig");
const shifter = @import("../barrel_shifter.zig");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
const rotr = @import("../../util.zig").rotr;
const rotr = @import("../../../util.zig").rotr;
pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool, comptime B: bool, comptime W: bool, comptime L: bool) InstrFn {
return struct {
@@ -14,15 +11,10 @@ pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool,
const rn = opcode >> 16 & 0xF;
const rd = opcode >> 12 & 0xF;
var base: u32 = undefined;
if (rn == 0xF) {
base = cpu.fakePC();
if (!L) base += 4; // Offset of 12
} else {
base = cpu.r[rn];
}
// rn is r15 and L is not set, the PC is 12 ahead
const base = cpu.r[rn] + if (!L and rn == 0xF) 4 else @as(u32, 0);
const offset = if (I) shifter.immShift(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;
var address = if (P) modified_base else base;
@@ -40,18 +32,26 @@ pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool,
} else {
if (B) {
// STRB
const value = if (rd == 0xF) cpu.r[rd] + 8 else cpu.r[rd];
const value = cpu.r[rd] + if (rd == 0xF) 4 else @as(u32, 0); // PC is 12 ahead
bus.write(u8, address, @truncate(u8, value));
} else {
// STR
const value = if (rd == 0xF) cpu.r[rd] + 8 else cpu.r[rd];
const value = cpu.r[rd] + if (rd == 0xF) 4 else @as(u32, 0);
bus.write(u32, address, value);
}
}
address = modified_base;
if (W and P or !P) cpu.r[rn] = address;
if (L) cpu.r[rd] = result; // This emulates the LDR rd == rn behaviour
if (W and P or !P) {
cpu.r[rn] = address;
if (rn == 0xF) cpu.pipe.reload(cpu);
}
if (L) {
// This emulates the LDR rd == rn behaviour
cpu.r[rd] = result;
if (rd == 0xF) cpu.pipe.reload(cpu);
}
}
}.inner;
}

View File

@@ -6,7 +6,7 @@ pub fn armSoftwareInterrupt() InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, _: u32) void {
// Copy Values from Current Mode
const r15 = cpu.r[15];
const ret_addr = cpu.r[15] - 4;
const cpsr = cpu.cpsr.raw;
// Switch Mode
@@ -14,9 +14,10 @@ pub fn armSoftwareInterrupt() InstrFn {
cpu.cpsr.t.write(false); // Force ARM Mode
cpu.cpsr.i.write(true); // Disable normal interrupts
cpu.r[14] = r15; // Resume Execution
cpu.r[14] = ret_addr; // Resume Execution
cpu.spsr.raw = cpsr; // Previous mode CPSR
cpu.r[15] = 0x0000_0008;
cpu.pipe.reload(cpu);
}
}.inner;
}

View File

@@ -1,41 +1,35 @@
const std = @import("std");
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
const CPSR = @import("../cpu.zig").PSR;
const rotr = @import("../util.zig").rotr;
const rotr = @import("../../util.zig").rotr;
pub fn execute(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
pub fn exec(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
var result: u32 = undefined;
if (opcode >> 4 & 1 == 1) {
result = registerShift(S, cpu, opcode);
result = register(S, cpu, opcode);
} else {
result = immShift(S, cpu, opcode);
result = immediate(S, cpu, opcode);
}
return result;
}
fn registerShift(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
fn register(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
const rs_idx = opcode >> 8 & 0xF;
const rm = cpu.r[opcode & 0xF];
const rs = @truncate(u8, cpu.r[rs_idx]);
const rm_idx = opcode & 0xF;
const rm = if (rm_idx == 0xF) cpu.fakePC() else cpu.r[rm_idx];
return switch (@truncate(u2, opcode >> 5)) {
0b00 => logicalLeft(S, &cpu.cpsr, rm, rs),
0b01 => logicalRight(S, &cpu.cpsr, rm, rs),
0b10 => arithmeticRight(S, &cpu.cpsr, rm, rs),
0b11 => rotateRight(S, &cpu.cpsr, rm, rs),
0b00 => lsl(S, &cpu.cpsr, rm, rs),
0b01 => lsr(S, &cpu.cpsr, rm, rs),
0b10 => asr(S, &cpu.cpsr, rm, rs),
0b11 => ror(S, &cpu.cpsr, rm, rs),
};
}
pub fn immShift(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
pub fn immediate(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
const amount = @truncate(u8, opcode >> 7 & 0x1F);
const rm_idx = opcode & 0xF;
const rm = if (rm_idx == 0xF) cpu.fakePC() else cpu.r[rm_idx];
const rm = cpu.r[opcode & 0xF];
var result: u32 = undefined;
if (amount == 0) {
@@ -64,17 +58,17 @@ pub fn immShift(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
}
} else {
switch (@truncate(u2, opcode >> 5)) {
0b00 => result = logicalLeft(S, &cpu.cpsr, rm, amount),
0b01 => result = logicalRight(S, &cpu.cpsr, rm, amount),
0b10 => result = arithmeticRight(S, &cpu.cpsr, rm, amount),
0b11 => result = rotateRight(S, &cpu.cpsr, rm, amount),
0b00 => result = lsl(S, &cpu.cpsr, rm, amount),
0b01 => result = lsr(S, &cpu.cpsr, rm, amount),
0b10 => result = asr(S, &cpu.cpsr, rm, amount),
0b11 => result = ror(S, &cpu.cpsr, rm, amount),
}
}
return result;
}
pub fn logicalLeft(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
pub fn lsl(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
const amount = @truncate(u5, total_amount);
const bit_count: u8 = @typeInfo(u32).Int.bits;
@@ -101,7 +95,7 @@ pub fn logicalLeft(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32
return result;
}
pub fn logicalRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u32) u32 {
pub fn lsr(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u32) u32 {
const amount = @truncate(u5, total_amount);
const bit_count: u8 = @typeInfo(u32).Int.bits;
@@ -125,7 +119,7 @@ pub fn logicalRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u32) u
return result;
}
pub fn arithmeticRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
pub fn asr(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
const amount = @truncate(u5, total_amount);
const bit_count: u8 = @typeInfo(u32).Int.bits;
@@ -142,7 +136,7 @@ pub fn arithmeticRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8)
return result;
}
pub fn rotateRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
pub fn ror(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
const result = rotr(u32, rm, total_amount);
if (S and total_amount != 0) {

View File

@@ -4,16 +4,11 @@ const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
const adc = @import("../arm/data_processing.zig").adc;
const sbc = @import("../arm/data_processing.zig").sbc;
const sub = @import("../arm/data_processing.zig").sub;
const cmp = @import("../arm/data_processing.zig").cmp;
const cmn = @import("../arm/data_processing.zig").cmn;
const setTestOpFlags = @import("../arm/data_processing.zig").setTestOpFlags;
const setLogicOpFlags = @import("../arm/data_processing.zig").setLogicOpFlags;
const logicalLeft = @import("../barrel_shifter.zig").logicalLeft;
const logicalRight = @import("../barrel_shifter.zig").logicalRight;
const arithmeticRight = @import("../barrel_shifter.zig").arithmeticRight;
const rotateRight = @import("../barrel_shifter.zig").rotateRight;
const lsl = @import("../barrel_shifter.zig").lsl;
const lsr = @import("../barrel_shifter.zig").lsr;
const asr = @import("../barrel_shifter.zig").asr;
const ror = @import("../barrel_shifter.zig").ror;
pub fn fmt4(comptime op: u4) InstrFn {
return struct {
@@ -22,96 +17,85 @@ pub fn fmt4(comptime op: u4) InstrFn {
const rd = opcode & 0x7;
const carry = @boolToInt(cpu.cpsr.c.read());
const op1 = cpu.r[rd];
const op2 = cpu.r[rs];
var result: u32 = undefined;
var overflow: bool = undefined;
switch (op) {
0x0 => {
// AND
const result = cpu.r[rd] & cpu.r[rs];
cpu.r[rd] = result;
setLogicOpFlags(true, cpu, result);
0x0 => result = op1 & op2, // AND
0x1 => result = op1 ^ op2, // EOR
0x2 => result = lsl(true, &cpu.cpsr, op1, @truncate(u8, op2)), // LSL
0x3 => result = lsr(true, &cpu.cpsr, op1, @truncate(u8, op2)), // LSR
0x4 => result = asr(true, &cpu.cpsr, op1, @truncate(u8, op2)), // ASR
0x5 => result = adc(&overflow, op1, op2, carry), // ADC
0x6 => result = sbc(op1, op2, carry), // SBC
0x7 => result = ror(true, &cpu.cpsr, op1, @truncate(u8, op2)), // ROR
0x8 => result = op1 & op2, // TST
0x9 => result = 0 -% op2, // NEG
0xA => result = op1 -% op2, // CMP
0xB => overflow = @addWithOverflow(u32, op1, op2, &result), // CMN
0xC => result = op1 | op2, // ORR
0xD => result = @truncate(u32, @as(u64, op2) * @as(u64, op1)),
0xE => result = op1 & ~op2,
0xF => result = ~op2,
}
// Write to Destination Register
switch (op) {
0x8, 0xA, 0xB => {},
else => cpu.r[rd] = result,
}
// Write Flags
switch (op) {
0x0, 0x1, 0x2, 0x3, 0x4, 0x7, 0xC, 0xE, 0xF => {
// Logic Operations
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
// C set by Barrel Shifter, V is unaffected
},
0x1 => {
// EOR
const result = cpu.r[rd] ^ cpu.r[rs];
cpu.r[rd] = result;
setLogicOpFlags(true, cpu, result);
0x8, 0xA => {
// Test Flags
// CMN (0xB) is handled with ADC
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
if (op == 0xA) {
// CMP specific
cpu.cpsr.c.write(op2 <= op1);
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
}
},
0x2 => {
// LSL
const result = logicalLeft(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
cpu.r[rd] = result;
setLogicOpFlags(true, cpu, result);
},
0x3 => {
// LSR
const result = logicalRight(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
cpu.r[rd] = result;
setLogicOpFlags(true, cpu, result);
},
0x4 => {
// ASR
const result = arithmeticRight(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
cpu.r[rd] = result;
setLogicOpFlags(true, cpu, result);
},
0x5 => {
// ADC
cpu.r[rd] = adc(true, cpu, cpu.r[rd], cpu.r[rs], carry);
0x5, 0xB => {
// ADC, CMN
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
cpu.cpsr.c.write(overflow);
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
},
0x6 => {
// SBC
cpu.r[rd] = sbc(true, cpu, cpu.r[rd], cpu.r[rs], carry);
},
0x7 => {
// ROR
const result = rotateRight(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
cpu.r[rd] = result;
setLogicOpFlags(true, cpu, result);
},
0x8 => {
// TST
const result = cpu.r[rd] & cpu.r[rs];
setLogicOpFlags(true, cpu, result);
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
const subtrahend = @as(u64, op2) -% carry +% 1;
cpu.cpsr.c.write(subtrahend <= op1);
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
},
0x9 => {
// NEG
cpu.r[rd] = sub(true, cpu, 0, cpu.r[rs]);
},
0xA => {
// CMP
cmp(cpu, cpu.r[rd], cpu.r[rs]);
},
0xB => {
// CMN
cmn(cpu, cpu.r[rd], cpu.r[rs]);
},
0xC => {
// ORR
const result = cpu.r[rd] | cpu.r[rs];
cpu.r[rd] = result;
setLogicOpFlags(true, cpu, result);
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
cpu.cpsr.c.write(op2 <= 0);
cpu.cpsr.v.write(((0 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
},
0xD => {
// MUL
const temp = @as(u64, cpu.r[rs]) * @as(u64, cpu.r[rd]);
const result = @truncate(u32, temp);
cpu.r[rd] = result;
// Multiplication
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
// V is unaffected, assuming similar behaviour to ARMv4 MUL C is undefined
},
0xE => {
// BIC
const result = cpu.r[rd] & ~cpu.r[rs];
cpu.r[rd] = result;
setLogicOpFlags(true, cpu, result);
},
0xF => {
// MVN
const result = ~cpu.r[rs];
cpu.r[rd] = result;
setLogicOpFlags(true, cpu, result);
},
}
}
}.inner;

View File

@@ -33,7 +33,8 @@ pub fn fmt14(comptime L: bool, comptime R: bool) InstrFn {
if (R) {
if (L) {
const value = bus.read(u32, address);
cpu.r[15] = value & 0xFFFF_FFFE;
cpu.r[15] = value & ~@as(u32, 1);
cpu.pipe.reload(cpu);
} else {
bus.write(u32, address, cpu.r[14]);
}
@@ -52,7 +53,13 @@ pub fn fmt15(comptime L: bool, comptime rb: u3) InstrFn {
const end_address = cpu.r[rb] + 4 * countRlist(opcode);
if (opcode & 0xFF == 0) {
if (L) cpu.r[15] = bus.read(u32, address) else bus.write(u32, address, cpu.r[15] + 4);
if (L) {
cpu.r[15] = bus.read(u32, address);
cpu.pipe.reload(cpu);
} else {
bus.write(u32, address, cpu.r[15] + 2);
}
cpu.r[rb] += 0x40;
return;
}

View File

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

View File

@@ -1,16 +1,12 @@
const std = @import("std");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
const shifter = @import("../barrel_shifter.zig");
const add = @import("../arm/data_processing.zig").add;
const sub = @import("../arm/data_processing.zig").sub;
const cmp = @import("../arm/data_processing.zig").cmp;
const setLogicOpFlags = @import("../arm/data_processing.zig").setLogicOpFlags;
const log = std.log.scoped(.Thumb1);
const lsl = @import("../barrel_shifter.zig").lsl;
const lsr = @import("../barrel_shifter.zig").lsr;
const asr = @import("../barrel_shifter.zig").asr;
pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
return struct {
@@ -24,7 +20,7 @@ pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
if (offset == 0) {
break :blk cpu.r[rs];
} else {
break :blk shifter.logicalLeft(true, &cpu.cpsr, cpu.r[rs], offset);
break :blk lsl(true, &cpu.cpsr, cpu.r[rs], offset);
}
},
0b01 => blk: {
@@ -33,7 +29,7 @@ pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
cpu.cpsr.c.write(cpu.r[rs] >> 31 & 1 == 1);
break :blk @as(u32, 0);
} else {
break :blk shifter.logicalRight(true, &cpu.cpsr, cpu.r[rs], offset);
break :blk lsr(true, &cpu.cpsr, cpu.r[rs], offset);
}
},
0b10 => blk: {
@@ -42,7 +38,7 @@ pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
cpu.cpsr.c.write(cpu.r[rs] >> 31 & 1 == 1);
break :blk @bitCast(u32, @bitCast(i32, cpu.r[rs]) >> 31);
} else {
break :blk shifter.arithmeticRight(true, &cpu.cpsr, cpu.r[rs], offset);
break :blk asr(true, &cpu.cpsr, cpu.r[rs], offset);
}
},
else => cpu.panic("[CPU/THUMB.1] 0b{b:0>2} is not a valid op", .{op}),
@@ -50,7 +46,10 @@ pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
// Equivalent to an ARM MOVS
cpu.r[rd] = result;
setLogicOpFlags(true, cpu, result);
// Write Flags
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
}
}.inner;
}
@@ -58,28 +57,51 @@ pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
pub fn fmt5(comptime op: u2, comptime h1: u1, comptime h2: u1) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
const src_idx = @as(u4, h2) << 3 | (opcode >> 3 & 0x7);
const dst_idx = @as(u4, h1) << 3 | (opcode & 0x7);
const rs = @as(u4, h2) << 3 | (opcode >> 3 & 0x7);
const rd = @as(u4, h1) << 3 | (opcode & 0x7);
const src = if (src_idx == 0xF) (cpu.r[src_idx] + 2) & 0xFFFF_FFFE else cpu.r[src_idx];
const dst = if (dst_idx == 0xF) (cpu.r[dst_idx] + 2) & 0xFFFF_FFFE else cpu.r[dst_idx];
const op1 = cpu.r[rd];
const op2 = cpu.r[rs];
var result: u32 = undefined;
var overflow: bool = undefined;
switch (op) {
0b00 => {
// ADD
const sum = add(false, cpu, dst, src);
cpu.r[dst_idx] = if (dst_idx == 0xF) sum & 0xFFFF_FFFE else sum;
},
0b01 => cmp(cpu, dst, src), // CMP
0b10 => {
// MOV
cpu.r[dst_idx] = if (dst_idx == 0xF) src & 0xFFFF_FFFE else src;
},
0b00 => result = add(&overflow, op1, op2), // ADD
0b01 => result = op1 -% op2, // CMP
0b10 => result = op2, // MOV
0b11 => {},
}
// Write to Destination Register
switch (op) {
0b01 => {}, // Test Instruction
0b11 => {
// BX
cpu.cpsr.t.write(src & 1 == 1);
cpu.r[15] = src & 0xFFFF_FFFE;
const is_thumb = op2 & 1 == 1;
cpu.r[15] = op2 & ~@as(u32, 1);
cpu.cpsr.t.write(is_thumb);
cpu.pipe.reload(cpu);
},
else => {
cpu.r[rd] = result;
if (rd == 0xF) {
cpu.r[15] &= ~@as(u32, 1);
cpu.pipe.reload(cpu);
}
},
}
// Write Flags
switch (op) {
0b01 => {
// CMP
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
cpu.cpsr.c.write(op2 <= op1);
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
},
0b00, 0b10, 0b11 => {}, // MOV and Branch Instruction
}
}
}.inner;
@@ -90,21 +112,28 @@ pub fn fmt2(comptime I: bool, is_sub: bool, rn: u3) InstrFn {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
const rs = opcode >> 3 & 0x7;
const rd = @truncate(u3, opcode);
const op1 = cpu.r[rs];
const op2: u32 = if (I) rn else cpu.r[rn];
if (is_sub) {
// SUB
cpu.r[rd] = if (I) blk: {
break :blk sub(true, cpu, cpu.r[rs], rn);
} else blk: {
break :blk sub(true, cpu, cpu.r[rs], cpu.r[rn]);
};
const result = op1 -% op2;
cpu.r[rd] = result;
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
cpu.cpsr.c.write(op2 <= op1);
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
} else {
// ADD
cpu.r[rd] = if (I) blk: {
break :blk add(true, cpu, cpu.r[rs], rn);
} else blk: {
break :blk add(true, cpu, cpu.r[rs], cpu.r[rn]);
};
var overflow: bool = undefined;
const result = add(&overflow, op1, op2);
cpu.r[rd] = result;
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
cpu.cpsr.c.write(overflow);
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
}
}
}.inner;
@@ -113,17 +142,36 @@ pub fn fmt2(comptime I: bool, is_sub: bool, rn: u3) InstrFn {
pub fn fmt3(comptime op: u2, comptime rd: u3) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
const offset = @truncate(u8, opcode);
const op1 = cpu.r[rd];
const op2: u32 = opcode & 0xFF; // Offset
var overflow: bool = undefined;
const result: u32 = switch (op) {
0b00 => op2, // MOV
0b01 => op1 -% op2, // CMP
0b10 => add(&overflow, op1, op2), // ADD
0b11 => op1 -% op2, // SUB
};
// Write to Register
if (op != 0b01) cpu.r[rd] = result;
// Write Flags
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
switch (op) {
0b00 => {
// MOV
cpu.r[rd] = offset;
setLogicOpFlags(true, cpu, offset);
0b00 => {}, // MOV | C set by Barrel Shifter, V is unaffected
0b01, 0b11 => {
// SUB, CMP
cpu.cpsr.c.write(op2 <= op1);
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
},
0b10 => {
// ADD
cpu.cpsr.c.write(overflow);
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
},
0b01 => cmp(cpu, cpu.r[rd], offset), // CMP
0b10 => cpu.r[rd] = add(true, cpu, cpu.r[rd], offset), // ADD
0b11 => cpu.r[rd] = sub(true, cpu, cpu.r[rd], offset), // SUB
}
}
}.inner;
@@ -133,10 +181,9 @@ pub fn fmt12(comptime isSP: bool, comptime rd: u3) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
// ADD
const left = if (isSP) cpu.r[13] else (cpu.r[15] + 2) & 0xFFFF_FFFD;
const left = if (isSP) cpu.r[13] else cpu.r[15] & ~@as(u32, 2);
const right = (opcode & 0xFF) << 2;
const result = left + right;
cpu.r[rd] = result;
cpu.r[rd] = left + right;
}
}.inner;
}

View File

@@ -1,23 +1,22 @@
const std = @import("std");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
const rotr = @import("../../util.zig").rotr;
const rotr = @import("../../../util.zig").rotr;
const sext = @import("../../../util.zig").sext;
pub fn fmt6(comptime rd: u3) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
// LDR
const offset = (opcode & 0xFF) << 2;
cpu.r[rd] = bus.read(u32, (cpu.r[15] + 2 & 0xFFFF_FFFD) + offset);
// Bit 1 of the PC intentionally ignored
cpu.r[rd] = bus.read(u32, (cpu.r[15] & ~@as(u32, 2)) + offset);
}
}.inner;
}
const sext = @import("../../util.zig").sext;
pub fn fmt78(comptime op: u2, comptime T: bool) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
@@ -45,11 +44,8 @@ pub fn fmt78(comptime op: u2, comptime T: bool) InstrFn {
},
0b11 => {
// LDRSH
cpu.r[rd] = if (address & 1 == 1) blk: {
break :blk sext(u32, u8, bus.read(u8, address));
} else blk: {
break :blk sext(u32, u16, bus.read(u16, address));
};
const value = bus.read(u16, address);
cpu.r[rd] = if (address & 1 == 1) sext(u32, u8, @truncate(u8, value >> 8)) else sext(u32, u16, value);
},
}
} else {

View File

@@ -6,7 +6,7 @@ pub fn fmt17() InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, _: u16) void {
// Copy Values from Current Mode
const r15 = cpu.r[15];
const ret_addr = cpu.r[15] - 2;
const cpsr = cpu.cpsr.raw;
// Switch Mode
@@ -14,9 +14,10 @@ pub fn fmt17() InstrFn {
cpu.cpsr.t.write(false); // Force ARM Mode
cpu.cpsr.i.write(true); // Disable normal interrupts
cpu.r[14] = r15; // Resume Execution
cpu.r[14] = ret_addr; // Resume Execution
cpu.spsr.raw = cpsr; // Previous mode CPSR
cpu.r[15] = 0x0000_0008;
cpu.pipe.reload(cpu);
}
}.inner;
}

View File

@@ -1,37 +1,31 @@
const std = @import("std");
const SDL = @import("sdl2");
const config = @import("../config.zig");
const Bus = @import("Bus.zig");
const Scheduler = @import("scheduler.zig").Scheduler;
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
const FpsTracker = @import("util.zig").FpsTracker;
const FilePaths = @import("util.zig").FilePaths;
const FpsTracker = @import("../util.zig").FpsTracker;
const RingBuffer = @import("../util.zig").RingBuffer;
const Timer = std.time.Timer;
const Thread = std.Thread;
const Atomic = std.atomic.Atomic;
const Allocator = std.mem.Allocator;
// TODO: Move these to a TOML File
const sync_audio = false; // Enable Audio Sync
const sync_video: RunKind = .LimitedFPS; // Configure Video Sync
pub const win_scale = 3; // 1x, 2x, 3x, etc. Window Scaling
pub const cpu_logging = false; // Enable detailed CPU logging
pub const allow_unhandled_io = true; // Only relevant in Debug Builds
pub const force_rtc = false;
/// 4 Cycles in 1 dot
const cycles_per_dot = 4;
// 228 Lines which consist of 308 dots (which are 4 cycles long)
const cycles_per_frame: u64 = 228 * (308 * 4); //280896
const clock_rate: u64 = 1 << 24; // 16.78MHz
/// The GBA draws 228 Horizontal which each consist 308 dots
/// (note: not all lines are visible)
const cycles_per_frame = 228 * (308 * cycles_per_dot); //280896
// TODO: Don't truncate this, be more accurate w/ timing
// 59.6046447754ns (truncated to just 59ns)
const clock_period: u64 = std.time.ns_per_s / clock_rate;
const frame_period = (clock_period * cycles_per_frame);
/// The GBA ARM7TDMI runs at 2^24 Hz
const clock_rate = 1 << 24; // 16.78MHz
// 59.7275005696Hz
pub const frame_rate = @intToFloat(f64, std.time.ns_per_s) /
((@intToFloat(f64, std.time.ns_per_s) / @intToFloat(f64, clock_rate)) * @intToFloat(f64, cycles_per_frame));
/// The # of nanoseconds a frame should take
const frame_period = (std.time.ns_per_s * cycles_per_frame) / clock_rate;
/// Exact Value: 59.7275005696Hz
/// The inverse of the frame period
pub const frame_rate: f64 = @intToFloat(f64, clock_rate) / cycles_per_frame;
const log = std.log.scoped(.Emulation);
@@ -40,18 +34,57 @@ const RunKind = enum {
UnlimitedFPS,
Limited,
LimitedFPS,
LimitedBusy,
};
pub fn run(quit: *Atomic(bool), fps: *FpsTracker, sched: *Scheduler, cpu: *Arm7tdmi) void {
if (sync_audio) log.info("Audio sync enabled", .{});
pub fn run(quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: *FpsTracker) void {
const audio_sync = config.config().guest.audio_sync and !config.config().host.mute;
if (audio_sync) log.info("Audio sync enabled", .{});
switch (sync_video) {
.Unlimited => runUnsynchronized(quit, sched, cpu, null),
.Limited => runSynchronized(quit, sched, cpu, null),
.UnlimitedFPS => runUnsynchronized(quit, sched, cpu, fps),
.LimitedFPS => runSynchronized(quit, sched, cpu, fps),
.LimitedBusy => runBusyLoop(quit, sched, cpu),
if (config.config().guest.video_sync) {
inner(.LimitedFPS, audio_sync, quit, scheduler, cpu, tracker);
} else {
inner(.UnlimitedFPS, audio_sync, quit, scheduler, cpu, tracker);
}
}
fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: ?*FpsTracker) void {
if (kind == .UnlimitedFPS or kind == .LimitedFPS) {
std.debug.assert(tracker != null);
log.info("FPS tracking enabled", .{});
}
switch (kind) {
.Unlimited, .UnlimitedFPS => {
log.info("Emulation w/out video sync", .{});
while (!quit.load(.Monotonic)) {
runFrame(scheduler, cpu);
audioSync(audio_sync, &cpu.bus.apu.sample_queue);
if (kind == .UnlimitedFPS) tracker.?.tick();
}
},
.Limited, .LimitedFPS => {
log.info("Emulation w/ video sync", .{});
var timer = Timer.start() catch @panic("failed to initalize std.timer.Timer");
var wake_time: u64 = frame_period;
while (!quit.load(.Monotonic)) {
runFrame(scheduler, cpu);
const new_wake_time = videoSync(&timer, wake_time);
// Spin to make up the difference of OS scheduler innacuracies
// If we happen to also be syncing to audio, we choose to spin on
// the amount of time needed for audio to catch up rather than
// our expected wake-up time
audioSync(audio_sync, &cpu.bus.apu.sample_queue);
if (!audio_sync) spinLoop(&timer, wake_time);
wake_time = new_wake_time;
if (kind == .LimitedFPS) tracker.?.tick();
}
},
}
}
@@ -72,109 +105,30 @@ pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
}
}
fn syncToAudio(stream: *SDL.SDL_AudioStream, is_buffer_full: *bool) void {
const sample_size = 2 * @sizeOf(u16);
const max_buf_size: c_int = 0x400;
fn audioSync(audio_sync: bool, sample_queue: *RingBuffer(u16)) void {
comptime std.debug.assert(@import("../platform.zig").sample_format == SDL.AUDIO_U16);
// const sample_size = 2 * @sizeOf(u16);
// const max_buf_size: c_int = 0x400;
// Determine whether the APU is busy right at this moment
var still_full: bool = SDL.SDL_AudioStreamAvailable(stream) > sample_size * if (is_buffer_full.*) max_buf_size >> 1 else max_buf_size;
defer is_buffer_full.* = still_full; // Update APU Busy status right before exiting scope
// If Busy is false, there's no need to sync here
if (!still_full) return;
while (true) {
still_full = SDL.SDL_AudioStreamAvailable(stream) > sample_size * max_buf_size >> 1;
if (!sync_audio or !still_full) break;
}
_ = audio_sync;
_ = sample_queue;
}
pub fn runUnsynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*FpsTracker) void {
log.info("Emulation thread w/out video sync", .{});
if (fps) |tracker| {
log.info("FPS Tracking Enabled", .{});
while (!quit.load(.SeqCst)) {
runFrame(sched, cpu);
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
tracker.tick();
}
} else {
while (!quit.load(.SeqCst)) {
runFrame(sched, cpu);
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
}
}
}
pub fn runSynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*FpsTracker) void {
log.info("Emulation thread w/ video sync", .{});
var timer = Timer.start() catch std.debug.panic("Failed to initialize std.timer.Timer", .{});
var wake_time: u64 = frame_period;
if (fps) |tracker| {
log.info("FPS Tracking Enabled", .{});
while (!quit.load(.SeqCst)) {
runFrame(sched, cpu);
const new_wake_time = blockOnVideo(&timer, wake_time);
// Spin to make up the difference of OS scheduler innacuracies
// If we happen to also be syncing to audio, we choose to spin on
// the amount of time needed for audio to catch up rather than
// our expected wake-up time
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
if (!sync_audio) spinLoop(&timer, wake_time);
wake_time = new_wake_time;
tracker.tick();
}
} else {
while (!quit.load(.SeqCst)) {
runFrame(sched, cpu);
const new_wake_time = blockOnVideo(&timer, wake_time);
// see above comment
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
if (!sync_audio) spinLoop(&timer, wake_time);
wake_time = new_wake_time;
}
}
}
inline fn blockOnVideo(timer: *Timer, wake_time: u64) u64 {
fn videoSync(timer: *Timer, wake_time: u64) u64 {
// Use the OS scheduler to put the emulation thread to sleep
const maybe_recalc_wake_time = sleep(timer, wake_time);
const recalculated = sleep(timer, wake_time);
// If sleep() determined we need to adjust our wake up time, do so
// otherwise predict our next wake up time according to the frame period
return if (maybe_recalc_wake_time) |recalc| recalc else wake_time + frame_period;
}
pub fn runBusyLoop(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi) void {
log.info("Emulation thread with video sync using busy loop", .{});
var timer = Timer.start() catch unreachable;
var wake_time: u64 = frame_period;
while (!quit.load(.SeqCst)) {
runFrame(sched, cpu);
spinLoop(&timer, wake_time);
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
// Update to the new wake time
wake_time += frame_period;
}
return recalculated orelse wake_time + frame_period;
}
// TODO: Better sleep impl?
fn sleep(timer: *Timer, wake_time: u64) ?u64 {
// const step = std.time.ns_per_ms * 10; // 10ms
const timestamp = timer.read();
// ns_late is non zero if we are late.
const ns_late = timestamp -| wake_time;
var ns_late = timestamp -| wake_time;
// If we're more than a frame late, skip the rest of this loop
// Recalculate what our new wake time should be so that we can
@@ -182,15 +136,18 @@ fn sleep(timer: *Timer, wake_time: u64) ?u64 {
if (ns_late > frame_period) return timestamp + frame_period;
const sleep_for = frame_period - ns_late;
// // Employ several sleep calls in periods of 10ms
// // By doing this the behaviour should average out to be
// // more consistent
// const loop_count = sleep_for / step; // How many groups of 10ms
const step = 2 * std.time.ns_per_ms; // Granularity of 2ms
const times = sleep_for / step;
var i: usize = 0;
// var i: usize = 0;
// while (i < loop_count) : (i += 1) std.time.sleep(step);
while (i < times) : (i += 1) {
std.time.sleep(step);
std.time.sleep(sleep_for);
// Upon wakeup, check to see if this particular sleep was longer than expected
// if so we should exit early, but probably not skip a whole frame period
ns_late = timer.read() -| wake_time;
if (ns_late > frame_period) return null;
}
return null;
}

View File

@@ -1,7 +1,7 @@
const std = @import("std");
const io = @import("bus/io.zig");
const util = @import("../util.zig");
const EventKind = @import("scheduler.zig").EventKind;
const Scheduler = @import("scheduler.zig").Scheduler;
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
@@ -10,15 +10,229 @@ const Bitfield = @import("bitfield").Bitfield;
const Allocator = std.mem.Allocator;
const log = std.log.scoped(.PPU);
const pollBlankingDma = @import("bus/dma.zig").pollBlankingDma;
/// This is used to generate byuu / Talurabi's Color Correction algorithm
const COLOUR_LUT = genColourLut();
const getHalf = util.getHalf;
const setHalf = util.setHalf;
const setQuart = util.setQuart;
const pollDmaOnBlank = @import("bus/dma.zig").pollDmaOnBlank;
pub const width = 240;
pub const height = 160;
pub const framebuf_pitch = width * @sizeOf(u32);
pub fn read(comptime T: type, ppu: *const Ppu, addr: u32) ?T {
const byte_addr = @truncate(u8, addr);
return switch (T) {
u32 => switch (byte_addr) {
0x00 => ppu.dispcnt.raw, // Green Swap is in high half-word
0x04 => @as(T, ppu.vcount.raw) << 16 | ppu.dispstat.raw,
0x08 => @as(T, ppu.bg[1].bg1Cnt()) << 16 | ppu.bg[0].bg0Cnt(),
0x0C => @as(T, ppu.bg[3].cnt.raw) << 16 | ppu.bg[2].cnt.raw,
0x10, 0x14, 0x18, 0x1C => null, // BGXHOFS/VOFS
0x20, 0x24, 0x28, 0x2C => null, // BG2 Rot/Scaling
0x30, 0x34, 0x38, 0x3C => null, // BG3 Rot/Scaling
0x40, 0x44 => null, // WINXH/V Registers
0x48 => @as(T, ppu.win.getOut()) << 16 | ppu.win.getIn(),
0x4C => null, // MOSAIC, undefined in high byte
0x50 => @as(T, ppu.bld.getAlpha()) << 16 | ppu.bld.getCnt(),
0x54 => null, // BLDY, undefined in high half-wrd
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
},
u16 => switch (byte_addr) {
0x00 => ppu.dispcnt.raw,
0x02 => null, // Green Swap
0x04 => ppu.dispstat.raw,
0x06 => ppu.vcount.raw,
0x08 => ppu.bg[0].bg0Cnt(),
0x0A => ppu.bg[1].bg1Cnt(),
0x0C => ppu.bg[2].cnt.raw,
0x0E => ppu.bg[3].cnt.raw,
0x10, 0x12, 0x14, 0x16, 0x18, 0x1A, 0x1C, 0x1E => null, // BGXHOFS/VOFS
0x20, 0x22, 0x24, 0x26, 0x28, 0x2A, 0x2C, 0x2E => null, // BG2 Rot/Scaling
0x30, 0x32, 0x34, 0x36, 0x38, 0x3A, 0x3C, 0x3E => null, // BG3 Rot/Scaling
0x40, 0x42, 0x44, 0x46 => null, // WINXH/V Registers
0x48 => ppu.win.getIn(),
0x4A => ppu.win.getOut(),
0x4C => null, // MOSAIC
0x4E => null,
0x50 => ppu.bld.getCnt(),
0x52 => ppu.bld.getAlpha(),
0x54 => null, // BLDY
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
},
u8 => switch (byte_addr) {
0x00, 0x01 => @truncate(T, ppu.dispcnt.raw >> getHalf(byte_addr)),
0x02, 0x03 => null,
0x04, 0x05 => @truncate(T, ppu.dispstat.raw >> getHalf(byte_addr)),
0x06, 0x07 => @truncate(T, ppu.vcount.raw >> getHalf(byte_addr)),
0x08, 0x09 => @truncate(T, ppu.bg[0].bg0Cnt() >> getHalf(byte_addr)),
0x0A, 0x0B => @truncate(T, ppu.bg[1].bg1Cnt() >> getHalf(byte_addr)),
0x0C, 0x0D => @truncate(T, ppu.bg[2].cnt.raw >> getHalf(byte_addr)),
0x0E, 0x0F => @truncate(T, ppu.bg[3].cnt.raw >> getHalf(byte_addr)),
0x10...0x1F => null, // BGXHOFS/VOFS
0x20...0x2F => null, // BG2 Rot/Scaling
0x30...0x3F => null, // BG3 Rot/Scaling
0x40...0x47 => null, // WINXH/V Registers
0x48, 0x49 => @truncate(T, ppu.win.getIn() >> getHalf(byte_addr)),
0x4A, 0x4B => @truncate(T, ppu.win.getOut() >> getHalf(byte_addr)),
0x4C, 0x4D => null, // MOSAIC
0x4E, 0x4F => null,
0x50, 0x51 => @truncate(T, ppu.bld.getCnt() >> getHalf(byte_addr)),
0x52, 0x53 => @truncate(T, ppu.bld.getAlpha() >> getHalf(byte_addr)),
0x54, 0x55 => null, // BLDY
else => util.io.read.err(T, log, "unexpected {} read from 0x{X:0>8}", .{ T, addr }),
},
else => @compileError("PPU: Unsupported read width"),
};
}
pub fn write(comptime T: type, ppu: *Ppu, addr: u32, value: T) void {
const byte_addr = @truncate(u8, addr); // prefixed with 0x0400_00
switch (T) {
u32 => switch (byte_addr) {
0x00 => ppu.dispcnt.raw = @truncate(u16, value),
0x04 => {
ppu.dispstat.set(@truncate(u16, value));
ppu.vcount.raw = @truncate(u16, value >> 16);
},
0x08 => ppu.setAdjCnts(0, value),
0x0C => ppu.setAdjCnts(2, value),
0x10 => ppu.setBgOffsets(0, value),
0x14 => ppu.setBgOffsets(1, value),
0x18 => ppu.setBgOffsets(2, value),
0x1C => ppu.setBgOffsets(3, value),
0x20 => ppu.aff_bg[0].writePaPb(value),
0x24 => ppu.aff_bg[0].writePcPd(value),
0x28 => ppu.aff_bg[0].setX(ppu.dispstat.vblank.read(), value),
0x2C => ppu.aff_bg[0].setY(ppu.dispstat.vblank.read(), value),
0x30 => ppu.aff_bg[1].writePaPb(value),
0x34 => ppu.aff_bg[1].writePcPd(value),
0x38 => ppu.aff_bg[1].setX(ppu.dispstat.vblank.read(), value),
0x3C => ppu.aff_bg[1].setY(ppu.dispstat.vblank.read(), value),
0x40 => ppu.win.setH(value),
0x44 => ppu.win.setV(value),
0x48 => ppu.win.setIo(value),
0x4C => log.debug("Wrote 0x{X:0>8} to MOSAIC", .{value}),
0x50 => {
ppu.bld.cnt.raw = @truncate(u16, value);
ppu.bld.alpha.raw = @truncate(u16, value >> 16);
},
0x54 => ppu.bld.y.raw = @truncate(u16, value),
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
},
u16 => switch (byte_addr) {
0x00 => ppu.dispcnt.raw = value,
0x02 => {}, // Green Swap
0x04 => ppu.dispstat.set(value),
0x06 => {}, // VCOUNT
0x08 => ppu.bg[0].cnt.raw = value,
0x0A => ppu.bg[1].cnt.raw = value,
0x0C => ppu.bg[2].cnt.raw = value,
0x0E => ppu.bg[3].cnt.raw = value,
0x10 => ppu.bg[0].hofs.raw = value, // TODO: Don't write out every HOFS / VOFS?
0x12 => ppu.bg[0].vofs.raw = value,
0x14 => ppu.bg[1].hofs.raw = value,
0x16 => ppu.bg[1].vofs.raw = value,
0x18 => ppu.bg[2].hofs.raw = value,
0x1A => ppu.bg[2].vofs.raw = value,
0x1C => ppu.bg[3].hofs.raw = value,
0x1E => ppu.bg[3].vofs.raw = value,
0x20 => ppu.aff_bg[0].pa = @bitCast(i16, value),
0x22 => ppu.aff_bg[0].pb = @bitCast(i16, value),
0x24 => ppu.aff_bg[0].pc = @bitCast(i16, value),
0x26 => ppu.aff_bg[0].pd = @bitCast(i16, value),
0x28, 0x2A => ppu.aff_bg[0].x = @bitCast(i32, setHalf(u32, @bitCast(u32, ppu.aff_bg[0].x), byte_addr, value)),
0x2C, 0x2E => ppu.aff_bg[0].y = @bitCast(i32, setHalf(u32, @bitCast(u32, ppu.aff_bg[0].y), byte_addr, value)),
0x30 => ppu.aff_bg[1].pa = @bitCast(i16, value),
0x32 => ppu.aff_bg[1].pb = @bitCast(i16, value),
0x34 => ppu.aff_bg[1].pc = @bitCast(i16, value),
0x36 => ppu.aff_bg[1].pd = @bitCast(i16, value),
0x38, 0x3A => ppu.aff_bg[1].x = @bitCast(i32, setHalf(u32, @bitCast(u32, ppu.aff_bg[1].x), byte_addr, value)),
0x3C, 0x3E => ppu.aff_bg[1].y = @bitCast(i32, setHalf(u32, @bitCast(u32, ppu.aff_bg[1].y), byte_addr, value)),
0x40 => ppu.win.h[0].raw = value,
0x42 => ppu.win.h[1].raw = value,
0x44 => ppu.win.v[0].raw = value,
0x46 => ppu.win.v[1].raw = value,
0x48 => ppu.win.in.raw = value,
0x4A => ppu.win.out.raw = value,
0x4C => log.debug("Wrote 0x{X:0>4} to MOSAIC", .{value}),
0x4E => {},
0x50 => ppu.bld.cnt.raw = value,
0x52 => ppu.bld.alpha.raw = value,
0x54 => ppu.bld.y.raw = value,
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
},
u8 => switch (byte_addr) {
0x00, 0x01 => ppu.dispcnt.raw = setHalf(u16, ppu.dispcnt.raw, byte_addr, value),
0x02, 0x03 => {}, // Green Swap
0x04, 0x05 => ppu.dispstat.set(setHalf(u16, ppu.dispstat.raw, byte_addr, value)),
0x06, 0x07 => {}, // VCOUNT
// BGXCNT
0x08, 0x09 => ppu.bg[0].cnt.raw = setHalf(u16, ppu.bg[0].cnt.raw, byte_addr, value),
0x0A, 0x0B => ppu.bg[1].cnt.raw = setHalf(u16, ppu.bg[1].cnt.raw, byte_addr, value),
0x0C, 0x0D => ppu.bg[2].cnt.raw = setHalf(u16, ppu.bg[2].cnt.raw, byte_addr, value),
0x0E, 0x0F => ppu.bg[3].cnt.raw = setHalf(u16, ppu.bg[3].cnt.raw, byte_addr, value),
// BGX HOFS/VOFS
0x10, 0x11 => ppu.bg[0].hofs.raw = setHalf(u16, ppu.bg[0].hofs.raw, byte_addr, value),
0x12, 0x13 => ppu.bg[0].vofs.raw = setHalf(u16, ppu.bg[0].vofs.raw, byte_addr, value),
0x14, 0x15 => ppu.bg[1].hofs.raw = setHalf(u16, ppu.bg[1].hofs.raw, byte_addr, value),
0x16, 0x17 => ppu.bg[1].vofs.raw = setHalf(u16, ppu.bg[1].vofs.raw, byte_addr, value),
0x18, 0x19 => ppu.bg[2].hofs.raw = setHalf(u16, ppu.bg[2].hofs.raw, byte_addr, value),
0x1A, 0x1B => ppu.bg[2].vofs.raw = setHalf(u16, ppu.bg[2].vofs.raw, byte_addr, value),
0x1C, 0x1D => ppu.bg[3].hofs.raw = setHalf(u16, ppu.bg[3].hofs.raw, byte_addr, value),
0x1E, 0x1F => ppu.bg[3].vofs.raw = setHalf(u16, ppu.bg[3].vofs.raw, byte_addr, value),
// BG2 Rot/Scaling
0x20, 0x21 => ppu.aff_bg[0].pa = @bitCast(i16, setHalf(u16, @bitCast(u16, ppu.aff_bg[0].pa), byte_addr, value)),
0x22, 0x23 => ppu.aff_bg[0].pb = @bitCast(i16, setHalf(u16, @bitCast(u16, ppu.aff_bg[0].pb), byte_addr, value)),
0x24, 0x25 => ppu.aff_bg[0].pc = @bitCast(i16, setHalf(u16, @bitCast(u16, ppu.aff_bg[0].pc), byte_addr, value)),
0x26, 0x27 => ppu.aff_bg[0].pd = @bitCast(i16, setHalf(u16, @bitCast(u16, ppu.aff_bg[0].pd), byte_addr, value)),
0x28, 0x29, 0x2A, 0x2B => ppu.aff_bg[0].x = @bitCast(i32, setQuart(@bitCast(u32, ppu.aff_bg[0].x), byte_addr, value)),
0x2C, 0x2D, 0x2E, 0x2F => ppu.aff_bg[0].y = @bitCast(i32, setQuart(@bitCast(u32, ppu.aff_bg[0].y), byte_addr, value)),
// BG3 Rot/Scaling
0x30, 0x31 => ppu.aff_bg[1].pa = @bitCast(i16, setHalf(u16, @bitCast(u16, ppu.aff_bg[1].pa), byte_addr, value)),
0x32, 0x33 => ppu.aff_bg[1].pb = @bitCast(i16, setHalf(u16, @bitCast(u16, ppu.aff_bg[1].pb), byte_addr, value)),
0x34, 0x35 => ppu.aff_bg[1].pc = @bitCast(i16, setHalf(u16, @bitCast(u16, ppu.aff_bg[1].pc), byte_addr, value)),
0x36, 0x37 => ppu.aff_bg[1].pd = @bitCast(i16, setHalf(u16, @bitCast(u16, ppu.aff_bg[1].pd), byte_addr, value)),
0x38, 0x39, 0x3A, 0x3B => ppu.aff_bg[1].x = @bitCast(i32, setQuart(@bitCast(u32, ppu.aff_bg[1].x), byte_addr, value)),
0x3C, 0x3D, 0x3E, 0x3F => ppu.aff_bg[1].y = @bitCast(i32, setQuart(@bitCast(u32, ppu.aff_bg[1].y), byte_addr, value)),
// Window
0x40, 0x41 => ppu.win.h[0].raw = setHalf(u16, ppu.win.h[0].raw, byte_addr, value),
0x42, 0x43 => ppu.win.h[1].raw = setHalf(u16, ppu.win.h[1].raw, byte_addr, value),
0x44, 0x45 => ppu.win.v[0].raw = setHalf(u16, ppu.win.v[0].raw, byte_addr, value),
0x46, 0x47 => ppu.win.v[1].raw = setHalf(u16, ppu.win.v[1].raw, byte_addr, value),
0x48, 0x49 => ppu.win.in.raw = setHalf(u16, ppu.win.in.raw, byte_addr, value),
0x4A, 0x4B => ppu.win.out.raw = setHalf(u16, ppu.win.out.raw, byte_addr, value),
0x4C, 0x4D => log.debug("Wrote 0x{X:0>2} to MOSAIC", .{value}),
0x4E, 0x4F => {},
// Blending
0x50, 0x51 => ppu.bld.cnt.raw = setHalf(u16, ppu.bld.cnt.raw, byte_addr, value),
0x52, 0x53 => ppu.bld.alpha.raw = setHalf(u16, ppu.bld.alpha.raw, byte_addr, value),
0x54, 0x55 => ppu.bld.y.raw = setHalf(u16, ppu.bld.y.raw, byte_addr, value),
else => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
},
else => @compileError("PPU: Unsupported write width"),
}
}
pub const Ppu = struct {
const Self = @This();
@@ -32,9 +246,7 @@ pub const Ppu = struct {
dispstat: io.DisplayStatus,
vcount: io.VCount,
bldcnt: io.BldCnt,
bldalpha: io.BldAlpha,
bldy: io.BldY,
bld: Blend,
vram: Vram,
palette: Palette,
@@ -65,12 +277,10 @@ pub const Ppu = struct {
.win = Window.init(),
.bg = [_]Background{Background.init()} ** 4,
.aff_bg = [_]AffineBackground{AffineBackground.init()} ** 2,
.bld = Blend.create(),
.dispcnt = .{ .raw = 0x0000 },
.dispstat = .{ .raw = 0x0000 },
.vcount = .{ .raw = 0x0000 },
.bldcnt = .{ .raw = 0x0000 },
.bldalpha = .{ .raw = 0x0000 },
.bldy = .{ .raw = 0x0000 },
.scanline = try Scanline.init(allocator),
.scanline_sprites = sprites,
@@ -165,7 +375,7 @@ pub const Ppu = struct {
const x = (sprite.x() +% i) % width;
const ix = @bitCast(i9, x);
if (!shouldDrawSprite(self.bldcnt, &self.scanline, x)) continue;
if (!shouldDrawSprite(self.bld.cnt, &self.scanline, x)) continue;
const sprite_start = sprite.x();
const isprite_start = @bitCast(i9, sprite_start);
@@ -194,7 +404,7 @@ pub const Ppu = struct {
// Sprite Palette starts at 0x0500_0200
if (pal_id != 0) {
const bgr555 = self.palette.read(u16, 0x200 + pal_id * 2);
copyToSpriteBuffer(self.bldcnt, &self.scanline, x, bgr555);
copyToSpriteBuffer(self.bld.cnt, &self.scanline, x, bgr555);
}
}
}
@@ -215,7 +425,7 @@ pub const Ppu = struct {
const x = (sprite.x() +% i) % width;
const ix = @bitCast(i9, x);
if (!shouldDrawSprite(self.bldcnt, &self.scanline, x)) continue;
if (!shouldDrawSprite(self.bld.cnt, &self.scanline, x)) continue;
const sprite_start = sprite.x();
const isprite_start = @bitCast(i9, sprite_start);
@@ -250,7 +460,7 @@ pub const Ppu = struct {
// Sprite Palette starts at 0x0500_0200
if (pal_id != 0) {
const bgr555 = self.palette.read(u16, 0x200 + pal_id * 2);
copyToSpriteBuffer(self.bldcnt, &self.scanline, x, bgr555);
copyToSpriteBuffer(self.bld.cnt, &self.scanline, x, bgr555);
}
}
}
@@ -277,7 +487,7 @@ pub const Ppu = struct {
aff_x += self.aff_bg[n - 2].pa;
aff_y += self.aff_bg[n - 2].pc;
if (!shouldDrawBackground(n, self.bldcnt, &self.scanline, i)) continue;
if (!shouldDrawBackground(n, self.bld.cnt, &self.scanline, i)) continue;
if (self.bg[n].cnt.display_overflow.read()) {
ix = if (ix > px_width) @rem(ix, px_width) else if (ix < 0) px_width + @rem(ix, px_width) else ix;
@@ -296,7 +506,7 @@ pub const Ppu = struct {
if (pal_id != 0) {
const bgr555 = self.palette.read(u16, pal_id * 2);
copyToBackgroundBuffer(n, self.bldcnt, &self.scanline, i, bgr555);
copyToBackgroundBuffer(n, self.bld.cnt, &self.scanline, i, bgr555);
}
}
@@ -325,7 +535,7 @@ pub const Ppu = struct {
var i: u32 = 0;
while (i < width) : (i += 1) {
if (!shouldDrawBackground(n, self.bldcnt, &self.scanline, i)) continue;
if (!shouldDrawBackground(n, self.bld.cnt, &self.scanline, i)) continue;
const x = hofs + i;
@@ -353,7 +563,7 @@ pub const Ppu = struct {
if (pal_id != 0) {
const bgr555 = self.palette.read(u16, pal_id * 2);
copyToBackgroundBuffer(n, self.bldcnt, &self.scanline, i, bgr555);
copyToBackgroundBuffer(n, self.bld.cnt, &self.scanline, i, bgr555);
}
}
}
@@ -392,7 +602,7 @@ pub const Ppu = struct {
const maybe_btm = self.scanline.btm()[i];
const bgr555 = self.getBgr555(maybe_top, maybe_btm);
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
}
// Reset Current Scanline Pixel Buffer and list of fetched sprites
@@ -419,7 +629,7 @@ pub const Ppu = struct {
const maybe_btm = self.scanline.btm()[i];
const bgr555 = self.getBgr555(maybe_top, maybe_btm);
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
}
// Reset Current Scanline Pixel Buffer and list of fetched sprites
@@ -445,7 +655,7 @@ pub const Ppu = struct {
const maybe_btm = self.scanline.btm()[i];
const bgr555 = self.getBgr555(maybe_top, maybe_btm);
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
}
// Reset Current Scanline Pixel Buffer and list of fetched sprites
@@ -460,7 +670,7 @@ pub const Ppu = struct {
var i: usize = 0;
while (i < width) : (i += 1) {
const bgr555 = self.vram.read(u16, vram_base + i * @sizeOf(u16));
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
}
},
0x4 => {
@@ -471,7 +681,7 @@ pub const Ppu = struct {
// Render Current Scanline
for (self.vram.buf[vram_base .. vram_base + width]) |byte, i| {
const bgr555 = self.palette.read(u16, @as(u16, byte) * @sizeOf(u16));
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
}
},
0x5 => {
@@ -488,7 +698,7 @@ pub const Ppu = struct {
const bgr555 =
if (scanline < m5_height and i < m5_width) self.vram.read(u16, vram_base + i * @sizeOf(u16)) else self.palette.getBackdrop();
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
}
},
else => std.debug.panic("[PPU] TODO: Implement BG Mode {}", .{bg_mode}),
@@ -497,11 +707,11 @@ pub const Ppu = struct {
fn getBgr555(self: *Self, maybe_top: ?u16, maybe_btm: ?u16) u16 {
if (maybe_btm) |btm| {
return switch (self.bldcnt.mode.read()) {
return switch (self.bld.cnt.mode.read()) {
0b00 => if (maybe_top) |top| top else btm,
0b01 => if (maybe_top) |top| alphaBlend(btm, top, self.bldalpha) else btm,
0b01 => if (maybe_top) |top| alphaBlend(btm, top, self.bld.alpha) else btm,
0b10 => blk: {
const evy: u16 = self.bldy.evy.read();
const evy: u16 = self.bld.y.evy.read();
const r = btm & 0x1F;
const g = (btm >> 5) & 0x1F;
@@ -514,7 +724,7 @@ pub const Ppu = struct {
break :blk (bld_b << 10) | (bld_g << 5) | bld_r;
},
0b11 => blk: {
const evy: u16 = self.bldy.evy.read();
const evy: u16 = self.bld.y.evy.read();
const btm_r = btm & 0x1F;
const btm_g = (btm >> 5) & 0x1F;
@@ -562,7 +772,7 @@ pub const Ppu = struct {
};
}
pub fn handleHDrawEnd(self: *Self, cpu: *Arm7tdmi, late: u64) void {
pub fn onHdrawEnd(self: *Self, cpu: *Arm7tdmi, late: u64) void {
// Transitioning to a Hblank
if (self.dispstat.hblank_irq.read()) {
cpu.bus.io.irq.hblank.set();
@@ -572,13 +782,13 @@ pub const Ppu = struct {
// See if HBlank DMA is present and not enabled
if (!self.dispstat.vblank.read())
pollBlankingDma(cpu.bus, .HBlank);
pollDmaOnBlank(cpu.bus, .HBlank);
self.dispstat.hblank.set();
self.sched.push(.HBlank, 68 * 4 -| late);
}
pub fn handleHBlankEnd(self: *Self, cpu: *Arm7tdmi, late: u64) void {
pub fn onHblankEnd(self: *Self, cpu: *Arm7tdmi, late: u64) void {
// The End of a Hblank (During Draw or Vblank)
const old_scanline = self.vcount.scanline.read();
const scanline = (old_scanline + 1) % 228;
@@ -614,7 +824,7 @@ pub const Ppu = struct {
self.aff_bg[1].latchRefPoints();
// See if Vblank DMA is present and not enabled
pollBlankingDma(cpu.bus, .VBlank);
pollDmaOnBlank(cpu.bus, .VBlank);
}
if (scanline == 227) self.dispstat.vblank.unset();
@@ -672,7 +882,7 @@ const Palette = struct {
}
};
const Vram = struct {
pub const Vram = struct {
const vram_size = 0x18000;
const Self = @This();
@@ -723,7 +933,7 @@ const Vram = struct {
}
}
fn mirror(address: usize) usize {
pub fn mirror(address: usize) usize {
// Mirrored in steps of 128K (64K + 32K + 32K) (abcc)
const addr = address & 0x1FFFF;
@@ -775,6 +985,30 @@ const Oam = struct {
}
};
const Blend = struct {
const Self = @This();
cnt: io.BldCnt,
alpha: io.BldAlpha,
y: io.BldY,
pub fn create() Self {
return .{
.cnt = .{ .raw = 0x000 },
.alpha = .{ .raw = 0x000 },
.y = .{ .raw = 0x000 },
};
}
pub fn getCnt(self: *const Self) u16 {
return self.cnt.raw & 0x3FFF;
}
pub fn getAlpha(self: *const Self) u16 {
return self.alpha.raw & 0x1F1F;
}
};
const Window = struct {
const Self = @This();
@@ -794,6 +1028,14 @@ const Window = struct {
};
}
pub fn getIn(self: *const Self) u16 {
return self.in.raw & 0x3F3F;
}
pub fn getOut(self: *const Self) u16 {
return self.out.raw & 0x3F3F;
}
pub fn setH(self: *Self, value: u32) void {
self.h[0].raw = @truncate(u16, value);
self.h[1].raw = @truncate(u16, value >> 16);
@@ -808,18 +1050,6 @@ const Window = struct {
self.in.raw = @truncate(u16, value);
self.out.raw = @truncate(u16, value >> 16);
}
pub fn setInL(self: *Self, value: u8) void {
self.in.raw = (self.in.raw & 0xFF00) | value;
}
pub fn setInH(self: *Self, value: u8) void {
self.in.raw = (self.in.raw & 0x00FF) | (@as(u16, value) << 8);
}
pub fn setOutL(self: *Self, value: u8) void {
self.out.raw = (self.out.raw & 0xFF00) | value;
}
};
const Background = struct {
@@ -839,6 +1069,17 @@ const Background = struct {
.vofs = .{ .raw = 0x0000 },
};
}
/// For whatever reason, some higher bits of BG0CNT
/// are masked out
pub inline fn bg0Cnt(self: *const Self) u16 {
return self.cnt.raw & 0xDFFF;
}
/// BG1CNT inherits the same mask as BG0CNTs
pub inline fn bg1Cnt(self: *const Self) u16 {
return self.bg0Cnt();
}
};
const AffineBackground = struct {
@@ -1075,7 +1316,7 @@ fn spriteDimensions(shape: u2, size: u2) [2]u8 {
};
}
fn toRgba8888(bgr555: u16) u32 {
inline fn rgba888(bgr555: u16) u32 {
const b = @as(u32, bgr555 >> 10 & 0x1F);
const g = @as(u32, bgr555 >> 5 & 0x1F);
const r = @as(u32, bgr555 & 0x1F);
@@ -1083,39 +1324,6 @@ fn toRgba8888(bgr555: u16) u32 {
return (r << 3 | r >> 2) << 24 | (g << 3 | g >> 2) << 16 | (b << 3 | b >> 2) << 8 | 0xFF;
}
fn genColourLut() [0x8000]u32 {
return comptime {
@setEvalBranchQuota(0x10001);
var lut: [0x8000]u32 = undefined;
for (lut) |*px, i| px.* = toRgba8888(i);
return lut;
};
}
// FIXME: The implementation is incorrect and using it in the LUT crashes the compiler (OOM)
/// Implementation courtesy of byuu and Talarubi at https://near.sh/articles/video/color-emulation
fn toRgba8888Talarubi(bgr555: u16) u32 {
@setRuntimeSafety(false);
const lcd_gamma: f64 = 4;
const out_gamma: f64 = 2.2;
const b = @as(u32, bgr555 >> 10 & 0x1F);
const g = @as(u32, bgr555 >> 5 & 0x1F);
const r = @as(u32, bgr555 & 0x1F);
const lb = std.math.pow(f64, @intToFloat(f64, b << 3 | b >> 2) / 31, lcd_gamma);
const lg = std.math.pow(f64, @intToFloat(f64, g << 3 | g >> 2) / 31, lcd_gamma);
const lr = std.math.pow(f64, @intToFloat(f64, r << 3 | r >> 2) / 31, lcd_gamma);
const out_b = std.math.pow(f64, (220 * lb + 10 * lg + 50 * lr) / 255, 1 / out_gamma);
const out_g = std.math.pow(f64, (30 * lb + 230 * lg + 10 * lr) / 255, 1 / out_gamma);
const out_r = std.math.pow(f64, (0 * lb + 50 * lg + 255 * lr) / 255, 1 / out_gamma);
return @floatToInt(u32, out_r) << 24 | @floatToInt(u32, out_g) << 16 | @floatToInt(u32, out_b) << 8 | 0xFF;
}
fn alphaBlend(top: u16, btm: u16, bldalpha: io.BldAlpha) u16 {
const eva: u16 = bldalpha.eva.read();
const evb: u16 = bldalpha.evb.read();
@@ -1256,7 +1464,7 @@ const FrameBuffer = struct {
layers: [2][]u8,
buf: []u8,
current: u1,
current: std.atomic.Atomic(u8),
allocator: Allocator,
@@ -1275,7 +1483,7 @@ const FrameBuffer = struct {
// Front and Back Framebuffers
.layers = [_][]u8{ buf[0..][0..framebuf_len], buf[framebuf_len..][0..framebuf_len] },
.buf = buf,
.current = 0,
.current = std.atomic.Atomic(u8).init(0),
.allocator = allocator,
};
@@ -1287,10 +1495,12 @@ const FrameBuffer = struct {
}
pub fn swap(self: *Self) void {
self.current = ~self.current;
_ = self.current.fetchXor(1, .Release); // fetchNot(.Release)
}
pub fn get(self: *Self, comptime dev: Device) []u8 {
return self.layers[if (dev == .Emulator) self.current else ~self.current];
const current = @intCast(u1, self.current.load(.Acquire));
return self.layers[if (dev == .Emulator) current else ~current];
}
};

View File

@@ -1,8 +1,7 @@
const std = @import("std");
const Bus = @import("Bus.zig");
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
const Clock = @import("bus/GamePak.zig").Clock;
const Clock = @import("bus/gpio.zig").Clock;
const Order = std.math.Order;
const PriorityQueue = std.PriorityQueue;
@@ -43,22 +42,19 @@ pub const Scheduler = struct {
.Draw => {
// The end of a VDraw
cpu.bus.ppu.drawScanline();
cpu.bus.ppu.handleHDrawEnd(cpu, late);
cpu.bus.ppu.onHdrawEnd(cpu, late);
},
.TimerOverflow => |id| {
switch (id) {
0 => cpu.bus.tim[0].handleOverflow(cpu, late),
1 => cpu.bus.tim[1].handleOverflow(cpu, late),
2 => cpu.bus.tim[2].handleOverflow(cpu, late),
3 => cpu.bus.tim[3].handleOverflow(cpu, late),
inline 0...3 => |idx| cpu.bus.tim[idx].onTimerExpire(cpu, late),
}
},
.ApuChannel => |id| {
switch (id) {
0 => cpu.bus.apu.ch1.channelTimerOverflow(late),
1 => cpu.bus.apu.ch2.channelTimerOverflow(late),
2 => cpu.bus.apu.ch3.channelTimerOverflow(late),
3 => cpu.bus.apu.ch4.channelTimerOverflow(late),
0 => cpu.bus.apu.ch1.onToneSweepEvent(late),
1 => cpu.bus.apu.ch2.onToneEvent(late),
2 => cpu.bus.apu.ch3.onWaveEvent(late),
3 => cpu.bus.apu.ch4.onNoiseEvent(late),
}
},
.RealTimeClock => {
@@ -66,12 +62,12 @@ pub const Scheduler = struct {
if (device.kind != .Rtc or device.ptr == null) return;
const clock = @ptrCast(*Clock, @alignCast(@alignOf(*Clock), device.ptr.?));
clock.updateTime(late);
clock.onClockUpdate(late);
},
.FrameSequencer => cpu.bus.apu.tickFrameSequencer(late),
.FrameSequencer => cpu.bus.apu.onSequencerTick(late),
.SampleAudio => cpu.bus.apu.sampleAudio(late),
.HBlank => cpu.bus.ppu.handleHBlankEnd(cpu, late), // The end of a HBlank
.VBlank => cpu.bus.ppu.handleHDrawEnd(cpu, late), // The end of a VBlank
.HBlank => cpu.bus.ppu.onHblankEnd(cpu, late), // The end of a HBlank
.VBlank => cpu.bus.ppu.onHdrawEnd(cpu, late), // The end of a VBlank
}
}
}

View File

@@ -1,200 +0,0 @@
const std = @import("std");
const builtin = @import("builtin");
const Log2Int = std.math.Log2Int;
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
const allow_unhandled_io = @import("emu.zig").allow_unhandled_io;
// Sign-Extend value of type `T` to type `U`
pub fn sext(comptime T: type, comptime U: type, value: T) T {
// U must have less bits than T
comptime std.debug.assert(@typeInfo(U).Int.bits <= @typeInfo(T).Int.bits);
const iT = std.meta.Int(.signed, @typeInfo(T).Int.bits);
const ExtU = if (@typeInfo(U).Int.signedness == .unsigned) T else iT;
const shift = @intCast(Log2Int(T), @typeInfo(T).Int.bits - @typeInfo(U).Int.bits);
return @bitCast(T, @bitCast(iT, @as(ExtU, @truncate(U, value)) << shift) >> shift);
}
/// See https://godbolt.org/z/W3en9Eche
pub inline fn rotr(comptime T: type, x: T, r: anytype) T {
if (@typeInfo(T).Int.signedness == .signed)
@compileError("cannot rotate signed integer");
const ar = @intCast(Log2Int(T), @mod(r, @typeInfo(T).Int.bits));
return x >> ar | x << (1 +% ~ar);
}
pub const FpsTracker = struct {
const Self = @This();
fps: u32,
count: std.atomic.Atomic(u32),
timer: std.time.Timer,
pub fn init() Self {
return .{
.fps = 0,
.count = std.atomic.Atomic(u32).init(0),
.timer = std.time.Timer.start() catch unreachable,
};
}
pub fn tick(self: *Self) void {
_ = self.count.fetchAdd(1, .Monotonic);
}
pub fn value(self: *Self) u32 {
if (self.timer.read() >= std.time.ns_per_s) {
self.fps = self.count.swap(0, .SeqCst);
self.timer.reset();
}
return self.fps;
}
};
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 may be null padded to a maximum
/// length of 12 bytes.
///
/// This function returns a slice of everything just before the first
/// `\0`
pub fn asStringSlice(title: *const [12]u8) []const u8 {
var len = title.len;
for (title) |char, i| {
if (char == 0) {
len = i;
break;
}
}
return title[0..len];
}
/// Copies a Title and returns either an identical or similar
/// array consisting of ASCII that won't make any file system angry
///
/// e.g. POKEPIN R/S to POKEPIN R_S
pub fn escape(title: [12]u8) [12]u8 {
var result: [12]u8 = title;
for (result) |*char| {
if (char.* == '/' or char.* == '\\') char.* = '_';
if (char.* == 0) break;
}
return result;
}
pub const FilePaths = struct {
rom: []const u8,
bios: ?[]const u8,
save: ?[]const u8,
};
pub const io = struct {
pub const read = struct {
pub fn todo(comptime log: anytype, comptime format: []const u8, args: anytype) u8 {
log.debug(format, args);
return 0;
}
pub fn undef(comptime T: type, log: anytype, comptime format: []const u8, args: anytype) ?T {
log.warn(format, args);
if (builtin.mode == .Debug and !allow_unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{});
return null;
}
};
pub const write = struct {
pub fn undef(log: anytype, comptime format: []const u8, args: anytype) void {
log.warn(format, args);
if (builtin.mode == .Debug and !allow_unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{});
}
};
};
pub fn readUndefined(log: anytype, comptime format: []const u8, args: anytype) u8 {
log.warn(format, args);
if (builtin.mode == .Debug) std.debug.panic("TODO: Implement I/O Register", .{});
return 0;
}
pub fn writeUndefined(log: anytype, comptime format: []const u8, args: anytype) void {
log.warn(format, args);
if (builtin.mode == .Debug) std.debug.panic("TODO: Implement I/O Register", .{});
}
pub const Logger = struct {
const Self = @This();
buf: std.io.BufferedWriter(4096 << 2, std.fs.File.Writer),
pub fn init(file: std.fs.File) Self {
return .{
.buf = .{ .unbuffered_writer = file.writer() },
};
}
pub fn print(self: *Self, comptime format: []const u8, args: anytype) !void {
try self.buf.writer().print(format, args);
}
pub fn mgbaLog(self: *Self, cpu: *const Arm7tdmi, opcode: u32) void {
const fmt_base = "{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} | ";
const thumb_fmt = fmt_base ++ "{X:0>4}:\n";
const arm_fmt = fmt_base ++ "{X:0>8}:\n";
if (cpu.cpsr.t.read()) {
if (opcode >> 11 == 0x1E) {
// Instruction 1 of a BL Opcode, print in ARM mode
const low = cpu.bus.dbgRead(u16, cpu.r[15]);
const bl_opcode = @as(u32, opcode) << 16 | low;
self.print(arm_fmt, Self.fmtArgs(cpu, bl_opcode)) catch @panic("failed to write to log file");
} else {
self.print(thumb_fmt, Self.fmtArgs(cpu, opcode)) catch @panic("failed to write to log file");
}
} else {
self.print(arm_fmt, Self.fmtArgs(cpu, opcode)) catch @panic("failed to write to log file");
}
}
fn fmtArgs(cpu: *const Arm7tdmi, opcode: u32) FmtArgTuple {
return .{
cpu.r[0],
cpu.r[1],
cpu.r[2],
cpu.r[3],
cpu.r[4],
cpu.r[5],
cpu.r[6],
cpu.r[7],
cpu.r[8],
cpu.r[9],
cpu.r[10],
cpu.r[11],
cpu.r[12],
cpu.r[13],
cpu.r[14],
cpu.r[15],
cpu.cpsr.raw,
opcode,
};
}
};
const FmtArgTuple = std.meta.Tuple(&.{ u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32 });

View File

@@ -1,46 +1,76 @@
const std = @import("std");
const builtin = @import("builtin");
const known_folders = @import("known_folders");
const clap = @import("clap");
const Gui = @import("Gui.zig");
const config = @import("config.zig");
const Gui = @import("platform.zig").Gui;
const Bus = @import("core/Bus.zig");
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
const Scheduler = @import("core/scheduler.zig").Scheduler;
const FilePaths = @import("core/util.zig").FilePaths;
const FilePaths = @import("util.zig").FilePaths;
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;
const cpu_logging = @import("core/emu.zig").cpu_logging;
pub const log_level = if (builtin.mode != .Debug) .info else std.log.default_level;
// TODO: Reimpl Logging
// CLI Arguments + Help Text
const params = clap.parseParamsComptime(
\\-h, --help Display this help and exit.
\\-s, --skip Skip BIOS.
\\-b, --bios <str> Optional path to a GBA BIOS ROM.
\\<str> Path to the GBA GamePak ROM
\\<str> Path to the GBA GamePak ROM.
\\
);
pub fn main() anyerror!void {
pub fn main() void {
// Main Allocator for ZBA
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer std.debug.assert(!gpa.deinit());
const allocator = gpa.allocator();
// Handle CLI Input
const result = try clap.parse(clap.Help, &params, clap.parsers.default, .{});
// Determine the Data Directory (stores saves)
const data_path = blk: {
const result = known_folders.getPath(allocator, .data);
const option = result catch |e| exitln("interrupted while determining the data folder: {}", .{e});
const path = option orelse exitln("no valid data folder found", .{});
ensureDataDirsExist(path) catch |e| exitln("failed to create folders under \"{s}\": {}", .{ path, e });
break :blk path;
};
defer allocator.free(data_path);
// Determine the Config Directory
const config_path = blk: {
const result = known_folders.getPath(allocator, .roaming_configuration);
const option = result catch |e| exitln("interreupted while determining the config folder: {}", .{e});
const path = option orelse exitln("no valid config folder found", .{});
ensureConfigDirExists(path) catch |e| exitln("failed to create required folder \"{s}\": {}", .{ path, e });
break :blk path;
};
defer allocator.free(config_path);
// Parse CLI
const result = clap.parse(clap.Help, &params, clap.parsers.default, .{}) catch |e| exitln("failed to parse cli: {}", .{e});
defer result.deinit();
const paths = try handleArguments(allocator, &result);
// TODO: Move config file to XDG Config directory?
const cfg_file_path = configFilePath(allocator, config_path) catch |e| exitln("failed to ready config file for access: {}", .{e});
defer allocator.free(cfg_file_path);
config.load(allocator, cfg_file_path) catch |e| exitln("failed to load config file: {}", .{e});
const paths = handleArguments(allocator, data_path, &result) catch |e| exitln("failed to handle cli arguments: {}", .{e});
defer if (paths.save) |path| allocator.free(path);
const log_file: ?std.fs.File = if (cpu_logging) try std.fs.cwd().createFile("zba.log", .{}) else null;
const log_file = if (config.config().debug.cpu_trace) blk: {
break :blk std.fs.cwd().createFile("zba.log", .{}) catch |e| exitln("failed to create trace log file: {}", .{e});
} else null;
defer if (log_file) |file| file.close();
// TODO: Take Emulator Init Code out of main.zig
@@ -49,55 +79,82 @@ pub fn main() anyerror!void {
var bus: Bus = undefined;
var cpu = Arm7tdmi.init(&scheduler, &bus, log_file);
if (paths.bios == null) cpu.fastBoot();
try bus.init(allocator, &scheduler, &cpu, paths);
bus.init(allocator, &scheduler, &cpu, paths) catch |e| exitln("failed to init zba bus: {}", .{e});
defer bus.deinit();
var gui = Gui.init(bus.pak.title, width, height);
gui.initAudio(&bus.apu);
defer gui.deinit();
try gui.run(&cpu, &scheduler);
}
fn getSavePath(allocator: Allocator) !?[]const u8 {
const save_subpath = "zba" ++ [_]u8{std.fs.path.sep} ++ "save";
const maybe_data_path = try known_folders.getPath(allocator, .data);
defer if (maybe_data_path) |path| allocator.free(path);
const save_path = if (maybe_data_path) |base| try std.fs.path.join(allocator, &[_][]const u8{ base, "zba", "save" }) else null;
if (save_path) |_| {
// If we've determined what our save path should be, ensure the prereq directories
// are present so that we can successfully write to the path when necessary
const maybe_data_dir = try known_folders.open(allocator, .data, .{});
if (maybe_data_dir) |data_dir| try data_dir.makePath(save_subpath);
if (config.config().guest.skip_bios or result.args.skip or paths.bios == null) {
cpu.fastBoot();
}
return save_path;
var gui = Gui.init(&bus.pak.title, &bus.apu, width, height) catch |e| exitln("failed to init gui: {}", .{e});
defer gui.deinit();
gui.run(&cpu, &scheduler) catch |e| exitln("failed to run gui thread: {}", .{e});
}
fn getRomPath(result: *const clap.Result(clap.Help, &params, clap.parsers.default)) ![]const u8 {
return switch (result.positionals.len) {
1 => result.positionals[0],
0 => std.debug.panic("ZBA requires a positional path to a GamePak ROM.\n", .{}),
else => std.debug.panic("ZBA received too many arguments.\n", .{}),
};
}
pub fn handleArguments(allocator: Allocator, result: *const clap.Result(clap.Help, &params, clap.parsers.default)) !FilePaths {
const rom_path = try getRomPath(result);
pub fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const clap.Result(clap.Help, &params, clap.parsers.default)) !FilePaths {
const rom_path = romPath(result);
log.info("ROM path: {s}", .{rom_path});
const bios_path = result.args.bios;
if (bios_path) |path| log.info("BIOS path: {s}", .{path}) else log.info("No BIOS provided", .{});
const save_path = try getSavePath(allocator);
if (save_path) |path| log.info("Save path: {s}", .{path});
return FilePaths{
const bios_path = result.args.bios;
if (bios_path) |path| log.info("BIOS path: {s}", .{path}) else log.warn("No BIOS provided", .{});
const save_path = try std.fs.path.join(allocator, &[_][]const u8{ data_path, "zba", "save" });
log.info("Save path: {s}", .{save_path});
return .{
.rom = rom_path,
.bios = bios_path,
.save = save_path,
};
}
fn configFilePath(allocator: Allocator, config_path: []const u8) ![]const u8 {
const path = try std.fs.path.join(allocator, &[_][]const u8{ config_path, "zba", "config.toml" });
errdefer allocator.free(path);
// We try to create the file exclusively, meaning that we err out if the file already exists.
// All we care about is a file being there so we can just ignore that error in particular and
// continue down the happy pathj
std.fs.accessAbsolute(path, .{}) catch |e| {
if (e != error.FileNotFound) return e;
const config_file = std.fs.createFileAbsolute(path, .{}) catch |err| exitln("failed to create \"{s}\": {}", .{ path, err });
defer config_file.close();
try config_file.writeAll(@embedFile("../example.toml"));
};
return path;
}
fn ensureDataDirsExist(data_path: []const u8) !void {
var dir = try std.fs.openDirAbsolute(data_path, .{});
defer dir.close();
// Will recursively create directories
try dir.makePath("zba" ++ std.fs.path.sep_str ++ "save");
}
fn ensureConfigDirExists(config_path: []const u8) !void {
var dir = try std.fs.openDirAbsolute(config_path, .{});
defer dir.close();
try dir.makePath("zba");
}
fn romPath(result: *const clap.Result(clap.Help, &params, clap.parsers.default)) []const u8 {
return switch (result.positionals.len) {
1 => result.positionals[0],
0 => exitln("ZBA requires a path to a GamePak ROM", .{}),
else => exitln("ZBA received too many positional arguments.", .{}),
};
}
fn exitln(comptime format: []const u8, args: anytype) noreturn {
const stderr = std.io.getStdErr().writer();
stderr.print(format, args) catch {}; // Just exit already...
stderr.writeByte('\n') catch {};
std.os.exit(1);
}

339
src/platform.zig Normal file
View File

@@ -0,0 +1,339 @@
const std = @import("std");
const SDL = @import("sdl2");
const gl = @import("gl");
const emu = @import("core/emu.zig");
const config = @import("config.zig");
const Apu = @import("core/apu.zig").Apu;
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
const Scheduler = @import("core/scheduler.zig").Scheduler;
const FpsTracker = @import("util.zig").FpsTracker;
const gba_width = @import("core/ppu.zig").width;
const gba_height = @import("core/ppu.zig").height;
pub const sample_rate = 1 << 16;
pub const sample_format = SDL.AUDIO_U16;
const default_title = "ZBA";
pub const Gui = struct {
const Self = @This();
const SDL_GLContext = *anyopaque; // SDL.SDL_GLContext is a ?*anyopaque
const log = std.log.scoped(.Gui);
// zig fmt: off
const vertices: [32]f32 = [_]f32{
// Positions // Colours // Texture Coords
1.0, -1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, // Top Right
1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, // Bottom Right
-1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, // Bottom Left
-1.0, -1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, // Top Left
};
const indices: [6]u32 = [_]u32{
0, 1, 3, // First Triangle
1, 2, 3, // Second Triangle
};
// zig fmt: on
window: *SDL.SDL_Window,
ctx: SDL_GLContext,
title: []const u8,
audio: Audio,
program_id: gl.GLuint,
pub fn init(title: *const [12]u8, apu: *Apu, width: i32, height: i32) !Self {
if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO) < 0) panic();
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_PROFILE_MASK, SDL.SDL_GL_CONTEXT_PROFILE_CORE) < 0) panic();
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic();
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic();
const win_scale = @intCast(c_int, config.config().host.win_scale);
const window = SDL.SDL_CreateWindow(
default_title,
SDL.SDL_WINDOWPOS_CENTERED,
SDL.SDL_WINDOWPOS_CENTERED,
@as(c_int, width * win_scale),
@as(c_int, height * win_scale),
SDL.SDL_WINDOW_OPENGL | SDL.SDL_WINDOW_SHOWN,
) orelse panic();
const ctx = SDL.SDL_GL_CreateContext(window) orelse panic();
if (SDL.SDL_GL_MakeCurrent(window, ctx) < 0) panic();
try gl.load(ctx, Self.glGetProcAddress);
if (SDL.SDL_GL_SetSwapInterval(@boolToInt(config.config().host.vsync)) < 0) panic();
const program_id = try compileShaders();
return Self{
.window = window,
.title = std.mem.sliceTo(title, 0),
.ctx = ctx,
.program_id = program_id,
.audio = Audio.init(apu),
};
}
fn compileShaders() !gl.GLuint {
// TODO: Panic on Shader Compiler Failure + Error Message
const vert_shader = @embedFile("shader/pixelbuf.vert");
const frag_shader = @embedFile("shader/pixelbuf.frag");
const vs = gl.createShader(gl.VERTEX_SHADER);
defer gl.deleteShader(vs);
gl.shaderSource(vs, 1, &[_][*c]const u8{vert_shader}, 0);
gl.compileShader(vs);
if (!shader.didCompile(vs)) return error.VertexCompileError;
const fs = gl.createShader(gl.FRAGMENT_SHADER);
defer gl.deleteShader(fs);
gl.shaderSource(fs, 1, &[_][*c]const u8{frag_shader}, 0);
gl.compileShader(fs);
if (!shader.didCompile(fs)) return error.FragmentCompileError;
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
return program;
}
// Returns the VAO ID since it's used in run()
fn generateBuffers() struct { c_uint, c_uint, c_uint } {
var vao_id: c_uint = undefined;
var vbo_id: c_uint = undefined;
var ebo_id: c_uint = undefined;
gl.genVertexArrays(1, &vao_id);
gl.genBuffers(1, &vbo_id);
gl.genBuffers(1, &ebo_id);
gl.bindVertexArray(vao_id);
gl.bindBuffer(gl.ARRAY_BUFFER, vbo_id);
gl.bufferData(gl.ARRAY_BUFFER, @sizeOf(@TypeOf(vertices)), &vertices, gl.STATIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo_id);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, @sizeOf(@TypeOf(indices)), &indices, gl.STATIC_DRAW);
// Position
gl.vertexAttribPointer(0, 3, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, 0)); // lmao
gl.enableVertexAttribArray(0);
// Colour
gl.vertexAttribPointer(1, 3, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, (3 * @sizeOf(f32))));
gl.enableVertexAttribArray(1);
// Texture Coord
gl.vertexAttribPointer(2, 2, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, (6 * @sizeOf(f32))));
gl.enableVertexAttribArray(2);
return .{ vao_id, vbo_id, ebo_id };
}
fn generateTexture(buf: []const u8) c_uint {
var tex_id: c_uint = undefined;
gl.genTextures(1, &tex_id);
gl.bindTexture(gl.TEXTURE_2D, tex_id);
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gba_width, gba_height, 0, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, buf.ptr);
// gl.generateMipmap(gl.TEXTURE_2D); // TODO: Remove?
return tex_id;
}
pub fn run(self: *Self, cpu: *Arm7tdmi, scheduler: *Scheduler) !void {
var quit = std.atomic.Atomic(bool).init(false);
var tracker = FpsTracker.init();
var buffer_ids = Self.generateBuffers();
defer {
gl.deleteBuffers(1, &buffer_ids[2]); // EBO
gl.deleteBuffers(1, &buffer_ids[1]); // VBO
gl.deleteVertexArrays(1, &buffer_ids[0]); // VAO
}
const vao_id = buffer_ids[0];
const tex_id = Self.generateTexture(cpu.bus.ppu.framebuf.get(.Renderer));
defer gl.deleteTextures(1, &tex_id);
const thread = try std.Thread.spawn(.{}, emu.run, .{ &quit, scheduler, cpu, &tracker });
defer thread.join();
var title_buf: [0x100]u8 = undefined;
emu_loop: while (true) {
var event: SDL.SDL_Event = undefined;
while (SDL.SDL_PollEvent(&event) != 0) {
switch (event.type) {
SDL.SDL_QUIT => break :emu_loop,
SDL.SDL_KEYDOWN => {
const key_code = event.key.keysym.sym;
var keyinput = cpu.bus.io.keyinput.load(.Monotonic);
switch (key_code) {
SDL.SDLK_UP => keyinput.up.unset(),
SDL.SDLK_DOWN => keyinput.down.unset(),
SDL.SDLK_LEFT => keyinput.left.unset(),
SDL.SDLK_RIGHT => keyinput.right.unset(),
SDL.SDLK_x => keyinput.a.unset(),
SDL.SDLK_z => keyinput.b.unset(),
SDL.SDLK_a => keyinput.shoulder_l.unset(),
SDL.SDLK_s => keyinput.shoulder_r.unset(),
SDL.SDLK_RETURN => keyinput.start.unset(),
SDL.SDLK_RSHIFT => keyinput.select.unset(),
else => {},
}
cpu.bus.io.keyinput.store(keyinput.raw, .Monotonic);
},
SDL.SDL_KEYUP => {
const key_code = event.key.keysym.sym;
var keyinput = cpu.bus.io.keyinput.load(.Monotonic);
switch (key_code) {
SDL.SDLK_UP => keyinput.up.set(),
SDL.SDLK_DOWN => keyinput.down.set(),
SDL.SDLK_LEFT => keyinput.left.set(),
SDL.SDLK_RIGHT => keyinput.right.set(),
SDL.SDLK_x => keyinput.a.set(),
SDL.SDLK_z => keyinput.b.set(),
SDL.SDLK_a => keyinput.shoulder_l.set(),
SDL.SDLK_s => keyinput.shoulder_r.set(),
SDL.SDLK_RETURN => keyinput.start.set(),
SDL.SDLK_RSHIFT => keyinput.select.set(),
SDL.SDLK_i => {
comptime std.debug.assert(sample_format == SDL.AUDIO_U16);
log.err("Sample Count: {}", .{cpu.bus.apu.sample_queue.len() / 2});
},
// SDL.SDLK_j => log.err("Scheduler Capacity: {} | Scheduler Event Count: {}", .{ scheduler.queue.capacity(), scheduler.queue.count() }),
SDL.SDLK_k => {},
else => {},
}
cpu.bus.io.keyinput.store(keyinput.raw, .Monotonic);
},
else => {},
}
}
// Emulator has an internal Double Buffer
const framebuf = cpu.bus.ppu.framebuf.get(.Renderer);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gba_width, gba_height, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, framebuf.ptr);
gl.useProgram(self.program_id);
gl.bindVertexArray(vao_id);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, null);
SDL.SDL_GL_SwapWindow(self.window);
const dyn_title = std.fmt.bufPrintZ(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, tracker.value() }) catch unreachable;
SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr);
}
quit.store(true, .Monotonic); // Terminate Emulator Thread
}
pub fn deinit(self: *Self) void {
self.audio.deinit();
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 {
_ = ctx;
return SDL.SDL_GL_GetProcAddress(proc.ptr);
}
};
const Audio = struct {
const Self = @This();
const log = std.log.scoped(.PlatformAudio);
device: SDL.SDL_AudioDeviceID,
fn init(apu: *Apu) Self {
var have: SDL.SDL_AudioSpec = undefined;
var want: SDL.SDL_AudioSpec = std.mem.zeroes(SDL.SDL_AudioSpec);
want.freq = sample_rate;
want.format = sample_format;
want.channels = 2;
want.samples = 0x100;
want.callback = Self.callback;
want.userdata = apu;
std.debug.assert(sample_format == SDL.AUDIO_U16);
log.info("Host Sample Rate: {}Hz, Host Format: SDL.AUDIO_U16", .{sample_rate});
const device = SDL.SDL_OpenAudioDevice(null, 0, &want, &have, 0);
if (device == 0) panic();
if (!config.config().host.mute) {
SDL.SDL_PauseAudioDevice(device, 0); // Unpause Audio
log.info("Unpaused Device", .{});
}
return .{ .device = device };
}
fn deinit(self: *Self) void {
SDL.SDL_CloseAudioDevice(self.device);
self.* = undefined;
}
export fn callback(userdata: ?*anyopaque, stream: [*c]u8, len: c_int) void {
const T = *Apu;
const apu = @ptrCast(T, @alignCast(@alignOf(T), userdata));
comptime std.debug.assert(sample_format == SDL.AUDIO_U16);
const sample_buf = @ptrCast([*]u16, @alignCast(@alignOf(u16), stream))[0 .. @intCast(u32, len) / @sizeOf(u16)];
var previous: u16 = 0x8000;
for (sample_buf) |*sample| {
if (apu.sample_queue.pop()) |value| previous = value;
sample.* = previous;
}
}
};
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 {
const str = @as(?[*:0]const u8, SDL.SDL_GetError()) orelse "unknown error";
@panic(std.mem.sliceTo(str, 0));
}

25
src/shader/pixelbuf.frag Normal file
View File

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

13
src/shader/pixelbuf.vert Normal file
View File

@@ -0,0 +1,13 @@
#version 330 core
layout (location = 0) in vec3 pos;
layout (location = 1) in vec3 in_color;
layout (location = 2) in vec2 in_uv;
out vec3 color;
out vec2 uv;
void main() {
color = in_color;
uv = in_uv;
gl_Position = vec4(pos, 1.0);
}

384
src/util.zig Normal file
View File

@@ -0,0 +1,384 @@
const std = @import("std");
const builtin = @import("builtin");
const config = @import("config.zig");
const Log2Int = std.math.Log2Int;
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
// Sign-Extend value of type `T` to type `U`
pub fn sext(comptime T: type, comptime U: type, value: T) T {
// U must have less bits than T
comptime std.debug.assert(@typeInfo(U).Int.bits <= @typeInfo(T).Int.bits);
const iT = std.meta.Int(.signed, @typeInfo(T).Int.bits);
const ExtU = if (@typeInfo(U).Int.signedness == .unsigned) T else iT;
const shift_amt = @intCast(Log2Int(T), @typeInfo(T).Int.bits - @typeInfo(U).Int.bits);
return @bitCast(T, @bitCast(iT, @as(ExtU, @truncate(U, value)) << shift_amt) >> shift_amt);
}
/// See https://godbolt.org/z/W3en9Eche
pub inline fn rotr(comptime T: type, x: T, r: anytype) T {
if (@typeInfo(T).Int.signedness == .signed)
@compileError("cannot rotate signed integer");
const ar = @intCast(Log2Int(T), @mod(r, @typeInfo(T).Int.bits));
return x >> ar | x << (1 +% ~ar);
}
pub const FpsTracker = struct {
const Self = @This();
fps: u32,
count: std.atomic.Atomic(u32),
timer: std.time.Timer,
pub fn init() Self {
return .{
.fps = 0,
.count = std.atomic.Atomic(u32).init(0),
.timer = std.time.Timer.start() catch unreachable,
};
}
pub fn tick(self: *Self) void {
_ = self.count.fetchAdd(1, .Monotonic);
}
pub fn value(self: *Self) u32 {
if (self.timer.read() >= std.time.ns_per_s) {
self.fps = self.count.swap(0, .Monotonic);
self.timer.reset();
}
return self.fps;
}
};
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;
}
/// Creates a copy of a title with all Filesystem-invalid characters replaced
///
/// e.g. POKEPIN R/S to POKEPIN R_S
pub fn escape(title: [12]u8) [12]u8 {
var ret: [12]u8 = title;
//TODO: Add more replacements
std.mem.replaceScalar(u8, &ret, '/', '_');
std.mem.replaceScalar(u8, &ret, '\\', '_');
return ret;
}
pub const FilePaths = struct {
rom: []const u8,
bios: ?[]const u8,
save: ?[]const u8,
};
pub const io = struct {
pub const read = struct {
pub fn todo(comptime log: anytype, comptime format: []const u8, args: anytype) u8 {
log.debug(format, args);
return 0;
}
pub fn undef(comptime T: type, comptime log: anytype, comptime format: []const u8, args: anytype) ?T {
@setCold(true);
const unhandled_io = config.config().debug.unhandled_io;
log.warn(format, args);
if (builtin.mode == .Debug and !unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{});
return null;
}
pub fn err(comptime T: type, comptime log: anytype, comptime format: []const u8, args: anytype) ?T {
@setCold(true);
log.err(format, args);
return null;
}
};
pub const write = struct {
pub fn undef(log: anytype, comptime format: []const u8, args: anytype) void {
const unhandled_io = config.config().debug.unhandled_io;
log.warn(format, args);
if (builtin.mode == .Debug and !unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{});
}
};
};
pub const Logger = struct {
const Self = @This();
buf: std.io.BufferedWriter(4096 << 2, std.fs.File.Writer),
pub fn init(file: std.fs.File) Self {
return .{
.buf = .{ .unbuffered_writer = file.writer() },
};
}
pub fn print(self: *Self, comptime format: []const u8, args: anytype) !void {
try self.buf.writer().print(format, args);
try self.buf.flush(); // FIXME: On panics, whatever is in the buffer isn't written to file
}
pub fn mgbaLog(self: *Self, cpu: *const Arm7tdmi, opcode: u32) void {
const fmt_base = "{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} | ";
const thumb_fmt = fmt_base ++ "{X:0>4}:\n";
const arm_fmt = fmt_base ++ "{X:0>8}:\n";
if (cpu.cpsr.t.read()) {
if (opcode >> 11 == 0x1E) {
// Instruction 1 of a BL Opcode, print in ARM mode
const low = cpu.bus.dbgRead(u16, cpu.r[15] - 2);
const bl_opcode = @as(u32, opcode) << 16 | low;
self.print(arm_fmt, Self.fmtArgs(cpu, bl_opcode)) catch @panic("failed to write to log file");
} else {
self.print(thumb_fmt, Self.fmtArgs(cpu, opcode)) catch @panic("failed to write to log file");
}
} else {
self.print(arm_fmt, Self.fmtArgs(cpu, opcode)) catch @panic("failed to write to log file");
}
}
fn fmtArgs(cpu: *const Arm7tdmi, opcode: u32) FmtArgTuple {
return .{
cpu.r[0],
cpu.r[1],
cpu.r[2],
cpu.r[3],
cpu.r[4],
cpu.r[5],
cpu.r[6],
cpu.r[7],
cpu.r[8],
cpu.r[9],
cpu.r[10],
cpu.r[11],
cpu.r[12],
cpu.r[13],
cpu.r[14],
cpu.r[15] - if (cpu.cpsr.t.read()) 2 else @as(u32, 4),
cpu.cpsr.raw,
opcode,
};
}
};
const FmtArgTuple = struct { u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32 };
pub const audio = struct {
const _io = @import("core/bus/io.zig");
const ToneSweep = @import("core/apu/ToneSweep.zig");
const Tone = @import("core/apu/Tone.zig");
const Wave = @import("core/apu/Wave.zig");
const Noise = @import("core/apu/Noise.zig");
pub const length = struct {
const FrameSequencer = @import("core/apu.zig").FrameSequencer;
/// Update State of Ch1, Ch2 and Ch3 length timer
pub fn update(comptime T: type, self: *T, fs: *const FrameSequencer, nrx34: _io.Frequency) void {
comptime std.debug.assert(T == ToneSweep or T == Tone or T == Wave);
// Write to NRx4 when FS's next step is not one that clocks the length counter
if (!fs.isLengthNext()) {
// If length_enable was disabled but is now enabled and length timer is not 0 already,
// decrement the length timer
if (!self.freq.length_enable.read() and nrx34.length_enable.read() and self.len_dev.timer != 0) {
self.len_dev.timer -= 1;
// If Length Timer is now 0 and trigger is clear, disable the channel
if (self.len_dev.timer == 0 and !nrx34.trigger.read()) self.enabled = false;
}
}
}
pub const ch4 = struct {
/// update state of ch4 length timer
pub fn update(self: *Noise, fs: *const FrameSequencer, nr44: _io.NoiseControl) void {
// Write to NRx4 when FS's next step is not one that clocks the length counter
if (!fs.isLengthNext()) {
// If length_enable was disabled but is now enabled and length timer is not 0 already,
// decrement the length timer
if (!self.cnt.length_enable.read() and nr44.length_enable.read() and self.len_dev.timer != 0) {
self.len_dev.timer -= 1;
// If Length Timer is now 0 and trigger is clear, disable the channel
if (self.len_dev.timer == 0 and !nr44.trigger.read()) self.enabled = false;
}
}
}
};
};
};
/// Sets a quarter (8) of the bits of the u32 `left` to the value of u8 `right`
pub inline fn setQuart(left: u32, addr: u8, right: u8) u32 {
const offset = @truncate(u2, addr);
return switch (offset) {
0b00 => (left & 0xFFFF_FF00) | right,
0b01 => (left & 0xFFFF_00FF) | @as(u32, right) << 8,
0b10 => (left & 0xFF00_FFFF) | @as(u32, right) << 16,
0b11 => (left & 0x00FF_FFFF) | @as(u32, right) << 24,
};
}
/// Calculates the correct shift offset for an aligned/unaligned u8 read
///
/// TODO: Support u16 reads of u32 values?
pub inline fn getHalf(byte: u8) u4 {
return @truncate(u4, byte & 1) << 3;
}
pub inline fn setHalf(comptime T: type, left: T, addr: u8, right: HalfInt(T)) T {
const offset = @truncate(u1, addr >> if (T == u32) 1 else 0);
return switch (T) {
u32 => switch (offset) {
0b0 => (left & 0xFFFF_0000) | right,
0b1 => (left & 0x0000_FFFF) | @as(u32, right) << 16,
},
u16 => switch (offset) {
0b0 => (left & 0xFF00) | right,
0b1 => (left & 0x00FF) | @as(u16, right) << 8,
},
else => @compileError("unsupported type"),
};
}
/// The Integer type which corresponds to T with exactly half the amount of bits
fn HalfInt(comptime T: type) type {
const type_info = @typeInfo(T);
comptime std.debug.assert(type_info == .Int); // Type must be an integer
comptime std.debug.assert(type_info.Int.bits % 2 == 0); // Type must have an even amount of bits
return std.meta.Int(type_info.Int.signedness, type_info.Int.bits >> 1);
}
const Mutex = std.Thread.Mutex;
pub fn RingBuffer(comptime T: type) type {
return struct {
const Self = @This();
const Index = usize;
const max_capacity = (@as(Index, 1) << @typeInfo(Index).Int.bits - 1) - 1; // half the range of index type
const log = std.log.scoped(.RingBuffer);
read: Index,
write: Index,
buf: []T,
mutex: Mutex,
const Error = error{buffer_full};
pub fn init(buf: []T) Self {
std.mem.set(T, buf, 0);
std.debug.assert(std.math.isPowerOfTwo(buf.len)); // capacity must be a power of two
std.debug.assert(buf.len <= max_capacity);
return .{ .read = 0, .write = 0, .buf = buf, .mutex = .{} };
}
pub fn push(self: *Self, left: T, right: T) Error!void {
self.mutex.lock();
defer self.mutex.unlock();
try self._push(left);
self._push(right) catch |e| {
self.write -= 1; // undo the previous write;
return e;
};
}
pub fn pop(self: *Self) ?T {
self.mutex.lock();
defer self.mutex.unlock();
return self._pop();
}
pub fn len(self: *Self) Index {
self.mutex.lock();
defer self.mutex.unlock();
return self._len();
}
fn _push(self: *Self, value: T) Error!void {
if (self.isFull()) return error.buffer_full;
defer self.write += 1;
self.buf[self.mask(self.write)] = value;
}
fn _pop(self: *Self) ?T {
if (self.isEmpty()) return null;
defer self.read += 1;
return self.buf[self.mask(self.read)];
}
fn _len(self: *const Self) Index {
return self.write - self.read;
}
fn isFull(self: *const Self) bool {
return self._len() == self.buf.len;
}
fn isEmpty(self: *const Self) bool {
return self.read == self.write;
}
fn mask(self: *const Self, idx: Index) Index {
return idx & (self.buf.len - 1);
}
};
}
test "RingBuffer" {
const Queue = RingBuffer(u8);
var buf: [4]u8 = undefined;
var queue = Queue.init(&buf);
try queue.push(1, 2);
try std.testing.expectEqual(@as(?u8, 1), queue.pop());
try queue.push(3, 4);
try std.testing.expectError(Queue.Error.buffer_full, queue.push(5, 6));
try std.testing.expectEqual(@as(?u8, 2), queue.pop());
try queue.push(7, 8);
try std.testing.expectEqual(@as(?u8, 3), queue.pop());
try std.testing.expectEqual(@as(?u8, 4), queue.pop());
try std.testing.expectEqual(@as(?u8, 7), queue.pop());
try std.testing.expectEqual(@as(?u8, 8), queue.pop());
try std.testing.expectEqual(@as(?u8, null), queue.pop());
}