Compare commits
563 Commits
6ab4610a81
...
paoda/upgr
| Author | SHA1 | Date | |
|---|---|---|---|
| bd02f625a5 | |||
| 6cacdc7180 | |||
| 56ac755a73 | |||
| c5ffa19e06 | |||
| 2d3c659b85 | |||
| 94894fadc6 | |||
| 0e02d9aaab | |||
| b4830326ff | |||
| ef93bbe084 | |||
| f71aaafe41 | |||
| 66192daf6c | |||
| 05b7a9014d | |||
| 493d7aeede | |||
| 9183e6850d | |||
| d54202bf8b | |||
| d097dcc2f5 | |||
| 203971c91a | |||
| 78b849b6ff | |||
| 557b90a39f | |||
| dd3158bcfc | |||
| 64cd373957 | |||
| 64a30b190c | |||
| f2c728ef44 | |||
| 8b4faca80f | |||
| f73b096d62 | |||
| d4b7167e29 | |||
| d96c9c01ff | |||
| 954fb279ad | |||
| 5b6650ef34 | |||
| 10215d4e99 | |||
| e8bc798120 | |||
| 44818a4d5b | |||
| 07d85628ac | |||
| a8cd510da6 | |||
| 3040a9f45c | |||
| ccdc2cbad4 | |||
| 16c3eceffd | |||
| 8f5a0cab9c | |||
| 79514b0cd0 | |||
| a048263fd6 | |||
| d9e09a9cbe | |||
| 2b9a479b96 | |||
| 21295b8d03 | |||
| 89671f767e | |||
| a92598d17d | |||
| c677957725 | |||
| a5e636d9c5 | |||
| f6527da948 | |||
| 53fb1d163b | |||
| df005d7fb6 | |||
| 3c619df3dc | |||
| 13f5e7a480 | |||
| 8519187d9b | |||
| a66428f24e | |||
| 1d8b21d6b4 | |||
| b879c76510 | |||
| 0dbba2fb9a | |||
| 49b0620c48 | |||
| a6a9e3ac72 | |||
| aeefff86f8 | |||
| 91aa98eef7 | |||
| f3b6c4f3fe | |||
| 5aa5ac2a8b | |||
| b1827ccea0 | |||
| 2629d15e2f | |||
| c7b62d3202 | |||
| 85ec9a84c4 | |||
| 5adbc354d6 | |||
| f8477714ae | |||
| bd872ee1c0 | |||
| 11eae091db | |||
| 72b702cb21 | |||
| d985eac0fc | |||
| 3fff4fd742 | |||
| e90d5a17ba | |||
| 54143332ab | |||
| baa3fb7905 | |||
| 57c7437f77 | |||
| eef5a238a0 | |||
| 6048458f9b | |||
| ff609c85ba | |||
| 3e98f4053a | |||
| 1d601dba39 | |||
| a8fac5f3c6 | |||
| ae78588b80 | |||
| fe6fc0e517 | |||
| 3dcc4cb385 | |||
| 5e94cbfbea | |||
| 3b13102abb | |||
| 7234ecab37 | |||
| ddf4599162 | |||
| 01f5410180 | |||
| 49706842af | |||
| 2798a90d83 | |||
| 518b868249 | |||
| 755115660b | |||
| 6709f8c551 | |||
| 1f3cdd9513 | |||
| 65af6aa499 | |||
| 024151a5c1 | |||
| e380af7056 | |||
| e654abfd1d | |||
| 3510a6cff8 | |||
| 3fb351e762 | |||
| a11b96b84e | |||
| c3be1c0a67 | |||
| fdf7399e52 | |||
| ed8155139a | |||
| 8112b1aab2 | |||
| c0e583d20d | |||
| 3f72367aaf | |||
| c27f487bf0 | |||
| ae3bb94036 | |||
| ddc54e2977 | |||
| ed49d7c460 | |||
| 59baa14bde | |||
| 6bf1c44961 | |||
| 94702b9b51 | |||
| 0f148507e4 | |||
| 0cec779545 | |||
| 1ecbbc7d29 | |||
| caaa60d1a8 | |||
| 39d50466c9 | |||
| 5a452d85c1 | |||
| 4326ae7a0a | |||
| 905c4448d0 | |||
| 0de44835e5 | |||
| 5aac04faf5 | |||
| f98a1700e0 | |||
| acdb270793 | |||
| 4ceed382ed | |||
| 52ce4f3d20 | |||
| c1c8cac6e4 | |||
| be7a34f719 | |||
| f7a94634f9 | |||
| 7d4ab6db2c | |||
| 0a78587d8e | |||
| b753ceef8e | |||
| 8963fe205b | |||
| e906506e16 | |||
| 3195a45e3d | |||
| 6aad911985 | |||
| e3b45ef794 | |||
| 8e1a539e70 | |||
| 63fa972afa | |||
| bf95eee3f1 | |||
| 240fbcb1df | |||
| 26db340077 | |||
| 20f611b7b5 | |||
| f9aefedf60 | |||
| d7e3d34726 | |||
| 2294dc8832 | |||
| 4af86e1cb3 | |||
| 9fcbbe7d57 | |||
| c3f67e38a1 | |||
| 46e29245b7 | |||
| 002e33b48b | |||
| 5bb25fe214 | |||
| 66db2e6049 | |||
| c5cf471912 | |||
| 4ed4f8e143 | |||
| f31699d921 | |||
| 96a9ae2ca5 | |||
| ee1c0bb313 | |||
| 558c03b12b | |||
| 7d8fbbb086 | |||
| 9fd405a896 | |||
| 5d7cf3a8a2 | |||
| 1230aa1e91 | |||
| accecb3350 | |||
| 1e0ade8f55 | |||
| 429676ad43 | |||
| ef39d9a7b8 | |||
| 986bc9448e | |||
| d34893ba72 | |||
| b8a5fb95c1 | |||
| 102b2c946b | |||
| 505b1b9608 | |||
| 2851c140ea | |||
| 637d81ce44 | |||
| bc52461f0f | |||
| c395c04a6e | |||
| 9eb4f8f191 | |||
| f774256c42 | |||
| 5c15d039e1 | |||
| 28e9342c25 | |||
| af8ec4db5b | |||
| 5d47e5d167 | |||
| 5101fbd809 | |||
| 472457b9f3 | |||
| 2ef4bb7dcc | |||
| 9a732ea6f8 | |||
| f80799a593 | |||
| ca67ca3183 | |||
| 47fc49deb6 | |||
| 472215b4c2 | |||
| c9a423d094 | |||
| 1d163fa56f | |||
| 13710a3236 | |||
| 6154585e77 | |||
| 7debdc490d | |||
| 58375795bf | |||
| f0dca29836 | |||
| c75682dbd4 | |||
| 36832ba1fb | |||
| 647bd83224 | |||
| c831f67d1a | |||
| 268961262d | |||
| 3e62feacba | |||
| d859cee365 | |||
| 371cf4cc12 | |||
| 10aec67ee0 | |||
| 4eb715a138 | |||
| 14b24787ab | |||
| eb7ffa29f4 | |||
| 4b8ed3cebb | |||
| 928ce674d9 | |||
| 945dbec013 | |||
| dd98066a34 | |||
| a2868dfe9e | |||
| 22979d9450 | |||
| 712c58391d | |||
| 407774d798 | |||
| 16f8f4c953 | |||
| 143ffd95f7 | |||
| 250ff25ed7 | |||
| eff52ac1bb | |||
| e60b556f72 | |||
| 3a3e6acc6a | |||
| 4b4bc7f894 | |||
| 325208d460 | |||
| f44a1a49fd | |||
| 1575f517a9 | |||
| 26dba16789 | |||
| b133880064 | |||
| 2474daa3ae | |||
| fc53a40b3c | |||
| 7097e21361 | |||
| a9fe24b1b4 | |||
| f38c840d32 | |||
| 19e70c39d1 | |||
| 5a72a8e7f3 | |||
| 7b146ad7ca | |||
| 822eed1f3a | |||
| b37a14900c | |||
| f5bd20bc2a | |||
| d3514b14f3 | |||
| 06c60dad74 | |||
| 870e991862 | |||
| 5bb5bdf389 | |||
| a3996cbc58 | |||
| a948c6f900 | |||
| 014180cbd0 | |||
| e4451738b5 | |||
| 48b81c8e7a | |||
| 3cf1bf54e9 | |||
| 1f9eeedfe8 | |||
| 72a63eeb98 | |||
| 2799c3f202 | |||
| b3ada64e64 | |||
| 62162ba492 | |||
| aa100de581 | |||
| 7142831284 | |||
| 97f48c730e | |||
| 293fbd9f55 | |||
| 622f479e07 | |||
| 0204eb6f94 | |||
| 86d2224cfc | |||
| 21eddac31e | |||
| 785135a074 | |||
| fd38fd6506 | |||
| bcacac64df | |||
| dc7cad9691 | |||
| b5d8a65e69 | |||
| 8028394105 | |||
| cb0eb67e4b | |||
| 13f6ee8ec4 | |||
| c71e954748 | |||
| c697dec716 | |||
| 92cfc763c0 | |||
| e192c6712f | |||
| 3466bf6c0a | |||
| fbe3de0eb3 | |||
| 4af144fca2 | |||
| 9a8aaba1ab | |||
| fa3b9c21b9 | |||
| d3efa432fa | |||
| 50adb5fbac | |||
| 19d78b9292 | |||
| a2e702c366 | |||
| 12c138364d | |||
| 7783c11fac | |||
| 3fc3366c8a | |||
| d6b182f245 | |||
| 3857c44e68 | |||
| 089c5fa025 | |||
| c977f3f965 | |||
| 92417025e9 | |||
| 1c52c0bf91 | |||
| 617f7f4690 | |||
| 434a0dfac9 | |||
| 4ec8dab460 | |||
| 59c9ff910e | |||
| 0027d3f8a3 | |||
| 9f45888910 | |||
| bf442d5a40 | |||
| 65cfc97f28 | |||
| fa862f095a | |||
| f3c05b6fe6 | |||
| 3fb7f2f814 | |||
| 59669ba3a5 | |||
| 6a798d2c9d | |||
| 5f8c6833f4 | |||
| aa52bb5917 | |||
| e57f918856 | |||
| e5b7441740 | |||
| 2ab8769b7a | |||
| 3c3c0d32dd | |||
| 739db99c83 | |||
| 5a18b1dcc7 | |||
| 2c8616f610 | |||
| 53eec5c3ff | |||
| c397b7069d | |||
| 9d037fdc3e | |||
| 53191b0eeb | |||
| c7c4a90948 | |||
| 03ded099d2 | |||
| 5d9a57c7eb | |||
| 769fad8996 | |||
| 3b3eb52c48 | |||
| d798aea6ea | |||
| 5d37c212e2 | |||
| 887bd89668 | |||
| 81c669fe64 | |||
| 265234ee97 | |||
| 02534c5c19 | |||
| 5e4fb7b952 | |||
| 7e15e83d38 | |||
| c9ea80e03b | |||
| 4cd722e447 | |||
| adfb23fab4 | |||
| 5bbbdc3469 | |||
| 47adc0c5ae | |||
| 601b0b2aae | |||
| 3becd790cf | |||
| 460f8308a7 | |||
| cc8c1c1e21 | |||
| f5e401a4ee | |||
| dba8873f76 | |||
| db08edbdb9 | |||
| 35dba63b94 | |||
| a753912cb5 | |||
| 7441af9582 | |||
| 708f64035f | |||
| e4bbd33a49 | |||
| 25d13722f7 | |||
| 4eb0d469b3 | |||
| ce2271100b | |||
| e226a59a2f | |||
| 7ff5f3b8e7 | |||
| b6f5517c89 | |||
| 2dc3864dca | |||
| deff74d804 | |||
| 4f93f3e454 | |||
| 38afb567b9 | |||
| 4006888629 | |||
| e7faa9713b | |||
| 413ff02ad9 | |||
| 517ed7a835 | |||
| d4bc1f2cd0 | |||
| c6ce810afe | |||
| 1170673447 | |||
| c007bf4d8e | |||
| 697ec46cf1 | |||
| 41ee32b118 | |||
| 8f5d054195 | |||
| c907552864 | |||
| ff3f79801c | |||
| f130d1991c | |||
| 24a8905c29 | |||
| e70fe73899 | |||
| a2d2a84850 | |||
| 109561310e | |||
| 5164aa961d | |||
| c9b0030b4b | |||
| af2ad6c924 | |||
| e7777737b3 | |||
| c40cc2ba30 | |||
| 8dddb865cc | |||
| 2e821ab79c | |||
| a667269d26 | |||
| daf977ef06 | |||
| 660c8a2d62 | |||
| 9d590b099a | |||
| d5443d9c2f | |||
| f0ce39230b | |||
| c0e026b9a8 | |||
| 208e88e869 | |||
| 5df023fb41 | |||
| 46ac1542a6 | |||
| c2f55e0bfb | |||
| 12f9bb51c1 | |||
| 41558c9103 | |||
| 68012f84d3 | |||
| 640b1f7c5d | |||
| f2f4bb205a | |||
| 6c88a0aec2 | |||
| 002287ecfe | |||
| a87b46898b | |||
| 417810581b | |||
| bc2950916f | |||
| d9c9105449 | |||
| 05a432f1c1 | |||
| f4a48d536c | |||
| 81db06d2fc | |||
| 1812fb8008 | |||
| 762494453f | |||
| e3553bcbd6 | |||
| 9cce4d9859 | |||
| 75ba9a4bf9 | |||
| 85e8ca9146 | |||
| 2f07c18f0b | |||
| ed3d275974 | |||
| 0184ec3e5e | |||
| 97a689ab55 | |||
| c3611a0f00 | |||
| d270ec711f | |||
| 172a59aefb | |||
| 92cabd248b | |||
| 6b09250a56 | |||
| f6d746e810 | |||
| 9b9b6c0d6f | |||
| 6d5c30ac25 | |||
| c1b74d556a | |||
| dfe94fb931 | |||
| ffbb31c767 | |||
| 714209565b | |||
| 643cd13952 | |||
| 2c763e9772 | |||
| ad1f5ea8b8 | |||
| 76b4d56ca6 | |||
| c100d64fcb | |||
| 5da84aff36 | |||
| 76789aa8bc | |||
| 80e714e2eb | |||
| 37a360ec07 | |||
| a976a5769e | |||
| 6df55c2d86 | |||
| a1008738d2 | |||
| 11a034658a | |||
| aac01b0bfe | |||
| 5310c12669 | |||
| 9b9de11e0c | |||
| f8018854be | |||
| fae4b430ab | |||
| 1bb3659df6 | |||
| 3046e6243a | |||
| e127669549 | |||
| 7f555095f2 | |||
| 29da7b294e | |||
| bf7b533b3c | |||
| 37c039fb92 | |||
| 4c172cff70 | |||
| bd54cba8a0 | |||
| da4bb17782 | |||
| 5dd69500ca | |||
| eff25a0ab2 | |||
| 1901a471e4 | |||
| 3d61c0dba4 | |||
| 39ab363afa | |||
| 1921218c7b | |||
| 40968f0990 | |||
| 04d54ec97a | |||
| 1fd80c1c23 | |||
| 48679fa4ca | |||
| bdea19f280 | |||
| 5579643d65 | |||
| c6e6b42869 | |||
| 3623362f72 | |||
| c538079ad4 | |||
| 8e3f48837d | |||
| 025f295c08 | |||
| 017ec407f5 | |||
| 4a76611fca | |||
| cb10dfbdfd | |||
| 0c50ef1e6d | |||
| 05e67da181 | |||
| f1df3d6615 | |||
| c5b4b51ae0 | |||
| 2e4854c2ff | |||
| 85f0b13f4a | |||
| dccd00782b | |||
| 3d8c944bcc | |||
| 90302d1c52 | |||
| cb4d3a9a51 | |||
| ddb68a7952 | |||
| 97de5d1a96 | |||
| d6ef53fd67 | |||
| b65f833b28 | |||
| ac0486be1b | |||
| 441ebc38c7 | |||
| be2dfb379a | |||
| bc66be6c06 | |||
| 5368ff912d | |||
| c2cf2d2965 | |||
| e5ab8b51a9 | |||
| c767e88e8d | |||
| 9e2e8c3d1a | |||
| e426f2459e | |||
| 3746cf6025 | |||
| f6c8d7ca07 | |||
| 07343efdf3 | |||
| 4018f3875b | |||
| 034f2e8d1d | |||
| d275a4890f | |||
| ce97a52868 | |||
| d2d4667f7b | |||
| 5835b509e4 | |||
| 338122ed43 | |||
| e5a76a3c02 | |||
| 31fa06ac4a | |||
| ec25a9aae4 | |||
| b238a3e8f3 | |||
| aca7fc9a60 | |||
| d2740e30d9 | |||
| 8ab7a178c1 | |||
| d897c2fdcc | |||
| 783706193b | |||
| b93bd53529 | |||
| f9013cf9db | |||
| eaac49cebb | |||
| ee27053db3 | |||
| 7441dd151c | |||
| bbd4447734 | |||
| 225c0f7d55 | |||
| fcde905ae1 | |||
| 798987eba0 | |||
| adfd501fc4 | |||
| 9581e3b3cb | |||
| 1b9ab1f1d7 | |||
| c52dc5adb1 | |||
| 7bfb87a859 | |||
| aec189ac6a | |||
| 0aece06107 | |||
| 2842345111 | |||
| aa6f3c7a92 | |||
| 3ae24d6977 | |||
| 0a22730479 | |||
| 166bc6fc6d | |||
| bf4207ba8c | |||
| 78080b4682 | |||
| 9159270e87 | |||
| 428eff1468 | |||
| 5ec8d4b0a5 | |||
| 3a51707280 | |||
| b4d20fb264 | |||
| 746158043d | |||
| 25300c8a9f | |||
| 27d0ba8c7e | |||
| 2f74b61f2e | |||
| b233981a34 | |||
| 1b8db0c427 | |||
| 3e4f9eddb2 |
59
.github/workflows/main.yml
vendored
Normal file
59
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
name: Nightly
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "**.zig"
|
||||||
|
- "dl_sdl2.ps1"
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest] # TODO: Figure out Apple Silicon macOS
|
||||||
|
runs-on: ${{matrix.os}}
|
||||||
|
steps:
|
||||||
|
- uses: goto-bus-stop/setup-zig@v2
|
||||||
|
with:
|
||||||
|
version: 0.13.0
|
||||||
|
- run: |
|
||||||
|
git config --global core.autocrlf false
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
- name: prepare-linux
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install libgtk-3-dev libsdl2-dev
|
||||||
|
- name: prepare-windows
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
run: |
|
||||||
|
.\dl_sdl2.ps1
|
||||||
|
- name: prepare-macos
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
run: |
|
||||||
|
brew install sdl2
|
||||||
|
- name: build
|
||||||
|
run: zig build -Doptimize=ReleaseSafe -Dcpu=baseline
|
||||||
|
- name: upload
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: zba-${{matrix.os}}
|
||||||
|
path: zig-out
|
||||||
|
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
- uses: goto-bus-stop/setup-zig@v2
|
||||||
|
with:
|
||||||
|
version: 0.13.0
|
||||||
|
- run: zig fmt --check {src,lib}/**/*.zig build.zig build.zig.zon
|
||||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -1,11 +1,19 @@
|
|||||||
/.vscode
|
/.vscode
|
||||||
/bin
|
/bin
|
||||||
/zig-cache
|
**/zig-cache
|
||||||
/zig-out
|
**/.zig-cache
|
||||||
|
**/zig-out
|
||||||
/docs
|
/docs
|
||||||
**/*.log
|
**/*.log
|
||||||
**/*.bin
|
**/*.bin
|
||||||
|
|
||||||
# Build on WIndows
|
# Build on Windows
|
||||||
/.build_config
|
/.build_config
|
||||||
/lib/SDL2
|
/lib/SDL2
|
||||||
|
|
||||||
|
# Any Custom Scripts for Debugging purposes
|
||||||
|
*.sh
|
||||||
|
|
||||||
|
|
||||||
|
# Dear ImGui
|
||||||
|
**/imgui.ini
|
||||||
|
|||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
|||||||
[submodule "lib/SDL.zig"]
|
|
||||||
path = lib/SDL.zig
|
|
||||||
url = https://github.com/MasterQ32/SDL.zig
|
|
||||||
|
|||||||
8
.vscode/extensions.json
vendored
8
.vscode/extensions.json
vendored
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"augusterame.zls-vscode",
|
|
||||||
"usernamehw.errorlens",
|
|
||||||
"vadimcn.vscode-lldb",
|
|
||||||
"dan-c-underwood.arm"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
96
README.md
Normal file
96
README.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# ZBA (working title)
|
||||||
|
|
||||||
|
A Game Boy Advance Emulator written in Zig ⚡!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
I'm hardly the first to write a Game Boy Advance Emulator nor will I be the last. This project isn't going to compete with the GOATs like [mGBA](https://github.com/mgba-emu) or [NanoBoyAdvance](https://github.com/nba-emu/NanoBoyAdvance). There aren't any interesting ideas either like in [DSHBA](https://github.com/DenSinH/DSHBA).
|
||||||
|
|
||||||
|
This is a simple (read: incomplete) for-fun long-term project. I hope to get "mostly there", which to me means that I'm not missing any major hardware features and the set of possible improvements would be in memory timing or in UI/UX. With respect to that goal, here's what's outstanding:
|
||||||
|
|
||||||
|
### TODO
|
||||||
|
|
||||||
|
- [x] Affine Sprites
|
||||||
|
- [ ] Windowing (see [this branch](https://git.musuka.dev/paoda/zba/src/branch/window))
|
||||||
|
- [ ] Audio Resampler (Having issues with SDL2's)
|
||||||
|
- [ ] Refactoring for easy-ish perf boosts
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
ZBA supports both a CLI and a GUI. If running from the terminal, try using `zba --help` to see what you can do. If you want to use the GUI, feel free to just run `zba` without any arguments.
|
||||||
|
|
||||||
|
ZBA does not feature any BIOS HLE, so providing one will be necessary if a ROM makes use of it. Need one? Why not try using the open-source [Cult-Of-GBA BIOS](https://github.com/Cult-of-GBA/BIOS) written by [fleroviux](https://github.com/fleroviux) and [DenSinH](https://github.com/DenSinH)?
|
||||||
|
|
||||||
|
Finally it's worth noting that ZBA uses a TOML config file it'll store in your OS's data directory. See `example.toml` to learn about the defaults and what exactly you can mess around with.
|
||||||
|
|
||||||
|
## Compiling
|
||||||
|
|
||||||
|
Most recently built on Zig [v0.11.0](https://github.com/ziglang/zig/tree/0.11.0)
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
Dependency | Source
|
||||||
|
--- | ---
|
||||||
|
known-folders | <https://github.com/ziglibs/known-folders>
|
||||||
|
nfd-zig | <https://github.com/fabioarnold/nfd-zig>
|
||||||
|
SDL.zig | <https://github.com/MasterQ32/SDL.zig>
|
||||||
|
tomlz | <https://github.com/mattyhall/tomlz>
|
||||||
|
zba-gdbstub | <https://github.com/paoda/zba-gdbstub>
|
||||||
|
zba-util | <https://git.musuka.dev/paoda/zba-util>
|
||||||
|
zgui | <https://github.com/michal-z/zig-gamedev/tree/main/libs/zgui>
|
||||||
|
zig-clap | <https://github.com/Hejsil/zig-clap>
|
||||||
|
zig-datetime | <https://github.com/frmdstryr/zig-datetime>
|
||||||
|
`bitfield.zig` | [https://github.com/FlorenceOS/Florence](https://github.com/FlorenceOS/Florence/blob/aaa5a9e568/lib/util/bitfields.zig)
|
||||||
|
`gl.zig` | <https://github.com/MasterQ32/zig-opengl>
|
||||||
|
|
||||||
|
Use `git submodule update --init` from the project root to pull the git relevant git submodules
|
||||||
|
|
||||||
|
Be sure to provide SDL2 using:
|
||||||
|
|
||||||
|
- Linux: Your distro's package manager
|
||||||
|
- macOS: ¯\\\_(ツ)_/¯ (try [this formula](https://formulae.brew.sh/formula/sdl2)?)
|
||||||
|
- Windows: [`vcpkg`](https://github.com/Microsoft/vcpkg) (install `sdl2:x64-windows`)
|
||||||
|
|
||||||
|
`SDL.zig` will provide a helpful compile error if the zig compiler is unable to find SDL2.
|
||||||
|
|
||||||
|
Once you've got all the dependencies, execute `zig build -Doptimize=ReleaseSafe`. The executable will be under `zig-out/bin` and the shared libraries (if enabled) under `zig-out/lib`. If working with shared libraries on windows, be sure to add all artifacts to the same directory. On Unix, you'll want to make use of `LD_PRELOAD`.
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
Key | Button | | Key | Button
|
||||||
|
--- | --- | --- | --- | ---
|
||||||
|
<kbd>A</kbd> | L | | <kbd>S</kbd> | R
|
||||||
|
<kbd>X</kbd> | A | | <kbd>Z</kbd> | B
|
||||||
|
<kbd>Return</kbd> | Start | | <kbd>RShift</kbd> | Select
|
||||||
|
Arrow Keys | D-Pad
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
GBA Tests | [jsmolka](https://github.com/jsmolka/) | gba_tests | [destoer](https://github.com/destoer/)
|
||||||
|
--- | --- | --- | ---
|
||||||
|
`arm.gba`, `thumb.gba` | PASS | `cond_invalid.gba` | PASS
|
||||||
|
`memory.gba`, `bios.gba` | PASS | `dma_priority.gba` | PASS
|
||||||
|
`flash64.gba`, `flash128.gba` | PASS | `hello_world.gba` | PASS
|
||||||
|
`sram.gba` | PASS | `if_ack.gba` | PASS
|
||||||
|
`none.gba` | PASS | `line_timing.gba` | FAIL
|
||||||
|
`hello.gba`, `shades.gba`, `stripes.gba` | PASS | `lyc_midline.gba` | FAIL
|
||||||
|
`nes.gba` | PASS | `window_midframe.gba` | FAIL
|
||||||
|
|
||||||
|
GBARoms | [DenSinH](https://github.com/DenSinH/) | GBA Test Collection | [ladystarbreeze](https://github.com/ladystarbreeze)
|
||||||
|
--- | --- | --- | ---
|
||||||
|
`eeprom-test`, `flash-test` | PASS | `retAddr.gba` | PASS
|
||||||
|
`midikey2freq` | PASS | `helloWorld.gba` | PASS
|
||||||
|
`swi-tests-random` | FAIL | `helloAudio.gba` | PASS
|
||||||
|
|
||||||
|
FuzzARM | [DenSinH](https://github.com/DenSinH/) | arm7wrestler GBA Fixed | [destoer](https://github.com/destoer)
|
||||||
|
--- | --- | --- | ---
|
||||||
|
`main.gba` | PASS | `armwrestler-gba-fixed.gba` | PASS
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [GBATEK](https://problemkaputt.de/gbatek.htm)
|
||||||
|
- [TONC](https://coranac.com/tonc/text/toc.htm)
|
||||||
|
- [ARM Architecture Reference Manual](https://www.intel.com/content/dam/www/programmable/us/en/pdfs/literature/third-party/ddi0100e_arm_arm.pdf)
|
||||||
|
- [ARM7TDMI Data Sheet](https://www.dca.fee.unicamp.br/cursos/EA871/references/ARM/ARM7TDMIDataSheet.pdf)
|
||||||
BIN
assets/screenshot.png
Normal file
BIN
assets/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
157
build.zig
157
build.zig
@@ -1,45 +1,154 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Sdk = @import("lib/SDL.zig/Sdk.zig");
|
|
||||||
|
|
||||||
pub fn build(b: *std.build.Builder) void {
|
// Although this function looks imperative, it does not perform the build
|
||||||
// Standard target options allows the person running `zig build` to choose
|
// directly and instead it mutates the build graph (`b`) that will be then
|
||||||
|
// executed by an external runner. The functions in `std.Build` implement a DSL
|
||||||
|
// for defining build steps and express dependencies between them, allowing the
|
||||||
|
// build runner to parallelize the build automatically (and the cache system to
|
||||||
|
// know when a step doesn't need to be re-run).
|
||||||
|
pub fn build(b: *std.Build) void {
|
||||||
|
// Standard target options allow the person running `zig build` to choose
|
||||||
// what target to build for. Here we do not override the defaults, which
|
// 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
|
// means any target is allowed, and the default is native. Other options
|
||||||
// for restricting supported target set are available.
|
// for restricting supported target set are available.
|
||||||
const target = b.standardTargetOptions(.{});
|
const target = b.standardTargetOptions(.{});
|
||||||
|
// Standard optimization options allow the person running `zig build` to select
|
||||||
|
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
|
||||||
|
// set a preferred release mode, allowing the user to decide how to optimize.
|
||||||
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
// It's also possible to define more custom flags to toggle optional features
|
||||||
|
// of this build script using `b.option()`. All defined flags (including
|
||||||
|
// target and optimize options) will be listed when running `zig build --help`
|
||||||
|
// in this directory.
|
||||||
|
|
||||||
// Standard release options allow the person running `zig build` to select
|
// This creates a module, which represents a collection of source files alongside
|
||||||
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
|
// some compilation options, such as optimization mode and linked system libraries.
|
||||||
const mode = b.standardReleaseOptions();
|
// Zig modules are the preferred way of making Zig code available to consumers.
|
||||||
|
// addModule defines a module that we intend to make available for importing
|
||||||
|
// to our consumers. We must give it a name because a Zig package can expose
|
||||||
|
// multiple modules and consumers will need to be able to specify which
|
||||||
|
// module they want to access.
|
||||||
|
const exe_mod = b.addModule("zba", .{
|
||||||
|
// The root source file is the "entry point" of this module. Users of
|
||||||
|
// this module will only be able to access public declarations contained
|
||||||
|
// in this file, which means that if you have declarations that you
|
||||||
|
// intend to expose to consumers that were defined in other files part
|
||||||
|
// of this module, you will have to make sure to re-export them from
|
||||||
|
// the root file.
|
||||||
|
.root_source_file = b.path("src/main.zig"),
|
||||||
|
// Later on we'll use this module as the root module of a test executable
|
||||||
|
// which requires us to specify a target.
|
||||||
|
.target = target,
|
||||||
|
.link_libc = true,
|
||||||
|
});
|
||||||
|
|
||||||
const exe = b.addExecutable("zba", "src/main.zig");
|
// Here we define an executable. An executable needs to have a root module
|
||||||
|
// which needs to expose a `main` function. While we could add a main function
|
||||||
|
// to the module defined above, it's sometimes preferable to split business
|
||||||
|
// business logic and the CLI into two separate modules.
|
||||||
|
//
|
||||||
|
// If your goal is to create a Zig library for others to use, consider if
|
||||||
|
// it might benefit from also exposing a CLI tool. A parser library for a
|
||||||
|
// data serialization format could also bundle a CLI syntax checker, for example.
|
||||||
|
//
|
||||||
|
// If instead your goal is to create an executable, consider if users might
|
||||||
|
// be interested in also being able to embed the core functionality of your
|
||||||
|
// program in their own executable in order to avoid the overhead involved in
|
||||||
|
// subprocessing your CLI tool.
|
||||||
|
//
|
||||||
|
// If neither case applies to you, feel free to delete the declaration you
|
||||||
|
// don't need and to put everything under a single module.
|
||||||
|
const exe = b.addExecutable(.{ .name = "zba", .root_module = exe_mod });
|
||||||
|
|
||||||
// Bitfield type from FlorenceOS: https://github.com/FlorenceOS/
|
const zgui = b.dependency("zgui", .{ .shared = false, .with_implot = true, .backend = .sdl3_opengl3 });
|
||||||
exe.addPackage(.{ .name = "bitfield", .path = .{ .path = "lib/util/bitfield.zig" } });
|
const sdl = b.dependency("sdl", .{ .target = target, .optimize = optimize, .preferred_linkage = .static });
|
||||||
|
const gl = @import("zigglgen").generateBindingsModule(b, .{ .api = .gl, .version = .@"3.3", .profile = .core });
|
||||||
|
|
||||||
// Zig SDL Bindings: https://github.com/MasterQ32/SDL.zig
|
const sdl_lib = sdl.artifact("SDL3");
|
||||||
const sdk = Sdk.init(b);
|
const zgui_lib = zgui.artifact("imgui");
|
||||||
sdk.link(exe, .dynamic);
|
|
||||||
|
|
||||||
exe.addPackage(sdk.getNativePackage("sdl2"));
|
exe_mod.linkLibrary(sdl_lib);
|
||||||
|
exe_mod.linkLibrary(zgui_lib);
|
||||||
|
|
||||||
exe.setTarget(target);
|
exe_mod.addImport("gl", gl);
|
||||||
exe.setBuildMode(mode);
|
exe_mod.addImport("known_folders", b.dependency("known_folders", .{}).module("known-folders"));
|
||||||
exe.install();
|
exe_mod.addImport("datetime", b.dependency("datetime", .{}).module("datetime"));
|
||||||
|
exe_mod.addImport("clap", b.dependency("clap", .{}).module("clap"));
|
||||||
|
exe_mod.addImport("zba_util", b.dependency("zba_util", .{}).module("zba_util"));
|
||||||
|
exe_mod.addImport("arm32", b.dependency("arm32", .{}).module("arm32"));
|
||||||
|
exe_mod.addImport("gdbstub", b.dependency("zba_gdbstub", .{}).module("zba_gdbstub"));
|
||||||
|
exe_mod.addImport("nfd", b.dependency("nfdzig", .{}).module("nfd"));
|
||||||
|
exe_mod.addImport("zgui", zgui.module("root"));
|
||||||
|
exe_mod.addImport("bitjuggle", b.dependency("bitjuggle", .{}).module("bitjuggle"));
|
||||||
|
|
||||||
const run_cmd = exe.run();
|
// This declares intent for the executable to be installed into the
|
||||||
|
// install prefix when running `zig build` (i.e. when executing the default
|
||||||
|
// step). By default the install prefix is `zig-out/` but can be overridden
|
||||||
|
// by passing `--prefix` or `-p`.
|
||||||
|
b.installArtifact(exe);
|
||||||
|
|
||||||
|
// This creates a top level step. Top level steps have a name and can be
|
||||||
|
// invoked by name when running `zig build` (e.g. `zig build run`).
|
||||||
|
// This will evaluate the `run` step rather than the default step.
|
||||||
|
// For a top level step to actually do something, it must depend on other
|
||||||
|
// steps (e.g. a Run step, as we will see in a moment).
|
||||||
|
const run_step = b.step("run", "Run the app");
|
||||||
|
|
||||||
|
// This creates a RunArtifact step in the build graph. A RunArtifact step
|
||||||
|
// invokes an executable compiled by Zig. Steps will only be executed by the
|
||||||
|
// runner if invoked directly by the user (in the case of top level steps)
|
||||||
|
// or if another step depends on it, so it's up to you to define when and
|
||||||
|
// how this Run step will be executed. In our case we want to run it when
|
||||||
|
// the user runs `zig build run`, so we create a dependency link.
|
||||||
|
const run_cmd = b.addRunArtifact(exe);
|
||||||
|
run_step.dependOn(&run_cmd.step);
|
||||||
|
|
||||||
|
// By making the run step depend on the default step, it will be run from the
|
||||||
|
// installation directory rather than directly from within the cache directory.
|
||||||
run_cmd.step.dependOn(b.getInstallStep());
|
run_cmd.step.dependOn(b.getInstallStep());
|
||||||
|
|
||||||
|
// This allows the user to pass arguments to the application in the build
|
||||||
|
// command itself, like this: `zig build run -- arg1 arg2 etc`
|
||||||
if (b.args) |args| {
|
if (b.args) |args| {
|
||||||
run_cmd.addArgs(args);
|
run_cmd.addArgs(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
const run_step = b.step("run", "Run the app");
|
// Creates an executable that will run `test` blocks from the provided module.
|
||||||
run_step.dependOn(&run_cmd.step);
|
// Here `mod` needs to define a target, which is why earlier we made sure to
|
||||||
|
// set the releative field.
|
||||||
|
const mod_tests = b.addTest(.{
|
||||||
|
.root_module = exe_mod,
|
||||||
|
});
|
||||||
|
|
||||||
const exe_tests = b.addTest("src/main.zig");
|
// A run step that will run the test executable.
|
||||||
exe_tests.setTarget(target);
|
const run_mod_tests = b.addRunArtifact(mod_tests);
|
||||||
exe_tests.setBuildMode(mode);
|
|
||||||
|
|
||||||
const test_step = b.step("test", "Run unit tests");
|
// Creates an executable that will run `test` blocks from the executable's
|
||||||
test_step.dependOn(&exe_tests.step);
|
// root module. Note that test executables only test one module at a time,
|
||||||
|
// hence why we have to create two separate ones.
|
||||||
|
const exe_tests = b.addTest(.{
|
||||||
|
.root_module = exe.root_module,
|
||||||
|
});
|
||||||
|
|
||||||
|
// A run step that will run the second test executable.
|
||||||
|
const run_exe_tests = b.addRunArtifact(exe_tests);
|
||||||
|
|
||||||
|
// A top level step for running all tests. dependOn can be called multiple
|
||||||
|
// times and since the two run steps do not depend on one another, this will
|
||||||
|
// make the two of them run in parallel.
|
||||||
|
const test_step = b.step("test", "Run tests");
|
||||||
|
test_step.dependOn(&run_mod_tests.step);
|
||||||
|
test_step.dependOn(&run_exe_tests.step);
|
||||||
|
|
||||||
|
// Just like flags, top level steps are also listed in the `--help` menu.
|
||||||
|
//
|
||||||
|
// The Zig build system is entirely implemented in userland, which means
|
||||||
|
// that it cannot hook into private compiler APIs. All compilation work
|
||||||
|
// orchestrated by the build system will result in other Zig compiler
|
||||||
|
// subcommands being invoked with the right flags defined. You can observe
|
||||||
|
// these invocations when one fails (or you pass a flag to increase
|
||||||
|
// verbosity) to validate assumptions and diagnose problems.
|
||||||
|
//
|
||||||
|
// Lastly, the Zig build system is relatively simple and self-contained,
|
||||||
|
// and reading its source code will allow you to master it.
|
||||||
}
|
}
|
||||||
|
|||||||
55
build.zig.zon
Normal file
55
build.zig.zon
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
.{
|
||||||
|
.name = .zba,
|
||||||
|
.version = "0.1.0",
|
||||||
|
.paths = .{
|
||||||
|
"build.zig",
|
||||||
|
"build.zig.zon",
|
||||||
|
"lib/bitfield.zig",
|
||||||
|
"lib/gl.zig",
|
||||||
|
"src",
|
||||||
|
},
|
||||||
|
.minimum_zig_version = "0.15.1",
|
||||||
|
.fingerprint = 0xcb596c7fbdb20efc,
|
||||||
|
.dependencies = .{
|
||||||
|
// .tomlz = .{
|
||||||
|
// .url = "git+https://github.com/paoda/tomlz#9a16dd53927ef2012478b6494bafb4475e44f4c9",
|
||||||
|
// .hash = "12204f922cab84980e36b5c058d354ec0ee169bda401c8e0e80a463580349b476569",
|
||||||
|
// },
|
||||||
|
|
||||||
|
.zba_util = .{ .path = "../zba-util" },
|
||||||
|
.arm32 = .{ .path = "../arm32" },
|
||||||
|
.zba_gdbstub = .{ .path = "../zba-gdbstub" },
|
||||||
|
.known_folders = .{
|
||||||
|
.url = "git+https://github.com/ziglibs/known-folders.git#ab5cf5feb936fa3b72c95d3ad0c0c67791937ba1",
|
||||||
|
.hash = "known_folders-0.0.0-Fy-PJtTTAADUOhGKM0sxzG4eMkNQxRvx9e5dfHVyaeA3",
|
||||||
|
},
|
||||||
|
.nfdzig = .{
|
||||||
|
.url = "git+https://github.com/paoda/nfd-zig#0ad2a0c092ffba0c98613d619b82100c991f5ad6",
|
||||||
|
.hash = "nfdzig-0.1.0-11fxvN6IBgD5rvvfjrw1wPqibMsbUJ-h2ZcGR6FOEvrm",
|
||||||
|
},
|
||||||
|
.datetime = .{
|
||||||
|
.url = "git+https://github.com/frmdstryr/zig-datetime#3a39a21e6e34dcb0ade0ff828d0914d40ba535f3",
|
||||||
|
.hash = "datetime-0.8.0-cJNXzP_YAQBxQ5hkNNP6ScnG5XsqciJmeP5RVV4xwCBA",
|
||||||
|
},
|
||||||
|
.clap = .{
|
||||||
|
.url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.11.0.tar.gz",
|
||||||
|
.hash = "clap-0.11.0-oBajB-HnAQDPCKYzwF7rO3qDFwRcD39Q0DALlTSz5H7e",
|
||||||
|
},
|
||||||
|
.sdl = .{
|
||||||
|
.url = "git+https://github.com/castholm/SDL.git#b1913e7c31ad72ecfd3ab04aeac387027754cfaf",
|
||||||
|
.hash = "sdl-0.3.0+3.2.22-7uIn9Pg3fwGG2IyIOPxxOSVe-75nUng9clt7tXGFLzMr",
|
||||||
|
},
|
||||||
|
.zigglgen = .{
|
||||||
|
.url = "git+https://github.com/castholm/zigglgen.git#a1e969b3e35818785fab0373124f50463793b28a",
|
||||||
|
.hash = "zigglgen-0.4.0-bmyqLQGMLwA0EPVmSm-Nc6Olb84zBzvwMjguxwKYLf1S",
|
||||||
|
},
|
||||||
|
.bitjuggle = .{
|
||||||
|
.url = "git+https://github.com/leecannon/zig-bitjuggle#80111f4f8c672aaea94a8a189ae2a7c8bbaf883f",
|
||||||
|
.hash = "bitjuggle-2.0.0-SJdU76dvAAARompHEhqKDiwZ4FE4FZ8eHvPvmz5JUOS0",
|
||||||
|
},
|
||||||
|
.zgui = .{
|
||||||
|
.url = "git+https://github.com/zig-gamedev/zgui#7fa8081c208885b85e3fdfc043cd9d9cb9559123",
|
||||||
|
.hash = "zgui-0.6.0-dev--L6sZL7tbQAPRLYrcQAVx0V49tPHAXNxclZ-v8IP4wLr",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
36
dl_sdl2.ps1
Normal file
36
dl_sdl2.ps1
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
$SDL2Version = "2.30.0"
|
||||||
|
$ArchiveFile = ".\SDL2-devel-mingw.zip"
|
||||||
|
$Json = @"
|
||||||
|
{
|
||||||
|
"x86_64-windows-gnu": {
|
||||||
|
"include": ".build_config\\SDL2\\include",
|
||||||
|
"libs": ".build_config\\SDL2\\lib",
|
||||||
|
"bin": ".build_config\\SDL2\\bin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
|
||||||
|
New-Item -Force -ItemType Directory -Path .\.build_config
|
||||||
|
Set-Location -Path .build_config -PassThru
|
||||||
|
|
||||||
|
if (!(Test-Path -PathType Leaf $ArchiveFile)) {
|
||||||
|
Invoke-WebRequest "https://github.com/libsdl-org/SDL/releases/download/release-$SDL2Version/SDL2-devel-$SDL2Version-mingw.zip" -OutFile $ArchiveFile
|
||||||
|
}
|
||||||
|
|
||||||
|
Expand-Archive $ArchiveFile
|
||||||
|
|
||||||
|
if (Test-Path -PathType Container .\SDL2) {
|
||||||
|
Remove-Item -Recurse .\SDL2
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Item -Force -ItemType Directory -Path .\SDL2
|
||||||
|
Get-ChildItem -Path ".\SDL2-devel-mingw\SDL2-$SDL2Version\x86_64-w64-mingw32" | Move-Item -Destination .\SDL2
|
||||||
|
|
||||||
|
# #include <SDL.h>
|
||||||
|
Move-Item -Force -Path .\SDL2\include\SDL2\* -Destination .\SDL2\include
|
||||||
|
Remove-Item -Force .\SDL2\include\SDL2
|
||||||
|
|
||||||
|
New-Item -Force .\sdl.json -Value $Json
|
||||||
|
|
||||||
|
Remove-Item -Recurse .\SDL2-devel-mingw
|
||||||
|
Set-Location -Path .. -PassThru
|
||||||
25
example.toml
Normal file
25
example.toml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
[host]
|
||||||
|
# Using nearest-neighbour scaling, how many times the native resolution
|
||||||
|
# of the game bow should the screen be?
|
||||||
|
win_scale = 3
|
||||||
|
# Enable VSYNC on the UI thread
|
||||||
|
vsync = true
|
||||||
|
# Mute ZBA
|
||||||
|
mute = false
|
||||||
|
|
||||||
|
[guest]
|
||||||
|
# Sync Emulation to Audio
|
||||||
|
audio_sync = true
|
||||||
|
# Sync Emulation to Video
|
||||||
|
video_sync = true
|
||||||
|
# Force RTC support
|
||||||
|
force_rtc = false
|
||||||
|
# Skip BIOS
|
||||||
|
skip_bios = false
|
||||||
|
|
||||||
|
[debug]
|
||||||
|
# Enable detailed CPU logs
|
||||||
|
cpu_trace = false
|
||||||
|
# When false and builtin.mode == .Debug, ZBA will panic
|
||||||
|
# on unknown I/O reads
|
||||||
|
unhandled_io = true
|
||||||
Submodule lib/SDL.zig deleted from d3a764869b
@@ -1,146 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
fn PtrCastPreserveCV(comptime T: type, comptime PtrToT: type, comptime NewT: type) type {
|
|
||||||
return switch (PtrToT) {
|
|
||||||
*T => *NewT,
|
|
||||||
*const T => *const NewT,
|
|
||||||
*volatile T => *volatile NewT,
|
|
||||||
*const volatile T => *const volatile NewT,
|
|
||||||
|
|
||||||
else => @compileError("wtf you doing"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn BitType(comptime FieldType: type, comptime ValueType: type, comptime shamt: usize) type {
|
|
||||||
const self_bit: FieldType = (1 << shamt);
|
|
||||||
|
|
||||||
return struct {
|
|
||||||
bits: Bitfield(FieldType, shamt, 1),
|
|
||||||
|
|
||||||
pub fn set(self: anytype) void {
|
|
||||||
self.bits.field().* |= self_bit;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn unset(self: anytype) void {
|
|
||||||
self.bits.field().* &= ~self_bit;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read(self: anytype) ValueType {
|
|
||||||
return @bitCast(ValueType, @truncate(u1, self.bits.field().* >> shamt));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since these are mostly used with MMIO, I want to avoid
|
|
||||||
// reading the memory just to write it again, also races
|
|
||||||
pub fn write(self: anytype, val: ValueType) void {
|
|
||||||
if (@bitCast(bool, val)) {
|
|
||||||
self.set();
|
|
||||||
} else {
|
|
||||||
self.unset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Original Bit Constructor
|
|
||||||
// pub fn Bit(comptime FieldType: type, comptime shamt: usize) type {
|
|
||||||
// return BitType(FieldType, u1, shamt);
|
|
||||||
// }
|
|
||||||
|
|
||||||
pub fn Bit(comptime FieldType: type, comptime shamt: usize) type {
|
|
||||||
return BitType(FieldType, bool, shamt);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn Boolean(comptime FieldType: type, comptime shamt: usize) type {
|
|
||||||
return BitType(FieldType, bool, shamt);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn Bitfield(comptime FieldType: type, comptime shamt: usize, comptime num_bits: usize) type {
|
|
||||||
if (shamt + num_bits > @bitSizeOf(FieldType)) {
|
|
||||||
@compileError("bitfield doesn't fit");
|
|
||||||
}
|
|
||||||
|
|
||||||
const self_mask: FieldType = ((1 << num_bits) - 1) << shamt;
|
|
||||||
|
|
||||||
const ValueType = std.meta.Int(.unsigned, num_bits);
|
|
||||||
|
|
||||||
return struct {
|
|
||||||
dummy: FieldType,
|
|
||||||
|
|
||||||
fn field(self: anytype) PtrCastPreserveCV(@This(), @TypeOf(self), FieldType) {
|
|
||||||
return @ptrCast(PtrCastPreserveCV(@This(), @TypeOf(self), FieldType), self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write(self: anytype, val: ValueType) void {
|
|
||||||
self.field().* &= ~self_mask;
|
|
||||||
self.field().* |= @intCast(FieldType, val) << shamt;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read(self: anytype) ValueType {
|
|
||||||
const val: FieldType = self.field().*;
|
|
||||||
return @intCast(ValueType, (val & self_mask) >> shamt);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test "bit" {
|
|
||||||
const S = extern union {
|
|
||||||
low: Bit(u32, 0),
|
|
||||||
high: Bit(u32, 1),
|
|
||||||
val: u32,
|
|
||||||
};
|
|
||||||
|
|
||||||
std.testing.expect(@sizeOf(S) == 4);
|
|
||||||
std.testing.expect(@bitSizeOf(S) == 32);
|
|
||||||
|
|
||||||
var s: S = .{ .val = 1 };
|
|
||||||
|
|
||||||
std.testing.expect(s.low.read() == 1);
|
|
||||||
std.testing.expect(s.high.read() == 0);
|
|
||||||
|
|
||||||
s.low.write(0);
|
|
||||||
s.high.write(1);
|
|
||||||
|
|
||||||
std.testing.expect(s.val == 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "boolean" {
|
|
||||||
const S = extern union {
|
|
||||||
low: Boolean(u32, 0),
|
|
||||||
high: Boolean(u32, 1),
|
|
||||||
val: u32,
|
|
||||||
};
|
|
||||||
|
|
||||||
std.testing.expect(@sizeOf(S) == 4);
|
|
||||||
std.testing.expect(@bitSizeOf(S) == 32);
|
|
||||||
|
|
||||||
var s: S = .{ .val = 2 };
|
|
||||||
|
|
||||||
std.testing.expect(s.low.read() == false);
|
|
||||||
std.testing.expect(s.high.read() == true);
|
|
||||||
|
|
||||||
s.low.write(true);
|
|
||||||
s.high.write(false);
|
|
||||||
|
|
||||||
std.testing.expect(s.val == 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "bitfield" {
|
|
||||||
const S = extern union {
|
|
||||||
low: Bitfield(u32, 0, 16),
|
|
||||||
high: Bitfield(u32, 16, 16),
|
|
||||||
val: u32,
|
|
||||||
};
|
|
||||||
|
|
||||||
std.testing.expect(@sizeOf(S) == 4);
|
|
||||||
std.testing.expect(@bitSizeOf(S) == 32);
|
|
||||||
|
|
||||||
var s: S = .{ .val = 0x13376969 };
|
|
||||||
|
|
||||||
std.testing.expect(s.low.read() == 0x6969);
|
|
||||||
std.testing.expect(s.high.read() == 0x1337);
|
|
||||||
|
|
||||||
s.low.write(0x1337);
|
|
||||||
s.high.write(0x6969);
|
|
||||||
|
|
||||||
std.testing.expect(s.val == 0x69691337);
|
|
||||||
}
|
|
||||||
162
src/Bus.zig
162
src/Bus.zig
@@ -1,162 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bios = @import("bus/Bios.zig");
|
|
||||||
const Ewram = @import("bus/Ewram.zig");
|
|
||||||
const GamePak = @import("bus/GamePak.zig");
|
|
||||||
const Io = @import("bus/io.zig").Io;
|
|
||||||
const Iwram = @import("bus/Iwram.zig");
|
|
||||||
const Ppu = @import("ppu.zig").Ppu;
|
|
||||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
pak: GamePak,
|
|
||||||
bios: Bios,
|
|
||||||
ppu: Ppu,
|
|
||||||
iwram: Iwram,
|
|
||||||
ewram: Ewram,
|
|
||||||
io: Io,
|
|
||||||
|
|
||||||
pub fn init(alloc: Allocator, sched: *Scheduler, path: []const u8) !Self {
|
|
||||||
return Self{
|
|
||||||
.pak = try GamePak.init(alloc, path),
|
|
||||||
.bios = try Bios.init(alloc, "./bin/gba_bios.bin"), // TODO: don't hardcode this + bundle open-sorce Boot ROM
|
|
||||||
.ppu = try Ppu.init(alloc, sched),
|
|
||||||
.iwram = try Iwram.init(alloc),
|
|
||||||
.ewram = try Ewram.init(alloc),
|
|
||||||
.io = Io.init(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: Self) void {
|
|
||||||
self.iwram.deinit();
|
|
||||||
self.ewram.deinit();
|
|
||||||
self.pak.deinit();
|
|
||||||
self.bios.deinit();
|
|
||||||
self.ppu.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read32(self: *const Self, addr: u32) u32 {
|
|
||||||
return switch (addr) {
|
|
||||||
// General Internal Memory
|
|
||||||
0x0000_0000...0x0000_3FFF => self.bios.get32(@as(usize, addr)),
|
|
||||||
0x0200_0000...0x0203_FFFF => self.iwram.get32(addr - 0x0200_0000),
|
|
||||||
0x0300_0000...0x0300_7FFF => self.ewram.get32(addr - 0x0300_0000),
|
|
||||||
0x0400_0000...0x0400_03FE => self.io.read32(addr),
|
|
||||||
|
|
||||||
// Internal Display Memory
|
|
||||||
0x0500_0000...0x0500_03FF => self.ppu.palette.get32(@as(usize, addr - 0x0500_0000)),
|
|
||||||
0x0600_0000...0x0601_7FFF => self.ppu.vram.get32(@as(usize, addr - 0x0600_0000)),
|
|
||||||
0x0700_0000...0x0700_03FF => std.debug.panic("[Bus:32] read from 0x{X:} in OAM", .{addr}),
|
|
||||||
|
|
||||||
// External Memory (Game Pak)
|
|
||||||
0x0800_0000...0x09FF_FFFF => self.pak.get32(@as(usize, addr - 0x0800_0000)),
|
|
||||||
0x0A00_0000...0x0BFF_FFFF => self.pak.get32(@as(usize, addr - 0x0A00_0000)),
|
|
||||||
0x0C00_0000...0x0DFF_FFFF => self.pak.get32(@as(usize, addr - 0x0C00_0000)),
|
|
||||||
|
|
||||||
else => {
|
|
||||||
std.log.warn("[Bus:32] ZBA tried to read from 0x{X:}", .{addr});
|
|
||||||
return 0x0000_0000;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write32(self: *Self, addr: u32, word: u32) void {
|
|
||||||
// TODO: write32 can write to GamePak Flash
|
|
||||||
|
|
||||||
switch (addr) {
|
|
||||||
// General Internal Memory
|
|
||||||
0x0200_0000...0x0203_FFFF => self.iwram.set32(addr - 0x0200_0000, word),
|
|
||||||
0x0300_0000...0x0300_7FFF => self.ewram.set32(addr - 0x0300_0000, word),
|
|
||||||
0x0400_0000...0x0400_03FE => self.io.write32(addr, word),
|
|
||||||
|
|
||||||
// Internal Display Memory
|
|
||||||
0x0500_0000...0x0500_03FF => self.ppu.palette.set32(@as(usize, addr - 0x0500_0000), word),
|
|
||||||
0x0600_0000...0x0601_7FFF => self.ppu.vram.set32(@as(usize, addr - 0x0600_0000), word),
|
|
||||||
0x0700_0000...0x0700_03FF => std.debug.panic("[Bus:32] wrote 0x{X:} to 0x{X:} in OAM", .{ word, addr }),
|
|
||||||
|
|
||||||
else => std.log.warn("[Bus:32] ZBA tried to write 0x{X:} to 0x{X:}", .{ word, addr }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read16(self: *const Self, addr: u32) u16 {
|
|
||||||
return switch (addr) {
|
|
||||||
// General Internal Memory
|
|
||||||
0x0000_0000...0x0000_3FFF => self.bios.get16(@as(usize, addr)),
|
|
||||||
0x0200_0000...0x0203_FFFF => self.iwram.get16(addr - 0x0200_0000),
|
|
||||||
0x0300_0000...0x0300_7FFF => self.ewram.get16(addr - 0x0300_0000),
|
|
||||||
0x0400_0000...0x0400_03FE => self.io.read16(addr),
|
|
||||||
|
|
||||||
// Internal Display Memory
|
|
||||||
0x0500_0000...0x0500_03FF => self.ppu.palette.get16(@as(usize, addr - 0x0500_0000)),
|
|
||||||
0x0600_0000...0x0601_7FFF => self.ppu.vram.get16(@as(usize, addr - 0x0600_0000)),
|
|
||||||
0x0700_0000...0x0700_03FF => std.debug.panic("[Bus:16] read from 0x{X:} in OAM", .{addr}),
|
|
||||||
|
|
||||||
// External Memory (Game Pak)
|
|
||||||
0x0800_0000...0x09FF_FFFF => self.pak.get16(@as(usize, addr - 0x0800_0000)),
|
|
||||||
0x0A00_0000...0x0BFF_FFFF => self.pak.get16(@as(usize, addr - 0x0A00_0000)),
|
|
||||||
0x0C00_0000...0x0DFF_FFFF => self.pak.get16(@as(usize, addr - 0x0C00_0000)),
|
|
||||||
|
|
||||||
else => {
|
|
||||||
std.log.warn("[Bus:16] ZBA tried to read from 0x{X:}", .{addr});
|
|
||||||
return 0x0000;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write16(self: *Self, addr: u32, halfword: u16) void {
|
|
||||||
// TODO: write16 can write to GamePak Flash
|
|
||||||
switch (addr) {
|
|
||||||
// General Internal Memory
|
|
||||||
0x0200_0000...0x0203_FFFF => self.iwram.set16(addr - 0x0200_0000, halfword),
|
|
||||||
0x0300_0000...0x0300_7FFF => self.ewram.set16(addr - 0x0300_0000, halfword),
|
|
||||||
0x0400_0000...0x0400_03FE => self.io.write16(addr, halfword),
|
|
||||||
|
|
||||||
// Internal Display Memory
|
|
||||||
0x0500_0000...0x0500_03FF => self.ppu.palette.set16(@as(usize, addr - 0x0500_0000), halfword),
|
|
||||||
0x0600_0000...0x0601_7FFF => self.ppu.vram.set16(@as(usize, addr - 0x0600_0000), halfword),
|
|
||||||
0x0700_0000...0x0700_03FF => std.debug.panic("[Bus:16] write 0x{X:} to 0x{X:} in OAM", .{ halfword, addr }),
|
|
||||||
|
|
||||||
else => std.log.warn("[Bus:16] ZBA tried to write 0x{X:} to 0x{X:}", .{ halfword, addr }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read8(self: *const Self, addr: u32) u8 {
|
|
||||||
return switch (addr) {
|
|
||||||
// General Internal Memory
|
|
||||||
0x0000_0000...0x0000_3FFF => self.bios.get8(@as(usize, addr)),
|
|
||||||
0x0200_0000...0x0203_FFFF => self.iwram.get8(addr - 0x0200_0000),
|
|
||||||
0x0300_0000...0x0300_7FFF => self.ewram.get8(addr - 0x0300_0000),
|
|
||||||
0x0400_0000...0x0400_03FE => self.io.read8(addr),
|
|
||||||
|
|
||||||
// Internal Display Memory
|
|
||||||
0x0500_0000...0x0500_03FF => self.ppu.palette.get8(@as(usize, addr - 0x0500_0000)),
|
|
||||||
0x0600_0000...0x0601_7FFF => self.ppu.vram.get8(@as(usize, addr - 0x0600_0000)),
|
|
||||||
0x0700_0000...0x0700_03FF => std.debug.panic("[Bus:8] read from 0x{X:} in OAM", .{addr}),
|
|
||||||
|
|
||||||
// External Memory (Game Pak)
|
|
||||||
0x0800_0000...0x09FF_FFFF => self.pak.get8(@as(usize, addr - 0x0800_0000)),
|
|
||||||
0x0A00_0000...0x0BFF_FFFF => self.pak.get8(@as(usize, addr - 0x0A00_0000)),
|
|
||||||
0x0C00_0000...0x0DFF_FFFF => self.pak.get8(@as(usize, addr - 0x0C00_0000)),
|
|
||||||
0x0E00_0000...0x0E00_FFFF => std.debug.panic("[Bus:8] read from 0x{X:} in Game Pak SRAM", .{addr}),
|
|
||||||
|
|
||||||
else => {
|
|
||||||
std.log.warn("[Bus:8] ZBA tried to read from 0x{X:}", .{addr});
|
|
||||||
return 0x00;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write8(self: *Self, addr: u32, byte: u8) void {
|
|
||||||
switch (addr) {
|
|
||||||
// General Internal Memory
|
|
||||||
0x0200_0000...0x0203_FFFF => self.iwram.set8(addr - 0x0200_0000, byte),
|
|
||||||
0x0300_0000...0x0300_7FFF => self.ewram.set8(addr - 0x0300_0000, byte),
|
|
||||||
0x0400_0000...0x0400_03FE => self.io.write8(addr, byte),
|
|
||||||
|
|
||||||
// External Memory (Game Pak)
|
|
||||||
0x0E00_0000...0x0E00_FFFF => std.debug.panic("[Bus:8] write 0x{X:} to 0x{X:} in Game Pak SRAM", .{ byte, addr }),
|
|
||||||
else => std.log.warn("[Bus:8] ZBA tried to write 0x{X:} to 0x{X:}", .{ byte, addr }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
buf: []u8,
|
|
||||||
alloc: Allocator,
|
|
||||||
|
|
||||||
pub fn init(alloc: Allocator, path: []const u8) !Self {
|
|
||||||
const file = try std.fs.cwd().openFile(path, .{});
|
|
||||||
defer file.close();
|
|
||||||
|
|
||||||
const len = try file.getEndPos();
|
|
||||||
|
|
||||||
return Self{
|
|
||||||
.buf = try file.readToEndAlloc(alloc, len),
|
|
||||||
.alloc = alloc,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: Self) void {
|
|
||||||
self.alloc.free(self.buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get32(self: *const Self, idx: usize) u32 {
|
|
||||||
return (@as(u32, self.buf[idx + 3]) << 24) | (@as(u32, self.buf[idx + 2]) << 16) | (@as(u32, self.buf[idx + 1]) << 8) | (@as(u32, self.buf[idx]));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get16(self: *const Self, idx: usize) u16 {
|
|
||||||
return (@as(u16, self.buf[idx + 1]) << 8) | @as(u16, self.buf[idx]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get8(self: *const Self, idx: usize) u8 {
|
|
||||||
return self.buf[idx];
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
buf: []u8,
|
|
||||||
alloc: Allocator,
|
|
||||||
|
|
||||||
pub fn init(alloc: Allocator) !Self {
|
|
||||||
return Self{
|
|
||||||
.buf = try alloc.alloc(u8, 0x8000),
|
|
||||||
.alloc = alloc,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: Self) void {
|
|
||||||
self.alloc.free(self.buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get32(self: *const Self, idx: usize) u32 {
|
|
||||||
return (@as(u32, self.buf[idx + 3]) << 24) | (@as(u32, self.buf[idx + 2]) << 16) | (@as(u32, self.buf[idx + 1]) << 8) | (@as(u32, self.buf[idx]));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set32(self: *Self, idx: usize, word: u32) void {
|
|
||||||
self.buf[idx + 3] = @truncate(u8, word >> 24);
|
|
||||||
self.buf[idx + 2] = @truncate(u8, word >> 16);
|
|
||||||
self.buf[idx + 1] = @truncate(u8, word >> 8);
|
|
||||||
self.buf[idx] = @truncate(u8, word);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get16(self: *const Self, idx: usize) u16 {
|
|
||||||
return (@as(u16, self.buf[idx + 1]) << 8) | @as(u16, self.buf[idx]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set16(self: *Self, idx: usize, halfword: u16) void {
|
|
||||||
self.buf[idx + 1] = @truncate(u8, halfword >> 8);
|
|
||||||
self.buf[idx] = @truncate(u8, halfword);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get8(self: *const Self, idx: usize) u8 {
|
|
||||||
return self.buf[idx];
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set8(self: *Self, idx: usize, byte: u8) void {
|
|
||||||
self.buf[idx] = byte;
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
buf: []u8,
|
|
||||||
alloc: Allocator,
|
|
||||||
|
|
||||||
pub fn init(alloc: Allocator, path: []const u8) !Self {
|
|
||||||
const file = try std.fs.cwd().openFile(path, .{});
|
|
||||||
defer file.close();
|
|
||||||
|
|
||||||
const len = try file.getEndPos();
|
|
||||||
|
|
||||||
return Self{
|
|
||||||
.buf = try file.readToEndAlloc(alloc, len),
|
|
||||||
.alloc = alloc,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: Self) void {
|
|
||||||
self.alloc.free(self.buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get32(self: *const Self, idx: usize) u32 {
|
|
||||||
return (@as(u32, self.get16(idx + 2)) << 16) | @as(u32, self.get16(idx));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get16(self: *const Self, idx: usize) u16 {
|
|
||||||
return (@as(u16, self.buf[idx + 1]) << 8) | @as(u16, self.buf[idx]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get8(self: *const Self, idx: usize) u8 {
|
|
||||||
return self.buf[idx];
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
buf: []u8,
|
|
||||||
alloc: Allocator,
|
|
||||||
|
|
||||||
pub fn init(alloc: Allocator) !Self {
|
|
||||||
return Self{
|
|
||||||
.buf = try alloc.alloc(u8, 0x40000),
|
|
||||||
.alloc = alloc,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: Self) void {
|
|
||||||
self.alloc.free(self.buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get32(self: *const Self, idx: usize) u32 {
|
|
||||||
return (@as(u32, self.get16(idx + 2)) << 16) | @as(u32, self.get16(idx));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set32(self: *Self, idx: usize, word: u32) void {
|
|
||||||
self.set16(idx + 2, @truncate(u16, word >> 16));
|
|
||||||
self.set16(idx, @truncate(u16, word));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get16(self: *const Self, idx: usize) u16 {
|
|
||||||
return (@as(u16, self.buf[idx + 1]) << 8) | @as(u16, self.buf[idx]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set16(self: *Self, idx: usize, halfword: u16) void {
|
|
||||||
self.buf[idx + 1] = @truncate(u8, halfword >> 8);
|
|
||||||
self.buf[idx] = @truncate(u8, halfword);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get8(self: *const Self, idx: usize) u8 {
|
|
||||||
return self.buf[idx];
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set8(self: *Self, idx: usize, byte: u8) void {
|
|
||||||
self.buf[idx] = byte;
|
|
||||||
}
|
|
||||||
150
src/bus/io.zig
150
src/bus/io.zig
@@ -1,150 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bit = @import("bitfield").Bit;
|
|
||||||
const Bitfield = @import("bitfield").Bitfield;
|
|
||||||
|
|
||||||
pub const Io = struct {
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
dispcnt: DisplayControl,
|
|
||||||
dispstat: DisplayStatus,
|
|
||||||
vcount: VCount,
|
|
||||||
/// Read / Write
|
|
||||||
ime: bool,
|
|
||||||
ie: InterruptEnable,
|
|
||||||
keyinput: KeyInput,
|
|
||||||
|
|
||||||
pub fn init() Self {
|
|
||||||
return .{
|
|
||||||
.dispcnt = .{ .raw = 0x0000 },
|
|
||||||
.dispstat = .{ .raw = 0x0000 },
|
|
||||||
.vcount = .{ .raw = 0x0000 },
|
|
||||||
.ime = false,
|
|
||||||
.ie = .{ .raw = 0x0000 },
|
|
||||||
.keyinput = .{ .raw = 0x03FF },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read32(self: *const Self, addr: u32) u32 {
|
|
||||||
return switch (addr) {
|
|
||||||
0x0400_0000 => @as(u32, self.dispcnt.raw),
|
|
||||||
0x0400_0004 => @as(u32, self.dispstat.raw),
|
|
||||||
0x0400_0006 => @as(u32, self.vcount.raw),
|
|
||||||
0x0400_0200 => @as(u32, self.ie.raw),
|
|
||||||
0x0400_0208 => @boolToInt(self.ime),
|
|
||||||
else => std.debug.panic("[I/O:32] tried to read from {X:}", .{addr}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write32(self: *Self, addr: u32, word: u32) void {
|
|
||||||
switch (addr) {
|
|
||||||
0x0400_0000 => self.dispcnt.raw = @truncate(u16, word),
|
|
||||||
0x0400_0200 => self.ie.raw = @truncate(u16, word),
|
|
||||||
0x0400_0208 => self.ime = word & 1 == 1,
|
|
||||||
else => std.debug.panic("[I/O:32] tried to write 0x{X:} to 0x{X:}", .{ word, addr }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read16(self: *const Self, addr: u32) u16 {
|
|
||||||
return switch (addr) {
|
|
||||||
0x0400_0000 => self.dispcnt.raw,
|
|
||||||
0x0400_0004 => self.dispstat.raw,
|
|
||||||
0x0400_0006 => self.vcount.raw,
|
|
||||||
0x0400_0130 => self.keyinput.raw,
|
|
||||||
0x0400_0200 => self.ie.raw,
|
|
||||||
0x0400_0208 => @boolToInt(self.ime),
|
|
||||||
else => std.debug.panic("[I/O:16] tried to read from {X:}", .{addr}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write16(self: *Self, addr: u32, halfword: u16) void {
|
|
||||||
switch (addr) {
|
|
||||||
0x0400_0000 => self.dispcnt.raw = halfword,
|
|
||||||
0x0400_0004 => self.dispstat.raw = halfword,
|
|
||||||
0x0400_0200 => self.ie.raw = halfword,
|
|
||||||
0x0400_0208 => self.ime = halfword & 1 == 1,
|
|
||||||
else => std.debug.panic("[I/O:16] tried to write 0x{X:} to 0x{X:}", .{ halfword, addr }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read8(self: *const Self, addr: u32) u8 {
|
|
||||||
return switch (addr) {
|
|
||||||
0x0400_0000 => @truncate(u8, self.dispcnt.raw),
|
|
||||||
0x0400_0004 => @truncate(u8, self.dispstat.raw),
|
|
||||||
0x0400_0200 => @truncate(u8, self.ie.raw),
|
|
||||||
0x0400_0006 => @truncate(u8, self.vcount.raw),
|
|
||||||
else => std.debug.panic("[I/O:8] tried to read from {X:}", .{addr}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write8(_: *Self, addr: u32, byte: u8) void {
|
|
||||||
std.debug.panic("[I/0:8] tried to write 0x{X:} to 0x{X:}", .{ byte, addr });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Read / Write
|
|
||||||
const DisplayControl = extern union {
|
|
||||||
bg_mode: Bitfield(u16, 0, 3),
|
|
||||||
frame_select: Bit(u16, 4),
|
|
||||||
hblank_interval_free: Bit(u16, 5),
|
|
||||||
obj_mapping: Bit(u16, 6),
|
|
||||||
forced_blank: Bit(u16, 7),
|
|
||||||
bg_enable: Bitfield(u16, 8, 4),
|
|
||||||
obj_enable: Bit(u16, 12),
|
|
||||||
win_enable: Bitfield(u16, 13, 2),
|
|
||||||
obj_win_enable: Bit(u16, 15),
|
|
||||||
raw: u16,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Read / Write
|
|
||||||
const DisplayStatus = extern union {
|
|
||||||
vblank: Bit(u16, 0),
|
|
||||||
hblank: Bit(u16, 1),
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Read Only
|
|
||||||
const VCount = extern union {
|
|
||||||
scanline: Bitfield(u16, 0, 8),
|
|
||||||
raw: u16,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Read / Write
|
|
||||||
const InterruptEnable = extern union {
|
|
||||||
vblank: Bit(u16, 0),
|
|
||||||
hblank: Bit(u16, 1),
|
|
||||||
coincidence: Bit(u16, 2),
|
|
||||||
tm0_overflow: Bit(u16, 3),
|
|
||||||
tm1_overflow: Bit(u16, 4),
|
|
||||||
tm2_overflow: Bit(u16, 5),
|
|
||||||
tm3_overflow: Bit(u16, 6),
|
|
||||||
serial: Bit(u16, 7),
|
|
||||||
dma0: Bit(u16, 8),
|
|
||||||
dma1: Bit(u16, 9),
|
|
||||||
dma2: Bit(u16, 10),
|
|
||||||
dma3: Bit(u16, 11),
|
|
||||||
keypad: Bit(u16, 12),
|
|
||||||
game_pak: Bit(u16, 13),
|
|
||||||
raw: u16,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Read Only
|
|
||||||
/// 0 = Pressed, 1 = Released
|
|
||||||
const KeyInput = extern union {
|
|
||||||
a: Bit(u16, 0),
|
|
||||||
b: Bit(u16, 1),
|
|
||||||
select: Bit(u16, 2),
|
|
||||||
start: Bit(u16, 3),
|
|
||||||
right: Bit(u16, 4),
|
|
||||||
left: Bit(u16, 5),
|
|
||||||
up: Bit(u16, 6),
|
|
||||||
down: Bit(u16, 7),
|
|
||||||
shoulder_r: Bit(u16, 8),
|
|
||||||
shoulder_l: Bit(u16, 9),
|
|
||||||
raw: u16,
|
|
||||||
};
|
|
||||||
64
src/config.zig
Normal file
64
src/config.zig
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
// const tomlz = @import("tomlz");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const log = std.log.scoped(.Config);
|
||||||
|
var state: Config = .{};
|
||||||
|
|
||||||
|
const Config = struct {
|
||||||
|
// FIXME: tomlz expects these to be case sensitive
|
||||||
|
host: Host = .{},
|
||||||
|
guest: Guest = .{},
|
||||||
|
debug: Debug = .{},
|
||||||
|
|
||||||
|
/// 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);
|
||||||
|
|
||||||
|
// FIXME(2025-09-22): re-enable
|
||||||
|
// state = try tomlz.parser.decode(Config, allocator, contents);
|
||||||
|
}
|
||||||
535
src/core/Bus.zig
Normal file
535
src/core/Bus.zig
Normal file
@@ -0,0 +1,535 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||||
|
const Bios = @import("bus/Bios.zig");
|
||||||
|
const Ewram = @import("bus/Ewram.zig");
|
||||||
|
const GamePak = @import("bus/GamePak.zig");
|
||||||
|
const Io = @import("bus/io.zig").Io;
|
||||||
|
const Iwram = @import("bus/Iwram.zig");
|
||||||
|
const Ppu = @import("ppu.zig").Ppu;
|
||||||
|
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 _io = @import("bus/io.zig");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const log = std.log.scoped(.Bus);
|
||||||
|
|
||||||
|
const createDmaTuple = @import("bus/dma.zig").create;
|
||||||
|
const createTimerTuple = @import("bus/timer.zig").create;
|
||||||
|
const rotr = @import("zba_util").rotr;
|
||||||
|
|
||||||
|
const timings: [2][0x10]u8 = [_][0x10]u8{
|
||||||
|
// BIOS, Unused, EWRAM, IWRAM, I/0, PALRAM, VRAM, OAM, ROM0, ROM0, ROM1, ROM1, ROM2, ROM2, SRAM, Unused
|
||||||
|
[_]u8{ 1, 1, 3, 1, 1, 1, 1, 1, 5, 5, 5, 5, 5, 5, 5, 5 }, // 8-bit & 16-bit
|
||||||
|
[_]u8{ 1, 1, 6, 1, 1, 2, 2, 1, 8, 8, 8, 8, 8, 8, 8, 8 }, // 32-bit
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const fetch_timings: [2][0x10]u8 = [_][0x10]u8{
|
||||||
|
// BIOS, Unused, EWRAM, IWRAM, I/0, PALRAM, VRAM, OAM, ROM0, ROM0, ROM1, ROM1, ROM2, ROM2, SRAM, Unused
|
||||||
|
[_]u8{ 1, 1, 3, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 5, 5 }, // 8-bit & 16-bit
|
||||||
|
[_]u8{ 1, 1, 6, 1, 1, 2, 2, 1, 4, 4, 4, 4, 4, 4, 8, 8 }, // 32-bit
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fastmem Related
|
||||||
|
const page_size = 1 * 0x400; // 1KiB
|
||||||
|
const address_space_size = 0x1000_0000;
|
||||||
|
const table_len = address_space_size / page_size;
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pak: GamePak,
|
||||||
|
bios: Bios,
|
||||||
|
ppu: Ppu,
|
||||||
|
apu: Apu,
|
||||||
|
dma: DmaTuple,
|
||||||
|
tim: TimerTuple,
|
||||||
|
iwram: Iwram,
|
||||||
|
ewram: Ewram,
|
||||||
|
io: Io,
|
||||||
|
|
||||||
|
cpu: *Arm7tdmi,
|
||||||
|
sched: *Scheduler,
|
||||||
|
|
||||||
|
read_table: *const [table_len]?*const anyopaque,
|
||||||
|
write_tables: [2]*const [table_len]?*anyopaque,
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
pub fn init(self: *Self, allocator: Allocator, sched: *Scheduler, cpu: *Arm7tdmi, paths: FilePaths) !void {
|
||||||
|
const tables = try allocator.alloc(?*anyopaque, 3 * table_len); // Allocate all tables
|
||||||
|
|
||||||
|
const read_table = tables[0..table_len];
|
||||||
|
const write_tables = .{ tables[table_len .. 2 * table_len], tables[2 * table_len .. 3 * table_len] };
|
||||||
|
|
||||||
|
self.* = .{
|
||||||
|
.pak = try GamePak.init(allocator, cpu, paths.rom, paths.save),
|
||||||
|
.bios = try Bios.init(allocator, paths.bios),
|
||||||
|
.ppu = try Ppu.init(allocator, sched),
|
||||||
|
.apu = Apu.init(sched),
|
||||||
|
.iwram = try Iwram.init(allocator),
|
||||||
|
.ewram = try Ewram.init(allocator),
|
||||||
|
.dma = createDmaTuple(),
|
||||||
|
.tim = createTimerTuple(sched),
|
||||||
|
.io = Io.init(),
|
||||||
|
.cpu = cpu,
|
||||||
|
.sched = sched,
|
||||||
|
|
||||||
|
.read_table = read_table,
|
||||||
|
.write_tables = write_tables,
|
||||||
|
.allocator = allocator,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.fillReadTable(@ptrCast(read_table));
|
||||||
|
|
||||||
|
// Internal Display Memory behaves differently on 8-bit reads
|
||||||
|
self.fillWriteTable(u32, write_tables[0]);
|
||||||
|
self.fillWriteTable(u8, write_tables[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.iwram.deinit();
|
||||||
|
self.ewram.deinit();
|
||||||
|
self.pak.deinit();
|
||||||
|
self.bios.deinit();
|
||||||
|
self.ppu.deinit();
|
||||||
|
|
||||||
|
// This is so I can deallocate the original `allocator.alloc`. I have to re-make the type
|
||||||
|
// since I'm not keeping it around, This is very jank and bad though
|
||||||
|
// FIXME: please figure out another way
|
||||||
|
self.allocator.free(@as([*]const ?*anyopaque, @ptrCast(self.read_table[0..]))[0 .. 3 * table_len]);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
self.bios.reset();
|
||||||
|
self.ppu.reset();
|
||||||
|
self.apu.reset();
|
||||||
|
self.iwram.reset();
|
||||||
|
self.ewram.reset();
|
||||||
|
|
||||||
|
// https://github.com/ziglang/zig/issues/14705
|
||||||
|
{
|
||||||
|
comptime var i: usize = 0;
|
||||||
|
inline while (i < self.dma.len) : (i += 1) {
|
||||||
|
self.dma[0].reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/ziglang/zig/issues/14705
|
||||||
|
{
|
||||||
|
comptime var i: usize = 0;
|
||||||
|
inline while (i < self.tim.len) : (i += 1) {
|
||||||
|
self.tim[0].reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.io.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn replaceGamepak(self: *Self, file_path: []const u8) !void {
|
||||||
|
// Note: `save_path` isn't owned by `Backup`
|
||||||
|
const save_path = self.pak.backup.save_path;
|
||||||
|
self.pak.deinit();
|
||||||
|
|
||||||
|
self.pak = try GamePak.init(self.allocator, self.cpu, file_path, save_path);
|
||||||
|
|
||||||
|
// SAFETY: TODO: why do we know this is safe?
|
||||||
|
self.fillReadTable(@ptrCast(@constCast(self.read_table)));
|
||||||
|
self.fillWriteTable(u32, @constCast(self.write_tables[0]));
|
||||||
|
self.fillWriteTable(u8, @constCast(self.write_tables[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fillReadTable(self: *Self, table: *[table_len]?*const anyopaque) void {
|
||||||
|
const vramMirror = @import("ppu/Vram.zig").mirror;
|
||||||
|
|
||||||
|
for (table, 0..) |*ptr, i| {
|
||||||
|
const addr: u32 = @intCast(page_size * i);
|
||||||
|
|
||||||
|
ptr.* = switch (addr) {
|
||||||
|
// General Internal Memory
|
||||||
|
0x0000_0000...0x0000_3FFF => null, // BIOS has it's own checks
|
||||||
|
0x0200_0000...0x02FF_FFFF => &self.ewram.buf[addr & 0x3FFFF],
|
||||||
|
0x0300_0000...0x03FF_FFFF => &self.iwram.buf[addr & 0x7FFF],
|
||||||
|
0x0400_0000...0x0400_03FF => null, // I/O
|
||||||
|
|
||||||
|
// Internal Display Memory
|
||||||
|
0x0500_0000...0x05FF_FFFF => &self.ppu.palette.buf[addr & 0x3FF],
|
||||||
|
0x0600_0000...0x06FF_FFFF => &self.ppu.vram.buf[vramMirror(addr)],
|
||||||
|
0x0700_0000...0x07FF_FFFF => &self.ppu.oam.buf[addr & 0x3FF],
|
||||||
|
|
||||||
|
// External Memory (Game Pak)
|
||||||
|
0x0800_0000...0x0DFF_FFFF => self.fillReadTableExternal(addr),
|
||||||
|
0x0E00_0000...0x0FFF_FFFF => null, // SRAM
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fillWriteTable(self: *Self, comptime T: type, table: *[table_len]?*anyopaque) void {
|
||||||
|
comptime std.debug.assert(T == u32 or T == u16 or T == u8);
|
||||||
|
const vramMirror = @import("ppu/Vram.zig").mirror;
|
||||||
|
|
||||||
|
for (table, 0..) |*ptr, i| {
|
||||||
|
const addr: u32 = @intCast(page_size * i);
|
||||||
|
|
||||||
|
ptr.* = switch (addr) {
|
||||||
|
// General Internal Memory
|
||||||
|
0x0000_0000...0x0000_3FFF => null, // BIOS has it's own checks
|
||||||
|
0x0200_0000...0x02FF_FFFF => &self.ewram.buf[addr & 0x3FFFF],
|
||||||
|
0x0300_0000...0x03FF_FFFF => &self.iwram.buf[addr & 0x7FFF],
|
||||||
|
0x0400_0000...0x0400_03FF => null, // I/O
|
||||||
|
|
||||||
|
// Internal Display Memory
|
||||||
|
0x0500_0000...0x05FF_FFFF => if (T != u8) &self.ppu.palette.buf[addr & 0x3FF] else null,
|
||||||
|
0x0600_0000...0x06FF_FFFF => if (T != u8) &self.ppu.vram.buf[vramMirror(addr)] else null,
|
||||||
|
0x0700_0000...0x07FF_FFFF => if (T != u8) &self.ppu.oam.buf[addr & 0x3FF] else null,
|
||||||
|
|
||||||
|
// External Memory (Game Pak)
|
||||||
|
0x0800_0000...0x0DFF_FFFF => null, // ROM
|
||||||
|
0x0E00_0000...0x0FFF_FFFF => null, // SRAM
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fillReadTableExternal(self: *Self, addr: u32) ?*anyopaque {
|
||||||
|
// see `GamePak.zig` for more information about what conditions need to be true
|
||||||
|
// so that a simple pointer dereference isn't possible
|
||||||
|
|
||||||
|
std.debug.assert(addr & @as(u32, page_size - 1) == 0); // addr is guaranteed to be page-aligned
|
||||||
|
|
||||||
|
const start_addr = addr;
|
||||||
|
const end_addr = start_addr + page_size;
|
||||||
|
|
||||||
|
{
|
||||||
|
const data = start_addr <= 0x0800_00C4 and 0x0800_00C4 < end_addr; // GPIO Data
|
||||||
|
const direction = start_addr <= 0x0800_00C6 and 0x0800_00C6 < end_addr; // GPIO Direction
|
||||||
|
const control = start_addr <= 0x0800_00C8 and 0x0800_00C8 < end_addr; // GPIO Control
|
||||||
|
|
||||||
|
const has_gpio = data or direction or control;
|
||||||
|
const gpio_kind = self.pak.gpio.device.kind;
|
||||||
|
|
||||||
|
// There is a GPIO Device, and the current page contains at least one memory-mapped GPIO register
|
||||||
|
if (gpio_kind != .None and has_gpio) return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.pak.backup.kind == .Eeprom) {
|
||||||
|
if (self.pak.buf.len > 0x100_000) {
|
||||||
|
// We are using a "large" EEPROM which means that if the below check is true
|
||||||
|
// this page has an address that's reserved for the EEPROM and therefore must
|
||||||
|
// be handled in slowmem
|
||||||
|
if (addr & 0x1FF_FFFF > 0x1FF_FEFF) return null;
|
||||||
|
} else {
|
||||||
|
// We are using a "small" EEPROM which means that if the below check is true
|
||||||
|
// (that is, we're in the 0xD address page) then we must handle at least one
|
||||||
|
// address in this page in slowmem
|
||||||
|
if (@as(u4, @truncate(addr >> 24)) == 0xD) return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, the GamePak has some unique behaviour for reads past the end of the ROM,
|
||||||
|
// so those will be handled by slowmem as well
|
||||||
|
const masked_addr = addr & 0x1FF_FFFF;
|
||||||
|
if (masked_addr >= self.pak.buf.len) return null;
|
||||||
|
|
||||||
|
return &self.pak.buf[masked_addr];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readIo(self: *const Self, comptime T: type, address: u32) T {
|
||||||
|
return _io.read(self, T, address) orelse self.openBus(T, address);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn openBus(self: *const Self, comptime T: type, address: u32) T {
|
||||||
|
@branchHint(.cold);
|
||||||
|
const r15 = self.cpu.r[15];
|
||||||
|
|
||||||
|
const word = blk: {
|
||||||
|
// If Arm, get the most recently fetched instruction (PC + 8)
|
||||||
|
//
|
||||||
|
// FIXME: This is most likely a faulty assumption.
|
||||||
|
// I think what *actually* happens is that the Bus has a latch for the most
|
||||||
|
// recently fetched piece of data, which is then returned during Open Bus (also DMA open bus?)
|
||||||
|
// I can "get away" with this because it's very statistically likely that the most recently latched value is
|
||||||
|
// the most recently fetched instruction by the pipeline
|
||||||
|
if (!self.cpu.cpsr.t.read()) break :blk self.cpu.pipe.stage[1].?;
|
||||||
|
|
||||||
|
const page: u8 = @truncate(r15 >> 24);
|
||||||
|
|
||||||
|
// PC + 2 = stage[0]
|
||||||
|
// PC + 4 = stage[1]
|
||||||
|
// PC + 6 = Need a Debug Read for this?
|
||||||
|
|
||||||
|
switch (page) {
|
||||||
|
// EWRAM, PALRAM, VRAM, and Game ROM (16-bit)
|
||||||
|
0x02, 0x05, 0x06, 0x08...0x0D => {
|
||||||
|
const halfword: u32 = @as(u16, @truncate(self.cpu.pipe.stage[1].?));
|
||||||
|
break :blk halfword << 16 | halfword;
|
||||||
|
},
|
||||||
|
|
||||||
|
// BIOS or OAM (32-bit)
|
||||||
|
0x00, 0x07 => {
|
||||||
|
// Aligned: (PC + 6) | (PC + 4)
|
||||||
|
// Unaligned: (PC + 4) | (PC + 2)
|
||||||
|
const aligned = address & 3 == 0b00;
|
||||||
|
|
||||||
|
// TODO: What to do on PC + 6?
|
||||||
|
const high: u32 = if (aligned) self.dbgRead(u16, r15 + 4) else @as(u16, @truncate(self.cpu.pipe.stage[1].?));
|
||||||
|
const low: u32 = @as(u16, @truncate(self.cpu.pipe.stage[@intFromBool(aligned)].?));
|
||||||
|
|
||||||
|
break :blk high << 16 | low;
|
||||||
|
},
|
||||||
|
|
||||||
|
// IWRAM (16-bit but special)
|
||||||
|
0x03 => {
|
||||||
|
// Aligned: (PC + 2) | (PC + 4)
|
||||||
|
// Unaligned: (PC + 4) | (PC + 2)
|
||||||
|
const aligned = address & 3 == 0b00;
|
||||||
|
|
||||||
|
const high: u32 = @as(u16, @truncate(self.cpu.pipe.stage[1 - @intFromBool(aligned)].?));
|
||||||
|
const low: u32 = @as(u16, @truncate(self.cpu.pipe.stage[@intFromBool(aligned)].?));
|
||||||
|
|
||||||
|
break :blk high << 16 | low;
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
log.err("THUMB open bus read from 0x{X:0>2} page @0x{X:0>8}", .{ page, address });
|
||||||
|
@panic("invariant most-likely broken");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return @truncate(word);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// whether or not we do this in slowmem or fastmem, we should advance the scheduler
|
||||||
|
self.sched.tick += timings[@intFromBool(T == u32)][@as(u4, @truncate(unaligned_address >> 24))];
|
||||||
|
|
||||||
|
// We're doing some serious out-of-bounds open-bus reads
|
||||||
|
if (page >= table_len) return self.openBus(T, unaligned_address);
|
||||||
|
|
||||||
|
if (self.read_table[page]) |some_ptr| {
|
||||||
|
// We have a pointer to a page, cast the pointer to it's underlying type
|
||||||
|
const ptr: [*]const T = @ptrCast(@alignCast(some_ptr));
|
||||||
|
|
||||||
|
// Note: We don't check array length, since we force align the
|
||||||
|
// lower bits of the address as the GBA would
|
||||||
|
return ptr[forceAlign(T, offset) / @sizeOf(T)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.slowRead(T, unaligned_address);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dbgRead(self: *const Self, comptime T: type, unaligned_address: u32) T {
|
||||||
|
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).int.bits;
|
||||||
|
const page = unaligned_address >> bits;
|
||||||
|
const offset = unaligned_address & (page_size - 1);
|
||||||
|
|
||||||
|
// We're doing some serious out-of-bounds open-bus reads
|
||||||
|
if (page >= table_len) return self.openBus(T, unaligned_address);
|
||||||
|
|
||||||
|
if (self.read_table[page]) |some_ptr| {
|
||||||
|
// We have a pointer to a page, cast the pointer to it's underlying type
|
||||||
|
const ptr: [*]const T = @ptrCast(@alignCast(some_ptr));
|
||||||
|
|
||||||
|
// Note: We don't check array length, since we force align the
|
||||||
|
// lower bits of the address as the GBA would
|
||||||
|
return ptr[forceAlign(T, offset) / @sizeOf(T)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.dbgSlowRead(T, unaligned_address);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn slowRead(self: *Self, comptime T: type, unaligned_address: u32) T {
|
||||||
|
@branchHint(.cold);
|
||||||
|
|
||||||
|
const page: u8 = @truncate(unaligned_address >> 24);
|
||||||
|
const address = forceAlign(T, unaligned_address);
|
||||||
|
|
||||||
|
return switch (page) {
|
||||||
|
// General Internal Memory
|
||||||
|
0x00 => blk: {
|
||||||
|
if (address < Bios.size)
|
||||||
|
break :blk self.bios.read(T, self.cpu.r[15], unaligned_address);
|
||||||
|
|
||||||
|
break :blk self.openBus(T, address);
|
||||||
|
},
|
||||||
|
0x02 => unreachable, // completely handled by fastmeme
|
||||||
|
0x03 => unreachable, // completely handled by fastmeme
|
||||||
|
0x04 => self.readIo(T, address),
|
||||||
|
|
||||||
|
// Internal Display Memory
|
||||||
|
0x05 => unreachable, // completely handled by fastmeme
|
||||||
|
0x06 => unreachable, // completely handled by fastmeme
|
||||||
|
0x07 => unreachable, // completely handled by fastmeme
|
||||||
|
|
||||||
|
// External Memory (Game Pak)
|
||||||
|
0x08...0x0D => self.pak.read(T, address),
|
||||||
|
0x0E...0x0F => self.readBackup(T, unaligned_address),
|
||||||
|
else => self.openBus(T, address),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dbgSlowRead(self: *const Self, comptime T: type, unaligned_address: u32) T {
|
||||||
|
const page: u8 = @truncate(unaligned_address >> 24);
|
||||||
|
const address = forceAlign(T, unaligned_address);
|
||||||
|
|
||||||
|
return switch (page) {
|
||||||
|
// General Internal Memory
|
||||||
|
0x00 => blk: {
|
||||||
|
if (address < Bios.size)
|
||||||
|
break :blk self.bios.dbgRead(T, self.cpu.r[15], unaligned_address);
|
||||||
|
|
||||||
|
break :blk self.openBus(T, address);
|
||||||
|
},
|
||||||
|
0x02 => unreachable, // handled by fastmem
|
||||||
|
0x03 => unreachable, // handled by fastmem
|
||||||
|
0x04 => self.readIo(T, address),
|
||||||
|
|
||||||
|
// Internal Display Memory
|
||||||
|
0x05 => unreachable, // handled by fastmem
|
||||||
|
0x06 => unreachable, // handled by fastmem
|
||||||
|
0x07 => unreachable, // handled by fastmem
|
||||||
|
|
||||||
|
// External Memory (Game Pak)
|
||||||
|
0x08...0x0D => self.pak.dbgRead(T, address),
|
||||||
|
0x0E...0x0F => self.readBackup(T, unaligned_address),
|
||||||
|
else => self.openBus(T, address),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readBackup(self: *const Self, comptime T: type, unaligned_address: u32) T {
|
||||||
|
const value = self.pak.backup.read(unaligned_address);
|
||||||
|
|
||||||
|
const multiplier = switch (T) {
|
||||||
|
u32 => 0x01010101,
|
||||||
|
u16 => 0x0101,
|
||||||
|
u8 => 1,
|
||||||
|
else => @compileError("Backup: Unsupported read width"),
|
||||||
|
};
|
||||||
|
|
||||||
|
return @as(T, value) * multiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
|
||||||
|
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).int.bits;
|
||||||
|
const page = unaligned_address >> bits;
|
||||||
|
const offset = unaligned_address & (page_size - 1);
|
||||||
|
|
||||||
|
// whether or not we do this in slowmem or fastmem, we should advance the scheduler
|
||||||
|
self.sched.tick += timings[@intFromBool(T == u32)][@as(u4, @truncate(unaligned_address >> 24))];
|
||||||
|
|
||||||
|
// We're doing some serious out-of-bounds open-bus writes, they do nothing though
|
||||||
|
if (page >= table_len) return;
|
||||||
|
|
||||||
|
if (self.write_tables[@intFromBool(T == u8)][page]) |some_ptr| {
|
||||||
|
// We have a pointer to a page, cast the pointer to it's underlying type
|
||||||
|
const ptr: [*]T = @ptrCast(@alignCast(some_ptr));
|
||||||
|
|
||||||
|
// Note: We don't check array length, since we force align the
|
||||||
|
// lower bits of the address as the GBA would
|
||||||
|
ptr[forceAlign(T, offset) / @sizeOf(T)] = value;
|
||||||
|
} else {
|
||||||
|
// we can return early if this is an 8-bit OAM write
|
||||||
|
if (T == u8 and @as(u8, @truncate(unaligned_address >> 24)) == 0x07) return;
|
||||||
|
|
||||||
|
self.slowWrite(T, unaligned_address, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mostly Identical to `Bus.write`, slowmeme is handled by `Bus.dbgSlowWrite`
|
||||||
|
pub fn dbgWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
|
||||||
|
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).int.bits;
|
||||||
|
const page = unaligned_address >> bits;
|
||||||
|
const offset = unaligned_address & (page_size - 1);
|
||||||
|
|
||||||
|
// We're doing some serious out-of-bounds open-bus writes, they do nothing though
|
||||||
|
if (page >= table_len) return;
|
||||||
|
|
||||||
|
if (self.write_tables[@intFromBool(T == u8)][page]) |some_ptr| {
|
||||||
|
// We have a pointer to a page, cast the pointer to it's underlying type
|
||||||
|
const ptr: [*]T = @ptrCast(@alignCast(some_ptr));
|
||||||
|
|
||||||
|
// Note: We don't check array length, since we force align the
|
||||||
|
// lower bits of the address as the GBA would
|
||||||
|
ptr[forceAlign(T, offset) / @sizeOf(T)] = value;
|
||||||
|
} else {
|
||||||
|
// we can return early if this is an 8-bit OAM write
|
||||||
|
if (T == u8 and @as(u8, @truncate(unaligned_address >> 24)) == 0x07) return;
|
||||||
|
|
||||||
|
self.dbgSlowWrite(T, unaligned_address, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn slowWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
|
||||||
|
@branchHint(.cold);
|
||||||
|
|
||||||
|
const page: u8 = @truncate(unaligned_address >> 24);
|
||||||
|
const address = forceAlign(T, unaligned_address);
|
||||||
|
|
||||||
|
switch (page) {
|
||||||
|
// General Internal Memory
|
||||||
|
0x00 => self.bios.write(T, address, value),
|
||||||
|
0x02 => unreachable, // completely handled by fastmem
|
||||||
|
0x03 => unreachable, // completely handled by fastmem
|
||||||
|
0x04 => _io.write(self, T, address, value),
|
||||||
|
|
||||||
|
// Internal Display Memory
|
||||||
|
0x05 => self.ppu.palette.write(T, address, value),
|
||||||
|
0x06 => self.ppu.vram.write(T, self.ppu.dispcnt, address, value),
|
||||||
|
0x07 => unreachable, // completely handled by fastmem
|
||||||
|
|
||||||
|
// External Memory (Game Pak)
|
||||||
|
0x08...0x0D => self.pak.write(T, self.dma[3].word_count, address, value),
|
||||||
|
0x0E...0x0F => self.pak.backup.write(unaligned_address, @truncate(rotr(T, value, 8 * rotateBy(T, unaligned_address)))),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dbgSlowWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
|
||||||
|
@branchHint(.cold);
|
||||||
|
|
||||||
|
const page: u8 = @truncate(unaligned_address >> 24);
|
||||||
|
const address = forceAlign(T, unaligned_address);
|
||||||
|
|
||||||
|
switch (page) {
|
||||||
|
// General Internal Memory
|
||||||
|
0x00 => self.bios.write(T, address, value),
|
||||||
|
0x02 => unreachable, // completely handled by fastmem
|
||||||
|
0x03 => unreachable, // completely handled by fastmem
|
||||||
|
0x04 => return, // FIXME: Let debug writes mess with I/O
|
||||||
|
|
||||||
|
// Internal Display Memory
|
||||||
|
0x05 => self.ppu.palette.write(T, address, value),
|
||||||
|
0x06 => self.ppu.vram.write(T, self.ppu.dispcnt, address, value),
|
||||||
|
0x07 => unreachable, // completely handled by fastmem
|
||||||
|
|
||||||
|
// External Memory (Game Pak)
|
||||||
|
0x08...0x0D => return, // FIXME: Debug Write to Backup/GPIO w/out messing with state
|
||||||
|
0x0E...0x0F => return, // FIXME: Debug Write to Backup w/out messing with state
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn rotateBy(comptime T: type, address: u32) u32 {
|
||||||
|
return switch (T) {
|
||||||
|
u32 => address & 3,
|
||||||
|
u16 => address & 1,
|
||||||
|
u8 => 0,
|
||||||
|
else => @compileError("Unsupported write width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn forceAlign(comptime T: type, address: u32) u32 {
|
||||||
|
return switch (T) {
|
||||||
|
u32 => address & ~@as(u32, 3),
|
||||||
|
u16 => address & ~@as(u32, 1),
|
||||||
|
u8 => address,
|
||||||
|
else => @compileError("Bus: Invalid read/write type"),
|
||||||
|
};
|
||||||
|
}
|
||||||
633
src/core/apu.zig
Normal file
633
src/core/apu.zig
Normal file
@@ -0,0 +1,633 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const io = @import("bus/io.zig");
|
||||||
|
const util = @import("../util.zig");
|
||||||
|
const c = @import("../lib.zig").c;
|
||||||
|
|
||||||
|
const LinearFifo = @import("../lib/fifo.zig").LinearFifo;
|
||||||
|
const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||||
|
const Bus = @import("Bus.zig");
|
||||||
|
const Scheduler = @import("scheduler.zig").Scheduler;
|
||||||
|
const ToneSweep = @import("apu/ToneSweep.zig");
|
||||||
|
const Tone = @import("apu/Tone.zig");
|
||||||
|
const Wave = @import("apu/Wave.zig");
|
||||||
|
const Noise = @import("apu/Noise.zig");
|
||||||
|
|
||||||
|
const SoundFifo = LinearFifo(u8, .{ .static = 0x20 });
|
||||||
|
|
||||||
|
const getHalf = util.getHalf;
|
||||||
|
const setHalf = util.setHalf;
|
||||||
|
|
||||||
|
const log = std.log.scoped(.APU);
|
||||||
|
|
||||||
|
pub const host_rate = @import("../platform.zig").sample_rate;
|
||||||
|
pub const host_format = @import("../platform.zig").sample_format;
|
||||||
|
|
||||||
|
pub fn read(comptime T: type, apu: *const Apu, addr: u32) ?T {
|
||||||
|
const byte_addr: u8 = @truncate(addr);
|
||||||
|
|
||||||
|
return switch (T) {
|
||||||
|
u32 => switch (byte_addr) {
|
||||||
|
0x60 => @as(T, apu.ch1.sound1CntH()) << 16 | apu.ch1.sound1CntL(),
|
||||||
|
0x64 => apu.ch1.sound1CntX(),
|
||||||
|
0x68 => apu.ch2.sound2CntL(),
|
||||||
|
0x6C => apu.ch2.sound2CntH(),
|
||||||
|
0x70 => @as(T, apu.ch3.sound3CntH()) << 16 | apu.ch3.sound3CntL(),
|
||||||
|
0x74 => apu.ch3.sound3CntX(),
|
||||||
|
0x78 => apu.ch4.sound4CntL(),
|
||||||
|
0x7C => apu.ch4.sound4CntH(),
|
||||||
|
0x80 => @as(T, apu.dma_cnt.raw) << 16 | apu.psg_cnt.raw, // SOUNDCNT_H, SOUNDCNT_L
|
||||||
|
0x84 => apu.soundCntX(),
|
||||||
|
0x88 => apu.bias.raw, // SOUNDBIAS, high is unused
|
||||||
|
0x8C => null,
|
||||||
|
0x90, 0x94, 0x98, 0x9C => apu.ch3.wave_dev.read(T, apu.ch3.select, addr),
|
||||||
|
0xA0 => null, // FIFO_A
|
||||||
|
0xA4 => null, // FIFO_B
|
||||||
|
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
||||||
|
},
|
||||||
|
u16 => switch (byte_addr) {
|
||||||
|
0x60 => apu.ch1.sound1CntL(),
|
||||||
|
0x62 => apu.ch1.sound1CntH(),
|
||||||
|
0x64 => apu.ch1.sound1CntX(),
|
||||||
|
0x66 => 0x0000, // suite.gba expects 0x0000, not 0xDEAD
|
||||||
|
0x68 => apu.ch2.sound2CntL(),
|
||||||
|
0x6A => 0x0000,
|
||||||
|
0x6C => apu.ch2.sound2CntH(),
|
||||||
|
0x6E => 0x0000,
|
||||||
|
0x70 => apu.ch3.sound3CntL(),
|
||||||
|
0x72 => apu.ch3.sound3CntH(),
|
||||||
|
0x74 => apu.ch3.sound3CntX(),
|
||||||
|
0x76 => 0x0000,
|
||||||
|
0x78 => apu.ch4.sound4CntL(),
|
||||||
|
0x7A => 0x0000,
|
||||||
|
0x7C => apu.ch4.sound4CntH(),
|
||||||
|
0x7E => 0x0000,
|
||||||
|
0x80 => apu.soundCntL(),
|
||||||
|
0x82 => apu.soundCntH(),
|
||||||
|
0x84 => apu.soundCntX(),
|
||||||
|
0x86 => 0x0000,
|
||||||
|
0x88 => apu.bias.raw, // SOUNDBIAS
|
||||||
|
0x8A => 0x0000,
|
||||||
|
0x8C, 0x8E => null,
|
||||||
|
0x90, 0x92, 0x94, 0x96, 0x98, 0x9A, 0x9C, 0x9E => apu.ch3.wave_dev.read(T, apu.ch3.select, addr),
|
||||||
|
0xA0, 0xA2 => null, // FIFO_A
|
||||||
|
0xA4, 0xA6 => null, // FIFO_B
|
||||||
|
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
||||||
|
},
|
||||||
|
u8 => switch (byte_addr) {
|
||||||
|
0x60, 0x61 => @truncate(@as(u16, apu.ch1.sound1CntL()) >> getHalf(byte_addr)),
|
||||||
|
0x62, 0x63 => @truncate(apu.ch1.sound1CntH() >> getHalf(byte_addr)),
|
||||||
|
0x64, 0x65 => @truncate(apu.ch1.sound1CntX() >> getHalf(byte_addr)),
|
||||||
|
0x66, 0x67 => 0x00, // assuming behaviour is identical to that of 16-bit reads
|
||||||
|
0x68, 0x69 => @truncate(apu.ch2.sound2CntL() >> getHalf(byte_addr)),
|
||||||
|
0x6A, 0x6B => 0x00,
|
||||||
|
0x6C, 0x6D => @truncate(apu.ch2.sound2CntH() >> getHalf(byte_addr)),
|
||||||
|
0x6E, 0x6F => 0x00,
|
||||||
|
0x70, 0x71 => @truncate(@as(u16, apu.ch3.sound3CntL()) >> getHalf(byte_addr)), // SOUND3CNT_L
|
||||||
|
0x72, 0x73 => @truncate(apu.ch3.sound3CntH() >> getHalf(byte_addr)),
|
||||||
|
0x74, 0x75 => @truncate(apu.ch3.sound3CntX() >> getHalf(byte_addr)), // SOUND3CNT_L
|
||||||
|
0x76, 0x77 => 0x00,
|
||||||
|
0x78, 0x79 => @truncate(apu.ch4.sound4CntL() >> getHalf(byte_addr)),
|
||||||
|
0x7A, 0x7B => 0x00,
|
||||||
|
0x7C, 0x7D => @truncate(apu.ch4.sound4CntH() >> getHalf(byte_addr)),
|
||||||
|
0x7E, 0x7F => 0x00,
|
||||||
|
0x80, 0x81 => @truncate(apu.soundCntL() >> getHalf(byte_addr)), // SOUNDCNT_L
|
||||||
|
0x82, 0x83 => @truncate(apu.soundCntH() >> getHalf(byte_addr)), // SOUNDCNT_H
|
||||||
|
0x84, 0x85 => @truncate(@as(u16, apu.soundCntX()) >> getHalf(byte_addr)),
|
||||||
|
0x86, 0x87 => 0x00,
|
||||||
|
0x88, 0x89 => @truncate(apu.bias.raw >> getHalf(byte_addr)), // SOUNDBIAS
|
||||||
|
0x8A, 0x8B => 0x00,
|
||||||
|
0x8C...0x8F => null,
|
||||||
|
0x90...0x9F => apu.ch3.wave_dev.read(T, apu.ch3.select, addr),
|
||||||
|
0xA0, 0xA1, 0xA2, 0xA3 => null, // FIFO_A
|
||||||
|
0xA4, 0xA5, 0xA6, 0xA7 => null, // FIFO_B
|
||||||
|
else => util.io.read.err(T, log, "unexpected {} read from 0x{X:0>8}", .{ T, addr }),
|
||||||
|
},
|
||||||
|
else => @compileError("APU: Unsupported read width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void {
|
||||||
|
const byte_addr: u8 = @truncate(addr);
|
||||||
|
|
||||||
|
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return;
|
||||||
|
|
||||||
|
switch (T) {
|
||||||
|
u32 => {
|
||||||
|
// 0x80 and 0x81 handled in setSoundCnt
|
||||||
|
if (byte_addr < 0x80 and !apu.cnt.apu_enable.read()) return;
|
||||||
|
|
||||||
|
switch (byte_addr) {
|
||||||
|
0x60 => apu.ch1.setSound1Cnt(value),
|
||||||
|
0x64 => apu.ch1.setSound1CntX(&apu.fs, @truncate(value)),
|
||||||
|
|
||||||
|
0x68 => apu.ch2.setSound2CntL(@truncate(value)),
|
||||||
|
0x6C => apu.ch2.setSound2CntH(&apu.fs, @truncate(value)),
|
||||||
|
|
||||||
|
0x70 => apu.ch3.setSound3Cnt(value),
|
||||||
|
0x74 => apu.ch3.setSound3CntX(&apu.fs, @truncate(value)),
|
||||||
|
|
||||||
|
0x78 => apu.ch4.setSound4CntL(@truncate(value)),
|
||||||
|
0x7C => apu.ch4.setSound4CntH(&apu.fs, @truncate(value)),
|
||||||
|
|
||||||
|
0x80 => apu.setSoundCnt(value),
|
||||||
|
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1),
|
||||||
|
0x88 => apu.bias.raw = @truncate(value),
|
||||||
|
0x8C => {},
|
||||||
|
|
||||||
|
0x90, 0x94, 0x98, 0x9C => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
|
||||||
|
0xA0 => apu.chA.push(value), // FIFO_A
|
||||||
|
0xA4 => apu.chB.push(value), // FIFO_B
|
||||||
|
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
u16 => {
|
||||||
|
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return;
|
||||||
|
|
||||||
|
switch (byte_addr) {
|
||||||
|
0x60 => apu.ch1.setSound1CntL(@truncate(value)), // SOUND1CNT_L
|
||||||
|
0x62 => apu.ch1.setSound1CntH(value),
|
||||||
|
0x64 => apu.ch1.setSound1CntX(&apu.fs, value),
|
||||||
|
0x66 => {},
|
||||||
|
|
||||||
|
0x68 => apu.ch2.setSound2CntL(value),
|
||||||
|
0x6A => {},
|
||||||
|
0x6C => apu.ch2.setSound2CntH(&apu.fs, value),
|
||||||
|
0x6E => {},
|
||||||
|
|
||||||
|
0x70 => apu.ch3.setSound3CntL(@truncate(value)),
|
||||||
|
0x72 => apu.ch3.setSound3CntH(value),
|
||||||
|
0x74 => apu.ch3.setSound3CntX(&apu.fs, value),
|
||||||
|
0x76 => {},
|
||||||
|
|
||||||
|
0x78 => apu.ch4.setSound4CntL(value),
|
||||||
|
0x7A => {},
|
||||||
|
0x7C => apu.ch4.setSound4CntH(&apu.fs, value),
|
||||||
|
0x7E => {},
|
||||||
|
|
||||||
|
0x80 => apu.setSoundCntL(value),
|
||||||
|
0x82 => apu.setSoundCntH(value),
|
||||||
|
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1),
|
||||||
|
0x86 => {},
|
||||||
|
0x88 => apu.bias.raw = value, // SOUNDBIAS
|
||||||
|
0x8A, 0x8C, 0x8E => {},
|
||||||
|
|
||||||
|
0x90, 0x92, 0x94, 0x96, 0x98, 0x9A, 0x9C, 0x9E => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
|
||||||
|
0xA0, 0xA2 => log.err("Tried to write 0x{X:0>4}{} to FIFO_A", .{ value, T }),
|
||||||
|
0xA4, 0xA6 => log.err("Tried to write 0x{X:0>4}{} to FIFO_B", .{ value, T }),
|
||||||
|
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
u8 => {
|
||||||
|
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return;
|
||||||
|
|
||||||
|
switch (byte_addr) {
|
||||||
|
0x60 => apu.ch1.setSound1CntL(value),
|
||||||
|
0x61 => {},
|
||||||
|
0x62 => apu.ch1.setNr11(value),
|
||||||
|
0x63 => apu.ch1.setNr12(value),
|
||||||
|
0x64 => apu.ch1.setNr13(value),
|
||||||
|
0x65 => apu.ch1.setNr14(&apu.fs, value),
|
||||||
|
0x66, 0x67 => {},
|
||||||
|
|
||||||
|
0x68 => apu.ch2.setNr21(value),
|
||||||
|
0x69 => apu.ch2.setNr22(value),
|
||||||
|
0x6A, 0x6B => {},
|
||||||
|
0x6C => apu.ch2.setNr23(value),
|
||||||
|
0x6D => apu.ch2.setNr24(&apu.fs, value),
|
||||||
|
0x6E, 0x6F => {},
|
||||||
|
|
||||||
|
0x70 => apu.ch3.setSound3CntL(value), // NR30
|
||||||
|
0x71 => {},
|
||||||
|
0x72 => apu.ch3.setNr31(value),
|
||||||
|
0x73 => apu.ch3.vol.raw = value, // NR32
|
||||||
|
0x74 => apu.ch3.setNr33(value),
|
||||||
|
0x75 => apu.ch3.setNr34(&apu.fs, value),
|
||||||
|
0x76, 0x77 => {},
|
||||||
|
|
||||||
|
0x78 => apu.ch4.setNr41(value),
|
||||||
|
0x79 => apu.ch4.setNr42(value),
|
||||||
|
0x7A, 0x7B => {},
|
||||||
|
0x7C => apu.ch4.poly.raw = value, // NR 43
|
||||||
|
0x7D => apu.ch4.setNr44(&apu.fs, value),
|
||||||
|
0x7E, 0x7F => {},
|
||||||
|
|
||||||
|
0x80, 0x81 => apu.setSoundCntL(setHalf(u16, apu.psg_cnt.raw, byte_addr, value)),
|
||||||
|
0x82, 0x83 => apu.setSoundCntH(setHalf(u16, apu.dma_cnt.raw, byte_addr, value)),
|
||||||
|
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1),
|
||||||
|
0x85 => {},
|
||||||
|
0x86, 0x87 => {},
|
||||||
|
0x88, 0x89 => apu.bias.raw = setHalf(u16, apu.bias.raw, byte_addr, value), // SOUNDBIAS
|
||||||
|
0x8A...0x8F => {},
|
||||||
|
|
||||||
|
0x90...0x9F => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
|
||||||
|
0xA0...0xA3 => log.err("Tried to write 0x{X:0>2}{} to FIFO_A", .{ value, T }),
|
||||||
|
0xA4...0xA7 => log.err("Tried to write 0x{X:0>2}{} to FIFO_B", .{ value, T }),
|
||||||
|
else => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => @compileError("APU: Unsupported write width"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Apu = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
ch1: ToneSweep,
|
||||||
|
ch2: Tone,
|
||||||
|
ch3: Wave,
|
||||||
|
ch4: Noise,
|
||||||
|
chA: DmaSound(.A),
|
||||||
|
chB: DmaSound(.B),
|
||||||
|
|
||||||
|
bias: io.SoundBias,
|
||||||
|
/// NR50, NR51
|
||||||
|
psg_cnt: io.ChannelVolumeControl,
|
||||||
|
dma_cnt: io.DmaSoundControl,
|
||||||
|
cnt: io.SoundControl,
|
||||||
|
|
||||||
|
sampling_cycle: u2,
|
||||||
|
|
||||||
|
stream: *c.SDL_AudioStream,
|
||||||
|
sched: *Scheduler,
|
||||||
|
|
||||||
|
fs: FrameSequencer,
|
||||||
|
capacitor: f32,
|
||||||
|
|
||||||
|
is_buffer_full: bool,
|
||||||
|
|
||||||
|
pub const Tick = enum { Length, Envelope, Sweep };
|
||||||
|
|
||||||
|
pub fn init(sched: *Scheduler) Self {
|
||||||
|
const apu: Self = .{
|
||||||
|
.ch1 = ToneSweep.init(sched),
|
||||||
|
.ch2 = Tone.init(sched),
|
||||||
|
.ch3 = Wave.init(sched),
|
||||||
|
.ch4 = Noise.init(sched),
|
||||||
|
.chA = DmaSound(.A).init(),
|
||||||
|
.chB = DmaSound(.B).init(),
|
||||||
|
|
||||||
|
.psg_cnt = .{ .raw = 0 },
|
||||||
|
.dma_cnt = .{ .raw = 0 },
|
||||||
|
.cnt = .{ .raw = 0 },
|
||||||
|
.bias = .{ .raw = 0x0200 },
|
||||||
|
|
||||||
|
.sampling_cycle = 0b00,
|
||||||
|
.sched = sched,
|
||||||
|
.stream = undefined, // FIXME: bad practice
|
||||||
|
|
||||||
|
.capacitor = 0,
|
||||||
|
.fs = FrameSequencer.init(),
|
||||||
|
.is_buffer_full = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
Self.initEvents(apu.sched, apu.interval());
|
||||||
|
|
||||||
|
return apu;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initEvents(scheduler: *Scheduler, apu_interval: u64) void {
|
||||||
|
scheduler.push(.SampleAudio, apu_interval);
|
||||||
|
scheduler.push(.{ .ApuChannel = 0 }, @import("apu/signal/Square.zig").interval);
|
||||||
|
scheduler.push(.{ .ApuChannel = 1 }, @import("apu/signal/Square.zig").interval);
|
||||||
|
scheduler.push(.{ .ApuChannel = 2 }, @import("apu/signal/Wave.zig").interval);
|
||||||
|
scheduler.push(.{ .ApuChannel = 3 }, @import("apu/signal/Lfsr.zig").interval);
|
||||||
|
scheduler.push(.FrameSequencer, FrameSequencer.interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used when resetting the emulator
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
// FIXME: These reset functions are meant to emulate obscure APU behaviour. Write proper emu reset fns
|
||||||
|
self.ch1.reset();
|
||||||
|
self.ch2.reset();
|
||||||
|
self.ch3.reset();
|
||||||
|
self.ch4.reset();
|
||||||
|
|
||||||
|
self.chA.reset();
|
||||||
|
self.chB.reset();
|
||||||
|
|
||||||
|
self.psg_cnt = .{ .raw = 0 };
|
||||||
|
self.dma_cnt = .{ .raw = 0 };
|
||||||
|
self.cnt = .{ .raw = 0 };
|
||||||
|
self.bias = .{ .raw = 0x200 };
|
||||||
|
|
||||||
|
self.sampling_cycle = 0;
|
||||||
|
self.fs.reset();
|
||||||
|
|
||||||
|
Self.initEvents(self.sched, self.interval());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emulates the reset behaviour of the APU
|
||||||
|
fn _reset(self: *Self) void {
|
||||||
|
// All PSG Registers between 0x0400_0060..0x0400_0081 are zeroed
|
||||||
|
// 0x0400_0082 and 0x0400_0088 retain their values
|
||||||
|
self.ch1.reset();
|
||||||
|
self.ch2.reset();
|
||||||
|
self.ch3.reset();
|
||||||
|
self.ch4.reset();
|
||||||
|
|
||||||
|
// GBATEK says 4000060h..4000081h I take this to mean inclusive
|
||||||
|
self.psg_cnt.raw = 0x0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SOUNDCNT
|
||||||
|
fn setSoundCnt(self: *Self, value: u32) void {
|
||||||
|
if (self.cnt.apu_enable.read()) self.setSoundCntL(@truncate(value));
|
||||||
|
self.setSoundCntH(@truncate(value >> 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SOUNDCNT_L
|
||||||
|
pub fn soundCntL(self: *const Self) u16 {
|
||||||
|
return self.psg_cnt.raw & 0xFF77;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SOUNDCNT_L
|
||||||
|
pub fn setSoundCntL(self: *Self, value: u16) void {
|
||||||
|
self.psg_cnt.raw = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SOUNDCNT_H
|
||||||
|
pub fn setSoundCntH(self: *Self, value: u16) void {
|
||||||
|
const new: io.DmaSoundControl = .{ .raw = value };
|
||||||
|
|
||||||
|
// Reinitializing instead of resetting is fine because
|
||||||
|
// the FIFOs I'm using are stack allocated and 0x20 bytes big
|
||||||
|
if (new.chA_reset.read()) self.chA.fifo = SoundFifo.init();
|
||||||
|
if (new.chB_reset.read()) self.chB.fifo = SoundFifo.init();
|
||||||
|
|
||||||
|
self.dma_cnt = new;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SOUNDCNT_H
|
||||||
|
pub fn soundCntH(self: *const Self) u16 {
|
||||||
|
return self.dma_cnt.raw & 0x770F;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR52
|
||||||
|
pub fn setSoundCntX(self: *Self, value: bool) void {
|
||||||
|
self.cnt.apu_enable.write(value);
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
self.fs.step = 0; // Reset Frame Sequencer
|
||||||
|
|
||||||
|
// Reset Square Wave Offsets
|
||||||
|
self.ch1.square.reset();
|
||||||
|
self.ch2.square.reset();
|
||||||
|
|
||||||
|
// Reset Wave
|
||||||
|
self.ch3.wave_dev.reset();
|
||||||
|
|
||||||
|
// Rest Noise
|
||||||
|
self.ch4.lfsr.reset();
|
||||||
|
} else {
|
||||||
|
self._reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR52
|
||||||
|
pub fn soundCntX(self: *const Self) u8 {
|
||||||
|
const apu_enable: u8 = @intFromBool(self.cnt.apu_enable.read());
|
||||||
|
|
||||||
|
const ch1_enable: u8 = @intFromBool(self.ch1.enabled);
|
||||||
|
const ch2_enable: u8 = @intFromBool(self.ch2.enabled);
|
||||||
|
const ch3_enable: u8 = @intFromBool(self.ch3.enabled);
|
||||||
|
const ch4_enable: u8 = @intFromBool(self.ch4.enabled);
|
||||||
|
|
||||||
|
return apu_enable << 7 | ch4_enable << 3 | ch3_enable << 2 | ch2_enable << 1 | ch1_enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sampleAudio(self: *Self, late: u64) void {
|
||||||
|
self.sched.push(.SampleAudio, self.interval() -| late);
|
||||||
|
|
||||||
|
// Whether the APU is busy or not is determined by the main loop in emu.zig
|
||||||
|
// This should only ever be true (because this side of the emu is single threaded)
|
||||||
|
// When audio sync is disaabled
|
||||||
|
if (self.is_buffer_full) return;
|
||||||
|
|
||||||
|
var left: i16 = 0;
|
||||||
|
var right: i16 = 0;
|
||||||
|
|
||||||
|
// SOUNDCNT_L Channel Enable flags
|
||||||
|
const ch_left: u4 = self.psg_cnt.ch_left.read();
|
||||||
|
const ch_right: u4 = self.psg_cnt.ch_right.read();
|
||||||
|
|
||||||
|
// Determine SOUNDCNT_H volume modifications
|
||||||
|
const gba_vol: u4 = switch (self.dma_cnt.ch_vol.read()) {
|
||||||
|
0b00 => 2,
|
||||||
|
0b01 => 1,
|
||||||
|
else => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add all PSG channels together
|
||||||
|
left += if (ch_left & 1 == 1) @as(i16, self.ch1.sample) else 0;
|
||||||
|
left += if (ch_left >> 1 & 1 == 1) @as(i16, self.ch2.sample) else 0;
|
||||||
|
left += if (ch_left >> 2 & 1 == 1) @as(i16, self.ch3.sample) else 0;
|
||||||
|
left += if (ch_left >> 3 == 1) @as(i16, self.ch4.sample) else 0;
|
||||||
|
|
||||||
|
right += if (ch_right & 1 == 1) @as(i16, self.ch1.sample) else 0;
|
||||||
|
right += if (ch_right >> 1 & 1 == 1) @as(i16, self.ch2.sample) else 0;
|
||||||
|
right += if (ch_right >> 2 & 1 == 1) @as(i16, self.ch3.sample) else 0;
|
||||||
|
right += if (ch_right >> 3 == 1) @as(i16, self.ch4.sample) else 0;
|
||||||
|
|
||||||
|
// Multiply by master channel volume
|
||||||
|
left *= 1 + @as(i16, self.psg_cnt.left_vol.read());
|
||||||
|
right *= 1 + @as(i16, self.psg_cnt.right_vol.read());
|
||||||
|
|
||||||
|
// Apply GBA volume modifications to PSG Channels
|
||||||
|
left >>= gba_vol;
|
||||||
|
right >>= gba_vol;
|
||||||
|
|
||||||
|
const chA_sample = self.chA.amplitude() << if (self.dma_cnt.chA_vol.read()) @as(u4, 2) else 1;
|
||||||
|
const chB_sample = self.chB.amplitude() << if (self.dma_cnt.chB_vol.read()) @as(u4, 2) else 1;
|
||||||
|
|
||||||
|
left += if (self.dma_cnt.chA_left.read()) chA_sample else 0;
|
||||||
|
left += if (self.dma_cnt.chB_left.read()) chB_sample else 0;
|
||||||
|
|
||||||
|
right += if (self.dma_cnt.chA_right.read()) chA_sample else 0;
|
||||||
|
right += if (self.dma_cnt.chB_right.read()) chB_sample else 0;
|
||||||
|
|
||||||
|
// Add SOUNDBIAS
|
||||||
|
// FIXME: SOUNDBIAS is 10-bit but The waveform is centered around 0 if I treat it as 11-bit
|
||||||
|
const bias = @as(i16, self.bias.level.read()) << 2;
|
||||||
|
left += bias;
|
||||||
|
right += bias;
|
||||||
|
|
||||||
|
const clamped_left = std.math.clamp(@as(u16, @bitCast(left)), 0, std.math.maxInt(u11));
|
||||||
|
const clamped_right = std.math.clamp(@as(u16, @bitCast(right)), 0, std.math.maxInt(u11));
|
||||||
|
|
||||||
|
// Extend to 16-bit signed audio samples
|
||||||
|
const ext_left = (clamped_left << 5) | (clamped_left >> 6);
|
||||||
|
const ext_right = (clamped_right << 5) | (clamped_right >> 6);
|
||||||
|
|
||||||
|
if (self.sampling_cycle != self.bias.sampling_cycle.read()) self.replaceSDLResampler();
|
||||||
|
|
||||||
|
const ret = c.SDL_PutAudioStreamData(self.stream, &[2]i16{ @bitCast(ext_left ^ 0x8000), @bitCast(ext_right ^ 0x8000) }, 2 * @sizeOf(i16));
|
||||||
|
if (!ret) @panic("TODO: Failed to put i16s into SDL Audio Queue");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replaceSDLResampler(self: *Self) void {
|
||||||
|
@branchHint(.cold);
|
||||||
|
_ = self;
|
||||||
|
|
||||||
|
@panic("TODO: Implement Multiple Sample Rates...");
|
||||||
|
|
||||||
|
// const sample_rate = Self.sampleRate(self.bias.sampling_cycle.read());
|
||||||
|
// log.info("Sample Rate changed from {}Hz to {}Hz", .{ Self.sampleRate(self.sampling_cycle), sample_rate });
|
||||||
|
|
||||||
|
// // Sampling Cycle (Sample Rate) changed, Craete a new SDL Audio Resampler
|
||||||
|
// // FIXME: Replace SDL's Audio Resampler with either a custom or more reliable one
|
||||||
|
// const old_stream = self.stream;
|
||||||
|
// defer c.SDL_DestroyAudioStream(old_stream);
|
||||||
|
|
||||||
|
// self.sampling_cycle = self.bias.sampling_cycle.read();
|
||||||
|
|
||||||
|
// var desired: c.SDL_AudioSpec = std.mem.zeroes(c.SDL_AudioSpec);
|
||||||
|
// desired.format = c.SDL_AUDIO_S16;
|
||||||
|
// desired.channels = 2;
|
||||||
|
// desired.freq = @intCast(sample_rate);
|
||||||
|
|
||||||
|
// const new_stream = c.SDL_OpenAudioDeviceStream(c.SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desired, null, null) orelse @panic("TODO: Failed to replace SDL Audio Stream");
|
||||||
|
// self.stream = new_stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn interval(self: *const Self) u64 {
|
||||||
|
return (1 << 24) / Self.sampleRate(self.bias.sampling_cycle.read());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sampleRate(cycle: u2) u64 {
|
||||||
|
return @as(u64, 1) << (15 + @as(u6, cycle));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn onSequencerTick(self: *Self, late: u64) void {
|
||||||
|
self.fs.tick();
|
||||||
|
|
||||||
|
switch (self.fs.step) {
|
||||||
|
7 => self.tick(.Envelope), // Clock Envelope
|
||||||
|
0, 4 => self.tick(.Length), // Clock Length
|
||||||
|
2, 6 => {
|
||||||
|
// Clock Length and Sweep
|
||||||
|
self.tick(.Length);
|
||||||
|
self.tick(.Sweep);
|
||||||
|
},
|
||||||
|
1, 3, 5 => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
self.sched.push(.FrameSequencer, ((1 << 24) / 512) -| late);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick(self: *Self, comptime kind: Tick) void {
|
||||||
|
self.ch1.tick(kind);
|
||||||
|
|
||||||
|
switch (kind) {
|
||||||
|
.Length => {
|
||||||
|
self.ch2.tick(kind);
|
||||||
|
self.ch3.tick(kind);
|
||||||
|
self.ch4.tick(kind);
|
||||||
|
},
|
||||||
|
.Envelope => {
|
||||||
|
self.ch2.tick(kind);
|
||||||
|
self.ch4.tick(kind);
|
||||||
|
},
|
||||||
|
.Sweep => {}, // Already handled above (only for Ch1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn onDmaAudioSampleRequest(self: *Self, cpu: *Arm7tdmi, tim_id: u3) void {
|
||||||
|
if (!self.cnt.apu_enable.read()) return;
|
||||||
|
|
||||||
|
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
|
||||||
|
|
||||||
|
if (@intFromBool(self.dma_cnt.chA_timer.read()) == tim_id) {
|
||||||
|
if (!self.chA.enabled) return;
|
||||||
|
|
||||||
|
self.chA.updateSample();
|
||||||
|
if (self.chA.len() <= 15) bus_ptr.dma[1].requestAudio(0x0400_00A0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@intFromBool(self.dma_cnt.chB_timer.read()) == tim_id) {
|
||||||
|
if (!self.chB.enabled) return;
|
||||||
|
|
||||||
|
self.chB.updateSample();
|
||||||
|
if (self.chB.len() <= 15) bus_ptr.dma[2].requestAudio(0x0400_00A4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn DmaSound(comptime kind: DmaSoundKind) type {
|
||||||
|
return struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
fifo: SoundFifo,
|
||||||
|
kind: DmaSoundKind,
|
||||||
|
sample: i8,
|
||||||
|
enabled: bool,
|
||||||
|
|
||||||
|
fn init() Self {
|
||||||
|
return .{
|
||||||
|
.fifo = SoundFifo.init(),
|
||||||
|
.kind = kind,
|
||||||
|
.sample = 0,
|
||||||
|
.enabled = false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used when resetting hte emulator (not emulation code)
|
||||||
|
fn reset(self: *Self) void {
|
||||||
|
self.* = Self.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(self: *Self, value: u32) void {
|
||||||
|
if (!self.enabled) self.enable();
|
||||||
|
|
||||||
|
self.fifo.write(std.mem.asBytes(&value)) catch |e| log.err("{} Error: {}", .{ kind, e });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enable(self: *Self) void {
|
||||||
|
@branchHint(.cold);
|
||||||
|
self.enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(self: *const Self) usize {
|
||||||
|
return self.fifo.readableLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn updateSample(self: *Self) void {
|
||||||
|
if (self.fifo.readItem()) |sample| self.sample = @bitCast(sample);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn amplitude(self: *const Self) i16 {
|
||||||
|
return self.sample;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const DmaSoundKind = enum {
|
||||||
|
A,
|
||||||
|
B,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const FrameSequencer = struct {
|
||||||
|
const Self = @This();
|
||||||
|
pub const interval = (1 << 24) / 512;
|
||||||
|
|
||||||
|
step: u3 = 0,
|
||||||
|
|
||||||
|
pub fn init() Self {
|
||||||
|
return .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
self.* = .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(self: *Self) void {
|
||||||
|
self.step +%= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isLengthNext(self: *const Self) bool {
|
||||||
|
return (self.step +% 1) & 1 == 0; // Steps, 0, 2, 4, and 6 clock length
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isEnvelopeNext(self: *const Self) bool {
|
||||||
|
return (self.step +% 1) == 7;
|
||||||
|
}
|
||||||
|
};
|
||||||
145
src/core/apu/Noise.zig
Normal file
145
src/core/apu/Noise.zig
Normal 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(value));
|
||||||
|
self.setNr42(@truncate(value >> 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR41
|
||||||
|
pub fn setNr41(self: *Self, len: u8) void {
|
||||||
|
self.len = @truncate(len);
|
||||||
|
self.len_dev.timer = @as(u7, 64) - self.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR42
|
||||||
|
pub fn setNr42(self: *Self, value: u8) void {
|
||||||
|
self.envelope.raw = value;
|
||||||
|
if (!self.isDacEnabled()) self.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR43, NR44
|
||||||
|
pub fn sound4CntH(self: *const Self) u16 {
|
||||||
|
return @as(u16, self.poly.raw & 0x40) << 8 | self.cnt.raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR43, NR44
|
||||||
|
pub fn setSound4CntH(self: *Self, fs: *const FrameSequencer, value: u16) void {
|
||||||
|
self.poly.raw = @truncate(value);
|
||||||
|
self.setNr44(fs, @truncate(value >> 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR44
|
||||||
|
pub fn setNr44(self: *Self, fs: *const FrameSequencer, byte: u8) void {
|
||||||
|
var new: io.NoiseControl = .{ .raw = byte };
|
||||||
|
|
||||||
|
if (new.trigger.read()) {
|
||||||
|
self.enabled = true;
|
||||||
|
|
||||||
|
if (self.len_dev.timer == 0) {
|
||||||
|
self.len_dev.timer =
|
||||||
|
if (!fs.isLengthNext() and new.length_enable.read()) 63 else 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update The Frequency Timer
|
||||||
|
self.lfsr.reload(self.poly);
|
||||||
|
self.lfsr.shift = 0x7FFF;
|
||||||
|
|
||||||
|
// Update Envelope and Volume
|
||||||
|
self.env_dev.timer = self.envelope.period.read();
|
||||||
|
if (fs.isEnvelopeNext() and self.env_dev.timer != 0b111) self.env_dev.timer += 1;
|
||||||
|
|
||||||
|
self.env_dev.vol = self.envelope.init_vol.read();
|
||||||
|
|
||||||
|
self.enabled = self.isDacEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
util.audio.length.ch4.update(self, fs, new);
|
||||||
|
self.cnt = new;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn onNoiseEvent(self: *Self, late: u64) void {
|
||||||
|
self.lfsr.onLfsrTimerExpire(self.poly, late);
|
||||||
|
|
||||||
|
self.sample = 0;
|
||||||
|
if (!self.isDacEnabled()) return;
|
||||||
|
self.sample = if (self.enabled) self.lfsr.sample() * @as(i8, self.env_dev.vol) else 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isDacEnabled(self: *const Self) bool {
|
||||||
|
return self.envelope.raw & 0xF8 != 0x00;
|
||||||
|
}
|
||||||
141
src/core/apu/Tone.zig
Normal file
141
src/core/apu/Tone.zig
Normal 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(value));
|
||||||
|
self.setNr22(@truncate(value >> 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR21
|
||||||
|
pub fn setNr21(self: *Self, value: u8) void {
|
||||||
|
self.duty.raw = value;
|
||||||
|
self.len_dev.timer = @as(u7, 64) - @as(u6, @truncate(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR22
|
||||||
|
pub fn setNr22(self: *Self, value: u8) void {
|
||||||
|
self.envelope.raw = value;
|
||||||
|
if (!self.isDacEnabled()) self.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR23, NR24
|
||||||
|
pub fn sound2CntH(self: *const Self) u16 {
|
||||||
|
return self.freq.raw & 0x4000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR23, NR24
|
||||||
|
pub fn setSound2CntH(self: *Self, fs: *const FrameSequencer, value: u16) void {
|
||||||
|
self.setNr23(@truncate(value));
|
||||||
|
self.setNr24(fs, @truncate(value >> 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR23
|
||||||
|
pub fn setNr23(self: *Self, byte: u8) void {
|
||||||
|
self.freq.raw = (self.freq.raw & 0xFF00) | byte;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR24
|
||||||
|
pub fn setNr24(self: *Self, fs: *const FrameSequencer, byte: u8) void {
|
||||||
|
var new: io.Frequency = .{ .raw = (@as(u16, byte) << 8) | (self.freq.raw & 0xFF) };
|
||||||
|
|
||||||
|
if (new.trigger.read()) {
|
||||||
|
self.enabled = true;
|
||||||
|
|
||||||
|
if (self.len_dev.timer == 0) {
|
||||||
|
self.len_dev.timer =
|
||||||
|
if (!fs.isLengthNext() and new.length_enable.read()) 63 else 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.square.reload(Self, self.freq.frequency.read());
|
||||||
|
|
||||||
|
// Reload Envelope period and timer
|
||||||
|
self.env_dev.timer = self.envelope.period.read();
|
||||||
|
if (fs.isEnvelopeNext() and self.env_dev.timer != 0b111) self.env_dev.timer += 1;
|
||||||
|
|
||||||
|
self.env_dev.vol = self.envelope.init_vol.read();
|
||||||
|
|
||||||
|
self.enabled = self.isDacEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
util.audio.length.update(Self, self, fs, new);
|
||||||
|
self.freq = new;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isDacEnabled(self: *const Self) bool {
|
||||||
|
return self.envelope.raw & 0xF8 != 0;
|
||||||
|
}
|
||||||
185
src/core/apu/ToneSweep.zig
Normal file
185
src/core/apu/ToneSweep.zig
Normal 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(value));
|
||||||
|
self.setSound1CntH(@truncate(value >> 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR10
|
||||||
|
pub fn sound1CntL(self: *const Self) u8 {
|
||||||
|
return self.sweep.raw & 0x7F;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR10
|
||||||
|
pub fn setSound1CntL(self: *Self, value: u8) void {
|
||||||
|
const new = io.Sweep{ .raw = value };
|
||||||
|
|
||||||
|
if (!new.direction.read()) {
|
||||||
|
// If at least one (1) sweep calculation has been made with
|
||||||
|
// the negate bit set (since last trigger), disable the channel
|
||||||
|
|
||||||
|
if (self.sweep_dev.calc_performed) self.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.sweep.raw = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR11, NR12
|
||||||
|
pub fn sound1CntH(self: *const Self) u16 {
|
||||||
|
return @as(u16, self.envelope.raw) << 8 | (self.duty.raw & 0xC0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR11, NR12
|
||||||
|
pub fn setSound1CntH(self: *Self, value: u16) void {
|
||||||
|
self.setNr11(@truncate(value));
|
||||||
|
self.setNr12(@truncate(value >> 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR11
|
||||||
|
pub fn setNr11(self: *Self, value: u8) void {
|
||||||
|
self.duty.raw = value;
|
||||||
|
self.len_dev.timer = @as(u7, 64) - @as(u6, @truncate(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR12
|
||||||
|
pub fn setNr12(self: *Self, value: u8) void {
|
||||||
|
self.envelope.raw = value;
|
||||||
|
if (!self.isDacEnabled()) self.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR13, NR14
|
||||||
|
pub fn sound1CntX(self: *const Self) u16 {
|
||||||
|
return self.freq.raw & 0x4000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR13, NR14
|
||||||
|
pub fn setSound1CntX(self: *Self, fs: *const FrameSequencer, value: u16) void {
|
||||||
|
self.setNr13(@truncate(value));
|
||||||
|
self.setNr14(fs, @truncate(value >> 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR13
|
||||||
|
pub fn setNr13(self: *Self, byte: u8) void {
|
||||||
|
self.freq.raw = (self.freq.raw & 0xFF00) | byte;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR14
|
||||||
|
pub fn setNr14(self: *Self, fs: *const FrameSequencer, byte: u8) void {
|
||||||
|
var new: io.Frequency = .{ .raw = (@as(u16, byte) << 8) | (self.freq.raw & 0xFF) };
|
||||||
|
|
||||||
|
if (new.trigger.read()) {
|
||||||
|
self.enabled = true;
|
||||||
|
|
||||||
|
if (self.len_dev.timer == 0) {
|
||||||
|
self.len_dev.timer =
|
||||||
|
if (!fs.isLengthNext() and new.length_enable.read()) 63 else 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.square.reload(Self, self.freq.frequency.read());
|
||||||
|
|
||||||
|
// Reload Envelope period and timer
|
||||||
|
self.env_dev.timer = self.envelope.period.read();
|
||||||
|
if (fs.isEnvelopeNext() and self.env_dev.timer != 0b111) self.env_dev.timer += 1;
|
||||||
|
|
||||||
|
self.env_dev.vol = self.envelope.init_vol.read();
|
||||||
|
|
||||||
|
// Sweep Trigger Behaviour
|
||||||
|
const sw_period = self.sweep.period.read();
|
||||||
|
const sw_shift = self.sweep.shift.read();
|
||||||
|
|
||||||
|
self.sweep_dev.calc_performed = false;
|
||||||
|
self.sweep_dev.shadow = self.freq.frequency.read();
|
||||||
|
self.sweep_dev.timer = if (sw_period == 0) 8 else sw_period;
|
||||||
|
self.sweep_dev.enabled = sw_period != 0 or sw_shift != 0;
|
||||||
|
if (sw_shift != 0) _ = self.sweep_dev.calculate(self.sweep, &self.enabled);
|
||||||
|
|
||||||
|
self.enabled = self.isDacEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
util.audio.length.update(Self, self, fs, new);
|
||||||
|
self.freq = new;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isDacEnabled(self: *const Self) bool {
|
||||||
|
return self.envelope.raw & 0xF8 != 0;
|
||||||
|
}
|
||||||
145
src/core/apu/Wave.zig
Normal file
145
src/core/apu/Wave.zig
Normal 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(value));
|
||||||
|
self.setSound3CntH(@truncate(value >> 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR30
|
||||||
|
pub fn setSound3CntL(self: *Self, value: u8) void {
|
||||||
|
self.select.raw = value;
|
||||||
|
if (!self.select.enabled.read()) self.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR30
|
||||||
|
pub fn sound3CntL(self: *const Self) u8 {
|
||||||
|
return self.select.raw & 0xE0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR31, NR32
|
||||||
|
pub fn sound3CntH(self: *const Self) u16 {
|
||||||
|
return @as(u16, self.length & 0xE0) << 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR31, NR32
|
||||||
|
pub fn setSound3CntH(self: *Self, value: u16) void {
|
||||||
|
self.setNr31(@truncate(value));
|
||||||
|
self.vol.raw = @truncate(value >> 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR31
|
||||||
|
pub fn setNr31(self: *Self, len: u8) void {
|
||||||
|
self.length = len;
|
||||||
|
self.len_dev.timer = 256 - @as(u9, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR33, NR34
|
||||||
|
pub fn setSound3CntX(self: *Self, fs: *const FrameSequencer, value: u16) void {
|
||||||
|
self.setNr33(@truncate(value));
|
||||||
|
self.setNr34(fs, @truncate(value >> 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR33, NR34
|
||||||
|
pub fn sound3CntX(self: *const Self) u16 {
|
||||||
|
return self.freq.raw & 0x4000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR33
|
||||||
|
pub fn setNr33(self: *Self, byte: u8) void {
|
||||||
|
self.freq.raw = (self.freq.raw & 0xFF00) | byte;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NR34
|
||||||
|
pub fn setNr34(self: *Self, fs: *const FrameSequencer, byte: u8) void {
|
||||||
|
var new: io.Frequency = .{ .raw = (@as(u16, byte) << 8) | (self.freq.raw & 0xFF) };
|
||||||
|
|
||||||
|
if (new.trigger.read()) {
|
||||||
|
self.enabled = true;
|
||||||
|
|
||||||
|
if (self.len_dev.timer == 0) {
|
||||||
|
self.len_dev.timer =
|
||||||
|
if (!fs.isLengthNext() and new.length_enable.read()) 255 else 256;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update The Frequency Timer
|
||||||
|
self.wave_dev.reload(self.freq.frequency.read());
|
||||||
|
self.wave_dev.offset = 0;
|
||||||
|
|
||||||
|
self.enabled = self.select.enabled.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
util.audio.length.update(Self, self, fs, new);
|
||||||
|
self.freq = new;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn onWaveEvent(self: *Self, late: u64) void {
|
||||||
|
self.wave_dev.onWaveTimerExpire(self.freq, self.select, late);
|
||||||
|
|
||||||
|
self.sample = 0;
|
||||||
|
if (!self.select.enabled.read()) return;
|
||||||
|
// Convert unsigned 4-bit wave sample to signed 8-bit sample
|
||||||
|
self.sample = (2 * @as(i8, self.wave_dev.sample(self.select)) - 15) >> self.wave_dev.shift(self.vol);
|
||||||
|
}
|
||||||
32
src/core/apu/device/Envelope.zig
Normal file
32
src/core/apu/device/Envelope.zig
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const io = @import("../../bus/io.zig");
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
/// Period Timer
|
||||||
|
timer: u3 = 0,
|
||||||
|
/// Current Volume
|
||||||
|
vol: u4 = 0,
|
||||||
|
|
||||||
|
pub fn create() Self {
|
||||||
|
return .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
self.* = .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(self: *Self, nrx2: io.Envelope) void {
|
||||||
|
if (nrx2.period.read() != 0) {
|
||||||
|
if (self.timer != 0) self.timer -= 1;
|
||||||
|
|
||||||
|
if (self.timer == 0) {
|
||||||
|
self.timer = nrx2.period.read();
|
||||||
|
|
||||||
|
if (nrx2.direction.read()) {
|
||||||
|
if (self.vol < 0xF) self.vol += 1;
|
||||||
|
} else {
|
||||||
|
if (self.vol > 0x0) self.vol -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/core/apu/device/Length.zig
Normal file
22
src/core/apu/device/Length.zig
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
timer: u9 = 0,
|
||||||
|
|
||||||
|
pub fn create() Self {
|
||||||
|
return .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
self.* = .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(self: *Self, enabled: bool, ch_enable: *bool) void {
|
||||||
|
if (enabled) {
|
||||||
|
if (self.timer == 0) return;
|
||||||
|
self.timer -= 1;
|
||||||
|
|
||||||
|
// By returning early if timer == 0, this is only
|
||||||
|
// true if timer == 0 because of the decrement we just did
|
||||||
|
if (self.timer == 0) ch_enable.* = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/core/apu/device/Sweep.zig
Normal file
53
src/core/apu/device/Sweep.zig
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
const io = @import("../../bus/io.zig");
|
||||||
|
const ToneSweep = @import("../ToneSweep.zig");
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
timer: u8 = 0,
|
||||||
|
enabled: bool = false,
|
||||||
|
shadow: u11 = 0,
|
||||||
|
|
||||||
|
calc_performed: bool = false,
|
||||||
|
|
||||||
|
pub fn create() Self {
|
||||||
|
return .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
self.* = .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(self: *Self, ch1: *ToneSweep) void {
|
||||||
|
if (self.timer != 0) self.timer -= 1;
|
||||||
|
|
||||||
|
if (self.timer == 0) {
|
||||||
|
const period = ch1.sweep.period.read();
|
||||||
|
self.timer = if (period == 0) 8 else period;
|
||||||
|
|
||||||
|
if (self.enabled and period != 0) {
|
||||||
|
const new_freq = self.calculate(ch1.sweep, &ch1.enabled);
|
||||||
|
|
||||||
|
if (new_freq <= 0x7FF and ch1.sweep.shift.read() != 0) {
|
||||||
|
ch1.freq.frequency.write(@as(u11, @truncate(new_freq)));
|
||||||
|
self.shadow = @truncate(new_freq);
|
||||||
|
|
||||||
|
_ = self.calculate(ch1.sweep, &ch1.enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the Sweep Frequency
|
||||||
|
pub fn calculate(self: *Self, sweep: io.Sweep, ch_enable: *bool) u12 {
|
||||||
|
const shadow = @as(u12, self.shadow);
|
||||||
|
const shadow_shifted = shadow >> sweep.shift.read();
|
||||||
|
const decrease = sweep.direction.read();
|
||||||
|
|
||||||
|
const freq = if (decrease) blk: {
|
||||||
|
self.calc_performed = true;
|
||||||
|
break :blk shadow - shadow_shifted;
|
||||||
|
} else shadow + shadow_shifted;
|
||||||
|
if (freq > 0x7FF) ch_enable.* = false;
|
||||||
|
|
||||||
|
return freq;
|
||||||
|
}
|
||||||
62
src/core/apu/signal/Lfsr.zig
Normal file
62
src/core/apu/signal/Lfsr.zig
Normal 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;
|
||||||
|
}
|
||||||
62
src/core/apu/signal/Square.zig
Normal file
62
src/core/apu/signal/Square.zig
Normal 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;
|
||||||
|
}
|
||||||
84
src/core/apu/signal/Wave.zig
Normal file
84
src/core/apu/signal/Wave.zig
Normal 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.readInt(T, self.buf[i..][0..@sizeOf(T)], .little);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(self: *Self, comptime T: type, nr30: io.WaveSelect, addr: u32, value: T) void {
|
||||||
|
// TODO: Handle writes when Channel 3 is disabled
|
||||||
|
const base = if (!nr30.bank.read()) @as(u32, 0x10) else 0; // Write to the Opposite Bank in Use
|
||||||
|
|
||||||
|
const i = base + addr - 0x0400_0090;
|
||||||
|
std.mem.writeInt(T, self.buf[i..][0..@sizeOf(T)], value, .little);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(sched: *Scheduler) Self {
|
||||||
|
return .{
|
||||||
|
.buf = [_]u8{0x00} ** buf_len,
|
||||||
|
.timer = 0,
|
||||||
|
.offset = 0,
|
||||||
|
.sched = sched,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
self.timer = 0;
|
||||||
|
self.offset = 0;
|
||||||
|
|
||||||
|
// sample buffer isn't reset because it's outside of the range of what NR52{7}'s effects
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reload internal Wave Timer
|
||||||
|
pub fn reload(self: *Self, value: u11) void {
|
||||||
|
self.sched.removeScheduledEvent(.{ .ApuChannel = 2 });
|
||||||
|
|
||||||
|
self.timer = (@as(u16, 2048) - value) * 2;
|
||||||
|
self.sched.push(.{ .ApuChannel = 2 }, @as(u64, self.timer) * interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scheduler Event Handler
|
||||||
|
pub fn onWaveTimerExpire(self: *Self, nrx34: io.Frequency, nr30: io.WaveSelect, late: u64) void {
|
||||||
|
if (nr30.dimension.read()) {
|
||||||
|
self.offset = (self.offset + 1) % 0x40; // 0x20 bytes (both banks), which contain 2 samples each
|
||||||
|
} else {
|
||||||
|
self.offset = (self.offset + 1) % 0x20; // 0x10 bytes, which contain 2 samples each
|
||||||
|
}
|
||||||
|
|
||||||
|
self.timer = (@as(u16, 2048) - nrx34.frequency.read()) * 2;
|
||||||
|
self.sched.push(.{ .ApuChannel = 2 }, @as(u64, self.timer) * interval -| late);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate Sample from Wave Synth
|
||||||
|
pub fn sample(self: *const Self, nr30: io.WaveSelect) u4 {
|
||||||
|
const base = if (nr30.bank.read()) @as(u32, 0x10) else 0;
|
||||||
|
|
||||||
|
const value = self.buf[base + self.offset / 2];
|
||||||
|
return if (self.offset & 1 == 0) @truncate(value >> 4) else @truncate(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TODO: Write comment
|
||||||
|
pub fn shift(_: *const Self, nr32: io.WaveVolume) u2 {
|
||||||
|
return switch (nr32.kind.read()) {
|
||||||
|
0b00 => 3, // Mute / Zero
|
||||||
|
0b01 => 0, // 100% Volume
|
||||||
|
0b10 => 1, // 50% Volume
|
||||||
|
0b11 => 2, // 25% Volume
|
||||||
|
};
|
||||||
|
}
|
||||||
92
src/core/bus/Bios.zig
Normal file
92
src/core/bus/Bios.zig
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const log = std.log.scoped(.Bios);
|
||||||
|
|
||||||
|
const rotr = @import("zba_util").rotr;
|
||||||
|
const forceAlign = @import("../Bus.zig").forceAlign;
|
||||||
|
|
||||||
|
/// Size of the BIOS in bytes
|
||||||
|
pub const size = 0x4000;
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
buf: ?[]u8,
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
addr_latch: u32 = 0,
|
||||||
|
|
||||||
|
// https://github.com/ITotalJustice/notorious_beeg/issues/106
|
||||||
|
pub fn read(self: *Self, comptime T: type, r15: u32, address: u32) T {
|
||||||
|
if (r15 < Self.size) {
|
||||||
|
const addr = forceAlign(T, address);
|
||||||
|
|
||||||
|
self.addr_latch = addr;
|
||||||
|
return self._read(T, addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn("Open Bus! Read from 0x{X:0>8}, but PC was 0x{X:0>8}", .{ address, r15 });
|
||||||
|
const value = self._read(u32, self.addr_latch);
|
||||||
|
|
||||||
|
return @truncate(rotr(u32, value, 8 * rotateBy(T, address)));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rotateBy(comptime T: type, address: u32) u32 {
|
||||||
|
return switch (T) {
|
||||||
|
u8 => address & 3,
|
||||||
|
u16 => address & 2,
|
||||||
|
u32 => 0,
|
||||||
|
else => @compileError("bios: unsupported read width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dbgRead(self: *const Self, comptime T: type, r15: u32, address: u32) T {
|
||||||
|
if (r15 < Self.size) return self._read(T, forceAlign(T, address));
|
||||||
|
|
||||||
|
const value = self._read(u32, self.addr_latch);
|
||||||
|
return @truncate(rotr(u32, value, 8 * rotateBy(T, address)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read without the GBA safety checks
|
||||||
|
fn _read(self: *const Self, comptime T: type, addr: u32) T {
|
||||||
|
const buf = self.buf orelse std.debug.panic("[BIOS] ZBA tried to read {} from 0x{X:0>8} but not BIOS was present", .{ T, addr });
|
||||||
|
|
||||||
|
return switch (T) {
|
||||||
|
u32, u16, u8 => std.mem.readInt(T, buf[addr..][0..@sizeOf(T)], .little),
|
||||||
|
else => @compileError("BIOS: Unsupported read width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(_: *Self, comptime T: type, addr: u32, value: T) void {
|
||||||
|
@branchHint(.cold);
|
||||||
|
log.debug("Tried to write {} 0x{X:} to 0x{X:0>8} ", .{ T, value, addr });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator, maybe_path: ?[]const u8) !Self {
|
||||||
|
if (maybe_path == null) return .{ .buf = null, .allocator = allocator };
|
||||||
|
const file_path = maybe_path.?;
|
||||||
|
|
||||||
|
const buf = try allocator.alloc(u8, Self.size);
|
||||||
|
errdefer allocator.free(buf);
|
||||||
|
|
||||||
|
var self: Self = .{ .buf = buf, .allocator = allocator };
|
||||||
|
try self.load(file_path);
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(self: *Self, file_path: []const u8) !void {
|
||||||
|
const file = try std.fs.cwd().openFile(file_path, .{});
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
const len = try file.readAll(self.buf orelse return error.UnallocatedBuffer);
|
||||||
|
if (len != Self.size) log.err("Expected BIOS to be {}B, was {}B", .{ Self.size, len });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
self.addr_latch = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
if (self.buf) |buf| self.allocator.free(buf);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
45
src/core/bus/Ewram.zig
Normal file
45
src/core/bus/Ewram.zig
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const ewram_size = 0x40000;
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
buf: []u8,
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
pub fn read(self: *const Self, comptime T: type, address: usize) T {
|
||||||
|
const addr = address & 0x3FFFF;
|
||||||
|
|
||||||
|
return switch (T) {
|
||||||
|
u32, u16, u8 => std.mem.readInt(T, self.buf[addr..][0..@sizeOf(T)], .little),
|
||||||
|
else => @compileError("EWRAM: Unsupported read width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(self: *const Self, comptime T: type, address: usize, value: T) void {
|
||||||
|
const addr = address & 0x3FFFF;
|
||||||
|
|
||||||
|
return switch (T) {
|
||||||
|
u32, u16, u8 => std.mem.writeInt(T, self.buf[addr..][0..@sizeOf(T)], value, .little),
|
||||||
|
else => @compileError("EWRAM: Unsupported write width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator) !Self {
|
||||||
|
const buf = try allocator.alloc(u8, ewram_size);
|
||||||
|
@memset(buf, 0);
|
||||||
|
|
||||||
|
return Self{
|
||||||
|
.buf = buf,
|
||||||
|
.allocator = allocator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
@memset(self.buf, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.allocator.free(self.buf);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
261
src/core/bus/GamePak.zig
Normal file
261
src/core/bus/GamePak.zig
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const config = @import("../../config.zig");
|
||||||
|
|
||||||
|
const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||||
|
const Backup = @import("backup.zig").Backup;
|
||||||
|
const Gpio = @import("gpio.zig").Gpio;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const log = std.log.scoped(.GamePak);
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
title: [12]u8,
|
||||||
|
buf: []u8,
|
||||||
|
allocator: Allocator,
|
||||||
|
backup: Backup,
|
||||||
|
gpio: *Gpio,
|
||||||
|
|
||||||
|
pub fn read(self: *Self, comptime T: type, address: u32) T {
|
||||||
|
const addr = address & 0x1FF_FFFF;
|
||||||
|
|
||||||
|
if (self.backup.kind == .Eeprom) {
|
||||||
|
if (self.buf.len > 0x100_0000) { // Large
|
||||||
|
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
|
||||||
|
// * Backup type is EEPROM
|
||||||
|
// * Large ROM (Size is greater than 16MB)
|
||||||
|
if (addr > 0x1FF_FEFF)
|
||||||
|
return self.backup.eeprom.read();
|
||||||
|
} else {
|
||||||
|
// Addresses 0x0D00_0000 to 0x0DFF_FFFF are reserved for EEPROM accesses if
|
||||||
|
// * Backup type is EEPROM
|
||||||
|
// * Small ROM (less than 16MB)
|
||||||
|
if (@as(u8, @truncate(address >> 24)) == 0x0D)
|
||||||
|
return self.backup.eeprom.read();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// TODO: Do I even need to implement these?
|
||||||
|
0x0800_00C4 => std.debug.panic("Handle 32-bit GPIO Data/Direction Reads", .{}),
|
||||||
|
0x0800_00C6 => std.debug.panic("Handle 32-bit GPIO Direction/Control Reads", .{}),
|
||||||
|
0x0800_00C8 => std.debug.panic("Handle 32-bit GPIO Control Reads", .{}),
|
||||||
|
else => {},
|
||||||
|
},
|
||||||
|
u16 => switch (address) {
|
||||||
|
// FIXME: What do 16-bit GPIO Reads look like?
|
||||||
|
0x0800_00C4 => return self.gpio.read(.Data),
|
||||||
|
0x0800_00C6 => return self.gpio.read(.Direction),
|
||||||
|
0x0800_00C8 => return self.gpio.read(.Control),
|
||||||
|
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)),
|
||||||
|
u8 => self.get(addr),
|
||||||
|
else => @compileError("GamePak: Unsupported read width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn get(self: *const Self, i: u32) u8 {
|
||||||
|
@setRuntimeSafety(false);
|
||||||
|
if (i < self.buf.len) return self.buf[i];
|
||||||
|
|
||||||
|
const lhs = i >> 1 & 0xFFFF;
|
||||||
|
return @truncate(lhs >> 8 * @as(u5, @truncate(i & 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
|
||||||
|
const addr = address & 0x1FF_FFFF;
|
||||||
|
|
||||||
|
if (self.backup.kind == .Eeprom) {
|
||||||
|
if (self.buf.len > 0x100_0000) { // Large
|
||||||
|
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
|
||||||
|
// * Backup type is EEPROM
|
||||||
|
// * Large ROM (Size is greater than 16MB)
|
||||||
|
if (addr > 0x1FF_FEFF)
|
||||||
|
return self.backup.eeprom.dbgRead();
|
||||||
|
} else {
|
||||||
|
// Addresses 0x0D00_0000 to 0x0DFF_FFFF are reserved for EEPROM accesses if
|
||||||
|
// * Backup type is EEPROM
|
||||||
|
// * Small ROM (less than 16MB)
|
||||||
|
if (@as(u8, @truncate(address >> 24)) == 0x0D)
|
||||||
|
return self.backup.eeprom.dbgRead();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.gpio.cnt == 1) {
|
||||||
|
// GPIO Can be read from
|
||||||
|
// We assume that this will only be true when a ROM actually does want something from GPIO
|
||||||
|
|
||||||
|
switch (T) {
|
||||||
|
u32 => switch (address) {
|
||||||
|
// FIXME: Do I even need to implement these?
|
||||||
|
0x0800_00C4 => std.debug.panic("Handle 32-bit GPIO Data/Direction Reads", .{}),
|
||||||
|
0x0800_00C6 => std.debug.panic("Handle 32-bit GPIO Direction/Control Reads", .{}),
|
||||||
|
0x0800_00C8 => std.debug.panic("Handle 32-bit GPIO Control Reads", .{}),
|
||||||
|
else => {},
|
||||||
|
},
|
||||||
|
u16 => switch (address) {
|
||||||
|
0x0800_00C4 => return self.gpio.read(.Data),
|
||||||
|
0x0800_00C6 => return self.gpio.read(.Direction),
|
||||||
|
0x0800_00C8 => return self.gpio.read(.Control),
|
||||||
|
else => {},
|
||||||
|
},
|
||||||
|
u8 => switch (address) {
|
||||||
|
0x0800_00C4 => return self.gpio.read(.Data),
|
||||||
|
0x0800_00C6 => return self.gpio.read(.Direction),
|
||||||
|
0x0800_00C8 => return self.gpio.read(.Control),
|
||||||
|
else => {},
|
||||||
|
},
|
||||||
|
else => @compileError("GamePak[GPIO]: Unsupported read width"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch (T) {
|
||||||
|
u32 => (@as(T, self.get(addr + 3)) << 24) | (@as(T, self.get(addr + 2)) << 16) | (@as(T, self.get(addr + 1)) << 8) | (@as(T, self.get(addr))),
|
||||||
|
u16 => (@as(T, self.get(addr + 1)) << 8) | @as(T, self.get(addr)),
|
||||||
|
u8 => self.get(addr),
|
||||||
|
else => @compileError("GamePak: Unsupported read width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value: T) void {
|
||||||
|
const addr = address & 0x1FF_FFFF;
|
||||||
|
|
||||||
|
if (self.backup.kind == .Eeprom) {
|
||||||
|
const bit: u1 = @truncate(value);
|
||||||
|
|
||||||
|
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)
|
||||||
|
if (addr > 0x1FF_FEFF)
|
||||||
|
return self.backup.eeprom.write(word_count, &self.backup.buf, bit);
|
||||||
|
} else {
|
||||||
|
// Addresses 0x0D00_0000 to 0x0DFF_FFFF are reserved for EEPROM accesses if
|
||||||
|
// * Backup type is EEPROM
|
||||||
|
// * Small ROM (less than 16MB)
|
||||||
|
if (@as(u8, @truncate(address >> 24)) == 0x0D)
|
||||||
|
return self.backup.eeprom.write(word_count, &self.backup.buf, bit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (T) {
|
||||||
|
u32 => switch (address) {
|
||||||
|
0x0800_00C4 => {
|
||||||
|
self.gpio.write(.Data, @as(u4, @truncate(value)));
|
||||||
|
self.gpio.write(.Direction, @as(u4, @truncate(value >> 16)));
|
||||||
|
},
|
||||||
|
0x0800_00C6 => {
|
||||||
|
self.gpio.write(.Direction, @as(u4, @truncate(value)));
|
||||||
|
self.gpio.write(.Control, @as(u1, @truncate(value >> 16)));
|
||||||
|
},
|
||||||
|
else => log.err("Wrote {} 0x{X:0>8} to 0x{X:0>8}, Unhandled", .{ T, value, address }),
|
||||||
|
},
|
||||||
|
u16 => switch (address) {
|
||||||
|
0x0800_00C4 => self.gpio.write(.Data, @as(u4, @truncate(value))),
|
||||||
|
0x0800_00C6 => self.gpio.write(.Direction, @as(u4, @truncate(value))),
|
||||||
|
0x0800_00C8 => self.gpio.write(.Control, @as(u1, @truncate(value))),
|
||||||
|
else => log.err("Wrote {} 0x{X:0>4} to 0x{X:0>8}, Unhandled", .{ T, value, address }),
|
||||||
|
},
|
||||||
|
u8 => log.debug("Wrote {} 0x{X:0>2} to 0x{X:0>8}, Ignored.", .{ T, value, address }),
|
||||||
|
else => @compileError("GamePak: Unsupported write width"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, maybe_rom: ?[]const u8, maybe_save: ?[]const u8) !Self {
|
||||||
|
const Device = Gpio.Device;
|
||||||
|
|
||||||
|
const items: struct { []u8, [12]u8, Backup.Kind, Device.Kind } = if (maybe_rom) |file_path| blk: {
|
||||||
|
const file = try std.fs.cwd().openFile(file_path, .{});
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
const buffer = try file.readToEndAlloc(allocator, try file.getEndPos());
|
||||||
|
const title = buffer[0xA0..0xAC];
|
||||||
|
logHeader(buffer, title);
|
||||||
|
|
||||||
|
const device_kind = if (config.config().guest.force_rtc) .Rtc else guessDevice(buffer);
|
||||||
|
|
||||||
|
break :blk .{ buffer, title.*, Backup.guess(buffer), device_kind };
|
||||||
|
} else .{ try allocator.alloc(u8, 0), [_]u8{0} ** 12, .None, .None };
|
||||||
|
|
||||||
|
const title = items[1];
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.buf = items[0],
|
||||||
|
.allocator = allocator,
|
||||||
|
.title = title,
|
||||||
|
.backup = try Backup.init(allocator, items[2], title, maybe_save),
|
||||||
|
.gpio = try Gpio.init(allocator, cpu, items[3]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.backup.deinit();
|
||||||
|
self.gpio.deinit(self.allocator);
|
||||||
|
self.allocator.destroy(self.gpio);
|
||||||
|
self.allocator.free(self.buf);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Searches the ROM to see if it can determine whether the ROM it's searching uses
|
||||||
|
/// any GPIO device, like a RTC for example.
|
||||||
|
fn guessDevice(buf: []const u8) Gpio.Device.Kind {
|
||||||
|
// Try to Guess if ROM uses RTC
|
||||||
|
const needle = "RTC_V"; // I was told SIIRTC_V, though Pokemen Firered (USA) is a false negative
|
||||||
|
|
||||||
|
// TODO: Use new for loop syntax?
|
||||||
|
var i: usize = 0;
|
||||||
|
while ((i + needle.len) < buf.len) : (i += 1) {
|
||||||
|
if (std.mem.eql(u8, needle, buf[i..(i + needle.len)])) return .Rtc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Detect other GPIO devices
|
||||||
|
return .None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn logHeader(buf: []const u8, title: *const [12]u8) void {
|
||||||
|
const version = buf[0xBC];
|
||||||
|
|
||||||
|
log.info("Title: {s}", .{title});
|
||||||
|
if (version != 0) log.info("Version: {}", .{version});
|
||||||
|
|
||||||
|
log.info("Game Code: {s}", .{buf[0xAC..0xB0]});
|
||||||
|
log.info("Maker Code: {s}", .{buf[0xB0..0xB2]});
|
||||||
|
}
|
||||||
|
|
||||||
|
test "OOB Access" {
|
||||||
|
const title = .{ 'H', 'E', 'L', 'L', 'O', ' ', 'W', 'O', 'R', 'L', 'D', '!' };
|
||||||
|
const alloc = std.testing.allocator;
|
||||||
|
const pak = Self{
|
||||||
|
.buf = &.{},
|
||||||
|
.alloc = alloc,
|
||||||
|
.title = title,
|
||||||
|
.backup = try Backup.init(alloc, .None, title, null),
|
||||||
|
};
|
||||||
|
|
||||||
|
std.debug.assert(pak.get(0) == 0x00); // 0x0000
|
||||||
|
std.debug.assert(pak.get(1) == 0x00);
|
||||||
|
|
||||||
|
std.debug.assert(pak.get(2) == 0x01); // 0x0001
|
||||||
|
std.debug.assert(pak.get(3) == 0x00);
|
||||||
|
|
||||||
|
std.debug.assert(pak.get(4) == 0x02); // 0x0002
|
||||||
|
std.debug.assert(pak.get(5) == 0x00);
|
||||||
|
}
|
||||||
45
src/core/bus/Iwram.zig
Normal file
45
src/core/bus/Iwram.zig
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const iwram_size = 0x8000;
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
buf: []u8,
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
pub fn read(self: *const Self, comptime T: type, address: usize) T {
|
||||||
|
const addr = address & 0x7FFF;
|
||||||
|
|
||||||
|
return switch (T) {
|
||||||
|
u32, u16, u8 => std.mem.readInt(T, self.buf[addr..][0..@sizeOf(T)], .little),
|
||||||
|
else => @compileError("IWRAM: Unsupported read width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(self: *const Self, comptime T: type, address: usize, value: T) void {
|
||||||
|
const addr = address & 0x7FFF;
|
||||||
|
|
||||||
|
return switch (T) {
|
||||||
|
u32, u16, u8 => std.mem.writeInt(T, self.buf[addr..][0..@sizeOf(T)], value, .little),
|
||||||
|
else => @compileError("IWRAM: Unsupported write width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator) !Self {
|
||||||
|
const buf = try allocator.alloc(u8, iwram_size);
|
||||||
|
@memset(buf, 0);
|
||||||
|
|
||||||
|
return Self{
|
||||||
|
.buf = buf,
|
||||||
|
.allocator = allocator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
@memset(self.buf, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.allocator.free(self.buf);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
219
src/core/bus/backup.zig
Normal file
219
src/core/bus/backup.zig
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const log = std.log.scoped(.Backup);
|
||||||
|
|
||||||
|
const Eeprom = @import("backup/eeprom.zig").Eeprom;
|
||||||
|
const Flash = @import("backup/Flash.zig");
|
||||||
|
|
||||||
|
const escape = @import("../../util.zig").escape;
|
||||||
|
|
||||||
|
const 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();
|
||||||
|
|
||||||
|
buf: []u8,
|
||||||
|
allocator: Allocator,
|
||||||
|
kind: Kind,
|
||||||
|
|
||||||
|
title: [12]u8,
|
||||||
|
save_path: ?[]const u8,
|
||||||
|
|
||||||
|
flash: Flash,
|
||||||
|
eeprom: Eeprom,
|
||||||
|
|
||||||
|
pub const Kind = enum {
|
||||||
|
Eeprom,
|
||||||
|
Sram,
|
||||||
|
Flash,
|
||||||
|
Flash1M,
|
||||||
|
None,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn read(self: *const Self, address: usize) u8 {
|
||||||
|
const addr = address & 0xFFFF;
|
||||||
|
|
||||||
|
switch (self.kind) {
|
||||||
|
.Flash => {
|
||||||
|
switch (addr) {
|
||||||
|
0x0000 => if (self.flash.id_mode) return 0x32, // Panasonic manufacturer ID
|
||||||
|
0x0001 => if (self.flash.id_mode) return 0x1B, // Panasonic device ID
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.flash.read(self.buf, addr);
|
||||||
|
},
|
||||||
|
.Flash1M => {
|
||||||
|
switch (addr) {
|
||||||
|
0x0000 => if (self.flash.id_mode) return 0x62, // Sanyo manufacturer ID
|
||||||
|
0x0001 => if (self.flash.id_mode) return 0x13, // Sanyo device ID
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.flash.read(self.buf, addr);
|
||||||
|
},
|
||||||
|
.Sram => return self.buf[addr & 0x7FFF], // 32K SRAM chip is mirrored
|
||||||
|
.None, .Eeprom => return 0xFF,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(self: *Self, address: usize, byte: u8) void {
|
||||||
|
const addr = address & 0xFFFF;
|
||||||
|
|
||||||
|
switch (self.kind) {
|
||||||
|
.Flash, .Flash1M => {
|
||||||
|
if (self.flash.prep_write) return self.flash.write(self.buf, addr, byte);
|
||||||
|
if (self.flash.shouldEraseSector(addr, byte)) return self.flash.erase(self.buf, addr);
|
||||||
|
|
||||||
|
switch (addr) {
|
||||||
|
0x0000 => if (self.kind == .Flash1M and self.flash.set_bank) {
|
||||||
|
self.flash.bank = @truncate(byte);
|
||||||
|
},
|
||||||
|
0x5555 => {
|
||||||
|
if (self.flash.state == .Command) {
|
||||||
|
self.flash.handleCommand(self.buf, byte);
|
||||||
|
} else if (byte == 0xAA and self.flash.state == .Ready) {
|
||||||
|
self.flash.state = .Set;
|
||||||
|
} else if (byte == 0xF0) {
|
||||||
|
self.flash.state = .Ready;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x2AAA => if (byte == 0x55 and self.flash.state == .Set) {
|
||||||
|
self.flash.state = .Command;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Sram => self.buf[addr & 0x7FFF] = byte,
|
||||||
|
.None, .Eeprom => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
@memset(buf, 0xFF);
|
||||||
|
|
||||||
|
var backup = Self{
|
||||||
|
.buf = buf,
|
||||||
|
.allocator = allocator,
|
||||||
|
.kind = kind,
|
||||||
|
.title = title,
|
||||||
|
.save_path = path,
|
||||||
|
.flash = Flash.create(),
|
||||||
|
.eeprom = Eeprom.create(allocator),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (backup.save_path) |p| backup.readSave(allocator, p) catch |e| log.err("Failed to load save: {}", .{e});
|
||||||
|
return backup;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// TODO: Use new for loop syntax?
|
||||||
|
var i: usize = 0;
|
||||||
|
while ((i + needle_len) < rom.len) : (i += 1) {
|
||||||
|
if (std.mem.eql(u8, needle.str, rom[i..][0..needle_len])) return needle.kind;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readSave(self: *Self, allocator: Allocator, path: []const u8) !void {
|
||||||
|
const file_path = try self.savePath(allocator, path);
|
||||||
|
defer allocator.free(file_path);
|
||||||
|
|
||||||
|
const expected = "untitled.sav";
|
||||||
|
if (std.mem.eql(u8, file_path[file_path.len - expected.len .. file_path.len], expected)) {
|
||||||
|
return log.err("ROM header lacks title, no save loaded", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
@memcpy(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);
|
||||||
|
@memcpy(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.Unsupported,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
.Sram, .Flash, .Flash1M, .Eeprom => {
|
||||||
|
const file = try std.fs.createFileAbsolute(file_path, .{});
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
try file.writeAll(self.buf);
|
||||||
|
log.info("Wrote Save to {s}", .{file_path});
|
||||||
|
},
|
||||||
|
else => return SaveError.Unsupported,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
72
src/core/bus/backup/Flash.zig
Normal file
72
src/core/bus/backup/Flash.zig
Normal 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 => {
|
||||||
|
@memset(buf, 0xFF);
|
||||||
|
self.prep_erase = false;
|
||||||
|
},
|
||||||
|
0xA0 => self.prep_write = true,
|
||||||
|
else => std.debug.panic("Unhandled Flash Command: 0x{X:0>2}", .{byte}),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state = .Ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shouldEraseSector(self: *const Self, addr: usize, byte: u8) bool {
|
||||||
|
return self.state == .Command and self.prep_erase and byte == 0x30 and addr & 0xFFF == 0x000;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn erase(self: *Self, buf: []u8, sector: usize) void {
|
||||||
|
const start = self.address() + (sector & 0xF000);
|
||||||
|
|
||||||
|
@memset(buf[start..][0..0x1000], 0xFF);
|
||||||
|
self.prep_erase = false;
|
||||||
|
self.state = .Ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Base Address
|
||||||
|
inline fn address(self: *const Self) usize {
|
||||||
|
return if (self.bank == 1) 0x10000 else @as(usize, 0);
|
||||||
|
}
|
||||||
269
src/core/bus/backup/eeprom.zig
Normal file
269
src/core/bus/backup/eeprom.zig
Normal 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});
|
||||||
|
};
|
||||||
|
|
||||||
|
// FIXME: ptr to a slice?
|
||||||
|
@memset(buf.*, 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.state == .RequestEnd) {
|
||||||
|
// if (bit != 0) log.debug("EEPROM Request did not end in 0u1. TODO: is this ok?", .{});
|
||||||
|
self.state = .Ready;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (self.state) {
|
||||||
|
.Ready => self.writer.requestWrite(bit),
|
||||||
|
.Read, .Write => self.writer.addressWrite(self.kind, bit),
|
||||||
|
.WriteTransfer => self.writer.dataWrite(bit),
|
||||||
|
.RequestEnd => unreachable, // We return early just above this block
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tick(buf.*);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(allocator: Allocator) Self {
|
||||||
|
return .{
|
||||||
|
.kind = .Unknown,
|
||||||
|
.state = .Ready,
|
||||||
|
.writer = Writer.create(),
|
||||||
|
.reader = Reader.create(),
|
||||||
|
.addr = 0,
|
||||||
|
.allocator = allocator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn guessKind(self: *const Self, word_count: u16) ?Kind {
|
||||||
|
if (self.kind != .Unknown or self.state != .Read) return null;
|
||||||
|
|
||||||
|
return switch (word_count) {
|
||||||
|
17 => .Large,
|
||||||
|
9 => .Small,
|
||||||
|
else => blk: {
|
||||||
|
log.err("Unexpected length of DMA3 Transfer upon initial EEPROM read: {}", .{word_count});
|
||||||
|
break :blk null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick(self: *Self, buf: []u8) void {
|
||||||
|
switch (self.state) {
|
||||||
|
.Ready => {
|
||||||
|
if (self.writer.len() == 2) {
|
||||||
|
const req: u2 = @intCast(self.writer.finish());
|
||||||
|
switch (req) {
|
||||||
|
0b11 => self.state = .Read,
|
||||||
|
0b10 => self.state = .Write,
|
||||||
|
else => log.err("Unknown EEPROM Request 0b{b:0>2}", .{req}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Read => {
|
||||||
|
switch (self.kind) {
|
||||||
|
.Large => {
|
||||||
|
if (self.writer.len() == 14) {
|
||||||
|
const addr: u10 = @intCast(self.writer.finish());
|
||||||
|
const value = std.mem.readInt(u64, buf[@as(u13, addr) * 8 ..][0..8], .little);
|
||||||
|
|
||||||
|
self.reader.configure(value);
|
||||||
|
self.state = .RequestEnd;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Small => {
|
||||||
|
if (self.writer.len() == 6) {
|
||||||
|
// FIXME: Duplicated code from above
|
||||||
|
const addr: u6 = @intCast(self.writer.finish());
|
||||||
|
const value = std.mem.readInt(u64, buf[@as(u13, addr) * 8 ..][0..8], .little);
|
||||||
|
|
||||||
|
self.reader.configure(value);
|
||||||
|
self.state = .RequestEnd;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => log.err("Unable to calculate EEPROM read address. EEPROM size UNKNOWN", .{}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Write => {
|
||||||
|
switch (self.kind) {
|
||||||
|
.Large => {
|
||||||
|
if (self.writer.len() == 14) {
|
||||||
|
self.addr = @as(u10, @intCast(self.writer.finish()));
|
||||||
|
self.state = .WriteTransfer;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Small => {
|
||||||
|
if (self.writer.len() == 6) {
|
||||||
|
self.addr = @as(u6, @intCast(self.writer.finish()));
|
||||||
|
self.state = .WriteTransfer;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => log.err("Unable to calculate EEPROM write address. EEPROM size UNKNOWN", .{}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.WriteTransfer => {
|
||||||
|
if (self.writer.len() == 64) {
|
||||||
|
std.mem.writeInt(u64, buf[self.addr * 8 ..][0..8], self.writer.finish(), .little);
|
||||||
|
self.state = .RequestEnd;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.RequestEnd => unreachable, // We return early in write() if state is .RequestEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Reader = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
data: u64,
|
||||||
|
i: u8,
|
||||||
|
enabled: bool,
|
||||||
|
|
||||||
|
fn create() Self {
|
||||||
|
return .{
|
||||||
|
.data = 0,
|
||||||
|
.i = 0,
|
||||||
|
.enabled = false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read(self: *Self) u1 {
|
||||||
|
if (!self.enabled) return 1;
|
||||||
|
|
||||||
|
const bit: u1 = if (self.i < 4) 0 else blk: {
|
||||||
|
const idx: u6 = @intCast(63 - (self.i - 4));
|
||||||
|
break :blk @truncate(self.data >> idx);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.i = (self.i + 1) % (64 + 4);
|
||||||
|
if (self.i == 0) self.enabled = false;
|
||||||
|
|
||||||
|
return bit;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dbgRead(self: *const Self) u1 {
|
||||||
|
if (!self.enabled) return 1;
|
||||||
|
|
||||||
|
const bit: u1 = if (self.i < 4) blk: {
|
||||||
|
break :blk 0;
|
||||||
|
} else blk: {
|
||||||
|
const idx: u6 = @intCast(63 - (self.i - 4));
|
||||||
|
break :blk @truncate(self.data >> idx);
|
||||||
|
};
|
||||||
|
|
||||||
|
return bit;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure(self: *Self, value: u64) void {
|
||||||
|
self.data = value;
|
||||||
|
self.i = 0;
|
||||||
|
self.enabled = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Writer = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
data: u64,
|
||||||
|
i: u8,
|
||||||
|
|
||||||
|
fn create() Self {
|
||||||
|
return .{ .data = 0, .i = 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn requestWrite(self: *Self, bit: u1) void {
|
||||||
|
const idx: u1 = @intCast(1 - self.i);
|
||||||
|
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn addressWrite(self: *Self, kind: Eeprom.Kind, bit: u1) void {
|
||||||
|
if (kind == .Unknown) return;
|
||||||
|
|
||||||
|
const size: u4 = switch (kind) {
|
||||||
|
.Large => 13,
|
||||||
|
.Small => 5,
|
||||||
|
.Unknown => unreachable,
|
||||||
|
};
|
||||||
|
|
||||||
|
const idx: u4 = @intCast(size - self.i);
|
||||||
|
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dataWrite(self: *Self, bit: u1) void {
|
||||||
|
const idx: u6 = @intCast(63 - self.i);
|
||||||
|
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn len(self: *const Self) u8 {
|
||||||
|
return self.i;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(self: *Self) u64 {
|
||||||
|
defer self.reset();
|
||||||
|
return self.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(self: *Self) void {
|
||||||
|
self.i = 0;
|
||||||
|
self.data = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
364
src/core/bus/dma.zig
Normal file
364
src/core/bus/dma.zig
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const util = @import("../../util.zig");
|
||||||
|
|
||||||
|
const DmaControl = @import("io.zig").DmaControl;
|
||||||
|
const Bus = @import("../Bus.zig");
|
||||||
|
const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||||
|
|
||||||
|
pub const DmaTuple = struct { DmaController(0), DmaController(1), DmaController(2), DmaController(3) };
|
||||||
|
const log = std.log.scoped(.DmaTransfer);
|
||||||
|
|
||||||
|
const getHalf = util.getHalf;
|
||||||
|
const setHalf = util.setHalf;
|
||||||
|
const setQuart = util.setQuart;
|
||||||
|
const handleInterrupt = @import("../cpu_util.zig").handleInterrupt;
|
||||||
|
|
||||||
|
const rotr = @import("zba_util").rotr;
|
||||||
|
|
||||||
|
pub fn create() DmaTuple {
|
||||||
|
return .{ DmaController(0).init(), DmaController(1).init(), DmaController(2).init(), DmaController(3).init() };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(comptime T: type, dma: *const DmaTuple, addr: u32) ?T {
|
||||||
|
const byte_addr: u8 = @truncate(addr);
|
||||||
|
|
||||||
|
return switch (T) {
|
||||||
|
u32 => switch (byte_addr) {
|
||||||
|
0xB0, 0xB4 => null, // DMA0SAD, DMA0DAD,
|
||||||
|
0xB8 => @as(T, dma.*[0].dmacntH()) << 16, // DMA0CNT_L is write-only
|
||||||
|
0xBC, 0xC0 => null, // DMA1SAD, DMA1DAD
|
||||||
|
0xC4 => @as(T, dma.*[1].dmacntH()) << 16, // DMA1CNT_L is write-only
|
||||||
|
0xC8, 0xCC => null, // DMA2SAD, DMA2DAD
|
||||||
|
0xD0 => @as(T, dma.*[2].dmacntH()) << 16, // DMA2CNT_L is write-only
|
||||||
|
0xD4, 0xD8 => null, // DMA3SAD, DMA3DAD
|
||||||
|
0xDC => @as(T, dma.*[3].dmacntH()) << 16, // DMA3CNT_L is write-only
|
||||||
|
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
||||||
|
},
|
||||||
|
u16 => switch (byte_addr) {
|
||||||
|
0xB0, 0xB2, 0xB4, 0xB6 => null, // DMA0SAD, DMA0DAD
|
||||||
|
0xB8 => 0x0000, // DMA0CNT_L, suite.gba expects 0x0000 instead of 0xDEAD
|
||||||
|
0xBA => dma.*[0].dmacntH(),
|
||||||
|
|
||||||
|
0xBC, 0xBE, 0xC0, 0xC2 => null, // DMA1SAD, DMA1DAD
|
||||||
|
0xC4 => 0x0000, // DMA1CNT_L
|
||||||
|
0xC6 => dma.*[1].dmacntH(),
|
||||||
|
|
||||||
|
0xC8, 0xCA, 0xCC, 0xCE => null, // DMA2SAD, DMA2DAD
|
||||||
|
0xD0 => 0x0000, // DMA2CNT_L
|
||||||
|
0xD2 => dma.*[2].dmacntH(),
|
||||||
|
|
||||||
|
0xD4, 0xD6, 0xD8, 0xDA => null, // DMA3SAD, DMA3DAD
|
||||||
|
0xDC => 0x0000, // DMA3CNT_L
|
||||||
|
0xDE => dma.*[3].dmacntH(),
|
||||||
|
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
||||||
|
},
|
||||||
|
u8 => switch (byte_addr) {
|
||||||
|
0xB0...0xB7 => null, // DMA0SAD, DMA0DAD
|
||||||
|
0xB8, 0xB9 => 0x00, // DMA0CNT_L
|
||||||
|
0xBA, 0xBB => @truncate(dma.*[0].dmacntH() >> getHalf(byte_addr)),
|
||||||
|
|
||||||
|
0xBC...0xC3 => null, // DMA1SAD, DMA1DAD
|
||||||
|
0xC4, 0xC5 => 0x00, // DMA1CNT_L
|
||||||
|
0xC6, 0xC7 => @truncate(dma.*[1].dmacntH() >> getHalf(byte_addr)),
|
||||||
|
|
||||||
|
0xC8...0xCF => null, // DMA2SAD, DMA2DAD
|
||||||
|
0xD0, 0xD1 => 0x00, // DMA2CNT_L
|
||||||
|
0xD2, 0xD3 => @truncate(dma.*[2].dmacntH() >> getHalf(byte_addr)),
|
||||||
|
|
||||||
|
0xD4...0xDB => null, // DMA3SAD, DMA3DAD
|
||||||
|
0xDC, 0xDD => 0x00, // DMA3CNT_L
|
||||||
|
0xDE, 0xDF => @truncate(dma.*[3].dmacntH() >> getHalf(byte_addr)),
|
||||||
|
else => util.io.read.err(T, log, "unexpected {} read from 0x{X:0>8}", .{ T, addr }),
|
||||||
|
},
|
||||||
|
else => @compileError("DMA: Unsupported read width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(comptime T: type, dma: *DmaTuple, addr: u32, value: T) void {
|
||||||
|
const byte_addr: u8 = @truncate(addr);
|
||||||
|
|
||||||
|
switch (T) {
|
||||||
|
u32 => switch (byte_addr) {
|
||||||
|
0xB0 => dma.*[0].setDmasad(value),
|
||||||
|
0xB4 => dma.*[0].setDmadad(value),
|
||||||
|
0xB8 => dma.*[0].setDmacnt(value),
|
||||||
|
|
||||||
|
0xBC => dma.*[1].setDmasad(value),
|
||||||
|
0xC0 => dma.*[1].setDmadad(value),
|
||||||
|
0xC4 => dma.*[1].setDmacnt(value),
|
||||||
|
|
||||||
|
0xC8 => dma.*[2].setDmasad(value),
|
||||||
|
0xCC => dma.*[2].setDmadad(value),
|
||||||
|
0xD0 => dma.*[2].setDmacnt(value),
|
||||||
|
|
||||||
|
0xD4 => dma.*[3].setDmasad(value),
|
||||||
|
0xD8 => dma.*[3].setDmadad(value),
|
||||||
|
0xDC => dma.*[3].setDmacnt(value),
|
||||||
|
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||||
|
},
|
||||||
|
u16 => switch (byte_addr) {
|
||||||
|
0xB0, 0xB2 => dma.*[0].setDmasad(setHalf(u32, dma.*[0].sad, byte_addr, value)),
|
||||||
|
0xB4, 0xB6 => dma.*[0].setDmadad(setHalf(u32, dma.*[0].dad, byte_addr, value)),
|
||||||
|
0xB8 => dma.*[0].setDmacntL(value),
|
||||||
|
0xBA => dma.*[0].setDmacntH(value),
|
||||||
|
|
||||||
|
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, 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, 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 => 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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Function that creates a DMAController. Determines unique DMA Controller behaiour at compile-time
|
||||||
|
fn DmaController(comptime id: u2) type {
|
||||||
|
return struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
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
|
||||||
|
sad: u32,
|
||||||
|
/// Write-only. The final address in a DMA transffer. (DMADAD)
|
||||||
|
/// Note: Use writeDst instead of manipulatig dst_addr directly
|
||||||
|
dad: u32,
|
||||||
|
/// Write-only. The Word Count for the DMA Transfer (DMACNT_L)
|
||||||
|
word_count: WordCount,
|
||||||
|
/// Read / Write. DMACNT_H
|
||||||
|
/// Note: Use writeControl instead of manipulating cnt directly.
|
||||||
|
cnt: DmaControl,
|
||||||
|
|
||||||
|
/// Internal. The last successfully read value
|
||||||
|
data_latch: u32,
|
||||||
|
/// Internal. Currrent Source Address
|
||||||
|
sad_latch: u32,
|
||||||
|
/// Internal. Current Destination Address
|
||||||
|
dad_latch: u32,
|
||||||
|
/// Internal. Word Count
|
||||||
|
_word_count: WordCount,
|
||||||
|
|
||||||
|
/// Some DMA Transfers are enabled during Hblank / VBlank and / or
|
||||||
|
/// have delays. Thefore bit 15 of DMACNT isn't actually something
|
||||||
|
/// we can use to control when we do or do not execute a step in a DMA Transfer
|
||||||
|
in_progress: bool,
|
||||||
|
|
||||||
|
pub fn init() Self {
|
||||||
|
return .{
|
||||||
|
.sad = 0,
|
||||||
|
.dad = 0,
|
||||||
|
.word_count = 0,
|
||||||
|
.cnt = .{ .raw = 0x000 },
|
||||||
|
|
||||||
|
// Internals
|
||||||
|
.sad_latch = 0,
|
||||||
|
.dad_latch = 0,
|
||||||
|
.data_latch = 0,
|
||||||
|
|
||||||
|
._word_count = 0,
|
||||||
|
.in_progress = false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
self.* = Self.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setDmasad(self: *Self, addr: u32) void {
|
||||||
|
self.sad = addr & sad_mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setDmadad(self: *Self, addr: u32) void {
|
||||||
|
self.dad = addr & dad_mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setDmacntL(self: *Self, halfword: u16) void {
|
||||||
|
self.word_count = @truncate(halfword);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn 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_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;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cnt.raw = halfword;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setDmacnt(self: *Self, word: u32) void {
|
||||||
|
self.setDmacntL(@truncate(word));
|
||||||
|
self.setDmacntH(@truncate(word >> 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step(self: *Self, cpu: *Arm7tdmi) void {
|
||||||
|
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
|
||||||
|
|
||||||
|
const is_fifo = (id == 1 or id == 2) and self.cnt.start_timing.read() == 0b11;
|
||||||
|
const sad_adj: Adjustment = @enumFromInt(self.cnt.sad_adj.read());
|
||||||
|
const dad_adj: Adjustment = if (is_fifo) .Fixed else @enumFromInt(self.cnt.dad_adj.read());
|
||||||
|
|
||||||
|
const transfer_type = is_fifo or self.cnt.transfer_type.read();
|
||||||
|
const offset: u32 = if (transfer_type) @sizeOf(u32) else @sizeOf(u16);
|
||||||
|
|
||||||
|
const mask = if (transfer_type) ~@as(u32, 3) else ~@as(u32, 1);
|
||||||
|
const sad_addr = self.sad_latch & mask;
|
||||||
|
const dad_addr = self.dad_latch & mask;
|
||||||
|
|
||||||
|
if (transfer_type) {
|
||||||
|
if (sad_addr >= 0x0200_0000) self.data_latch = cpu.bus.read(u32, sad_addr);
|
||||||
|
cpu.bus.write(u32, dad_addr, self.data_latch);
|
||||||
|
} else {
|
||||||
|
if (sad_addr >= 0x0200_0000) {
|
||||||
|
const value: u32 = cpu.bus.read(u16, sad_addr);
|
||||||
|
self.data_latch = value << 16 | value;
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu.bus.write(u16, dad_addr, @as(u16, @truncate(rotr(u32, self.data_latch, 8 * (dad_addr & 3)))));
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (@as(u8, @truncate(sad_addr >> 24))) {
|
||||||
|
// according to fleroviux, DMAs with a source address in ROM misbehave
|
||||||
|
// the resultant behaviour is that the source address will increment despite what DMAXCNT says
|
||||||
|
0x08...0x0D => self.sad_latch +%= offset, // obscure behaviour
|
||||||
|
else => switch (sad_adj) {
|
||||||
|
.Increment => self.sad_latch +%= offset,
|
||||||
|
.Decrement => self.sad_latch -%= offset,
|
||||||
|
.IncrementReload => log.err("{} is a prohibited adjustment on SAD", .{sad_adj}),
|
||||||
|
.Fixed => {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (dad_adj) {
|
||||||
|
.Increment, .IncrementReload => self.dad_latch +%= offset,
|
||||||
|
.Decrement => self.dad_latch -%= offset,
|
||||||
|
.Fixed => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
self._word_count -= 1;
|
||||||
|
|
||||||
|
if (self._word_count == 0) {
|
||||||
|
if (self.cnt.irq.read()) {
|
||||||
|
switch (id) {
|
||||||
|
0 => bus_ptr.io.irq.dma0.write(true),
|
||||||
|
1 => bus_ptr.io.irq.dma1.write(true),
|
||||||
|
2 => bus_ptr.io.irq.dma2.write(true),
|
||||||
|
3 => bus_ptr.io.irq.dma3.write(true),
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInterrupt(cpu);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're not repeating, Fire the IRQs and disable the DMA
|
||||||
|
if (!self.cnt.repeat.read()) self.cnt.enabled.write(false);
|
||||||
|
|
||||||
|
// We want to disable our internal enabled flag regardless of repeat
|
||||||
|
// because we only want to step A DMA that repeats during it's specific
|
||||||
|
// timing window
|
||||||
|
self.in_progress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
// Determined by the repeat bit and whether the DMA is in the right start_timing
|
||||||
|
switch (kind) {
|
||||||
|
.VBlank => self.in_progress = self.cnt.enabled.read() and self.cnt.start_timing.read() == 0b01,
|
||||||
|
.HBlank => self.in_progress = self.cnt.enabled.read() and self.cnt.start_timing.read() == 0b10,
|
||||||
|
.Immediate, .Special => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we determined that the repeat bit is set (and now the Hblank / Vblank DMA is now in progress)
|
||||||
|
// Reload internal word count latch
|
||||||
|
// Reload internal DAD latch if we are in IncrementRelaod
|
||||||
|
if (self.in_progress) {
|
||||||
|
self._word_count = if (self.word_count == 0) std.math.maxInt(@TypeOf(self._word_count)) else self.word_count;
|
||||||
|
if (@as(Adjustment, @enumFromInt(self.cnt.dad_adj.read())) == .IncrementReload) self.dad_latch = self.dad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// DMA May not be configured for handling DMAs
|
||||||
|
if (self.cnt.start_timing.read() != 0b11) return;
|
||||||
|
|
||||||
|
// We Assume the Repeat Bit is Set
|
||||||
|
// We Assume that DAD is set to 0x0400_00A0 or 0x0400_00A4 (fifo_addr)
|
||||||
|
// We Assume DMACNT_L is set to 4
|
||||||
|
|
||||||
|
// FIXME: Safe to just assume whatever DAD is set to is the FIFO Address?
|
||||||
|
// self.dad_latch = fifo_addr;
|
||||||
|
self.cnt.repeat.write(true);
|
||||||
|
self._word_count = 4;
|
||||||
|
self.in_progress = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn onBlanking(bus: *Bus, comptime kind: DmaKind) void {
|
||||||
|
inline for (0..4) |i| bus.dma[i].poll(kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Adjustment = enum(u2) {
|
||||||
|
Increment = 0,
|
||||||
|
Decrement = 1,
|
||||||
|
Fixed = 2,
|
||||||
|
IncrementReload = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DmaKind = enum(u2) {
|
||||||
|
Immediate = 0,
|
||||||
|
HBlank,
|
||||||
|
VBlank,
|
||||||
|
Special,
|
||||||
|
};
|
||||||
464
src/core/bus/gpio.zig
Normal file
464
src/core/bus/gpio.zig
Normal file
@@ -0,0 +1,464 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Bit = @import("bitjuggle").Boolean;
|
||||||
|
const DateTime = @import("datetime").datetime.Datetime;
|
||||||
|
|
||||||
|
const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||||
|
const Bus = @import("../Bus.zig");
|
||||||
|
const Scheduler = @import("../scheduler.zig").Scheduler;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const handleInterrupt = @import("../cpu_util.zig").handleInterrupt;
|
||||||
|
|
||||||
|
/// GPIO Register Implementation
|
||||||
|
pub const Gpio = struct {
|
||||||
|
const Self = @This();
|
||||||
|
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: *Clock = @ptrCast(@alignCast(self.ptr.?));
|
||||||
|
break :blk clock.step(.{ .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(@as(*Clock, @ptrCast(@alignCast(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: u3 = @intCast(self.i);
|
||||||
|
defer self.i += 1;
|
||||||
|
|
||||||
|
// FIXME: What do I do about the unused bits?
|
||||||
|
return switch (register) {
|
||||||
|
.Control => @truncate(switch (self.count) {
|
||||||
|
0 => clock.cnt.raw >> idx,
|
||||||
|
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 1 byte)", .{ self.count, register }),
|
||||||
|
}),
|
||||||
|
.DateTime => @truncate(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(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: u3 = @intCast(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
|
||||||
|
};
|
||||||
|
|
||||||
|
const sched_ptr: *Scheduler = @ptrCast(@alignCast(cpu.sched.ptr));
|
||||||
|
sched_ptr.push(.RealTimeClock, 1 << 24); // Every Second
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn onClockUpdate(self: *Self, late: u64) void {
|
||||||
|
const sched_ptr: *Scheduler = @ptrCast(@alignCast(self.cpu.sched.ptr));
|
||||||
|
sched_ptr.push(.RealTimeClock, (1 << 24) -| late); // Reschedule
|
||||||
|
|
||||||
|
const now = DateTime.now();
|
||||||
|
self.year = bcd(@intCast(now.date.year - 2000));
|
||||||
|
self.month = @truncate(bcd(now.date.month));
|
||||||
|
self.day = @truncate(bcd(now.date.day));
|
||||||
|
self.weekday = @truncate(bcd((now.date.weekday() + 1) % 7)); // API is Monday = 0, Sunday = 6. We want Sunday = 0, Saturday = 6
|
||||||
|
self.hour = @truncate(bcd(now.time.hour));
|
||||||
|
self.minute = @truncate(bcd(now.time.minute));
|
||||||
|
self.second = @truncate(bcd(now.time.second));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step(self: *Self, value: Data) u4 {
|
||||||
|
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(value.raw);
|
||||||
|
},
|
||||||
|
.Command => blk: {
|
||||||
|
if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state});
|
||||||
|
|
||||||
|
// If SCK rises, sample SIO
|
||||||
|
if (!cache.sck.read() and value.sck.read()) {
|
||||||
|
self.writer.push(@intFromBool(value.sio.read()));
|
||||||
|
|
||||||
|
if (self.writer.finished()) {
|
||||||
|
self.state = self.processCommand(self.writer.buf);
|
||||||
|
self.writer.reset();
|
||||||
|
|
||||||
|
log.debug("Switching to {}", .{self.state});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk @truncate(value.raw);
|
||||||
|
},
|
||||||
|
.Write => |register| blk: {
|
||||||
|
if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state});
|
||||||
|
|
||||||
|
// If SCK rises, sample SIO
|
||||||
|
if (!cache.sck.read() and value.sck.read()) {
|
||||||
|
self.writer.push(@intFromBool(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(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(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 {
|
||||||
|
const bus_ptr: *Bus = @ptrCast(@alignCast(self.cpu.bus.ptr));
|
||||||
|
|
||||||
|
// TODO: Confirm that this is the right behaviour
|
||||||
|
log.debug("Force GamePak IRQ", .{});
|
||||||
|
|
||||||
|
bus_ptr.io.irq.game_pak.write(true);
|
||||||
|
handleInterrupt(self.cpu);
|
||||||
|
}
|
||||||
|
|
||||||
|
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: u3 = @truncate(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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Converts an 8-bit unsigned integer to its BCD representation.
|
||||||
|
/// Note: Algorithm only works for values between 0 and 99 inclusive.
|
||||||
|
fn bcd(value: u8) u8 {
|
||||||
|
return ((value / 10) << 4) + (value % 10);
|
||||||
|
}
|
||||||
680
src/core/bus/io.zig
Normal file
680
src/core/bus/io.zig
Normal file
@@ -0,0 +1,680 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const timer = @import("timer.zig");
|
||||||
|
const dma = @import("dma.zig");
|
||||||
|
const apu = @import("../apu.zig");
|
||||||
|
const ppu = @import("../ppu.zig");
|
||||||
|
const util = @import("../../util.zig");
|
||||||
|
|
||||||
|
const Bit = @import("bitjuggle").Boolean;
|
||||||
|
const Bitfield = @import("bitjuggle").Bitfield;
|
||||||
|
const Bus = @import("../Bus.zig");
|
||||||
|
|
||||||
|
const getHalf = util.getHalf;
|
||||||
|
const setHalf = util.setHalf;
|
||||||
|
|
||||||
|
const log = std.log.scoped(.@"I/O");
|
||||||
|
|
||||||
|
pub const Io = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
/// Read / Write
|
||||||
|
ime: bool,
|
||||||
|
ie: InterruptEnable,
|
||||||
|
irq: InterruptRequest,
|
||||||
|
postflg: PostFlag,
|
||||||
|
waitcnt: WaitControl,
|
||||||
|
haltcnt: HaltControl,
|
||||||
|
keyinput: AtomicKeyInput,
|
||||||
|
|
||||||
|
pub fn init() Self {
|
||||||
|
return .{
|
||||||
|
.ime = false,
|
||||||
|
.ie = .{ .raw = 0x0000 },
|
||||||
|
.irq = .{ .raw = 0x0000 },
|
||||||
|
.keyinput = AtomicKeyInput.init(.{ .raw = 0x03FF }),
|
||||||
|
.waitcnt = .{ .raw = 0x0000_0000 }, // Bit 15 == 0 for GBA
|
||||||
|
.postflg = .FirstBoot,
|
||||||
|
.haltcnt = .Execute,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
self.* = Self.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setIrqs(self: *Io, word: u32) void {
|
||||||
|
self.ie.raw = @truncate(word);
|
||||||
|
self.irq.raw &= ~@as(u16, @truncate(word >> 16));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn read(bus: *const Bus, comptime T: type, address: u32) ?T {
|
||||||
|
return switch (T) {
|
||||||
|
u32 => switch (address) {
|
||||||
|
// Display
|
||||||
|
0x0400_0000...0x0400_0054 => ppu.read(T, &bus.ppu, address),
|
||||||
|
|
||||||
|
// Sound
|
||||||
|
0x0400_0060...0x0400_00A4 => apu.read(T, &bus.apu, address),
|
||||||
|
|
||||||
|
// DMA Transfers
|
||||||
|
0x0400_00B0...0x0400_00DC => dma.read(T, &bus.dma, address),
|
||||||
|
|
||||||
|
// Timers
|
||||||
|
0x0400_0100...0x0400_010C => timer.read(T, &bus.tim, address),
|
||||||
|
|
||||||
|
// Serial Communication 1
|
||||||
|
0x0400_0128 => util.io.read.todo(log, "Read {} from SIOCNT and SIOMLT_SEND", .{T}),
|
||||||
|
|
||||||
|
// Keypad Input
|
||||||
|
0x0400_0130 => util.io.read.todo(log, "Read {} from KEYINPUT", .{T}),
|
||||||
|
|
||||||
|
// Serial Communication 2
|
||||||
|
0x0400_0150 => util.io.read.todo(log, "Read {} from JOY_RECV", .{T}),
|
||||||
|
|
||||||
|
// Interrupts
|
||||||
|
0x0400_0200 => @as(u32, bus.io.irq.raw) << 16 | bus.io.ie.raw,
|
||||||
|
0x0400_0204 => bus.io.waitcnt.raw,
|
||||||
|
0x0400_0208 => @intFromBool(bus.io.ime),
|
||||||
|
0x0400_0300 => @intFromEnum(bus.io.postflg),
|
||||||
|
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
|
||||||
|
},
|
||||||
|
u16 => switch (address) {
|
||||||
|
// Display
|
||||||
|
0x0400_0000...0x0400_0054 => ppu.read(T, &bus.ppu, address),
|
||||||
|
|
||||||
|
// Sound
|
||||||
|
0x0400_0060...0x0400_00A6 => apu.read(T, &bus.apu, address),
|
||||||
|
|
||||||
|
// DMA Transfers
|
||||||
|
0x0400_00B0...0x0400_00DE => dma.read(T, &bus.dma, address),
|
||||||
|
|
||||||
|
// Timers
|
||||||
|
0x0400_0100...0x0400_010E => timer.read(T, &bus.tim, address),
|
||||||
|
|
||||||
|
// Serial Communication 1
|
||||||
|
0x0400_0128 => util.io.read.todo(log, "Read {} from SIOCNT", .{T}),
|
||||||
|
|
||||||
|
// Keypad Input
|
||||||
|
0x0400_0130 => bus.io.keyinput.load(.monotonic),
|
||||||
|
|
||||||
|
// Serial Communication 2
|
||||||
|
0x0400_0134 => util.io.read.todo(log, "Read {} from RCNT", .{T}),
|
||||||
|
0x0400_0136 => 0x0000,
|
||||||
|
0x0400_0142 => 0x0000,
|
||||||
|
0x0400_015A => 0x0000,
|
||||||
|
|
||||||
|
// Interrupts
|
||||||
|
0x0400_0200 => bus.io.ie.raw,
|
||||||
|
0x0400_0202 => bus.io.irq.raw,
|
||||||
|
0x0400_0204 => bus.io.waitcnt.raw,
|
||||||
|
0x0400_0206 => 0x0000,
|
||||||
|
0x0400_0208 => @intFromBool(bus.io.ime),
|
||||||
|
0x0400_020A => 0x0000,
|
||||||
|
0x0400_0300 => @intFromEnum(bus.io.postflg),
|
||||||
|
0x0400_0302 => 0x0000,
|
||||||
|
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
|
||||||
|
},
|
||||||
|
u8 => return switch (address) {
|
||||||
|
// Display
|
||||||
|
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}),
|
||||||
|
|
||||||
|
// Keypad Input
|
||||||
|
0x0400_0130 => util.io.read.todo(log, "read {} from KEYINPUT_L", .{T}),
|
||||||
|
|
||||||
|
// Serial Communication 2
|
||||||
|
0x0400_0135 => util.io.read.todo(log, "Read {} from RCNT_H", .{T}),
|
||||||
|
0x0400_0136, 0x0400_0137 => 0x00,
|
||||||
|
0x0400_0142, 0x0400_0143 => 0x00,
|
||||||
|
0x0400_015A, 0x0400_015B => 0x00,
|
||||||
|
|
||||||
|
// Interrupts
|
||||||
|
0x0400_0200, 0x0400_0201 => @truncate(bus.io.ie.raw >> getHalf(@truncate(address))),
|
||||||
|
0x0400_0202, 0x0400_0203 => @truncate(bus.io.irq.raw >> getHalf(@truncate(address))),
|
||||||
|
0x0400_0204, 0x0400_0205 => @truncate(bus.io.waitcnt.raw >> getHalf(@truncate(address))),
|
||||||
|
0x0400_0206, 0x0400_0207 => 0x00,
|
||||||
|
0x0400_0208, 0x0400_0209 => @truncate(@as(u16, @intFromBool(bus.io.ime)) >> getHalf(@truncate(address))),
|
||||||
|
0x0400_020A, 0x0400_020B => 0x00,
|
||||||
|
0x0400_0300 => @intFromEnum(bus.io.postflg),
|
||||||
|
0x0400_0301 => null,
|
||||||
|
0x0400_0302, 0x0400_0303 => 0x00,
|
||||||
|
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"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
|
||||||
|
return switch (T) {
|
||||||
|
u32 => switch (address) {
|
||||||
|
// Display
|
||||||
|
0x0400_0000...0x0400_0054 => ppu.write(T, &bus.ppu, address, value),
|
||||||
|
0x0400_0058...0x0400_005C => {}, // Unused
|
||||||
|
|
||||||
|
// Sound
|
||||||
|
0x0400_0060...0x0400_00A4 => apu.write(T, &bus.apu, address, value),
|
||||||
|
0x0400_00A8, 0x0400_00AC => {}, // Unused
|
||||||
|
|
||||||
|
// DMA Transfers
|
||||||
|
0x0400_00B0...0x0400_00DC => dma.write(T, &bus.dma, address, value),
|
||||||
|
0x0400_00E0...0x0400_00FC => {}, // Unused
|
||||||
|
|
||||||
|
// Timers
|
||||||
|
0x0400_0100...0x0400_010C => timer.write(T, &bus.tim, address, value),
|
||||||
|
0x0400_0110...0x0400_011C => {}, // Unused
|
||||||
|
|
||||||
|
// Serial Communication 1
|
||||||
|
0x0400_0120 => log.debug("Wrote 0x{X:0>8} to SIODATA32/(SIOMULTI0 and SIOMULTI1)", .{value}),
|
||||||
|
0x0400_0124 => log.debug("Wrote 0x{X:0>8} to SIOMULTI2 and SIOMULTI3", .{value}),
|
||||||
|
0x0400_0128 => log.debug("Wrote 0x{X:0>8} to SIOCNT and SIOMLT_SEND/SIODATA8", .{value}),
|
||||||
|
0x0400_012C => {}, // Unused
|
||||||
|
|
||||||
|
// Keypad Input
|
||||||
|
0x0400_0130 => log.debug("Wrote 0x{X:0>8} to KEYINPUT and KEYCNT", .{value}),
|
||||||
|
0x0400_0134 => log.debug("Wrote 0x{X:0>8} to RCNT and IR", .{value}),
|
||||||
|
0x0400_0138, 0x0400_013C => {}, // Unused
|
||||||
|
|
||||||
|
// Serial Communication 2
|
||||||
|
0x0400_0140 => log.debug("Wrote 0x{X:0>8} to JOYCNT", .{value}),
|
||||||
|
0x0400_0150 => log.debug("Wrote 0x{X:0>8} to JOY_RECV", .{value}),
|
||||||
|
0x0400_0154 => log.debug("Wrote 0x{X:0>8} to JOY_TRANS", .{value}),
|
||||||
|
0x0400_0158 => log.debug("Wrote 0x{X:0>8} to JOYSTAT (?)", .{value}),
|
||||||
|
0x0400_0144...0x0400_014C, 0x0400_015C => {}, // Unused
|
||||||
|
0x0400_0160...0x0400_01FC => {},
|
||||||
|
|
||||||
|
// Interrupts
|
||||||
|
0x0400_0200 => bus.io.setIrqs(value),
|
||||||
|
0x0400_0204 => bus.io.waitcnt.set(@truncate(value)),
|
||||||
|
0x0400_0208 => bus.io.ime = value & 1 == 1,
|
||||||
|
0x0400_0300 => {
|
||||||
|
bus.io.postflg = @enumFromInt(value & 1);
|
||||||
|
bus.io.haltcnt = if (value >> 15 & 1 == 0) .Halt else @panic("TODO: Implement STOP");
|
||||||
|
},
|
||||||
|
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, address }),
|
||||||
|
},
|
||||||
|
u16 => switch (address) {
|
||||||
|
// Display
|
||||||
|
0x0400_0000...0x0400_0054 => ppu.write(T, &bus.ppu, address, value),
|
||||||
|
0x0400_0056 => {}, // Not used
|
||||||
|
|
||||||
|
// Sound
|
||||||
|
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 => {},
|
||||||
|
0x0400_0110 => {}, // Not Used,
|
||||||
|
|
||||||
|
// Serial Communication 1
|
||||||
|
0x0400_0120 => log.debug("Wrote 0x{X:0>4} to SIOMULTI0", .{value}),
|
||||||
|
0x0400_0122 => log.debug("Wrote 0x{X:0>4} to SIOMULTI1", .{value}),
|
||||||
|
0x0400_0124 => log.debug("Wrote 0x{X:0>4} to SIOMULTI2", .{value}),
|
||||||
|
0x0400_0126 => log.debug("Wrote 0x{X:0>4} to SIOMULTI3", .{value}),
|
||||||
|
0x0400_0128 => log.debug("Wrote 0x{X:0>4} to SIOCNT", .{value}),
|
||||||
|
0x0400_012A => log.debug("Wrote 0x{X:0>4} to SIOMLT_SEND", .{value}),
|
||||||
|
|
||||||
|
// Keypad Input
|
||||||
|
0x0400_0130 => log.debug("Wrote 0x{X:0>4} to KEYINPUT. Ignored", .{value}),
|
||||||
|
0x0400_0132 => log.debug("Wrote 0x{X:0>4} to KEYCNT", .{value}),
|
||||||
|
|
||||||
|
// Serial Communication 2
|
||||||
|
0x0400_0134 => log.debug("Wrote 0x{X:0>4} to RCNT", .{value}),
|
||||||
|
0x0400_0140 => log.debug("Wrote 0x{X:0>4} to JOYCNT", .{value}),
|
||||||
|
0x0400_0158 => log.debug("Wrote 0x{X:0>4} to JOYSTAT", .{value}),
|
||||||
|
0x0400_0142, 0x0400_015A => {}, // Not Used
|
||||||
|
|
||||||
|
// Interrupts
|
||||||
|
0x0400_0200 => bus.io.ie.raw = value,
|
||||||
|
0x0400_0202 => bus.io.irq.raw &= ~value,
|
||||||
|
0x0400_0204 => bus.io.waitcnt.set(value),
|
||||||
|
0x0400_0206 => {},
|
||||||
|
0x0400_0208 => bus.io.ime = value & 1 == 1,
|
||||||
|
0x0400_020A => {},
|
||||||
|
0x0400_0300 => {
|
||||||
|
bus.io.postflg = @enumFromInt(value & 1);
|
||||||
|
bus.io.haltcnt = if (value >> 15 & 1 == 0) .Halt else @panic("TODO: Implement STOP");
|
||||||
|
},
|
||||||
|
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, address }),
|
||||||
|
},
|
||||||
|
u8 => switch (address) {
|
||||||
|
// Display
|
||||||
|
0x0400_0000...0x0400_0055 => ppu.write(T, &bus.ppu, address, value),
|
||||||
|
|
||||||
|
// 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}),
|
||||||
|
|
||||||
|
// Serial Communication 2
|
||||||
|
0x0400_0135 => log.debug("Wrote 0x{X:0>2} to RCNT_H", .{value}),
|
||||||
|
0x0400_0140 => log.debug("Wrote 0x{X:0>2} to JOYCNT_L", .{value}),
|
||||||
|
|
||||||
|
// Interrupts
|
||||||
|
0x0400_0200, 0x0400_0201 => bus.io.ie.raw = setHalf(u16, bus.io.ie.raw, @truncate(address), value),
|
||||||
|
0x0400_0202 => bus.io.irq.raw &= ~@as(u16, value),
|
||||||
|
0x0400_0203 => bus.io.irq.raw &= ~@as(u16, value) << 8, // TODO: Is this good?
|
||||||
|
0x0400_0204, 0x0400_0205 => bus.io.waitcnt.set(setHalf(u16, bus.io.waitcnt.raw, @truncate(address), value)),
|
||||||
|
0x0400_0206, 0x0400_0207 => {},
|
||||||
|
0x0400_0208 => bus.io.ime = value & 1 == 1,
|
||||||
|
0x0400_0209 => {},
|
||||||
|
0x0400_020A, 0x0400_020B => {},
|
||||||
|
|
||||||
|
0x0400_0300 => bus.io.postflg = @enumFromInt(value & 1),
|
||||||
|
0x0400_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 }),
|
||||||
|
else => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, address }),
|
||||||
|
},
|
||||||
|
else => @compileError("I/O: Unsupported write width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read / Write
|
||||||
|
pub const PostFlag = enum(u1) {
|
||||||
|
FirstBoot = 0,
|
||||||
|
FurtherBoots = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Write Only
|
||||||
|
pub const HaltControl = enum {
|
||||||
|
Halt,
|
||||||
|
Stop,
|
||||||
|
Execute,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read / Write
|
||||||
|
pub const DisplayControl = extern union {
|
||||||
|
bg_mode: Bitfield(u16, 0, 3),
|
||||||
|
frame_select: Bit(u16, 4),
|
||||||
|
hblank_interval_free: Bit(u16, 5),
|
||||||
|
obj_mapping: Bit(u16, 6),
|
||||||
|
forced_blank: Bit(u16, 7),
|
||||||
|
bg_enable: Bitfield(u16, 8, 4),
|
||||||
|
obj_enable: Bit(u16, 12),
|
||||||
|
win_enable: Bitfield(u16, 13, 2),
|
||||||
|
obj_win_enable: Bit(u16, 15),
|
||||||
|
raw: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
pub const VCount = extern union {
|
||||||
|
scanline: Bitfield(u16, 0, 8),
|
||||||
|
raw: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read / Write
|
||||||
|
const InterruptEnable = extern union {
|
||||||
|
vblank: Bit(u16, 0),
|
||||||
|
hblank: Bit(u16, 1),
|
||||||
|
coincidence: Bit(u16, 2),
|
||||||
|
tim0: Bit(u16, 3),
|
||||||
|
tim1: Bit(u16, 4),
|
||||||
|
tim2: Bit(u16, 5),
|
||||||
|
tim3: Bit(u16, 6),
|
||||||
|
serial: Bit(u16, 7),
|
||||||
|
dma0: Bit(u16, 8),
|
||||||
|
dma1: Bit(u16, 9),
|
||||||
|
dma2: Bit(u16, 10),
|
||||||
|
dma3: Bit(u16, 11),
|
||||||
|
keypad: Bit(u16, 12),
|
||||||
|
game_pak: Bit(u16, 13),
|
||||||
|
raw: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read Only
|
||||||
|
/// 0 = Pressed, 1 = Released
|
||||||
|
pub const KeyInput = extern union {
|
||||||
|
a: Bit(u16, 0),
|
||||||
|
b: Bit(u16, 1),
|
||||||
|
select: Bit(u16, 2),
|
||||||
|
start: Bit(u16, 3),
|
||||||
|
right: Bit(u16, 4),
|
||||||
|
left: Bit(u16, 5),
|
||||||
|
up: Bit(u16, 6),
|
||||||
|
down: Bit(u16, 7),
|
||||||
|
shoulder_r: Bit(u16, 8),
|
||||||
|
shoulder_l: Bit(u16, 9),
|
||||||
|
raw: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
const AtomicKeyInput = struct {
|
||||||
|
const Self = @This();
|
||||||
|
const AtomicOrder = std.builtin.AtomicOrder;
|
||||||
|
|
||||||
|
inner: KeyInput,
|
||||||
|
|
||||||
|
pub fn init(value: KeyInput) Self {
|
||||||
|
return .{ .inner = value };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn load(self: *const Self, comptime ordering: AtomicOrder) u16 {
|
||||||
|
return switch (ordering) {
|
||||||
|
.acq_rel, .release => @compileError("not supported for atomic loads"),
|
||||||
|
else => @atomicLoad(u16, &self.inner.raw, ordering),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn fetchOr(self: *Self, value: u16, comptime ordering: AtomicOrder) void {
|
||||||
|
_ = @atomicRmw(u16, &self.inner.raw, .Or, value, ordering);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn fetchAnd(self: *Self, value: u16, comptime ordering: AtomicOrder) void {
|
||||||
|
_ = @atomicRmw(u16, &self.inner.raw, .And, value, ordering);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read / Write
|
||||||
|
pub const BackgroundControl = extern union {
|
||||||
|
priority: Bitfield(u16, 0, 2),
|
||||||
|
char_base: Bitfield(u16, 2, 2),
|
||||||
|
mosaic_enable: Bit(u16, 6),
|
||||||
|
colour_mode: Bit(u16, 7),
|
||||||
|
screen_base: Bitfield(u16, 8, 5),
|
||||||
|
display_overflow: Bit(u16, 13),
|
||||||
|
size: Bitfield(u16, 14, 2),
|
||||||
|
raw: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Write Only
|
||||||
|
pub const BackgroundOffset = extern union {
|
||||||
|
offset: Bitfield(u16, 0, 9),
|
||||||
|
raw: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read / Write
|
||||||
|
pub const BldCnt = extern union {
|
||||||
|
/// BLDCNT{0} is BG0 A
|
||||||
|
/// BLDCNT{4} is OBJ A
|
||||||
|
/// BLDCNT{5} is BD A
|
||||||
|
layer_a: Bitfield(u16, 0, 6),
|
||||||
|
mode: Bitfield(u16, 6, 2),
|
||||||
|
|
||||||
|
/// BLDCNT{8} is BG0 B
|
||||||
|
/// BLDCNT{12} is OBJ B
|
||||||
|
/// BLDCNT{13} is BD B
|
||||||
|
layer_b: Bitfield(u16, 8, 6),
|
||||||
|
raw: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read-only?
|
||||||
|
/// Alpha Blending Coefficients
|
||||||
|
pub const BldAlpha = extern union {
|
||||||
|
eva: Bitfield(u16, 0, 5),
|
||||||
|
evb: Bitfield(u16, 8, 5),
|
||||||
|
raw: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Write-only?
|
||||||
|
/// Brightness COefficients
|
||||||
|
pub const BldY = extern union {
|
||||||
|
evy: Bitfield(u16, 0, 5),
|
||||||
|
raw: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
const u8WriteKind = enum { Hi, Lo };
|
||||||
|
|
||||||
|
/// Write-only
|
||||||
|
pub const WinH = extern union {
|
||||||
|
x2: Bitfield(u16, 0, 8),
|
||||||
|
x1: Bitfield(u16, 8, 8),
|
||||||
|
raw: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Write-only
|
||||||
|
pub const WinV = extern union {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
y2: Bitfield(u16, 0, 8),
|
||||||
|
y1: Bitfield(u16, 8, 8),
|
||||||
|
raw: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WinIn = extern union {
|
||||||
|
w0_bg: Bitfield(u16, 0, 4),
|
||||||
|
w0_obj: Bit(u16, 4),
|
||||||
|
w0_bld: Bit(u16, 5),
|
||||||
|
w1_bg: Bitfield(u16, 8, 4),
|
||||||
|
w1_obj: Bit(u16, 12),
|
||||||
|
w1_bld: Bit(u16, 13),
|
||||||
|
raw: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WinOut = extern union {
|
||||||
|
out_bg: Bitfield(u16, 0, 4),
|
||||||
|
out_obj: Bit(u16, 4),
|
||||||
|
out_bld: Bit(u16, 5),
|
||||||
|
obj_bg: Bitfield(u16, 8, 4),
|
||||||
|
obj_obj: Bit(u16, 12),
|
||||||
|
obj_bld: Bit(u16, 13),
|
||||||
|
raw: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read / Write
|
||||||
|
const InterruptRequest = extern union {
|
||||||
|
vblank: Bit(u16, 0),
|
||||||
|
hblank: Bit(u16, 1),
|
||||||
|
coincidence: Bit(u16, 2),
|
||||||
|
tim0: Bit(u16, 3),
|
||||||
|
tim1: Bit(u16, 4),
|
||||||
|
tim2: Bit(u16, 5),
|
||||||
|
tim3: Bit(u16, 6),
|
||||||
|
serial: Bit(u16, 7),
|
||||||
|
dma0: Bit(u16, 8),
|
||||||
|
dma1: Bit(u16, 9),
|
||||||
|
dma2: Bit(u16, 10),
|
||||||
|
dma3: Bit(u16, 11),
|
||||||
|
keypad: Bit(u16, 12),
|
||||||
|
game_pak: Bit(u16, 13),
|
||||||
|
raw: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read / Write
|
||||||
|
pub const DmaControl = extern union {
|
||||||
|
dad_adj: Bitfield(u16, 5, 2),
|
||||||
|
sad_adj: Bitfield(u16, 7, 2),
|
||||||
|
repeat: Bit(u16, 9),
|
||||||
|
transfer_type: Bit(u16, 10),
|
||||||
|
pak_drq: Bit(u16, 11),
|
||||||
|
start_timing: Bitfield(u16, 12, 2),
|
||||||
|
irq: Bit(u16, 14),
|
||||||
|
enabled: Bit(u16, 15),
|
||||||
|
raw: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read / Write
|
||||||
|
pub const TimerControl = extern union {
|
||||||
|
frequency: Bitfield(u16, 0, 2),
|
||||||
|
cascade: Bit(u16, 2),
|
||||||
|
irq: Bit(u16, 6),
|
||||||
|
enabled: Bit(u16, 7),
|
||||||
|
raw: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read / Write
|
||||||
|
/// NR10
|
||||||
|
pub const Sweep = extern union {
|
||||||
|
shift: Bitfield(u8, 0, 3),
|
||||||
|
direction: Bit(u8, 3),
|
||||||
|
period: Bitfield(u8, 4, 3),
|
||||||
|
raw: u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read / Write
|
||||||
|
/// This represents the Duty / Len
|
||||||
|
/// NRx1
|
||||||
|
pub const Duty = extern union {
|
||||||
|
/// Write-only
|
||||||
|
/// Only used when bit 6 is set
|
||||||
|
length: Bitfield(u16, 0, 6),
|
||||||
|
pattern: Bitfield(u16, 6, 2),
|
||||||
|
raw: u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read / Write
|
||||||
|
/// NRx2
|
||||||
|
pub const Envelope = extern union {
|
||||||
|
period: Bitfield(u8, 0, 3),
|
||||||
|
direction: Bit(u8, 3),
|
||||||
|
init_vol: Bitfield(u8, 4, 4),
|
||||||
|
raw: u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read / Write
|
||||||
|
/// NRx3, NRx4
|
||||||
|
pub const Frequency = extern union {
|
||||||
|
/// Write-only
|
||||||
|
frequency: Bitfield(u16, 0, 11),
|
||||||
|
length_enable: Bit(u16, 14),
|
||||||
|
/// Write-only
|
||||||
|
trigger: Bit(u16, 15),
|
||||||
|
|
||||||
|
raw: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read / Write
|
||||||
|
/// NR30
|
||||||
|
pub const WaveSelect = extern union {
|
||||||
|
dimension: Bit(u8, 5),
|
||||||
|
bank: Bit(u8, 6),
|
||||||
|
enabled: Bit(u8, 7),
|
||||||
|
raw: u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read / Write
|
||||||
|
/// NR32
|
||||||
|
pub const WaveVolume = extern union {
|
||||||
|
kind: Bitfield(u8, 5, 2),
|
||||||
|
force: Bit(u8, 7),
|
||||||
|
raw: u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read / Write
|
||||||
|
/// NR43
|
||||||
|
pub const PolyCounter = extern union {
|
||||||
|
div_ratio: Bitfield(u8, 0, 3),
|
||||||
|
width: Bit(u8, 3),
|
||||||
|
shift: Bitfield(u8, 4, 4),
|
||||||
|
raw: u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read / Write
|
||||||
|
/// NR44
|
||||||
|
pub const NoiseControl = extern union {
|
||||||
|
length_enable: Bit(u8, 6),
|
||||||
|
trigger: Bit(u8, 7),
|
||||||
|
raw: u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read / Write
|
||||||
|
pub const ChannelVolumeControl = extern union {
|
||||||
|
right_vol: Bitfield(u16, 0, 3),
|
||||||
|
left_vol: Bitfield(u16, 4, 3),
|
||||||
|
ch_right: Bitfield(u16, 8, 4),
|
||||||
|
ch_left: Bitfield(u16, 12, 4),
|
||||||
|
raw: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read / Write
|
||||||
|
pub const DmaSoundControl = extern union {
|
||||||
|
ch_vol: Bitfield(u16, 0, 2),
|
||||||
|
chA_vol: Bit(u16, 2),
|
||||||
|
chB_vol: Bit(u16, 3),
|
||||||
|
|
||||||
|
chA_right: Bit(u16, 8),
|
||||||
|
chA_left: Bit(u16, 9),
|
||||||
|
chA_timer: Bit(u16, 10),
|
||||||
|
/// Write only?
|
||||||
|
chA_reset: Bit(u16, 11),
|
||||||
|
|
||||||
|
chB_right: Bit(u16, 12),
|
||||||
|
chB_left: Bit(u16, 13),
|
||||||
|
chB_timer: Bit(u16, 14),
|
||||||
|
/// Write only?
|
||||||
|
chB_reset: Bit(u16, 15),
|
||||||
|
raw: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read / Write
|
||||||
|
pub const SoundControl = extern union {
|
||||||
|
/// Read-only
|
||||||
|
ch1_enable: Bit(u8, 0),
|
||||||
|
/// Read-only
|
||||||
|
ch2_enable: Bit(u8, 1),
|
||||||
|
/// Read-only
|
||||||
|
ch3_enable: Bit(u8, 2),
|
||||||
|
/// Read-only
|
||||||
|
ch4_enable: Bit(u8, 3),
|
||||||
|
apu_enable: Bit(u8, 7),
|
||||||
|
raw: u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read / Write
|
||||||
|
pub const SoundBias = extern union {
|
||||||
|
level: Bitfield(u16, 1, 9),
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
254
src/core/bus/timer.zig
Normal file
254
src/core/bus/timer.zig
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const util = @import("../../util.zig");
|
||||||
|
|
||||||
|
const TimerControl = @import("io.zig").TimerControl;
|
||||||
|
const Scheduler = @import("../scheduler.zig").Scheduler;
|
||||||
|
const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||||
|
const Bus = @import("../Bus.zig");
|
||||||
|
|
||||||
|
const handleInterrupt = @import("../cpu_util.zig").handleInterrupt;
|
||||||
|
|
||||||
|
pub const TimerTuple = struct { Timer(0), Timer(1), Timer(2), Timer(3) };
|
||||||
|
const log = std.log.scoped(.Timer);
|
||||||
|
|
||||||
|
const getHalf = util.getHalf;
|
||||||
|
const setHalf = util.setHalf;
|
||||||
|
|
||||||
|
pub fn create(sched: *Scheduler) TimerTuple {
|
||||||
|
return .{ Timer(0).init(sched), Timer(1).init(sched), Timer(2).init(sched), Timer(3).init(sched) };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(comptime T: type, tim: *const TimerTuple, addr: u32) ?T {
|
||||||
|
const nybble_addr: u4 = @truncate(addr);
|
||||||
|
|
||||||
|
return switch (T) {
|
||||||
|
u32 => switch (nybble_addr) {
|
||||||
|
0x0 => @as(T, tim.*[0].cnt.raw) << 16 | tim.*[0].timcntL(),
|
||||||
|
0x4 => @as(T, tim.*[1].cnt.raw) << 16 | tim.*[1].timcntL(),
|
||||||
|
0x8 => @as(T, tim.*[2].cnt.raw) << 16 | tim.*[2].timcntL(),
|
||||||
|
0xC => @as(T, tim.*[3].cnt.raw) << 16 | tim.*[3].timcntL(),
|
||||||
|
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
||||||
|
},
|
||||||
|
u16 => switch (nybble_addr) {
|
||||||
|
0x0 => tim.*[0].timcntL(),
|
||||||
|
0x2 => tim.*[0].cnt.raw,
|
||||||
|
|
||||||
|
0x4 => tim.*[1].timcntL(),
|
||||||
|
0x6 => tim.*[1].cnt.raw,
|
||||||
|
|
||||||
|
0x8 => tim.*[2].timcntL(),
|
||||||
|
0xA => tim.*[2].cnt.raw,
|
||||||
|
|
||||||
|
0xC => tim.*[3].timcntL(),
|
||||||
|
0xE => tim.*[3].cnt.raw,
|
||||||
|
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
||||||
|
},
|
||||||
|
u8 => switch (nybble_addr) {
|
||||||
|
0x0, 0x1 => @truncate(tim.*[0].timcntL() >> getHalf(nybble_addr)),
|
||||||
|
0x2, 0x3 => @truncate(tim.*[0].cnt.raw >> getHalf(nybble_addr)),
|
||||||
|
|
||||||
|
0x4, 0x5 => @truncate(tim.*[1].timcntL() >> getHalf(nybble_addr)),
|
||||||
|
0x6, 0x7 => @truncate(tim.*[1].cnt.raw >> getHalf(nybble_addr)),
|
||||||
|
|
||||||
|
0x8, 0x9 => @truncate(tim.*[2].timcntL() >> getHalf(nybble_addr)),
|
||||||
|
0xA, 0xB => @truncate(tim.*[2].cnt.raw >> getHalf(nybble_addr)),
|
||||||
|
|
||||||
|
0xC, 0xD => @truncate(tim.*[3].timcntL() >> getHalf(nybble_addr)),
|
||||||
|
0xE, 0xF => @truncate(tim.*[3].cnt.raw >> getHalf(nybble_addr)),
|
||||||
|
},
|
||||||
|
else => @compileError("TIM: Unsupported read width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(comptime T: type, tim: *TimerTuple, addr: u32, value: T) void {
|
||||||
|
const nybble_addr: u4 = @truncate(addr);
|
||||||
|
|
||||||
|
return switch (T) {
|
||||||
|
u32 => switch (nybble_addr) {
|
||||||
|
0x0 => tim.*[0].setTimcnt(value),
|
||||||
|
0x4 => tim.*[1].setTimcnt(value),
|
||||||
|
0x8 => tim.*[2].setTimcnt(value),
|
||||||
|
0xC => tim.*[3].setTimcnt(value),
|
||||||
|
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||||
|
},
|
||||||
|
u16 => switch (nybble_addr) {
|
||||||
|
0x0 => tim.*[0].setTimcntL(value),
|
||||||
|
0x2 => tim.*[0].setTimcntH(value),
|
||||||
|
|
||||||
|
0x4 => tim.*[1].setTimcntL(value),
|
||||||
|
0x6 => tim.*[1].setTimcntH(value),
|
||||||
|
|
||||||
|
0x8 => tim.*[2].setTimcntL(value),
|
||||||
|
0xA => tim.*[2].setTimcntH(value),
|
||||||
|
|
||||||
|
0xC => tim.*[3].setTimcntL(value),
|
||||||
|
0xE => tim.*[3].setTimcntH(value),
|
||||||
|
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||||
|
},
|
||||||
|
u8 => switch (nybble_addr) {
|
||||||
|
0x0, 0x1 => tim.*[0].setTimcntL(setHalf(u16, tim.*[0]._reload, nybble_addr, value)),
|
||||||
|
0x2, 0x3 => tim.*[0].setTimcntH(setHalf(u16, tim.*[0].cnt.raw, nybble_addr, value)),
|
||||||
|
|
||||||
|
0x4, 0x5 => tim.*[1].setTimcntL(setHalf(u16, tim.*[1]._reload, nybble_addr, value)),
|
||||||
|
0x6, 0x7 => tim.*[1].setTimcntH(setHalf(u16, tim.*[1].cnt.raw, nybble_addr, value)),
|
||||||
|
|
||||||
|
0x8, 0x9 => tim.*[2].setTimcntL(setHalf(u16, tim.*[2]._reload, nybble_addr, value)),
|
||||||
|
0xA, 0xB => tim.*[2].setTimcntH(setHalf(u16, tim.*[2].cnt.raw, nybble_addr, value)),
|
||||||
|
|
||||||
|
0xC, 0xD => tim.*[3].setTimcntL(setHalf(u16, tim.*[3]._reload, nybble_addr, value)),
|
||||||
|
0xE, 0xF => tim.*[3].setTimcntH(setHalf(u16, tim.*[3].cnt.raw, nybble_addr, value)),
|
||||||
|
},
|
||||||
|
else => @compileError("TIM: Unsupported write width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn Timer(comptime id: u2) type {
|
||||||
|
return struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
/// Read Only, Internal. Please use self.timcntL()
|
||||||
|
_counter: u16,
|
||||||
|
|
||||||
|
/// Write Only, Internal. Please use self.setTimcntL()
|
||||||
|
_reload: u16,
|
||||||
|
|
||||||
|
/// Write Only, Internal. Please use self.setTimcntH()
|
||||||
|
cnt: TimerControl,
|
||||||
|
|
||||||
|
/// Internal.
|
||||||
|
sched: *Scheduler,
|
||||||
|
|
||||||
|
/// Internal
|
||||||
|
_start_timestamp: u64,
|
||||||
|
|
||||||
|
pub fn init(sched: *Scheduler) Self {
|
||||||
|
return .{
|
||||||
|
._reload = 0,
|
||||||
|
._counter = 0,
|
||||||
|
.cnt = .{ .raw = 0x0000 },
|
||||||
|
.sched = sched,
|
||||||
|
._start_timestamp = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
const scheduler = self.sched;
|
||||||
|
|
||||||
|
self.* = Self.init(scheduler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TIMCNT_L Getter
|
||||||
|
pub fn timcntL(self: *const Self) u16 {
|
||||||
|
if (self.cnt.cascade.read() or !self.cnt.enabled.read()) return self._counter;
|
||||||
|
|
||||||
|
return self._counter +% @as(u16, @truncate((self.sched.now() - self._start_timestamp) / self.frequency()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TIMCNT_L Setter
|
||||||
|
pub fn setTimcntL(self: *Self, halfword: u16) void {
|
||||||
|
self._reload = halfword;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TIMCNT_L & TIMCNT_H
|
||||||
|
pub fn setTimcnt(self: *Self, word: u32) void {
|
||||||
|
self.setTimcntL(@truncate(word));
|
||||||
|
self.setTimcntH(@truncate(word >> 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TIMCNT_H
|
||||||
|
pub fn setTimcntH(self: *Self, halfword: u16) void {
|
||||||
|
const new = TimerControl{ .raw = halfword };
|
||||||
|
|
||||||
|
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 });
|
||||||
|
|
||||||
|
// Counter should hold the value it stopped at meaning we have to calculate it now
|
||||||
|
self._counter +%= @truncate((self.sched.now() - self._start_timestamp) / self.frequency());
|
||||||
|
}
|
||||||
|
|
||||||
|
// the timer has always been enabled, but the cascade bit which was blocking the timer has been unset
|
||||||
|
if (new.enabled.read() and (self.cnt.cascade.read() and !new.cascade.read())) {
|
||||||
|
// we want to reschedule the timer event, however we won't reload the counter.
|
||||||
|
// the invariant here is that self._counter holds the already calculated paused value
|
||||||
|
|
||||||
|
self.rescheduleTimerExpire(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// the timer was previously disabeld
|
||||||
|
|
||||||
|
if (new.enabled.read()) {
|
||||||
|
// timer should start counting (with a reloaded counter value)
|
||||||
|
self._counter = self._reload;
|
||||||
|
|
||||||
|
// if cascade happens to be set, the timer doesn't actually do anything though
|
||||||
|
if (!new.cascade.read()) self.rescheduleTimerExpire(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cnt.raw = halfword;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn onTimerExpire(self: *Self, cpu: *Arm7tdmi, late: u64) void {
|
||||||
|
// Fire IRQ if enabled
|
||||||
|
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
|
||||||
|
|
||||||
|
const io = &bus_ptr.io;
|
||||||
|
|
||||||
|
if (self.cnt.irq.read()) {
|
||||||
|
switch (id) {
|
||||||
|
0 => io.irq.tim0.write(true),
|
||||||
|
1 => io.irq.tim1.write(true),
|
||||||
|
2 => io.irq.tim2.write(true),
|
||||||
|
3 => io.irq.tim3.write(true),
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInterrupt(cpu);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DMA Sound Things
|
||||||
|
if (id == 0 or id == 1) {
|
||||||
|
bus_ptr.apu.onDmaAudioSampleRequest(cpu, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform Cascade Behaviour
|
||||||
|
switch (id) {
|
||||||
|
inline 0, 1, 2 => |idx| {
|
||||||
|
const next = idx + 1;
|
||||||
|
|
||||||
|
if (bus_ptr.tim[next].cnt.cascade.read()) {
|
||||||
|
bus_ptr.tim[next]._counter +%= 1;
|
||||||
|
if (bus_ptr.tim[next]._counter == 0) bus_ptr.tim[next].onTimerExpire(cpu, late);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
3 => {}, // THere is no timer for TIM3 to cascade to
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reschedule Timer if we're not cascading
|
||||||
|
// TIM0 cascade value is N/A
|
||||||
|
if (id == 0 or !self.cnt.cascade.read()) {
|
||||||
|
self._counter = self._reload;
|
||||||
|
self.rescheduleTimerExpire(late);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rescheduleTimerExpire(self: *Self, late: u64) void {
|
||||||
|
const when = (@as(u64, 0x10000) - self._counter) * self.frequency();
|
||||||
|
|
||||||
|
self._start_timestamp = self.sched.now();
|
||||||
|
self.sched.push(.{ .TimerOverflow = id }, when -| late);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frequency(self: *const Self) u16 {
|
||||||
|
return switch (self.cnt.frequency.read()) {
|
||||||
|
0 => 1,
|
||||||
|
1 => 64,
|
||||||
|
2 => 256,
|
||||||
|
3 => 1024,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
75
src/core/cpu_util.zig
Normal file
75
src/core/cpu_util.zig
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||||
|
const Bank = @import("arm32").Arm7tdmi.Bank;
|
||||||
|
const Bus = @import("Bus.zig");
|
||||||
|
|
||||||
|
pub inline fn isHalted(cpu: *const Arm7tdmi) bool {
|
||||||
|
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
|
||||||
|
|
||||||
|
return bus_ptr.io.haltcnt == .Halt;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stepDmaTransfer(cpu: *Arm7tdmi) bool {
|
||||||
|
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
|
||||||
|
|
||||||
|
inline for (0..4) |i| {
|
||||||
|
if (bus_ptr.dma[i].in_progress) {
|
||||||
|
bus_ptr.dma[i].step(cpu);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handleInterrupt(cpu: *Arm7tdmi) void {
|
||||||
|
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
|
||||||
|
const should_handle = bus_ptr.io.ie.raw & bus_ptr.io.irq.raw;
|
||||||
|
|
||||||
|
// Return if IME is disabled, CPSR I is set or there is nothing to handle
|
||||||
|
if (!bus_ptr.io.ime or cpu.cpsr.i.read() or should_handle == 0) return;
|
||||||
|
|
||||||
|
// If Pipeline isn't full, we have a bug
|
||||||
|
std.debug.assert(cpu.pipe.isFull());
|
||||||
|
|
||||||
|
// log.debug("Handling Interrupt!", .{});
|
||||||
|
bus_ptr.io.haltcnt = .Execute;
|
||||||
|
|
||||||
|
// FIXME: This seems weird, but retAddr.gba suggests I need to make these changes
|
||||||
|
const ret_addr = cpu.r[15] - if (cpu.cpsr.t.read()) 0 else @as(u32, 4);
|
||||||
|
const new_spsr = cpu.cpsr.raw;
|
||||||
|
|
||||||
|
cpu.changeMode(.Irq);
|
||||||
|
cpu.cpsr.t.write(false);
|
||||||
|
cpu.cpsr.i.write(true);
|
||||||
|
|
||||||
|
cpu.r[14] = ret_addr;
|
||||||
|
cpu.spsr.raw = new_spsr;
|
||||||
|
cpu.r[15] = 0x0000_0018;
|
||||||
|
cpu.pipe.reload(cpu);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advances state so that the BIOS is skipped
|
||||||
|
///
|
||||||
|
/// Note: This accesses the CPU's bus ptr so it only may be called
|
||||||
|
/// once the Bus has been properly initialized
|
||||||
|
///
|
||||||
|
/// TODO: Make above notice impossible to do in code
|
||||||
|
pub fn fastBoot(cpu: *Arm7tdmi) void {
|
||||||
|
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
|
||||||
|
cpu.r = std.mem.zeroes([16]u32);
|
||||||
|
|
||||||
|
// cpu.r[0] = 0x08000000;
|
||||||
|
// cpu.r[1] = 0x000000EA;
|
||||||
|
cpu.r[13] = 0x0300_7F00;
|
||||||
|
cpu.r[15] = 0x0800_0000;
|
||||||
|
|
||||||
|
cpu.bank.r[Bank.regIdx(.Irq, .R13)] = 0x0300_7FA0;
|
||||||
|
cpu.bank.r[Bank.regIdx(.Supervisor, .R13)] = 0x0300_7FE0;
|
||||||
|
|
||||||
|
// cpu.cpsr.raw = 0x6000001F;
|
||||||
|
cpu.cpsr.raw = 0x0000_001F;
|
||||||
|
|
||||||
|
bus_ptr.bios.addr_latch = 0x0000_00DC + 8;
|
||||||
|
}
|
||||||
357
src/core/emu.zig
Normal file
357
src/core/emu.zig
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const config = @import("../config.zig");
|
||||||
|
const c = @import("../lib.zig").c;
|
||||||
|
|
||||||
|
const Scheduler = @import("scheduler.zig").Scheduler;
|
||||||
|
const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||||
|
const Bus = @import("Bus.zig");
|
||||||
|
const Tracker = @import("../util.zig").FpsTracker;
|
||||||
|
const Channel = @import("../util.zig").Queue;
|
||||||
|
|
||||||
|
const stepDmaTransfer = @import("cpu_util.zig").stepDmaTransfer;
|
||||||
|
const isHalted = @import("cpu_util.zig").isHalted;
|
||||||
|
|
||||||
|
const Timer = std.time.Timer;
|
||||||
|
|
||||||
|
pub const Synchro = struct {
|
||||||
|
const AtomicBool = std.atomic.Value(bool);
|
||||||
|
|
||||||
|
// FIXME: This Enum ends up being really LARGE!!!
|
||||||
|
pub const Message = union(enum) {
|
||||||
|
rom_path: [std.fs.max_path_bytes]u8,
|
||||||
|
bios_path: [std.fs.max_path_bytes]u8,
|
||||||
|
restart: void,
|
||||||
|
};
|
||||||
|
|
||||||
|
paused: AtomicBool = AtomicBool.init(true), // FIXME: can ui_busy and paused be the same?
|
||||||
|
should_quit: AtomicBool = AtomicBool.init(false),
|
||||||
|
|
||||||
|
ch: Channel(Message),
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator) !@This() {
|
||||||
|
const msg_buf = try allocator.alloc(Message, 1);
|
||||||
|
return .{ .ch = Channel(Message).init(msg_buf) };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
|
||||||
|
allocator.free(self.ch.inner.buf);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 4 Cycles in 1 dot
|
||||||
|
const cycles_per_dot = 4;
|
||||||
|
|
||||||
|
/// The GBA draws 228 Horizontal which each consist 308 dots
|
||||||
|
/// (note: not all lines are visible)
|
||||||
|
const cycles_per_frame = 228 * (308 * cycles_per_dot); //280896
|
||||||
|
|
||||||
|
/// The GBA ARM7TDMI runs at 2^24 Hz
|
||||||
|
const clock_rate = 1 << 24; // 16.78MHz
|
||||||
|
|
||||||
|
/// The # of nanoseconds a frame should take
|
||||||
|
const frame_period = (std.time.ns_per_s * cycles_per_frame) / clock_rate;
|
||||||
|
|
||||||
|
/// Exact Value: 59.7275005696Hz
|
||||||
|
/// The inverse of the frame period
|
||||||
|
pub const frame_rate: f64 = @as(f64, @floatFromInt(clock_rate)) / cycles_per_frame;
|
||||||
|
|
||||||
|
const log = std.log.scoped(.Emulation);
|
||||||
|
|
||||||
|
const RunKind = enum {
|
||||||
|
Unlimited,
|
||||||
|
UnlimitedFPS,
|
||||||
|
Limited,
|
||||||
|
LimitedFPS,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn run(cpu: *Arm7tdmi, scheduler: *Scheduler, tracker: *Tracker, sync: *Synchro) void {
|
||||||
|
const audio_sync = config.config().guest.audio_sync and !config.config().host.mute;
|
||||||
|
if (audio_sync) log.info("Audio sync enabled", .{});
|
||||||
|
|
||||||
|
if (config.config().guest.video_sync) {
|
||||||
|
inner(.LimitedFPS, audio_sync, cpu, scheduler, tracker, sync);
|
||||||
|
} else {
|
||||||
|
inner(.UnlimitedFPS, audio_sync, cpu, scheduler, tracker, sync);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inner(comptime kind: RunKind, audio_sync: bool, cpu: *Arm7tdmi, scheduler: *Scheduler, tracker: ?*Tracker, sync: *Synchro) void {
|
||||||
|
if (kind == .UnlimitedFPS or kind == .LimitedFPS) {
|
||||||
|
std.debug.assert(tracker != null);
|
||||||
|
log.info("FPS tracking enabled", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
|
||||||
|
|
||||||
|
// FIXME: audioSync accesses emulator state without any guarantees
|
||||||
|
|
||||||
|
switch (kind) {
|
||||||
|
.Unlimited, .UnlimitedFPS => {
|
||||||
|
log.info("Emulation w/out video sync", .{});
|
||||||
|
|
||||||
|
while (!sync.should_quit.load(.monotonic)) {
|
||||||
|
handleChannel(cpu, &sync.ch);
|
||||||
|
if (sync.paused.load(.monotonic)) continue;
|
||||||
|
|
||||||
|
runFrame(scheduler, cpu);
|
||||||
|
audioSync(audio_sync, bus_ptr.apu.stream, &bus_ptr.apu.is_buffer_full);
|
||||||
|
|
||||||
|
if (kind == .UnlimitedFPS) tracker.?.tick();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Limited, .LimitedFPS => {
|
||||||
|
log.info("Emulation w/ video sync", .{});
|
||||||
|
var timer = Timer.start() catch @panic("failed to initalize std.timer.Timer");
|
||||||
|
var wake_time: u64 = frame_period;
|
||||||
|
|
||||||
|
while (!sync.should_quit.load(.monotonic)) {
|
||||||
|
handleChannel(cpu, &sync.ch);
|
||||||
|
if (sync.paused.load(.monotonic)) continue;
|
||||||
|
|
||||||
|
runFrame(scheduler, cpu);
|
||||||
|
const new_wake_time = videoSync(&timer, wake_time);
|
||||||
|
|
||||||
|
// 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, bus_ptr.apu.stream, &bus_ptr.apu.is_buffer_full);
|
||||||
|
if (!audio_sync) spinLoop(&timer, wake_time);
|
||||||
|
wake_time = new_wake_time;
|
||||||
|
|
||||||
|
if (kind == .LimitedFPS) tracker.?.tick();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn handleChannel(cpu: *Arm7tdmi, channel: *Channel(Synchro.Message)) void {
|
||||||
|
const message = channel.pop() orelse return;
|
||||||
|
|
||||||
|
switch (message) {
|
||||||
|
.rom_path => |path_buf| {
|
||||||
|
const path = std.mem.sliceTo(&path_buf, 0);
|
||||||
|
replaceGamepak(cpu, path) catch |e| log.err("failed to replace GamePak: {}", .{e});
|
||||||
|
},
|
||||||
|
.bios_path => |path_buf| {
|
||||||
|
const path = std.mem.sliceTo(&path_buf, 0);
|
||||||
|
replaceBios(cpu, path) catch |e| log.err("failed to replace BIOS: {}", .{e});
|
||||||
|
},
|
||||||
|
.restart => reset(cpu),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
|
||||||
|
const frame_end = sched.tick + cycles_per_frame;
|
||||||
|
|
||||||
|
while (sched.tick < frame_end) {
|
||||||
|
if (!stepDmaTransfer(cpu)) {
|
||||||
|
if (isHalted(cpu)) {
|
||||||
|
// Fast-forward to next Event
|
||||||
|
sched.tick = sched.nextTimestamp();
|
||||||
|
} else {
|
||||||
|
cpu.step();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sched.tick >= sched.nextTimestamp()) sched.handleEvent(cpu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn audioSync(audio_sync: bool, stream: *c.SDL_AudioStream, is_buffer_full: *bool) void {
|
||||||
|
// comptime std.debug.assert(@import("../platform.zig").sample_format == SDL.AUDIO_U16);
|
||||||
|
const sample_size = 2 * @sizeOf(u16);
|
||||||
|
const max_buf_size: c_int = 0x400;
|
||||||
|
|
||||||
|
_ = audio_sync;
|
||||||
|
_ = stream;
|
||||||
|
_ = is_buffer_full;
|
||||||
|
|
||||||
|
_ = sample_size;
|
||||||
|
_ = max_buf_size;
|
||||||
|
|
||||||
|
// TODO(paoda): re-enable
|
||||||
|
|
||||||
|
// // Determine whether the APU is busy right at this moment
|
||||||
|
// var still_full: bool = SDL.SDL_AudioStreamAvailable(stream) > sample_size * if (is_buffer_full.*) max_buf_size >> 1 else max_buf_size;
|
||||||
|
// defer is_buffer_full.* = still_full; // Update APU Busy status right before exiting scope
|
||||||
|
|
||||||
|
// // If Busy is false, there's no need to sync here
|
||||||
|
// if (!still_full) return;
|
||||||
|
|
||||||
|
// // TODO: Refactor!!!!
|
||||||
|
// // while (SDL.SDL_AudioStreamAvailable(stream) > sample_size * max_buf_size >> 1)
|
||||||
|
// // std.atomic.spinLoopHint();
|
||||||
|
|
||||||
|
// while (true) {
|
||||||
|
// still_full = SDL.SDL_AudioStreamAvailable(stream) > sample_size * max_buf_size >> 1;
|
||||||
|
// if (!audio_sync or !still_full) break;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn videoSync(timer: *Timer, wake_time: u64) u64 {
|
||||||
|
// Use the OS scheduler to put the emulation thread to sleep
|
||||||
|
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 recalculated orelse wake_time + frame_period;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Better sleep impl?
|
||||||
|
fn sleep(timer: *Timer, wake_time: u64) ?u64 {
|
||||||
|
const timestamp = timer.read();
|
||||||
|
|
||||||
|
// ns_late is non zero if we are late.
|
||||||
|
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
|
||||||
|
// get "back on track"
|
||||||
|
if (ns_late > frame_period) return timestamp + frame_period;
|
||||||
|
const sleep_for = frame_period - ns_late;
|
||||||
|
|
||||||
|
const step = 2 * std.time.ns_per_ms; // Granularity of 2ms
|
||||||
|
const times = sleep_for / step;
|
||||||
|
|
||||||
|
for (0..times) |_| {
|
||||||
|
std.Thread.sleep(step);
|
||||||
|
|
||||||
|
// Upon wakeup, check to see if this particular sleep was longer than expected
|
||||||
|
// if so we should exit early, but probably not skip a whole frame period
|
||||||
|
ns_late = timer.read() -| wake_time;
|
||||||
|
if (ns_late > frame_period) return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spinLoop(timer: *Timer, wake_time: u64) void {
|
||||||
|
while (timer.read() < wake_time)
|
||||||
|
std.atomic.spinLoopHint();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const EmuThing = struct {
|
||||||
|
const Self = @This();
|
||||||
|
const Interface = @import("gdbstub").Emulator;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
pub const target =
|
||||||
|
\\<target version="1.0">
|
||||||
|
\\ <architecture>armv4t</architecture>
|
||||||
|
\\ <feature name="org.gnu.gdb.arm.core">
|
||||||
|
\\ <reg name="r0" bitsize="32" type="uint32"/>
|
||||||
|
\\ <reg name="r1" bitsize="32" type="uint32"/>
|
||||||
|
\\ <reg name="r2" bitsize="32" type="uint32"/>
|
||||||
|
\\ <reg name="r3" bitsize="32" type="uint32"/>
|
||||||
|
\\ <reg name="r4" bitsize="32" type="uint32"/>
|
||||||
|
\\ <reg name="r5" bitsize="32" type="uint32"/>
|
||||||
|
\\ <reg name="r6" bitsize="32" type="uint32"/>
|
||||||
|
\\ <reg name="r7" bitsize="32" type="uint32"/>
|
||||||
|
\\ <reg name="r8" bitsize="32" type="uint32"/>
|
||||||
|
\\ <reg name="r9" bitsize="32" type="uint32"/>
|
||||||
|
\\ <reg name="r10" bitsize="32" type="uint32"/>
|
||||||
|
\\ <reg name="r11" bitsize="32" type="uint32"/>
|
||||||
|
\\ <reg name="r12" bitsize="32" type="uint32"/>
|
||||||
|
\\ <reg name="sp" bitsize="32" type="data_ptr"/>
|
||||||
|
\\ <reg name="lr" bitsize="32"/>
|
||||||
|
\\ <reg name="pc" bitsize="32" type="code_ptr"/>
|
||||||
|
\\
|
||||||
|
\\ <reg name="cpsr" bitsize="32" regnum="25"/>
|
||||||
|
\\ </feature>
|
||||||
|
\\</target>
|
||||||
|
;
|
||||||
|
|
||||||
|
// Game Pak SRAM isn't included
|
||||||
|
// TODO: Can i be more specific here?
|
||||||
|
pub const map =
|
||||||
|
\\ <memory-map version="1.0">
|
||||||
|
\\ <memory type="rom" start="0x00000000" length="0x00004000"/>
|
||||||
|
\\ <memory type="ram" start="0x02000000" length="0x00040000"/>
|
||||||
|
\\ <memory type="ram" start="0x03000000" length="0x00008000"/>
|
||||||
|
\\ <memory type="ram" start="0x04000000" length="0x00000400"/>
|
||||||
|
\\ <memory type="ram" start="0x05000000" length="0x00000400"/>
|
||||||
|
\\ <memory type="ram" start="0x06000000" length="0x00018000"/>
|
||||||
|
\\ <memory type="ram" start="0x07000000" length="0x00000400"/>
|
||||||
|
\\ <memory type="rom" start="0x08000000" length="0x02000000"/>
|
||||||
|
\\ <memory type="rom" start="0x0A000000" length="0x02000000"/>
|
||||||
|
\\ <memory type="rom" start="0x0C000000" length="0x02000000"/>
|
||||||
|
\\ </memory-map>
|
||||||
|
;
|
||||||
|
|
||||||
|
cpu: *Arm7tdmi,
|
||||||
|
scheduler: *Scheduler,
|
||||||
|
|
||||||
|
pub fn init(cpu: *Arm7tdmi, scheduler: *Scheduler) Self {
|
||||||
|
return .{ .cpu = cpu, .scheduler = scheduler };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn interface(self: *Self) Interface {
|
||||||
|
return Interface.init(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(self: *const Self, addr: u32) u8 {
|
||||||
|
return self.cpu.bus.dbgRead(u8, addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(self: *Self, addr: u32, value: u8) void {
|
||||||
|
self.cpu.bus.dbgWrite(u8, addr, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn registers(self: *const Self) *[16]u32 {
|
||||||
|
return &self.cpu.r;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cpsr(self: *const Self) u32 {
|
||||||
|
return self.cpu.cpsr.raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step(self: *Self) void {
|
||||||
|
const cpu = self.cpu;
|
||||||
|
const sched = self.scheduler;
|
||||||
|
|
||||||
|
// Is true when we have executed one (1) instruction
|
||||||
|
var did_step: bool = false;
|
||||||
|
|
||||||
|
// TODO: How can I make it easier to keep this in lock-step with runFrame?
|
||||||
|
while (!did_step) {
|
||||||
|
if (!stepDmaTransfer(cpu)) {
|
||||||
|
if (isHalted(cpu)) {
|
||||||
|
// Fast-forward to next Event
|
||||||
|
sched.tick = sched.queue.peek().?.tick;
|
||||||
|
} else {
|
||||||
|
cpu.step();
|
||||||
|
did_step = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sched.tick >= sched.nextTimestamp()) sched.handleEvent(cpu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn reset(cpu: *Arm7tdmi) void {
|
||||||
|
// @breakpoint();
|
||||||
|
cpu.sched.reset(); // Yes this is order sensitive, see the PPU reset for why
|
||||||
|
cpu.bus.reset();
|
||||||
|
cpu.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replaceGamepak(cpu: *Arm7tdmi, file_path: []const u8) !void {
|
||||||
|
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
|
||||||
|
|
||||||
|
try bus_ptr.replaceGamepak(file_path);
|
||||||
|
reset(cpu);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replaceBios(cpu: *Arm7tdmi, file_path: []const u8) !void {
|
||||||
|
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
|
||||||
|
|
||||||
|
const allocator = bus_ptr.bios.allocator;
|
||||||
|
const bios_len = 0x4000;
|
||||||
|
|
||||||
|
bus_ptr.bios.buf = try allocator.alloc(u8, bios_len);
|
||||||
|
try bus_ptr.bios.load(file_path);
|
||||||
|
}
|
||||||
1590
src/core/ppu.zig
Normal file
1590
src/core/ppu.zig
Normal file
File diff suppressed because it is too large
Load Diff
44
src/core/ppu/Oam.zig
Normal file
44
src/core/ppu/Oam.zig
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const buf_len = 0x400;
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
buf: []u8,
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
pub fn read(self: *const Self, comptime T: type, address: usize) T {
|
||||||
|
const addr = address & 0x3FF;
|
||||||
|
|
||||||
|
return switch (T) {
|
||||||
|
u32, u16, u8 => std.mem.readInt(T, self.buf[addr..][0..@sizeOf(T)], .little),
|
||||||
|
else => @compileError("OAM: Unsupported read width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(self: *Self, comptime T: type, address: usize, value: T) void {
|
||||||
|
const addr = address & 0x3FF;
|
||||||
|
|
||||||
|
switch (T) {
|
||||||
|
u32, u16 => std.mem.writeInt(T, self.buf[addr..][0..@sizeOf(T)], value, .little),
|
||||||
|
u8 => return, // 8-bit writes are explicitly ignored
|
||||||
|
else => @compileError("OAM: Unsupported write width"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator) !Self {
|
||||||
|
const buf = try allocator.alloc(u8, buf_len);
|
||||||
|
@memset(buf, 0);
|
||||||
|
|
||||||
|
return Self{ .buf = buf, .allocator = allocator };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
@memset(self.buf, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.allocator.free(self.buf);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
51
src/core/ppu/Palette.zig
Normal file
51
src/core/ppu/Palette.zig
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const buf_len = 0x400;
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
buf: []u8,
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
pub fn read(self: *const Self, comptime T: type, address: usize) T {
|
||||||
|
const addr = address & 0x3FF;
|
||||||
|
|
||||||
|
return switch (T) {
|
||||||
|
u32, u16, u8 => std.mem.readInt(T, self.buf[addr..][0..@sizeOf(T)], .little),
|
||||||
|
else => @compileError("PALRAM: Unsupported read width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(self: *Self, comptime T: type, address: usize, value: T) void {
|
||||||
|
const addr = address & 0x3FF;
|
||||||
|
|
||||||
|
switch (T) {
|
||||||
|
u32, u16 => std.mem.writeInt(T, self.buf[addr..][0..@sizeOf(T)], value, .little),
|
||||||
|
u8 => {
|
||||||
|
const align_addr = addr & ~@as(u32, 1); // Aligned to Halfword boundary
|
||||||
|
std.mem.writeInt(u16, self.buf[align_addr..][0..@sizeOf(u16)], @as(u16, value) * 0x101, .little);
|
||||||
|
},
|
||||||
|
else => @compileError("PALRAM: Unsupported write width"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator) !Self {
|
||||||
|
const buf = try allocator.alloc(u8, buf_len);
|
||||||
|
@memset(buf, 0);
|
||||||
|
|
||||||
|
return Self{ .buf = buf, .allocator = allocator };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
@memset(self.buf, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.allocator.free(self.buf);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn backdrop(self: *const Self) u16 {
|
||||||
|
return std.mem.readInt(u16, self.buf[0..2], .little);
|
||||||
|
}
|
||||||
64
src/core/ppu/Vram.zig
Normal file
64
src/core/ppu/Vram.zig
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const io = @import("../bus/io.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const buf_len = 0x18000;
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
buf: []u8,
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
pub fn read(self: *const Self, comptime T: type, address: usize) T {
|
||||||
|
const addr = Self.mirror(address);
|
||||||
|
|
||||||
|
return switch (T) {
|
||||||
|
u32, u16, u8 => std.mem.readInt(T, self.buf[addr..][0..@sizeOf(T)], .little),
|
||||||
|
else => @compileError("VRAM: Unsupported read width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(self: *Self, comptime T: type, dispcnt: io.DisplayControl, address: usize, value: T) void {
|
||||||
|
const mode: u3 = dispcnt.bg_mode.read();
|
||||||
|
const idx = Self.mirror(address);
|
||||||
|
|
||||||
|
switch (T) {
|
||||||
|
u32, u16 => std.mem.writeInt(T, self.buf[idx..][0..@sizeOf(T)], value, .little),
|
||||||
|
u8 => {
|
||||||
|
// Ignore write if it falls within the boundaries of OBJ VRAM
|
||||||
|
switch (mode) {
|
||||||
|
0, 1, 2 => if (0x0001_0000 <= idx) return,
|
||||||
|
else => if (0x0001_4000 <= idx) return,
|
||||||
|
}
|
||||||
|
|
||||||
|
const align_idx = idx & ~@as(u32, 1); // Aligned to a halfword boundary
|
||||||
|
std.mem.writeInt(u16, self.buf[align_idx..][0..@sizeOf(u16)], @as(u16, value) * 0x101, .little);
|
||||||
|
},
|
||||||
|
else => @compileError("VRAM: Unsupported write width"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator) !Self {
|
||||||
|
const buf = try allocator.alloc(u8, buf_len);
|
||||||
|
@memset(buf, 0);
|
||||||
|
|
||||||
|
return Self{ .buf = buf, .allocator = allocator };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
@memset(self.buf, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.allocator.free(self.buf);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mirror(address: usize) usize {
|
||||||
|
// Mirrored in steps of 128K (64K + 32K + 32K) (abcc)
|
||||||
|
const addr = address & 0x1FFFF;
|
||||||
|
|
||||||
|
// If the address is within 96K we don't do anything,
|
||||||
|
// otherwise we want to mirror the last 32K (addresses between 64K and 96K)
|
||||||
|
return if (addr < buf_len) addr else 0x10000 + (addr & 0x7FFF);
|
||||||
|
}
|
||||||
134
src/core/scheduler.zig
Normal file
134
src/core/scheduler.zig
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||||
|
const Bus = @import("Bus.zig");
|
||||||
|
const Clock = @import("bus/gpio.zig").Clock;
|
||||||
|
|
||||||
|
const Order = std.math.Order;
|
||||||
|
const PriorityQueue = std.PriorityQueue;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const log = std.log.scoped(.Scheduler);
|
||||||
|
|
||||||
|
pub const Scheduler = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
tick: u64 = 0,
|
||||||
|
queue: PriorityQueue(Event, void, lessThan),
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator) Self {
|
||||||
|
var sched = Self{ .queue = PriorityQueue(Event, void, lessThan).init(allocator, {}) };
|
||||||
|
sched.queue.add(.{ .kind = .HeatDeath, .tick = std.math.maxInt(u64) }) catch unreachable;
|
||||||
|
|
||||||
|
return sched;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.queue.deinit();
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
// `std.PriorityQueue` provides no reset function, so we will just create a new one
|
||||||
|
const allocator = self.queue.allocator;
|
||||||
|
self.queue.deinit();
|
||||||
|
|
||||||
|
var new_queue = PriorityQueue(Event, void, lessThan).init(allocator, {});
|
||||||
|
new_queue.add(.{ .kind = .HeatDeath, .tick = std.math.maxInt(u64) }) catch unreachable;
|
||||||
|
|
||||||
|
self.* = .{ .queue = new_queue };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn now(self: *const Self) u64 {
|
||||||
|
return self.tick;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handleEvent(self: *Self, cpu: *Arm7tdmi) void {
|
||||||
|
const event = self.queue.remove();
|
||||||
|
const late = self.tick - event.tick;
|
||||||
|
|
||||||
|
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
|
||||||
|
|
||||||
|
switch (event.kind) {
|
||||||
|
.HeatDeath => {
|
||||||
|
log.err("u64 overflow. This *actually* should never happen.", .{});
|
||||||
|
unreachable;
|
||||||
|
},
|
||||||
|
.Draw => {
|
||||||
|
// The end of a VDraw
|
||||||
|
bus_ptr.ppu.drawScanline();
|
||||||
|
bus_ptr.ppu.onHdrawEnd(cpu, late);
|
||||||
|
},
|
||||||
|
.TimerOverflow => |id| {
|
||||||
|
switch (id) {
|
||||||
|
inline 0...3 => |idx| bus_ptr.tim[idx].onTimerExpire(cpu, late),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.ApuChannel => |id| {
|
||||||
|
switch (id) {
|
||||||
|
0 => bus_ptr.apu.ch1.onToneSweepEvent(late),
|
||||||
|
1 => bus_ptr.apu.ch2.onToneEvent(late),
|
||||||
|
2 => bus_ptr.apu.ch3.onWaveEvent(late),
|
||||||
|
3 => bus_ptr.apu.ch4.onNoiseEvent(late),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.RealTimeClock => {
|
||||||
|
const device = &bus_ptr.pak.gpio.device;
|
||||||
|
if (device.kind != .Rtc or device.ptr == null) return;
|
||||||
|
|
||||||
|
const clock: *Clock = @ptrCast(@alignCast(device.ptr.?));
|
||||||
|
clock.onClockUpdate(late);
|
||||||
|
},
|
||||||
|
.FrameSequencer => bus_ptr.apu.onSequencerTick(late),
|
||||||
|
.SampleAudio => bus_ptr.apu.sampleAudio(late),
|
||||||
|
.HBlank => bus_ptr.ppu.onHblankEnd(cpu, late), // The end of a HBlank
|
||||||
|
.VBlank => bus_ptr.ppu.onHdrawEnd(cpu, late), // The end of a VBlank
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the **first** scheduled event of type `needle`
|
||||||
|
pub fn removeScheduledEvent(self: *Self, needle: EventKind) void {
|
||||||
|
for (self.queue.items, 0..) |event, i| {
|
||||||
|
if (std.meta.eql(event.kind, needle)) {
|
||||||
|
|
||||||
|
// invalidates the slice we're iterating over
|
||||||
|
_ = self.queue.removeIndex(i);
|
||||||
|
|
||||||
|
// log.debug("Removed {?}@{}", .{ event.kind, event.tick });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(self: *Self, kind: EventKind, end: u64) void {
|
||||||
|
self.queue.add(.{ .kind = kind, .tick = self.now() + end }) catch unreachable;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn nextTimestamp(self: *const Self) u64 {
|
||||||
|
@setRuntimeSafety(false);
|
||||||
|
|
||||||
|
// Typically you'd use PriorityQueue.peek here, but there's always at least a HeatDeath
|
||||||
|
// event in the PQ so we can just do this instead. Should be faster in ReleaseSafe
|
||||||
|
return self.queue.items[0].tick;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Event = struct {
|
||||||
|
kind: EventKind,
|
||||||
|
tick: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn lessThan(_: void, a: Event, b: Event) Order {
|
||||||
|
return std.math.order(a.tick, b.tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const EventKind = union(enum) {
|
||||||
|
HeatDeath,
|
||||||
|
HBlank,
|
||||||
|
VBlank,
|
||||||
|
Draw,
|
||||||
|
TimerOverflow: u2,
|
||||||
|
SampleAudio,
|
||||||
|
FrameSequencer,
|
||||||
|
ApuChannel: u2,
|
||||||
|
RealTimeClock,
|
||||||
|
};
|
||||||
659
src/cpu.zig
659
src/cpu.zig
@@ -1,659 +0,0 @@
|
|||||||
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 File = std.fs.File;
|
|
||||||
|
|
||||||
// ARM Instruction Groups
|
|
||||||
const dataProcessing = @import("cpu/arm/data_processing.zig").dataProcessing;
|
|
||||||
const psrTransfer = @import("cpu/arm/psr_transfer.zig").psrTransfer;
|
|
||||||
const singleDataTransfer = @import("cpu/arm/single_data_transfer.zig").singleDataTransfer;
|
|
||||||
const halfAndSignedDataTransfer = @import("cpu/arm/half_signed_data_transfer.zig").halfAndSignedDataTransfer;
|
|
||||||
const blockDataTransfer = @import("cpu/arm/block_data_transfer.zig").blockDataTransfer;
|
|
||||||
const branch = @import("cpu/arm/branch.zig").branch;
|
|
||||||
const branchAndExchange = @import("cpu/arm/branch.zig").branchAndExchange;
|
|
||||||
const softwareInterrupt = @import("cpu/arm/software_interrupt.zig").softwareInterrupt;
|
|
||||||
const multiply = @import("cpu/arm/multiply.zig").multiply;
|
|
||||||
const multiplyLong = @import("cpu/arm/multiply_long.zig").multiplyLong;
|
|
||||||
const singleDataSwap = @import("cpu/arm/single_data_swap.zig").singleDataSwap;
|
|
||||||
|
|
||||||
// THUMB Instruction Groups
|
|
||||||
const format1 = @import("cpu/thumb/format1.zig").format1;
|
|
||||||
const format2 = @import("cpu/thumb/format2.zig").format2;
|
|
||||||
const format3 = @import("cpu/thumb/format3.zig").format3;
|
|
||||||
const format4 = @import("cpu/thumb/format4.zig").format4;
|
|
||||||
const format5 = @import("cpu/thumb/format5.zig").format5;
|
|
||||||
const format6 = @import("cpu/thumb/format6.zig").format6;
|
|
||||||
const format78 = @import("cpu/thumb/format78.zig").format78;
|
|
||||||
const format9 = @import("cpu/thumb/format9.zig").format9;
|
|
||||||
const format10 = @import("cpu/thumb/format10.zig").format10;
|
|
||||||
const format11 = @import("cpu/thumb/format11.zig").format11;
|
|
||||||
const format12 = @import("cpu/thumb/format12.zig").format12;
|
|
||||||
const format13 = @import("cpu/thumb/format13.zig").format13;
|
|
||||||
const format14 = @import("cpu/thumb/format14.zig").format14;
|
|
||||||
const format15 = @import("cpu/thumb/format15.zig").format15;
|
|
||||||
const format16 = @import("cpu/thumb/format16.zig").format16;
|
|
||||||
const format17 = @import("cpu/thumb/format17.zig").format17;
|
|
||||||
const format18 = @import("cpu/thumb/format18.zig").format18;
|
|
||||||
const format19 = @import("cpu/thumb/format19.zig").format19;
|
|
||||||
|
|
||||||
pub const ArmInstrFn = fn (*Arm7tdmi, *Bus, u32) void;
|
|
||||||
pub const ThumbInstrFn = fn (*Arm7tdmi, *Bus, u16) void;
|
|
||||||
const arm_lut: [0x1000]ArmInstrFn = armPopulate();
|
|
||||||
const thumb_lut: [0x400]ThumbInstrFn = thumbPopulate();
|
|
||||||
|
|
||||||
const enable_logging = @import("main.zig").enable_logging;
|
|
||||||
|
|
||||||
pub const Arm7tdmi = struct {
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
r: [16]u32,
|
|
||||||
sched: *Scheduler,
|
|
||||||
bus: *Bus,
|
|
||||||
cpsr: PSR,
|
|
||||||
spsr: PSR,
|
|
||||||
|
|
||||||
/// Storage for R8_fiq -> R12_fiq and their normal counterparts
|
|
||||||
/// e.g [r[0 + 8], fiq_r[0 + 8], r[1 + 8], fiq_r[1 + 8]...]
|
|
||||||
banked_fiq: [2 * 5]u32,
|
|
||||||
|
|
||||||
/// Storage for r13_<mode>, r14_<mode>
|
|
||||||
/// e.g. [r13, r14, r13_svc, r14_svc]
|
|
||||||
banked_r: [2 * 6]u32,
|
|
||||||
|
|
||||||
banked_spsr: [5]PSR,
|
|
||||||
|
|
||||||
log_file: ?*const File,
|
|
||||||
log_buf: [0x100]u8,
|
|
||||||
binary_log: bool,
|
|
||||||
|
|
||||||
pub fn init(sched: *Scheduler, bus: *Bus) Self {
|
|
||||||
return .{
|
|
||||||
.r = [_]u32{0x00} ** 16,
|
|
||||||
.sched = sched,
|
|
||||||
.bus = bus,
|
|
||||||
.cpsr = .{ .raw = 0x0000_00DF },
|
|
||||||
.spsr = .{ .raw = 0x0000_0000 },
|
|
||||||
.banked_fiq = [_]u32{0x00} ** 10,
|
|
||||||
.banked_r = [_]u32{0x00} ** 12,
|
|
||||||
.banked_spsr = [_]PSR{.{ .raw = 0x0000_0000 }} ** 5,
|
|
||||||
.log_file = null,
|
|
||||||
.log_buf = undefined,
|
|
||||||
.binary_log = false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn useLogger(self: *Self, file: *const File, is_binary: bool) void {
|
|
||||||
self.log_file = file;
|
|
||||||
self.binary_log = is_binary;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fn bankedIdx(mode: Mode) usize {
|
|
||||||
return switch (mode) {
|
|
||||||
.User, .System => 0,
|
|
||||||
.Supervisor => 1,
|
|
||||||
.Abort => 2,
|
|
||||||
.Undefined => 3,
|
|
||||||
.Irq => 4,
|
|
||||||
.Fiq => 5,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fn spsrIdx(mode: Mode) usize {
|
|
||||||
return switch (mode) {
|
|
||||||
.Supervisor => 0,
|
|
||||||
.Abort => 1,
|
|
||||||
.Undefined => 2,
|
|
||||||
.Irq => 3,
|
|
||||||
.Fiq => 4,
|
|
||||||
else => std.debug.panic("{} does not have a SPSR Register", .{mode}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub inline fn hasSPSR(self: *const Self) bool {
|
|
||||||
const mode = getMode(self.cpsr.mode.read()) orelse unreachable;
|
|
||||||
return switch (mode) {
|
|
||||||
.System, .User => false,
|
|
||||||
else => true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub inline fn isPrivileged(self: *const Self) bool {
|
|
||||||
const mode = getMode(self.cpsr.mode.read()) orelse unreachable;
|
|
||||||
return switch (mode) {
|
|
||||||
.User => false,
|
|
||||||
else => true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setCpsr(self: *Self, value: u32) void {
|
|
||||||
if (value & 0x1F != self.cpsr.raw & 0x1F) self.changeModeFromIdx(@truncate(u5, value & 0x1F));
|
|
||||||
self.cpsr.raw = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn changeModeFromIdx(self: *Self, next: u5) void {
|
|
||||||
const mode = getMode(next) orelse unreachable;
|
|
||||||
self.changeMode(mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setUserModeRegister(self: *Self, idx: usize, value: u32) void {
|
|
||||||
const current = getMode(self.cpsr.mode.read()) orelse unreachable;
|
|
||||||
|
|
||||||
if (idx < 8) {
|
|
||||||
self.r[idx] = value;
|
|
||||||
} else if (idx < 13) {
|
|
||||||
if (current == .Fiq) {
|
|
||||||
const user_offset: usize = 0;
|
|
||||||
self.banked_fiq[(idx - 8) * 2 + user_offset] = value;
|
|
||||||
} else self.r[idx] = value;
|
|
||||||
} else if (idx < 15) {
|
|
||||||
switch (current) {
|
|
||||||
.User, .System => self.r[idx] = value,
|
|
||||||
else => {
|
|
||||||
self.banked_r[bankedIdx(.User) * 2 + (idx - 13)] = value;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else self.r[idx] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getUserModeRegister(self: *Self, idx: usize) u32 {
|
|
||||||
const current = getMode(self.cpsr.mode.read()) orelse unreachable;
|
|
||||||
|
|
||||||
var result: u32 = undefined;
|
|
||||||
if (idx < 8) {
|
|
||||||
result = self.r[idx];
|
|
||||||
} else if (idx < 13) {
|
|
||||||
if (current == .Fiq) {
|
|
||||||
const user_offset: usize = 0;
|
|
||||||
result = self.banked_fiq[(idx - 8) * 2 + user_offset];
|
|
||||||
} else result = self.r[idx];
|
|
||||||
} else if (idx < 15) {
|
|
||||||
switch (current) {
|
|
||||||
.User, .System => result = self.r[idx],
|
|
||||||
else => {
|
|
||||||
result = self.banked_r[bankedIdx(.User) * 2 + (idx - 13)];
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else result = self.r[idx];
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn changeMode(self: *Self, next: Mode) void {
|
|
||||||
const now = getMode(self.cpsr.mode.read()) orelse unreachable;
|
|
||||||
|
|
||||||
// Bank R8 -> r12
|
|
||||||
var r: usize = 8;
|
|
||||||
while (r <= 12) : (r += 1) {
|
|
||||||
self.banked_fiq[(r - 8) * 2 + if (now == .Fiq) @as(usize, 1) else 0] = self.r[r];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bank r13, r14, SPSR
|
|
||||||
switch (now) {
|
|
||||||
.User, .System => {
|
|
||||||
self.banked_r[bankedIdx(now) * 2 + 0] = self.r[13];
|
|
||||||
self.banked_r[bankedIdx(now) * 2 + 1] = self.r[14];
|
|
||||||
},
|
|
||||||
else => {
|
|
||||||
self.banked_r[bankedIdx(now) * 2 + 0] = self.r[13];
|
|
||||||
self.banked_r[bankedIdx(now) * 2 + 1] = self.r[14];
|
|
||||||
self.banked_spsr[spsrIdx(now)] = self.spsr;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab R8 -> R12
|
|
||||||
r = 8;
|
|
||||||
while (r <= 12) : (r += 1) {
|
|
||||||
self.r[r] = self.banked_fiq[(r - 8) * 2 + if (next == .Fiq) @as(usize, 1) else 0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab r13, r14, SPSR
|
|
||||||
switch (next) {
|
|
||||||
.User, .System => {
|
|
||||||
self.r[13] = self.banked_r[bankedIdx(next) * 2 + 0];
|
|
||||||
self.r[14] = self.banked_r[bankedIdx(next) * 2 + 1];
|
|
||||||
// FIXME: Should we clear out SPSR?
|
|
||||||
},
|
|
||||||
else => {
|
|
||||||
self.r[13] = self.banked_r[bankedIdx(next) * 2 + 0];
|
|
||||||
self.r[14] = self.banked_r[bankedIdx(next) * 2 + 1];
|
|
||||||
self.spsr = self.banked_spsr[spsrIdx(next)];
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
self.cpsr.mode.write(@enumToInt(next));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fastBoot(self: *Self) void {
|
|
||||||
self.r[0] = 0x08000000;
|
|
||||||
self.r[1] = 0x000000EA;
|
|
||||||
// GPRs 2 -> 12 *should* already be 0 initialized
|
|
||||||
self.r[13] = 0x0300_7F00;
|
|
||||||
self.r[14] = 0x0000_0000;
|
|
||||||
self.r[15] = 0x0800_0000;
|
|
||||||
|
|
||||||
// Set r13_irq and r14_svc to their respective values
|
|
||||||
self.banked_r[bankedIdx(.Irq) * 2 + 0] = 0x0300_7FA0;
|
|
||||||
self.banked_r[bankedIdx(.Supervisor) * 2 + 0] = 0x0300_7FE0;
|
|
||||||
|
|
||||||
self.cpsr.raw = 0x6000001F;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn step(self: *Self) u64 {
|
|
||||||
if (self.cpsr.t.read()) {
|
|
||||||
const opcode = self.thumbFetch();
|
|
||||||
if (enable_logging) if (self.log_file) |file| self.log(file, @as(u32, opcode));
|
|
||||||
|
|
||||||
thumb_lut[thumbIdx(opcode)](self, self.bus, opcode);
|
|
||||||
} else {
|
|
||||||
const opcode = self.fetch();
|
|
||||||
if (enable_logging) if (self.log_file) |file| self.log(file, opcode);
|
|
||||||
|
|
||||||
if (checkCond(self.cpsr, @truncate(u4, opcode >> 28))) {
|
|
||||||
arm_lut[armIdx(opcode)](self, self.bus, opcode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn thumbFetch(self: *Self) u16 {
|
|
||||||
const halfword = self.bus.read16(self.r[15]);
|
|
||||||
self.r[15] += 2;
|
|
||||||
return halfword;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fetch(self: *Self) u32 {
|
|
||||||
const word = self.bus.read32(self.r[15]);
|
|
||||||
self.r[15] += 4;
|
|
||||||
return word;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fakePC(self: *const Self) u32 {
|
|
||||||
return self.r[15] + 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn log(self: *const Self, file: *const File, opcode: u32) void {
|
|
||||||
if (self.binary_log) {
|
|
||||||
self.skyLog(file) catch unreachable;
|
|
||||||
} else {
|
|
||||||
self.mgbaLog(file, opcode) catch unreachable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn panic(self: *const Self, comptime format: []const u8, args: anytype) noreturn {
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < 16) : (i += 4) {
|
|
||||||
const i_1 = i + 1;
|
|
||||||
const i_2 = i + 2;
|
|
||||||
const i_3 = i + 3;
|
|
||||||
std.debug.print("R{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\n", .{ i, self.r[i], i_1, self.r[i_1], i_2, self.r[i_2], i_3, self.r[i_3] });
|
|
||||||
}
|
|
||||||
std.debug.print("cpsr: 0x{X:0>8} ", .{self.cpsr.raw});
|
|
||||||
prettyPrintPsr(&self.cpsr);
|
|
||||||
|
|
||||||
std.debug.print("spsr: 0x{X:0>8} ", .{self.spsr.raw});
|
|
||||||
prettyPrintPsr(&self.spsr);
|
|
||||||
|
|
||||||
std.debug.print("tick: {}\n\n", .{self.sched.tick});
|
|
||||||
|
|
||||||
std.debug.panic(format, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prettyPrintPsr(psr: *const PSR) void {
|
|
||||||
std.debug.print("[", .{});
|
|
||||||
|
|
||||||
if (psr.n.read()) std.debug.print("N", .{}) else std.debug.print("-", .{});
|
|
||||||
if (psr.z.read()) std.debug.print("Z", .{}) else std.debug.print("-", .{});
|
|
||||||
if (psr.c.read()) std.debug.print("C", .{}) else std.debug.print("-", .{});
|
|
||||||
if (psr.v.read()) std.debug.print("V", .{}) else std.debug.print("-", .{});
|
|
||||||
if (psr.i.read()) std.debug.print("I", .{}) else std.debug.print("-", .{});
|
|
||||||
if (psr.f.read()) std.debug.print("F", .{}) else std.debug.print("-", .{});
|
|
||||||
if (psr.t.read()) std.debug.print("T", .{}) else std.debug.print("-", .{});
|
|
||||||
std.debug.print("|", .{});
|
|
||||||
if (getMode(psr.mode.read())) |mode| std.debug.print("{s}", .{modeString(mode)}) else std.debug.print("---", .{});
|
|
||||||
|
|
||||||
std.debug.print("]\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn modeString(mode: Mode) []const u8 {
|
|
||||||
return switch (mode) {
|
|
||||||
.User => "usr",
|
|
||||||
.Fiq => "fiq",
|
|
||||||
.Irq => "irq",
|
|
||||||
.Supervisor => "svc",
|
|
||||||
.Abort => "abt",
|
|
||||||
.Undefined => "und",
|
|
||||||
.System => "sys",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn skyLog(self: *const Self, file: *const File) !void {
|
|
||||||
var buf: [18 * @sizeOf(u32)]u8 = undefined;
|
|
||||||
|
|
||||||
// Write Registers
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < 0x10) : (i += 1) {
|
|
||||||
skyWrite(&buf, i, self.r[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
skyWrite(&buf, 0x10, self.cpsr.raw);
|
|
||||||
skyWrite(&buf, 0x11, if (self.hasSPSR()) self.spsr.raw else self.cpsr.raw);
|
|
||||||
_ = try file.writeAll(&buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn skyWrite(buf: []u8, i: usize, num: u32) void {
|
|
||||||
buf[(@sizeOf(u32) * i) + 3] = @truncate(u8, num >> 24 & 0xFF);
|
|
||||||
buf[(@sizeOf(u32) * i) + 2] = @truncate(u8, num >> 16 & 0xFF);
|
|
||||||
buf[(@sizeOf(u32) * i) + 1] = @truncate(u8, num >> 8 & 0xFF);
|
|
||||||
buf[(@sizeOf(u32) * i) + 0] = @truncate(u8, num >> 0 & 0xFF);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 tmp_opcode = self.bus.read32(self.r[15] - 2);
|
|
||||||
const be_opcode = tmp_opcode << 16 | tmp_opcode >> 16;
|
|
||||||
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, be_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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
inline fn armIdx(opcode: u32) u12 {
|
|
||||||
return @truncate(u12, opcode >> 20 & 0xFF) << 4 | @truncate(u12, opcode >> 4 & 0xF);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fn thumbIdx(opcode: u16) u10 {
|
|
||||||
return @truncate(u10, opcode >> 6);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn checkCond(cpsr: PSR, cond: u4) bool {
|
|
||||||
// TODO: Should I implement an enum?
|
|
||||||
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() and 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 => std.debug.panic("[CPU] 0xF is a reserved condition field", .{}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn thumbPopulate() [0x400]ThumbInstrFn {
|
|
||||||
return comptime {
|
|
||||||
@setEvalBranchQuota(0x5000);
|
|
||||||
var lut = [_]ThumbInstrFn{thumbUndefined} ** 0x400;
|
|
||||||
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < lut.len) : (i += 1) {
|
|
||||||
if (i >> 4 & 0x3F == 0b010000) {
|
|
||||||
const op = i & 0xF;
|
|
||||||
|
|
||||||
lut[i] = format4(op);
|
|
||||||
} else if (i >> 4 & 0x3F == 0b010001) {
|
|
||||||
const op = i >> 2 & 0x3;
|
|
||||||
const h1 = i >> 1 & 1;
|
|
||||||
const h2 = i & 1;
|
|
||||||
|
|
||||||
lut[i] = format5(op, h1, h2);
|
|
||||||
} else if (i >> 5 & 0x1F == 0b00011) {
|
|
||||||
const I = i >> 4 & 1 == 1;
|
|
||||||
const is_sub = i >> 3 & 1 == 1;
|
|
||||||
const rn = i & 0x7;
|
|
||||||
|
|
||||||
lut[i] = format2(I, is_sub, rn);
|
|
||||||
} else if (i >> 5 & 0x1F == 0b01001) {
|
|
||||||
const rd = i >> 2 & 0x7;
|
|
||||||
|
|
||||||
lut[i] = format6(rd);
|
|
||||||
} else if (i >> 6 & 0xF == 0b0101) {
|
|
||||||
const op = i >> 4 & 0x3;
|
|
||||||
const T = i >> 3 & 1 == 1;
|
|
||||||
|
|
||||||
lut[i] = format78(op, T);
|
|
||||||
} else if (i >> 7 & 0x7 == 0b000) {
|
|
||||||
const op = i >> 5 & 0x3;
|
|
||||||
const offset = i & 0x1F;
|
|
||||||
|
|
||||||
lut[i] = format1(op, offset);
|
|
||||||
} else if (i >> 7 & 0x7 == 0b001) {
|
|
||||||
const op = i >> 5 & 0x3;
|
|
||||||
const rd = i >> 2 & 0x7;
|
|
||||||
|
|
||||||
lut[i] = format3(op, rd);
|
|
||||||
} else if (i >> 7 & 0x7 == 0b011) {
|
|
||||||
const B = i >> 6 & 1 == 1;
|
|
||||||
const L = i >> 5 & 1 == 1;
|
|
||||||
const offset = i & 0x1F;
|
|
||||||
|
|
||||||
lut[i] = format9(B, L, offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i >> 2 & 0xFF == 0xB0) {
|
|
||||||
const S = i >> 1 & 1 == 1;
|
|
||||||
|
|
||||||
lut[i] = format13(S);
|
|
||||||
} else if (i >> 2 & 0xFF == 0xDF) {
|
|
||||||
lut[i] = format17();
|
|
||||||
} else if (i >> 6 & 0xF == 0b1000) {
|
|
||||||
const L = i >> 5 & 1 == 1;
|
|
||||||
const offset = i & 0x1F;
|
|
||||||
|
|
||||||
lut[i] = format10(L, offset);
|
|
||||||
} else if (i >> 6 & 0xF == 0b1001) {
|
|
||||||
const L = i >> 5 & 1 == 1;
|
|
||||||
const rd = i >> 2 & 0x3;
|
|
||||||
|
|
||||||
lut[i] = format11(L, rd);
|
|
||||||
} else if (i >> 6 & 0xF == 0b1010) {
|
|
||||||
const isSP = i >> 5 & 1 == 1;
|
|
||||||
const rd = i >> 2 & 0x7;
|
|
||||||
|
|
||||||
lut[i] = format12(isSP, rd);
|
|
||||||
} else if (i >> 6 & 0xF == 0b1011 and i >> 3 & 0x3 == 0b10) {
|
|
||||||
const L = i >> 5 & 1 == 1;
|
|
||||||
const R = i >> 2 & 1 == 1;
|
|
||||||
|
|
||||||
lut[i] = format14(L, R);
|
|
||||||
} else if (i >> 6 & 0xF == 0b1100) {
|
|
||||||
const L = i >> 5 & 1 == 1;
|
|
||||||
const rb = i >> 2 & 0x7;
|
|
||||||
|
|
||||||
lut[i] = format15(L, rb);
|
|
||||||
} else if (i >> 6 & 0xF == 0b1101) {
|
|
||||||
const cond = i >> 2 & 0xF;
|
|
||||||
|
|
||||||
lut[i] = format16(cond);
|
|
||||||
} else if (i >> 5 & 0x1F == 0b11100) {
|
|
||||||
lut[i] = format18();
|
|
||||||
} else if (i >> 6 & 0xF == 0b1111) {
|
|
||||||
const is_low = i >> 5 & 1 == 1;
|
|
||||||
|
|
||||||
lut[i] = format19(is_low);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lut;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn armPopulate() [0x1000]ArmInstrFn {
|
|
||||||
return comptime {
|
|
||||||
@setEvalBranchQuota(0xE000);
|
|
||||||
var lut = [_]ArmInstrFn{armUndefined} ** 0x1000;
|
|
||||||
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < lut.len) : (i += 1) {
|
|
||||||
// Instructions with Opcode[27] == 0
|
|
||||||
if (i == 0x121) {
|
|
||||||
// Bits 27:20 and 7:4
|
|
||||||
lut[i] = branchAndExchange;
|
|
||||||
} else if (i >> 6 & 0x3F == 0b000000 and i & 0xF == 0b1001) {
|
|
||||||
// Bits 27:22 and 7:4
|
|
||||||
const A = i >> 5 & 1 == 1;
|
|
||||||
const S = i >> 4 & 1 == 1;
|
|
||||||
|
|
||||||
lut[i] = multiply(A, S);
|
|
||||||
} else if (i >> 7 & 0x1F == 0b00010 and i >> 4 & 0x3 == 0b00 and i & 0xF == 0b1001) {
|
|
||||||
// Bits 27:23, 21:20 and 7:4
|
|
||||||
const B = i >> 6 & 1 == 1;
|
|
||||||
|
|
||||||
lut[i] = singleDataSwap(B);
|
|
||||||
} else if (i >> 7 & 0x1F == 0b00001 and i & 0xF == 0b1001) {
|
|
||||||
// Bits 27:23 and bits 7:4
|
|
||||||
const U = i >> 6 & 1 == 1;
|
|
||||||
const A = i >> 5 & 1 == 1;
|
|
||||||
const S = i >> 4 & 1 == 1;
|
|
||||||
|
|
||||||
lut[i] = multiplyLong(U, A, S);
|
|
||||||
} else if (i >> 9 & 0x7 == 0b000 and i >> 3 & 1 == 1 and i & 1 == 1) {
|
|
||||||
// Bits 27:25, 7 and 4
|
|
||||||
const P = i >> 8 & 1 == 1;
|
|
||||||
const U = i >> 7 & 1 == 1;
|
|
||||||
const I = i >> 6 & 1 == 1;
|
|
||||||
const W = i >> 5 & 1 == 1;
|
|
||||||
const L = i >> 4 & 1 == 1;
|
|
||||||
|
|
||||||
lut[i] = halfAndSignedDataTransfer(P, U, I, W, L);
|
|
||||||
} else if (i >> 9 & 0x7 == 0b011 and i & 1 == 1) {
|
|
||||||
// Bits 27:25 and 4
|
|
||||||
lut[i] = armUndefined;
|
|
||||||
} else if (i >> 10 & 0x3 == 0b00 and i >> 7 & 0x3 == 0b10 and i >> 4 & 1 == 0) {
|
|
||||||
// Bits 27:26, 24:23 and 20
|
|
||||||
const I = i >> 9 & 1 == 1;
|
|
||||||
const R = i >> 6 & 1 == 1;
|
|
||||||
const kind = i >> 4 & 0x3;
|
|
||||||
|
|
||||||
lut[i] = psrTransfer(I, R, kind);
|
|
||||||
} else if (i >> 10 & 0x3 == 0b01) {
|
|
||||||
// Bits 27:26
|
|
||||||
const I = i >> 9 & 1 == 1;
|
|
||||||
const P = i >> 8 & 1 == 1;
|
|
||||||
const U = i >> 7 & 1 == 1;
|
|
||||||
const B = i >> 6 & 1 == 1;
|
|
||||||
const W = i >> 5 & 1 == 1;
|
|
||||||
const L = i >> 4 & 1 == 1;
|
|
||||||
|
|
||||||
lut[i] = singleDataTransfer(I, P, U, B, W, L);
|
|
||||||
} else if (i >> 10 & 0x3 == 0b00) {
|
|
||||||
// Bits 27:26
|
|
||||||
const I = i >> 9 & 1 == 1;
|
|
||||||
const S = i >> 4 & 1 == 1;
|
|
||||||
const instrKind = i >> 5 & 0xF;
|
|
||||||
|
|
||||||
lut[i] = dataProcessing(I, S, instrKind);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Instructions with Opcode[27] == 1
|
|
||||||
if (i >> 8 & 0xF == 0b1110) {
|
|
||||||
// bits 27:24
|
|
||||||
// Coprocessor Data Opertation + Register Transfer
|
|
||||||
lut[i] = armUndefined;
|
|
||||||
} else if (i >> 9 & 0x7 == 0b100) {
|
|
||||||
// Bits 27:25
|
|
||||||
const P = i >> 8 & 1 == 1;
|
|
||||||
const U = i >> 7 & 1 == 1;
|
|
||||||
const S = i >> 6 & 1 == 1;
|
|
||||||
const W = i >> 5 & 1 == 1;
|
|
||||||
const L = i >> 4 & 1 == 1;
|
|
||||||
|
|
||||||
lut[i] = blockDataTransfer(P, U, S, W, L);
|
|
||||||
} else if (i >> 9 & 0x7 == 0b101) {
|
|
||||||
// Bits 27:25
|
|
||||||
const L = i >> 8 & 1 == 1;
|
|
||||||
lut[i] = branch(L);
|
|
||||||
} else if (i >> 9 & 0x7 == 0b110) {
|
|
||||||
// Bits 27:25
|
|
||||||
// Coprocessor Data Transfer
|
|
||||||
lut[i] = armUndefined;
|
|
||||||
} else if (i >> 8 & 0xF == 0b1111) {
|
|
||||||
// Bits 27:24
|
|
||||||
lut[i] = softwareInterrupt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lut;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const PSR = extern union {
|
|
||||||
mode: Bitfield(u32, 0, 5),
|
|
||||||
t: Bit(u32, 5),
|
|
||||||
f: Bit(u32, 6),
|
|
||||||
i: Bit(u32, 7),
|
|
||||||
v: Bit(u32, 28),
|
|
||||||
c: Bit(u32, 29),
|
|
||||||
z: Bit(u32, 30),
|
|
||||||
n: Bit(u32, 31),
|
|
||||||
raw: u32,
|
|
||||||
};
|
|
||||||
|
|
||||||
const Mode = enum(u5) {
|
|
||||||
User = 0b10000,
|
|
||||||
Fiq = 0b10001,
|
|
||||||
Irq = 0b10010,
|
|
||||||
Supervisor = 0b10011,
|
|
||||||
Abort = 0b10111,
|
|
||||||
Undefined = 0b11011,
|
|
||||||
System = 0b11111,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn getMode(bits: u5) ?Mode {
|
|
||||||
return std.meta.intToEnum(Mode, bits) catch null;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn armUndefined(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
|
||||||
const id = armIdx(opcode);
|
|
||||||
cpu.panic("[CPU:ARM] ID: 0x{X:0>3} 0x{X:0>8} is an illegal opcode", .{ id, opcode });
|
|
||||||
}
|
|
||||||
|
|
||||||
fn thumbUndefined(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
|
||||||
const id = thumbIdx(opcode);
|
|
||||||
cpu.panic("[CPU:THUMB] ID: 0b{b:0>10} 0x{X:0>2} is an illegal opcode", .{ id, opcode });
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ArmInstrFn;
|
|
||||||
|
|
||||||
pub fn blockDataTransfer(comptime P: bool, comptime U: bool, comptime S: bool, comptime W: bool, comptime L: bool) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u32) void {
|
|
||||||
const r15_present = opcode >> 15 & 1 == 1;
|
|
||||||
const rn = opcode >> 16 & 0xF;
|
|
||||||
const base = cpu.r[rn];
|
|
||||||
|
|
||||||
const in_list = opcode >> @truncate(u4, rn) & 1 == 1;
|
|
||||||
|
|
||||||
var address: u32 = base;
|
|
||||||
if (U) {
|
|
||||||
// Increment
|
|
||||||
var i: u5 = 0;
|
|
||||||
while (i < 0x10) : (i += 1) {
|
|
||||||
if (opcode >> i & 1 == 1) {
|
|
||||||
if (P) address += 4;
|
|
||||||
transfer(cpu, bus, r15_present, i, address);
|
|
||||||
if (!P) address += 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Decrement
|
|
||||||
|
|
||||||
var i: u5 = 0x10;
|
|
||||||
while (i > 0) : (i -= 1) {
|
|
||||||
const j = i - 1;
|
|
||||||
|
|
||||||
if (opcode >> j & 1 == 1) {
|
|
||||||
if (P) address -= 4;
|
|
||||||
transfer(cpu, bus, r15_present, j, address);
|
|
||||||
if (!P) address -= 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (W) {
|
|
||||||
if (!L or (L and !in_list)) {
|
|
||||||
cpu.r[rn] = address;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transfer(cpu: *Arm7tdmi, bus: *Bus, r15_present: bool, i: u5, address: u32) void {
|
|
||||||
if (L) {
|
|
||||||
if (S and !r15_present) {
|
|
||||||
// Always Transfer User mode Registers
|
|
||||||
cpu.setUserModeRegister(i, bus.read32(address));
|
|
||||||
} else {
|
|
||||||
const value = bus.read32(address);
|
|
||||||
cpu.r[i] = if (i == 0xF) value & 0xFFFF_FFFC else value;
|
|
||||||
if (S and i == 0xF) cpu.setCpsr(cpu.spsr.raw);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (S) {
|
|
||||||
// Always Transfer User mode Registers
|
|
||||||
// This happens regardless if r15 is in the list
|
|
||||||
const value = cpu.getUserModeRegister(i);
|
|
||||||
bus.write32(address, value + if (i == 0xF) 8 else @as(u32, 0)); // PC is already 4 ahead to make 12
|
|
||||||
} else {
|
|
||||||
bus.write32(address, cpu.r[i] + if (i == 0xF) 8 else @as(u32, 0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const util = @import("../../util.zig");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ArmInstrFn;
|
|
||||||
|
|
||||||
pub fn branch(comptime L: bool) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
|
||||||
if (L) {
|
|
||||||
// TODO: Debugging beeg.gba w/ MGBA seems to suggest that I don't do anything here
|
|
||||||
cpu.r[14] = cpu.r[15];
|
|
||||||
}
|
|
||||||
|
|
||||||
cpu.r[15] = cpu.fakePC() +% util.u32SignExtend(24, opcode << 2);
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn branchAndExchange(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
|
||||||
const rn = opcode & 0xF;
|
|
||||||
cpu.cpsr.t.write(cpu.r[rn] & 1 == 1);
|
|
||||||
|
|
||||||
// TODO: Is this how I should do it?
|
|
||||||
cpu.r[15] = cpu.r[rn] & 0xFFFF_FFFE;
|
|
||||||
}
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const shifter = @import("../barrel_shifter.zig");
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ArmInstrFn;
|
|
||||||
|
|
||||||
pub fn dataProcessing(comptime I: bool, comptime S: bool, comptime instrKind: u4) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
|
||||||
const rd = @truncate(u4, opcode >> 12 & 0xF);
|
|
||||||
const rn = opcode >> 16 & 0xF;
|
|
||||||
const old_carry = @boolToInt(cpu.cpsr.c.read());
|
|
||||||
|
|
||||||
// If certain conditions are met, PC is 12 ahead instead of 8
|
|
||||||
if (!I and opcode >> 4 & 1 == 1) cpu.r[15] += 4;
|
|
||||||
|
|
||||||
const op1 = if (rn == 0xF) cpu.fakePC() else cpu.r[rn];
|
|
||||||
|
|
||||||
var op2: u32 = undefined;
|
|
||||||
if (I) {
|
|
||||||
const amount = @truncate(u8, (opcode >> 8 & 0xF) << 1);
|
|
||||||
op2 = shifter.rotateRight(S, &cpu.cpsr, opcode & 0xFF, amount);
|
|
||||||
} else {
|
|
||||||
op2 = shifter.execute(S, cpu, opcode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Undo special condition from above
|
|
||||||
if (!I and opcode >> 4 & 1 == 1) cpu.r[15] -= 4;
|
|
||||||
|
|
||||||
switch (instrKind) {
|
|
||||||
0x0 => {
|
|
||||||
// AND
|
|
||||||
const result = op1 & op2;
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
setArmLogicOpFlags(S, cpu, rd, result);
|
|
||||||
},
|
|
||||||
0x1 => {
|
|
||||||
// EOR
|
|
||||||
const result = op1 ^ op2;
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
setArmLogicOpFlags(S, cpu, rd, result);
|
|
||||||
},
|
|
||||||
0x2 => {
|
|
||||||
// SUB
|
|
||||||
cpu.r[rd] = armSub(S, cpu, rd, op1, op2);
|
|
||||||
},
|
|
||||||
0x3 => {
|
|
||||||
// RSB
|
|
||||||
cpu.r[rd] = armSub(S, cpu, rd, op2, op1);
|
|
||||||
},
|
|
||||||
0x4 => {
|
|
||||||
// ADD
|
|
||||||
cpu.r[rd] = armAdd(S, cpu, rd, op1, op2);
|
|
||||||
},
|
|
||||||
0x5 => {
|
|
||||||
// ADC
|
|
||||||
cpu.r[rd] = armAdc(S, cpu, rd, op1, op2, old_carry);
|
|
||||||
},
|
|
||||||
0x6 => {
|
|
||||||
// SBC
|
|
||||||
cpu.r[rd] = armSbc(S, cpu, rd, op1, op2, old_carry);
|
|
||||||
},
|
|
||||||
0x7 => {
|
|
||||||
// RSC
|
|
||||||
cpu.r[rd] = armSbc(S, cpu, rd, op2, op1, old_carry);
|
|
||||||
},
|
|
||||||
0x8 => {
|
|
||||||
// TST
|
|
||||||
if (rd == 0xF) {
|
|
||||||
undefinedTestBehaviour(cpu);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = op1 & op2;
|
|
||||||
setTestOpFlags(S, cpu, opcode, result);
|
|
||||||
},
|
|
||||||
0x9 => {
|
|
||||||
// TEQ
|
|
||||||
if (rd == 0xF) {
|
|
||||||
undefinedTestBehaviour(cpu);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = op1 ^ op2;
|
|
||||||
setTestOpFlags(S, cpu, opcode, result);
|
|
||||||
},
|
|
||||||
0xA => {
|
|
||||||
// CMP
|
|
||||||
if (rd == 0xF) {
|
|
||||||
undefinedTestBehaviour(cpu);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cmp(cpu, op1, op2);
|
|
||||||
},
|
|
||||||
0xB => {
|
|
||||||
// CMN
|
|
||||||
if (rd == 0xF) {
|
|
||||||
undefinedTestBehaviour(cpu);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cmn(cpu, op1, op2);
|
|
||||||
},
|
|
||||||
0xC => {
|
|
||||||
// ORR
|
|
||||||
const result = op1 | op2;
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
setArmLogicOpFlags(S, cpu, rd, result);
|
|
||||||
},
|
|
||||||
0xD => {
|
|
||||||
// MOV
|
|
||||||
cpu.r[rd] = op2;
|
|
||||||
setArmLogicOpFlags(S, cpu, rd, op2);
|
|
||||||
},
|
|
||||||
0xE => {
|
|
||||||
// BIC
|
|
||||||
const result = op1 & ~op2;
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
setArmLogicOpFlags(S, cpu, rd, result);
|
|
||||||
},
|
|
||||||
0xF => {
|
|
||||||
// MVN
|
|
||||||
const result = ~op2;
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
setArmLogicOpFlags(S, cpu, rd, result);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn armSbc(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32, old_carry: u1) u32 {
|
|
||||||
var result: u32 = undefined;
|
|
||||||
if (S and rd == 0xF) {
|
|
||||||
result = sbc(false, cpu, left, right, old_carry);
|
|
||||||
cpu.setCpsr(cpu.spsr.raw);
|
|
||||||
} else {
|
|
||||||
result = sbc(S, cpu, left, right, old_carry);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sbc(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32, old_carry: u1) u32 {
|
|
||||||
// TODO: Make your own version (thanks peach.bot)
|
|
||||||
const subtrahend = @as(u64, right) -% old_carry +% 1;
|
|
||||||
const result = @truncate(u32, left -% subtrahend);
|
|
||||||
|
|
||||||
if (S) {
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
cpu.cpsr.c.write(subtrahend <= left);
|
|
||||||
cpu.cpsr.v.write(((left ^ result) & (~right ^ result)) >> 31 & 1 == 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn armSub(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32) u32 {
|
|
||||||
var result: u32 = undefined;
|
|
||||||
if (S and rd == 0xF) {
|
|
||||||
result = sub(false, cpu, left, right);
|
|
||||||
cpu.setCpsr(cpu.spsr.raw);
|
|
||||||
} else {
|
|
||||||
result = sub(S, cpu, left, right);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sub(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32) u32 {
|
|
||||||
const result = left -% right;
|
|
||||||
|
|
||||||
if (S) {
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
cpu.cpsr.c.write(right <= left);
|
|
||||||
cpu.cpsr.v.write(((left ^ result) & (~right ^ result)) >> 31 & 1 == 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn armAdd(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32) u32 {
|
|
||||||
var result: u32 = undefined;
|
|
||||||
if (S and rd == 0xF) {
|
|
||||||
result = add(false, cpu, left, right);
|
|
||||||
cpu.setCpsr(cpu.spsr.raw);
|
|
||||||
} else {
|
|
||||||
result = add(S, cpu, left, right);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32) u32 {
|
|
||||||
var result: u32 = undefined;
|
|
||||||
const didOverflow = @addWithOverflow(u32, left, right, &result);
|
|
||||||
|
|
||||||
if (S) {
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
cpu.cpsr.c.write(didOverflow);
|
|
||||||
cpu.cpsr.v.write(((left ^ result) & (right ^ result)) >> 31 & 1 == 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn armAdc(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32, old_carry: u1) u32 {
|
|
||||||
var result: u32 = undefined;
|
|
||||||
if (S and rd == 0xF) {
|
|
||||||
result = adc(false, cpu, left, right, old_carry);
|
|
||||||
cpu.setCpsr(cpu.spsr.raw);
|
|
||||||
} else {
|
|
||||||
result = adc(S, cpu, left, right, old_carry);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn adc(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32, old_carry: u1) u32 {
|
|
||||||
var result: u32 = undefined;
|
|
||||||
const did = @addWithOverflow(u32, left, right, &result);
|
|
||||||
const overflow = @addWithOverflow(u32, result, old_carry, &result);
|
|
||||||
|
|
||||||
if (S) {
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
cpu.cpsr.c.write(did or overflow);
|
|
||||||
cpu.cpsr.v.write(((left ^ result) & (right ^ result)) >> 31 & 1 == 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cmp(cpu: *Arm7tdmi, left: u32, right: u32) void {
|
|
||||||
const result = left -% right;
|
|
||||||
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
cpu.cpsr.c.write(right <= left);
|
|
||||||
cpu.cpsr.v.write(((left ^ result) & (~right ^ result)) >> 31 & 1 == 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cmn(cpu: *Arm7tdmi, left: u32, right: u32) void {
|
|
||||||
var result: u32 = undefined;
|
|
||||||
const didOverflow = @addWithOverflow(u32, left, right, &result);
|
|
||||||
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
cpu.cpsr.c.write(didOverflow);
|
|
||||||
cpu.cpsr.v.write(((left ^ result) & (right ^ result)) >> 31 & 1 == 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setArmLogicOpFlags(comptime S: bool, cpu: *Arm7tdmi, rd: u4, result: u32) void {
|
|
||||||
if (S and rd == 0xF) {
|
|
||||||
cpu.setCpsr(cpu.spsr.raw);
|
|
||||||
} else {
|
|
||||||
setLogicOpFlags(S, cpu, result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setLogicOpFlags(comptime S: bool, cpu: *Arm7tdmi, result: u32) void {
|
|
||||||
if (S) {
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
// C set by Barrel Shifter, V is unaffected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setTestOpFlags(comptime S: bool, cpu: *Arm7tdmi, opcode: u32, result: u32) void {
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
// Barrel Shifter should always calc CPSR C in TST
|
|
||||||
if (!S) _ = shifter.execute(true, cpu, opcode);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn undefinedTestBehaviour(cpu: *Arm7tdmi) void {
|
|
||||||
@setCold(true);
|
|
||||||
cpu.setCpsr(cpu.spsr.raw);
|
|
||||||
cpu.r[15] += 4; // FIXME: This is objectively wrong I think
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const util = @import("../../util.zig");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ArmInstrFn;
|
|
||||||
|
|
||||||
pub fn halfAndSignedDataTransfer(comptime P: bool, comptime U: bool, comptime I: bool, comptime W: bool, comptime L: bool) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u32) void {
|
|
||||||
const rn = opcode >> 16 & 0xF;
|
|
||||||
const rd = opcode >> 12 & 0xF;
|
|
||||||
const rm = opcode & 0xF;
|
|
||||||
const imm_offset_high = opcode >> 8 & 0xF;
|
|
||||||
|
|
||||||
var base: u32 = undefined;
|
|
||||||
if (rn == 0xF) {
|
|
||||||
base = cpu.fakePC();
|
|
||||||
if (!L) base += 4;
|
|
||||||
} else {
|
|
||||||
base = cpu.r[rn];
|
|
||||||
}
|
|
||||||
|
|
||||||
var offset: u32 = undefined;
|
|
||||||
if (I) {
|
|
||||||
offset = imm_offset_high << 4 | rm;
|
|
||||||
} else {
|
|
||||||
offset = cpu.r[rm];
|
|
||||||
}
|
|
||||||
|
|
||||||
const modified_base = if (U) base +% offset else base -% offset;
|
|
||||||
var address = if (P) modified_base else base;
|
|
||||||
|
|
||||||
var result: u32 = undefined;
|
|
||||||
if (L) {
|
|
||||||
switch (@truncate(u2, opcode >> 5)) {
|
|
||||||
0b01 => {
|
|
||||||
// LDRH
|
|
||||||
const value = bus.read16(address & 0xFFFF_FFFE);
|
|
||||||
result = std.math.rotr(u32, @as(u32, value), 8 * (address & 1));
|
|
||||||
},
|
|
||||||
0b10 => {
|
|
||||||
// LDRSB
|
|
||||||
result = util.u32SignExtend(8, @as(u32, bus.read8(address)));
|
|
||||||
},
|
|
||||||
0b11 => {
|
|
||||||
// LDRSH
|
|
||||||
result = util.u32SignExtend(16, @as(u32, bus.read16(address & 0xFFFF_FFFE)));
|
|
||||||
},
|
|
||||||
0b00 => unreachable, // SWP
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (opcode >> 5 & 0x01 == 0x01) {
|
|
||||||
// STRH
|
|
||||||
bus.write16(address, @truncate(u16, cpu.r[rd]));
|
|
||||||
} else unreachable; // SWP
|
|
||||||
}
|
|
||||||
|
|
||||||
address = modified_base;
|
|
||||||
if (W and P or !P) cpu.r[rn] = address;
|
|
||||||
if (L) cpu.r[rd] = result; // // This emulates the LDR rd == rn behaviour
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ArmInstrFn;
|
|
||||||
|
|
||||||
pub fn multiply(comptime A: bool, comptime S: bool) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
|
||||||
const rd = opcode >> 16 & 0xF;
|
|
||||||
const rn = opcode >> 12 & 0xF;
|
|
||||||
const rs = opcode >> 8 & 0xF;
|
|
||||||
const rm = opcode & 0xF;
|
|
||||||
|
|
||||||
const temp: u64 = @as(u64, cpu.r[rm]) * @as(u64, cpu.r[rs]) + if (A) cpu.r[rn] else 0;
|
|
||||||
const result = @truncate(u32, temp);
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
|
|
||||||
if (S) {
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
// V is unaffected, C is *actually* undefined in ARMv4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ArmInstrFn;
|
|
||||||
|
|
||||||
pub fn multiplyLong(comptime U: bool, comptime A: bool, comptime S: bool) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
|
||||||
const rd_hi = opcode >> 16 & 0xF;
|
|
||||||
const rd_lo = opcode >> 12 & 0xF;
|
|
||||||
const rs = opcode >> 8 & 0xF;
|
|
||||||
const rm = opcode & 0xF;
|
|
||||||
|
|
||||||
if (U) {
|
|
||||||
// Signed (WHY IS IT U THEN?)
|
|
||||||
var result: i64 = @as(i64, @bitCast(i32, cpu.r[rm])) * @as(i64, @bitCast(i32, cpu.r[rs]));
|
|
||||||
if (A) result +%= @bitCast(i64, @as(u64, cpu.r[rd_hi]) << 32 | @as(u64, cpu.r[rd_lo]));
|
|
||||||
|
|
||||||
cpu.r[rd_hi] = @bitCast(u32, @truncate(i32, result >> 32));
|
|
||||||
cpu.r[rd_lo] = @bitCast(u32, @truncate(i32, result));
|
|
||||||
} else {
|
|
||||||
// Unsigned
|
|
||||||
var result: u64 = @as(u64, cpu.r[rm]) * @as(u64, cpu.r[rs]);
|
|
||||||
if (A) result +%= @as(u64, cpu.r[rd_hi]) << 32 | @as(u64, cpu.r[rd_lo]);
|
|
||||||
|
|
||||||
cpu.r[rd_hi] = @truncate(u32, result >> 32);
|
|
||||||
cpu.r[rd_lo] = @truncate(u32, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (S) {
|
|
||||||
cpu.cpsr.z.write(cpu.r[rd_hi] == 0 and cpu.r[rd_lo] == 0);
|
|
||||||
cpu.cpsr.n.write(cpu.r[rd_hi] >> 31 & 1 == 1);
|
|
||||||
// C and V are set to meaningless values
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ArmInstrFn;
|
|
||||||
const PSR = @import("../../cpu.zig").PSR;
|
|
||||||
|
|
||||||
pub fn psrTransfer(comptime I: bool, comptime R: bool, comptime kind: u2) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
|
||||||
switch (kind) {
|
|
||||||
0b00 => {
|
|
||||||
// MRS
|
|
||||||
const rd = opcode >> 12 & 0xF;
|
|
||||||
|
|
||||||
if (R and !cpu.hasSPSR()) std.log.warn("[CPU/PSR Transfer] Tried to read SPSR from User/System Mode", .{});
|
|
||||||
cpu.r[rd] = if (R) cpu.spsr.raw else cpu.cpsr.raw;
|
|
||||||
},
|
|
||||||
0b10 => {
|
|
||||||
// MSR
|
|
||||||
const field_mask = @truncate(u4, opcode >> 16 & 0xF);
|
|
||||||
const rm_idx = opcode & 0xF;
|
|
||||||
const right = if (I) std.math.rotr(u32, opcode & 0xFF, (opcode >> 8 & 0xF) << 1) else cpu.r[rm_idx];
|
|
||||||
|
|
||||||
if (R and !cpu.hasSPSR()) std.log.warn("[CPU/PSR Transfer] Tried to write to SPSR User/System Mode", .{});
|
|
||||||
|
|
||||||
if (R) {
|
|
||||||
if (cpu.isPrivileged()) cpu.spsr.raw = fieldMask(&cpu.spsr, field_mask, right);
|
|
||||||
} else {
|
|
||||||
if (cpu.isPrivileged()) cpu.setCpsr(fieldMask(&cpu.cpsr, field_mask, right));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
else => cpu.panic("[CPU/PSR Transfer] Bits 21:220 of {X:0>8} are undefined", .{opcode}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fieldMask(psr: *const PSR, field_mask: u4, right: u32) u32 {
|
|
||||||
const bits = @truncate(u2, (field_mask >> 2 & 0x2) | (field_mask & 1));
|
|
||||||
|
|
||||||
const mask: u32 = switch (bits) {
|
|
||||||
0b00 => 0x0000_0000,
|
|
||||||
0b01 => 0x0000_00FF,
|
|
||||||
0b10 => 0xF000_0000,
|
|
||||||
0b11 => 0xF000_00FF,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (psr.raw & ~mask) | (right & mask);
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ArmInstrFn;
|
|
||||||
|
|
||||||
pub fn singleDataSwap(comptime B: bool) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u32) void {
|
|
||||||
const rn = opcode >> 16 & 0xF;
|
|
||||||
const rd = opcode >> 12 & 0xF;
|
|
||||||
const rm = opcode & 0xF;
|
|
||||||
|
|
||||||
const address = cpu.r[rn];
|
|
||||||
|
|
||||||
if (B) {
|
|
||||||
// SWPB
|
|
||||||
const value = bus.read8(address);
|
|
||||||
bus.write8(address, @truncate(u8, cpu.r[rm]));
|
|
||||||
cpu.r[rd] = value;
|
|
||||||
} else {
|
|
||||||
// SWP
|
|
||||||
const value = std.math.rotr(u32, bus.read32(address), 8 * (address & 0x3));
|
|
||||||
bus.write32(address, cpu.r[rm]);
|
|
||||||
cpu.r[rd] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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").ArmInstrFn;
|
|
||||||
|
|
||||||
pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool, comptime B: bool, comptime W: bool, comptime L: bool) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u32) void {
|
|
||||||
const rn = opcode >> 16 & 0xF;
|
|
||||||
const rd = opcode >> 12 & 0xF;
|
|
||||||
|
|
||||||
var base: u32 = undefined;
|
|
||||||
if (rn == 0xF) {
|
|
||||||
base = cpu.fakePC();
|
|
||||||
if (!L) base += 4; // Offset of 12
|
|
||||||
} else {
|
|
||||||
base = cpu.r[rn];
|
|
||||||
}
|
|
||||||
|
|
||||||
const offset = if (I) shifter.immShift(false, cpu, opcode) else opcode & 0xFFF;
|
|
||||||
|
|
||||||
const modified_base = if (U) base +% offset else base -% offset;
|
|
||||||
var address = if (P) modified_base else base;
|
|
||||||
|
|
||||||
var result: u32 = undefined;
|
|
||||||
if (L) {
|
|
||||||
if (B) {
|
|
||||||
// LDRB
|
|
||||||
result = bus.read8(address);
|
|
||||||
} else {
|
|
||||||
// LDR
|
|
||||||
const value = bus.read32(address & 0xFFFF_FFFC);
|
|
||||||
result = std.math.rotr(u32, value, 8 * (address & 0x3));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (B) {
|
|
||||||
// STRB
|
|
||||||
bus.write8(address, @truncate(u8, cpu.r[rd]));
|
|
||||||
} else {
|
|
||||||
// STR
|
|
||||||
const force_aligned = address & 0xFFFF_FFFC;
|
|
||||||
bus.write32(force_aligned, cpu.r[rd]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
address = modified_base;
|
|
||||||
if (W and P or !P) cpu.r[rn] = address;
|
|
||||||
if (L) cpu.r[rd] = result; // This emulates the LDR rd == rn behaviour
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ArmInstrFn;
|
|
||||||
|
|
||||||
pub fn softwareInterrupt() InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, _: u32) void {
|
|
||||||
// Copy Values from Current Mode
|
|
||||||
const r15 = cpu.r[15];
|
|
||||||
const cpsr = cpu.cpsr.raw;
|
|
||||||
|
|
||||||
// Switch Mode
|
|
||||||
cpu.changeMode(.Supervisor);
|
|
||||||
cpu.cpsr.t.write(false); // Force ARM Mode
|
|
||||||
cpu.cpsr.i.write(true); // Disable normal interrupts
|
|
||||||
|
|
||||||
cpu.r[14] = r15; // Resume Execution
|
|
||||||
cpu.spsr.raw = cpsr; // Previous mode CPSR
|
|
||||||
cpu.r[15] = 0x0000_0008;
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
|
||||||
const CPSR = @import("../cpu.zig").PSR;
|
|
||||||
|
|
||||||
pub fn execute(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
|
||||||
var result: u32 = undefined;
|
|
||||||
if (opcode >> 4 & 1 == 1) {
|
|
||||||
result = registerShift(S, cpu, opcode);
|
|
||||||
} else {
|
|
||||||
result = immShift(S, cpu, opcode);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn registerShift(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
|
||||||
const rs_idx = opcode >> 8 & 0xF;
|
|
||||||
const rs = @truncate(u8, cpu.r[rs_idx]);
|
|
||||||
|
|
||||||
const rm_idx = opcode & 0xF;
|
|
||||||
const rm = if (rm_idx == 0xF) cpu.fakePC() else cpu.r[rm_idx];
|
|
||||||
|
|
||||||
return switch (@truncate(u2, opcode >> 5)) {
|
|
||||||
0b00 => logicalLeft(S, &cpu.cpsr, rm, rs),
|
|
||||||
0b01 => logicalRight(S, &cpu.cpsr, rm, rs),
|
|
||||||
0b10 => arithmeticRight(S, &cpu.cpsr, rm, rs),
|
|
||||||
0b11 => rotateRight(S, &cpu.cpsr, rm, rs),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn immShift(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
|
||||||
const amount = @truncate(u8, opcode >> 7 & 0x1F);
|
|
||||||
|
|
||||||
const rm_idx = opcode & 0xF;
|
|
||||||
const rm = if (rm_idx == 0xF) cpu.fakePC() else cpu.r[rm_idx];
|
|
||||||
|
|
||||||
var result: u32 = undefined;
|
|
||||||
if (amount == 0) {
|
|
||||||
switch (@truncate(u2, opcode >> 5)) {
|
|
||||||
0b00 => {
|
|
||||||
// LSL #0
|
|
||||||
result = rm;
|
|
||||||
},
|
|
||||||
0b01 => {
|
|
||||||
// LSR #0 aka LSR #32
|
|
||||||
if (S) cpu.cpsr.c.write(rm >> 31 & 1 == 1);
|
|
||||||
result = 0x0000_0000;
|
|
||||||
},
|
|
||||||
0b10 => {
|
|
||||||
// ASR #0 aka ASR #32
|
|
||||||
result = @bitCast(u32, @bitCast(i32, rm) >> 31);
|
|
||||||
if (S) cpu.cpsr.c.write(result >> 31 & 1 == 1);
|
|
||||||
},
|
|
||||||
0b11 => {
|
|
||||||
// ROR #0 aka RRX
|
|
||||||
const carry: u32 = @boolToInt(cpu.cpsr.c.read());
|
|
||||||
if (S) cpu.cpsr.c.write(rm & 1 == 1);
|
|
||||||
|
|
||||||
result = (carry << 31) | (rm >> 1);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch (@truncate(u2, opcode >> 5)) {
|
|
||||||
0b00 => result = logicalLeft(S, &cpu.cpsr, rm, amount),
|
|
||||||
0b01 => result = logicalRight(S, &cpu.cpsr, rm, amount),
|
|
||||||
0b10 => result = arithmeticRight(S, &cpu.cpsr, rm, amount),
|
|
||||||
0b11 => result = rotateRight(S, &cpu.cpsr, rm, amount),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn logicalLeft(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
|
||||||
const amount = @truncate(u5, total_amount);
|
|
||||||
const bit_count: u8 = @typeInfo(u32).Int.bits;
|
|
||||||
|
|
||||||
var result: u32 = 0x0000_0000;
|
|
||||||
if (total_amount < bit_count) {
|
|
||||||
// We can perform a well-defined shift here
|
|
||||||
result = rm << amount;
|
|
||||||
|
|
||||||
if (S and total_amount != 0) {
|
|
||||||
const carry_bit = @truncate(u5, bit_count - amount);
|
|
||||||
cpsr.c.write(rm >> carry_bit & 1 == 1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (S) {
|
|
||||||
if (total_amount == bit_count) {
|
|
||||||
// Shifted all bits out, carry bit is bit 0 of rm
|
|
||||||
cpsr.c.write(rm & 1 == 1);
|
|
||||||
} else {
|
|
||||||
cpsr.c.write(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn logicalRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u32) u32 {
|
|
||||||
const amount = @truncate(u5, total_amount);
|
|
||||||
const bit_count: u8 = @typeInfo(u32).Int.bits;
|
|
||||||
|
|
||||||
var result: u32 = 0x0000_0000;
|
|
||||||
if (total_amount < bit_count) {
|
|
||||||
// We can perform a well-defined shift
|
|
||||||
result = rm >> amount;
|
|
||||||
if (S and total_amount != 0) cpsr.c.write(rm >> (amount - 1) & 1 == 1);
|
|
||||||
} else {
|
|
||||||
if (S) {
|
|
||||||
if (total_amount == bit_count) {
|
|
||||||
// LSR #32
|
|
||||||
cpsr.c.write(rm >> 31 & 1 == 1);
|
|
||||||
} else {
|
|
||||||
// All bits have been shifted out, including carry bit
|
|
||||||
cpsr.c.write(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn arithmeticRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
|
||||||
const amount = @truncate(u5, total_amount);
|
|
||||||
const bit_count: u8 = @typeInfo(u32).Int.bits;
|
|
||||||
|
|
||||||
var result: u32 = 0x0000_0000;
|
|
||||||
if (total_amount < bit_count) {
|
|
||||||
result = @bitCast(u32, @bitCast(i32, rm) >> amount);
|
|
||||||
if (S and total_amount != 0) cpsr.c.write(rm >> (amount - 1) & 1 == 1);
|
|
||||||
} else {
|
|
||||||
// ASR #32 and ASR #>32 have the same result
|
|
||||||
result = @bitCast(u32, @bitCast(i32, rm) >> 31);
|
|
||||||
if (S) cpsr.c.write(result >> 31 & 1 == 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rotateRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
|
||||||
const result = std.math.rotr(u32, rm, total_amount);
|
|
||||||
|
|
||||||
if (S and total_amount != 0) {
|
|
||||||
cpsr.c.write(result >> 31 & 1 == 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ThumbInstrFn;
|
|
||||||
const shifter = @import("../barrel_shifter.zig");
|
|
||||||
|
|
||||||
const setLogicOpFlags = @import("../arm/data_processing.zig").setLogicOpFlags;
|
|
||||||
|
|
||||||
pub fn format1(comptime op: u2, comptime offset: u5) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
|
||||||
const rs = opcode >> 3 & 0x7;
|
|
||||||
const rd = opcode & 0x7;
|
|
||||||
|
|
||||||
const result = switch (op) {
|
|
||||||
0b00 => blk: {
|
|
||||||
// LSL
|
|
||||||
if (offset == 0) {
|
|
||||||
break :blk cpu.r[rs];
|
|
||||||
} else {
|
|
||||||
break :blk shifter.logicalLeft(true, &cpu.cpsr, cpu.r[rs], offset);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
0b01 => blk: {
|
|
||||||
// LSR
|
|
||||||
if (offset == 0) {
|
|
||||||
cpu.cpsr.c.write(cpu.r[rs] >> 31 & 1 == 1);
|
|
||||||
break :blk @as(u32, 0);
|
|
||||||
} else {
|
|
||||||
break :blk shifter.logicalRight(true, &cpu.cpsr, cpu.r[rs], offset);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
0b10 => blk: {
|
|
||||||
// ASR
|
|
||||||
if (offset == 0) {
|
|
||||||
cpu.cpsr.c.write(cpu.r[rs] >> 31 & 1 == 1);
|
|
||||||
break :blk @bitCast(u32, @bitCast(i32, cpu.r[rs]) >> 31);
|
|
||||||
} else {
|
|
||||||
break :blk shifter.arithmeticRight(true, &cpu.cpsr, cpu.r[rs], offset);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
else => cpu.panic("[CPU|THUMB|Fmt1] {} is an invalid op", .{op}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Equivalent to an ARM MOVS
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
setLogicOpFlags(true, cpu, result);
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ThumbInstrFn;
|
|
||||||
|
|
||||||
pub fn format10(comptime L: bool, comptime offset: u5) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
|
|
||||||
const rb = opcode >> 3 & 0x7;
|
|
||||||
const rd = opcode & 0x7;
|
|
||||||
|
|
||||||
const address = cpu.r[rb] + (offset << 1);
|
|
||||||
|
|
||||||
if (L) {
|
|
||||||
// LDRH
|
|
||||||
const value = bus.read16(address & 0xFFFF_FFFE);
|
|
||||||
cpu.r[rd] = std.math.rotr(u32, @as(u32, value), 8 * (address & 1));
|
|
||||||
} else {
|
|
||||||
// STRH
|
|
||||||
bus.write16(address & 0xFFFF_FFFE, @truncate(u16, cpu.r[rd]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ThumbInstrFn;
|
|
||||||
|
|
||||||
pub fn format11(comptime L: bool, comptime rd: u3) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
|
|
||||||
const offset = (opcode & 0xFF) << 2;
|
|
||||||
const address = cpu.r[13] + offset;
|
|
||||||
|
|
||||||
if (L) {
|
|
||||||
const value = bus.read32(address & 0xFFFF_FFFC);
|
|
||||||
cpu.r[rd] = std.math.rotr(u32, value, 8 * (address & 0x3));
|
|
||||||
} else {
|
|
||||||
bus.write32(address & 0xFFFF_FFFC, cpu.r[rd]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ThumbInstrFn;
|
|
||||||
|
|
||||||
pub fn format12(comptime isSP: bool, comptime rd: u3) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
|
||||||
// ADD
|
|
||||||
const left = if (isSP) cpu.r[13] else (cpu.r[15] + 2) & 0xFFFF_FFFD;
|
|
||||||
const right = (opcode & 0xFF) << 2;
|
|
||||||
const result = left + right; // TODO: What about overflows?
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ThumbInstrFn;
|
|
||||||
|
|
||||||
pub fn format13(comptime S: bool) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
|
||||||
const offset = (opcode & 0x7F) << 2;
|
|
||||||
cpu.r[13] = if (S) cpu.r[13] - offset else cpu.r[13] + offset;
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ThumbInstrFn;
|
|
||||||
|
|
||||||
pub fn format14(comptime L: bool, comptime R: bool) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
|
|
||||||
var address: u32 = undefined;
|
|
||||||
if (L) {
|
|
||||||
// POP
|
|
||||||
address = cpu.r[13];
|
|
||||||
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < 8) : (i += 1) {
|
|
||||||
if ((opcode >> @truncate(u3, i)) & 1 == 1) {
|
|
||||||
cpu.r[i] = bus.read32(address);
|
|
||||||
address += 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (R) {
|
|
||||||
const value = bus.read32(address);
|
|
||||||
cpu.r[15] = value & 0xFFFF_FFFE;
|
|
||||||
address += 4;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
address = cpu.r[13] - 4;
|
|
||||||
|
|
||||||
if (R) {
|
|
||||||
bus.write32(address, cpu.r[14]);
|
|
||||||
address -= 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
var i: usize = 8;
|
|
||||||
while (i > 0) : (i -= 1) {
|
|
||||||
const j = i - 1;
|
|
||||||
|
|
||||||
if ((opcode >> @truncate(u3, j)) & 1 == 1) {
|
|
||||||
bus.write32(address, cpu.r[j]);
|
|
||||||
address -= 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cpu.r[13] = address + if (!L) 4 else 0;
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ThumbInstrFn;
|
|
||||||
|
|
||||||
pub fn format15(comptime L: bool, comptime rb: u3) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
|
|
||||||
const base = cpu.r[rb];
|
|
||||||
|
|
||||||
var address: u32 = base;
|
|
||||||
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < 8) : (i += 1) {
|
|
||||||
if ((opcode >> @truncate(u3, i)) & 1 == 1) {
|
|
||||||
if (L) {
|
|
||||||
cpu.r[i] = bus.read32(address);
|
|
||||||
} else {
|
|
||||||
bus.write32(address, cpu.r[i]);
|
|
||||||
}
|
|
||||||
address += 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cpu.r[rb] = address;
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ThumbInstrFn;
|
|
||||||
|
|
||||||
const checkCond = @import("../../cpu.zig").checkCond;
|
|
||||||
const u32SignExtend = @import("../../util.zig").u32SignExtend;
|
|
||||||
|
|
||||||
pub fn format16(comptime cond: u4) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
|
||||||
// B
|
|
||||||
const offset = u32SignExtend(8, opcode & 0xFF) << 1;
|
|
||||||
|
|
||||||
const should_execute = switch (cond) {
|
|
||||||
0xE, 0xF => cpu.panic("[CPU/THUMB] Undefined conditional branch with condition {}", .{cond}),
|
|
||||||
else => checkCond(cpu.cpsr, cond),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (should_execute) {
|
|
||||||
cpu.r[15] = (cpu.r[15] + 2) +% offset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ThumbInstrFn;
|
|
||||||
|
|
||||||
pub fn format17() InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, _: u16) void {
|
|
||||||
// Copy Values from Current Mode
|
|
||||||
const r15 = cpu.r[15];
|
|
||||||
const cpsr = cpu.cpsr.raw;
|
|
||||||
|
|
||||||
// Switch Mode
|
|
||||||
cpu.changeMode(.Supervisor);
|
|
||||||
cpu.cpsr.t.write(false); // Force ARM Mode
|
|
||||||
cpu.cpsr.i.write(true); // Disable normal interrupts
|
|
||||||
|
|
||||||
cpu.r[14] = r15; // Resume Execution
|
|
||||||
cpu.spsr.raw = cpsr; // Previous mode CPSR
|
|
||||||
cpu.r[15] = 0x0000_0008;
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ThumbInstrFn;
|
|
||||||
const u32SignExtend = @import("../../util.zig").u32SignExtend;
|
|
||||||
|
|
||||||
pub fn format18() InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
|
||||||
const offset = u32SignExtend(11, opcode & 0x7FF) << 1;
|
|
||||||
cpu.r[15] = (cpu.r[15] + 2) +% offset;
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ThumbInstrFn;
|
|
||||||
const u32SignExtend = @import("../../util.zig").u32SignExtend;
|
|
||||||
|
|
||||||
pub fn format19(comptime is_low: bool) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
|
||||||
// BL
|
|
||||||
const offset = opcode & 0x7FF;
|
|
||||||
|
|
||||||
if (is_low) {
|
|
||||||
// Instruction 2
|
|
||||||
const old_pc = cpu.r[15];
|
|
||||||
|
|
||||||
cpu.r[15] = cpu.r[14] + (offset << 1);
|
|
||||||
cpu.r[14] = old_pc | 1;
|
|
||||||
} else {
|
|
||||||
// Instruction 1
|
|
||||||
cpu.r[14] = (cpu.r[15] + 2) +% (u32SignExtend(11, @as(u32, offset)) << 12);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ThumbInstrFn;
|
|
||||||
|
|
||||||
const add = @import("../arm/data_processing.zig").add;
|
|
||||||
const sub = @import("../arm/data_processing.zig").sub;
|
|
||||||
|
|
||||||
pub fn format2(comptime I: bool, is_sub: bool, rn: u3) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
|
||||||
const rs = opcode >> 3 & 0x7;
|
|
||||||
const rd = @truncate(u3, opcode);
|
|
||||||
|
|
||||||
if (is_sub) {
|
|
||||||
// SUB
|
|
||||||
cpu.r[rd] = if (I) blk: {
|
|
||||||
break :blk sub(true, cpu, cpu.r[rs], @as(u32, rn));
|
|
||||||
} else blk: {
|
|
||||||
break :blk sub(true, cpu, cpu.r[rs], cpu.r[rn]);
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// ADD
|
|
||||||
cpu.r[rd] = if (I) blk: {
|
|
||||||
break :blk add(true, cpu, cpu.r[rs], @as(u32, rn));
|
|
||||||
} else blk: {
|
|
||||||
break :blk add(true, cpu, cpu.r[rs], cpu.r[rn]);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ThumbInstrFn;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
pub fn format3(comptime op: u2, comptime rd: u3) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
|
||||||
const offset = @truncate(u8, opcode);
|
|
||||||
|
|
||||||
switch (op) {
|
|
||||||
0b00 => {
|
|
||||||
// MOV
|
|
||||||
cpu.r[rd] = offset;
|
|
||||||
setLogicOpFlags(true, cpu, offset);
|
|
||||||
},
|
|
||||||
0b01 => cmp(cpu, cpu.r[rd], offset), // CMP
|
|
||||||
0b10 => cpu.r[rd] = add(true, cpu, cpu.r[rd], offset), // ADD
|
|
||||||
0b11 => cpu.r[rd] = sub(true, cpu, cpu.r[rd], offset), // SUB
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ThumbInstrFn;
|
|
||||||
const shifter = @import("../barrel_shifter.zig");
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
pub fn format4(comptime op: u4) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
|
||||||
const rs = opcode >> 3 & 0x7;
|
|
||||||
const rd = opcode & 0x7;
|
|
||||||
const carry = @boolToInt(cpu.cpsr.c.read());
|
|
||||||
|
|
||||||
switch (op) {
|
|
||||||
0x0 => {
|
|
||||||
// AND
|
|
||||||
const result = cpu.r[rd] & cpu.r[rs];
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
setLogicOpFlags(true, cpu, result);
|
|
||||||
},
|
|
||||||
0x1 => {
|
|
||||||
// EOR
|
|
||||||
const result = cpu.r[rd] ^ cpu.r[rs];
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
setLogicOpFlags(true, cpu, result);
|
|
||||||
},
|
|
||||||
0x2 => {
|
|
||||||
// LSL
|
|
||||||
const result = shifter.logicalLeft(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
setLogicOpFlags(true, cpu, result);
|
|
||||||
},
|
|
||||||
0x3 => {
|
|
||||||
// LSR
|
|
||||||
const result = shifter.logicalRight(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
setLogicOpFlags(true, cpu, result);
|
|
||||||
},
|
|
||||||
0x4 => {
|
|
||||||
// ASR
|
|
||||||
const result = shifter.arithmeticRight(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
setLogicOpFlags(true, cpu, result);
|
|
||||||
},
|
|
||||||
0x5 => {
|
|
||||||
// ADC
|
|
||||||
cpu.r[rd] = adc(true, cpu, cpu.r[rd], cpu.r[rs], carry);
|
|
||||||
},
|
|
||||||
0x6 => {
|
|
||||||
// SBC
|
|
||||||
cpu.r[rd] = sbc(true, cpu, cpu.r[rd], cpu.r[rs], carry);
|
|
||||||
},
|
|
||||||
0x7 => {
|
|
||||||
// ROR
|
|
||||||
const result = shifter.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); // FIXME: Barrel Shifter?
|
|
||||||
},
|
|
||||||
0x9 => {
|
|
||||||
// NEG
|
|
||||||
cpu.r[rd] = sub(true, cpu, 0, cpu.r[rs]);
|
|
||||||
},
|
|
||||||
0xA => {
|
|
||||||
// CMP
|
|
||||||
cmp(cpu, cpu.r[rd], cpu.r[rs]);
|
|
||||||
},
|
|
||||||
0xB => {
|
|
||||||
// CMN
|
|
||||||
cmn(cpu, cpu.r[rd], cpu.r[rs]);
|
|
||||||
},
|
|
||||||
0xC => {
|
|
||||||
// ORR
|
|
||||||
const result = cpu.r[rd] | cpu.r[rs];
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
setLogicOpFlags(true, cpu, result);
|
|
||||||
},
|
|
||||||
0xD => {
|
|
||||||
// MUL
|
|
||||||
const temp = @as(u64, cpu.r[rs]) * @as(u64, cpu.r[rd]);
|
|
||||||
const result = @truncate(u32, temp);
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
// V is unaffected, assuming similar behaviour to ARMv4 MUL C is undefined
|
|
||||||
},
|
|
||||||
0xE => {
|
|
||||||
// BIC
|
|
||||||
const result = cpu.r[rd] & ~cpu.r[rs];
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
setLogicOpFlags(true, cpu, result);
|
|
||||||
},
|
|
||||||
0xF => {
|
|
||||||
// MVN
|
|
||||||
const result = ~cpu.r[rs];
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
setLogicOpFlags(true, cpu, result);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ThumbInstrFn;
|
|
||||||
|
|
||||||
const cmp = @import("../arm/data_processing.zig").cmp;
|
|
||||||
const add = @import("../arm/data_processing.zig").add;
|
|
||||||
|
|
||||||
pub fn format5(comptime op: u2, comptime h1: u1, comptime h2: u1) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
|
||||||
const src_idx = @as(u4, h2) << 3 | (opcode >> 3 & 0x7);
|
|
||||||
const dst_idx = @as(u4, h1) << 3 | (opcode & 0x7);
|
|
||||||
|
|
||||||
const src = if (src_idx == 0xF) (cpu.r[src_idx] + 2) & 0xFFFF_FFFE else cpu.r[src_idx];
|
|
||||||
const dst = if (dst_idx == 0xF) (cpu.r[dst_idx] + 2) & 0xFFFF_FFFE else cpu.r[dst_idx];
|
|
||||||
|
|
||||||
switch (op) {
|
|
||||||
0b00 => cpu.r[dst_idx] = add(false, cpu, dst, src) & if (dst_idx == 0xF) 0xFFFF_FFFC else @as(u32, 0xFFF_FFFF), // ADD
|
|
||||||
0b01 => cmp(cpu, dst, src), // CMP
|
|
||||||
0b10 => {
|
|
||||||
// MOV
|
|
||||||
cpu.r[dst_idx] = if (dst_idx == 0xF) src & 0xFFFF_FFFC else src;
|
|
||||||
},
|
|
||||||
0b11 => {
|
|
||||||
// BX
|
|
||||||
cpu.cpsr.t.write(src & 1 == 1);
|
|
||||||
cpu.r[15] = src & 0xFFFF_FFFE;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ThumbInstrFn;
|
|
||||||
|
|
||||||
pub fn format6(comptime rd: u3) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
|
|
||||||
// LDR
|
|
||||||
const offset = (opcode & 0xFF) << 2;
|
|
||||||
|
|
||||||
// FIXME: Should this overflow?
|
|
||||||
cpu.r[rd] = bus.read32((cpu.r[15] + 2 & 0xFFFF_FFFD) + offset);
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ThumbInstrFn;
|
|
||||||
const u32SignExtend = @import("../../util.zig").u32SignExtend;
|
|
||||||
|
|
||||||
pub fn format78(comptime op: u2, comptime T: bool) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
|
|
||||||
const ro = opcode >> 6 & 0x7;
|
|
||||||
const rb = opcode >> 3 & 0x7;
|
|
||||||
const rd = opcode & 0x7;
|
|
||||||
|
|
||||||
const address = cpu.r[rb] + cpu.r[ro];
|
|
||||||
|
|
||||||
if (T) {
|
|
||||||
switch (op) {
|
|
||||||
0b00 => {
|
|
||||||
// STRH
|
|
||||||
bus.write16(address & 0xFFFF_FFFE, @truncate(u16, cpu.r[rd]));
|
|
||||||
},
|
|
||||||
0b01 => {
|
|
||||||
// LDRH
|
|
||||||
const value = bus.read16(address & 0xFFFF_FFFE);
|
|
||||||
cpu.r[rd] = std.math.rotr(u32, @as(u32, value), 8 * (address & 1));
|
|
||||||
},
|
|
||||||
0b10 => {
|
|
||||||
// LDSB
|
|
||||||
cpu.r[rd] = u32SignExtend(8, @as(u32, bus.read8(address)));
|
|
||||||
},
|
|
||||||
0b11 => {
|
|
||||||
// LDSH
|
|
||||||
cpu.r[rd] = u32SignExtend(16, @as(u32, bus.read16(address & 0xFFFF_FFFE)));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch (op) {
|
|
||||||
0b00 => {
|
|
||||||
// STR
|
|
||||||
bus.write32(address & 0xFFFF_FFFC, cpu.r[rd]);
|
|
||||||
},
|
|
||||||
0b01 => {
|
|
||||||
// STRB
|
|
||||||
bus.write8(address, @truncate(u8, cpu.r[rd]));
|
|
||||||
},
|
|
||||||
0b10 => {
|
|
||||||
// LDR
|
|
||||||
const value = bus.read32(address & 0xFFFF_FFFC);
|
|
||||||
cpu.r[rd] = std.math.rotr(u32, value, 8 * (address & 0x3));
|
|
||||||
},
|
|
||||||
0b11 => {
|
|
||||||
// LDRB
|
|
||||||
cpu.r[rd] = bus.read8(address);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|
||||||
const InstrFn = @import("../../cpu.zig").ThumbInstrFn;
|
|
||||||
|
|
||||||
pub fn format9(comptime B: bool, comptime L: bool, comptime offset: u5) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
|
|
||||||
const rb = opcode >> 3 & 0x7;
|
|
||||||
const rd = opcode & 0x7;
|
|
||||||
|
|
||||||
if (L) {
|
|
||||||
if (B) {
|
|
||||||
// LDRB
|
|
||||||
const address = cpu.r[rb] + offset;
|
|
||||||
cpu.r[rd] = bus.read8(address);
|
|
||||||
} else {
|
|
||||||
// LDR
|
|
||||||
const address = cpu.r[rb] + (@as(u32, offset) << 2);
|
|
||||||
const value = bus.read32(address & 0xFFFF_FFFC);
|
|
||||||
cpu.r[rd] = std.math.rotr(u32, value, 8 * (address & 0x3));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (B) {
|
|
||||||
// STRB
|
|
||||||
const address = cpu.r[rb] + offset;
|
|
||||||
bus.write8(address, @truncate(u8, cpu.r[rd]));
|
|
||||||
} else {
|
|
||||||
// STR
|
|
||||||
const address = cpu.r[rb] + (@as(u32, offset) << 2);
|
|
||||||
bus.write32(address & 0xFFFF_FFFC, cpu.r[rd]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.inner;
|
|
||||||
}
|
|
||||||
26
src/emu.zig
26
src/emu.zig
@@ -1,26 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("Bus.zig");
|
|
||||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
|
||||||
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
|
||||||
|
|
||||||
const Atomic = std.atomic.Atomic;
|
|
||||||
const cycles_per_frame: u64 = 160 * (308 * 4);
|
|
||||||
|
|
||||||
pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi, bus: *Bus) void {
|
|
||||||
var cycles: u64 = 0;
|
|
||||||
while (cycles < cycles_per_frame) : (cycles += 1) {
|
|
||||||
sched.tick += 1;
|
|
||||||
_ = cpu.step();
|
|
||||||
|
|
||||||
while (sched.tick >= sched.nextTimestamp()) {
|
|
||||||
sched.handleEvent(cpu, bus);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn runEmuThread(quit: *Atomic(bool), pause: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, bus: *Bus) void {
|
|
||||||
while (!quit.load(.Unordered)) {
|
|
||||||
if (!pause.load(.Unordered)) runFrame(sched, cpu, bus);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
516
src/imgui.zig
Normal file
516
src/imgui.zig
Normal file
@@ -0,0 +1,516 @@
|
|||||||
|
//! Namespace for dealing with ZBA's immediate-mode GUI
|
||||||
|
//! Currently, ZBA uses zgui from https://github.com/michal-z/zig-gamedev
|
||||||
|
//! which provides Zig bindings for https://github.com/ocornut/imgui under the hood
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const zgui = @import("zgui");
|
||||||
|
const gl = @import("gl");
|
||||||
|
const nfd = @import("nfd");
|
||||||
|
const config = @import("config.zig");
|
||||||
|
const emu = @import("core/emu.zig");
|
||||||
|
|
||||||
|
const Gui = @import("platform.zig").Gui;
|
||||||
|
const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||||
|
const Scheduler = @import("core/scheduler.zig").Scheduler;
|
||||||
|
const Bus = @import("core/Bus.zig");
|
||||||
|
const Synchro = @import("core/emu.zig").Synchro;
|
||||||
|
|
||||||
|
const RingBuffer = @import("zba_util").RingBuffer;
|
||||||
|
const Dimensions = @import("platform.zig").Dimensions;
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const GLuint = gl.uint;
|
||||||
|
|
||||||
|
const gba_width = @import("core/ppu.zig").width;
|
||||||
|
const gba_height = @import("core/ppu.zig").height;
|
||||||
|
|
||||||
|
const log = std.log.scoped(.Imgui);
|
||||||
|
|
||||||
|
// two seconds worth of fps values into the past
|
||||||
|
const histogram_len = 0x80;
|
||||||
|
|
||||||
|
/// Immediate-Mode GUI State
|
||||||
|
pub const State = struct {
|
||||||
|
title: [12:0]u8,
|
||||||
|
|
||||||
|
fps_hist: RingBuffer(u32),
|
||||||
|
should_quit: bool = false,
|
||||||
|
emulation: Emulation,
|
||||||
|
|
||||||
|
win_stat: WindowStatus = .{},
|
||||||
|
|
||||||
|
const WindowStatus = struct {
|
||||||
|
show_deps: bool = false,
|
||||||
|
show_regs: bool = false,
|
||||||
|
show_schedule: bool = false,
|
||||||
|
show_perf: bool = false,
|
||||||
|
show_palette: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Emulation = union(enum) {
|
||||||
|
Active,
|
||||||
|
Inactive,
|
||||||
|
Transition: enum { Active, Inactive },
|
||||||
|
};
|
||||||
|
|
||||||
|
/// if zba is initialized with a ROM already provided, this initializer should be called
|
||||||
|
/// with `title_opt` being non-null
|
||||||
|
pub fn init(allocator: Allocator, title_opt: ?*const [12]u8) !@This() {
|
||||||
|
const history = try allocator.alloc(u32, histogram_len);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.title = handleTitle(title_opt),
|
||||||
|
.emulation = if (title_opt == null) .Inactive else .{ .Transition = .Active },
|
||||||
|
.fps_hist = RingBuffer(u32).init(history),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *@This(), allocator: Allocator) void {
|
||||||
|
allocator.free(self.fps_hist.buf);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn draw(state: *State, sync: *Synchro, dim: Dimensions, cpu: *const Arm7tdmi, tex_id: GLuint) bool {
|
||||||
|
const scn_scale = config.config().host.win_scale;
|
||||||
|
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
|
||||||
|
|
||||||
|
zgui.backend.newFrame(dim.width, dim.height);
|
||||||
|
|
||||||
|
state.title = handleTitle(&bus_ptr.pak.title);
|
||||||
|
|
||||||
|
{
|
||||||
|
_ = zgui.beginMainMenuBar();
|
||||||
|
defer zgui.endMainMenuBar();
|
||||||
|
|
||||||
|
if (zgui.beginMenu("File", true)) {
|
||||||
|
defer zgui.endMenu();
|
||||||
|
|
||||||
|
if (zgui.menuItem("Quit", .{}))
|
||||||
|
state.should_quit = true;
|
||||||
|
|
||||||
|
if (zgui.menuItem("Insert ROM", .{})) blk: {
|
||||||
|
const file_path = tmp: {
|
||||||
|
const path_opt = nfd.openFileDialog("gba", null) catch |e| {
|
||||||
|
log.err("file dialog failed to open: {}", .{e});
|
||||||
|
break :blk;
|
||||||
|
};
|
||||||
|
|
||||||
|
break :tmp path_opt orelse {
|
||||||
|
log.warn("did not receive a file path", .{});
|
||||||
|
break :blk;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
defer nfd.freePath(file_path);
|
||||||
|
|
||||||
|
log.info("user chose: \"{s}\"", .{file_path});
|
||||||
|
|
||||||
|
const message = tmp: {
|
||||||
|
var msg: Synchro.Message = .{ .rom_path = undefined };
|
||||||
|
@memcpy(msg.rom_path[0..file_path.len], file_path);
|
||||||
|
break :tmp msg;
|
||||||
|
};
|
||||||
|
|
||||||
|
sync.ch.push(message) catch |e| {
|
||||||
|
log.err("failed to send file path to emu thread: {}", .{e});
|
||||||
|
break :blk;
|
||||||
|
};
|
||||||
|
|
||||||
|
state.emulation = .{ .Transition = .Active };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zgui.menuItem("Load BIOS", .{})) blk: {
|
||||||
|
const file_path = tmp: {
|
||||||
|
const path_opt = nfd.openFileDialog("bin", null) catch |e| {
|
||||||
|
log.err("file dialog failed to open: {}", .{e});
|
||||||
|
break :blk;
|
||||||
|
};
|
||||||
|
|
||||||
|
break :tmp path_opt orelse {
|
||||||
|
log.warn("did not receive a file path", .{});
|
||||||
|
break :blk;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
defer nfd.freePath(file_path);
|
||||||
|
|
||||||
|
log.info("user chose: \"{s}\"", .{file_path});
|
||||||
|
|
||||||
|
const message = tmp: {
|
||||||
|
var msg: Synchro.Message = .{ .bios_path = undefined };
|
||||||
|
@memcpy(msg.bios_path[0..file_path.len], file_path);
|
||||||
|
break :tmp msg;
|
||||||
|
};
|
||||||
|
|
||||||
|
sync.ch.push(message) catch |e| {
|
||||||
|
log.err("failed to send file path to emu thread: {}", .{e});
|
||||||
|
break :blk;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zgui.beginMenu("Emulation", true)) {
|
||||||
|
defer zgui.endMenu();
|
||||||
|
|
||||||
|
if (zgui.menuItem("Registers", .{ .selected = state.win_stat.show_regs }))
|
||||||
|
state.win_stat.show_regs = true;
|
||||||
|
|
||||||
|
if (zgui.menuItem("Palette", .{ .selected = state.win_stat.show_palette }))
|
||||||
|
state.win_stat.show_palette = true;
|
||||||
|
|
||||||
|
if (zgui.menuItem("Schedule", .{ .selected = state.win_stat.show_schedule }))
|
||||||
|
state.win_stat.show_schedule = true;
|
||||||
|
|
||||||
|
if (zgui.menuItem("Paused", .{ .selected = state.emulation == .Inactive })) {
|
||||||
|
state.emulation = switch (state.emulation) {
|
||||||
|
.Active => .{ .Transition = .Inactive },
|
||||||
|
.Inactive => .{ .Transition = .Active },
|
||||||
|
else => state.emulation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zgui.menuItem("Restart", .{}))
|
||||||
|
sync.ch.push(.restart) catch |e| log.err("failed to send restart req to emu thread: {}", .{e});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zgui.beginMenu("Stats", true)) {
|
||||||
|
defer zgui.endMenu();
|
||||||
|
|
||||||
|
if (zgui.menuItem("Performance", .{ .selected = state.win_stat.show_perf }))
|
||||||
|
state.win_stat.show_perf = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zgui.beginMenu("Help", true)) {
|
||||||
|
defer zgui.endMenu();
|
||||||
|
|
||||||
|
if (zgui.menuItem("Dependencies", .{ .selected = state.win_stat.show_deps }))
|
||||||
|
state.win_stat.show_deps = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const w: f32 = @floatFromInt(gba_width * scn_scale);
|
||||||
|
const h: f32 = @floatFromInt(gba_height * scn_scale);
|
||||||
|
|
||||||
|
const window_title = std.mem.sliceTo(&state.title, 0);
|
||||||
|
_ = zgui.begin(window_title, .{ .flags = .{ .no_resize = true, .always_auto_resize = true } });
|
||||||
|
defer zgui.end();
|
||||||
|
|
||||||
|
// FIXME(paoda): go look at some documentation here
|
||||||
|
zgui.image(.{ .tex_data = null, .tex_id = @enumFromInt(tex_id) }, .{ .w = w, .h = h });
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Any other steps to respect the copyright of the libraries I use?
|
||||||
|
if (state.win_stat.show_deps) {
|
||||||
|
_ = zgui.begin("Dependencies", .{ .popen = &state.win_stat.show_deps });
|
||||||
|
defer zgui.end();
|
||||||
|
|
||||||
|
zgui.bulletText("known-folders by ziglibs", .{});
|
||||||
|
zgui.bulletText("nfd-zig ported by Fabio Arnold", .{});
|
||||||
|
{
|
||||||
|
zgui.indent(.{});
|
||||||
|
defer zgui.unindent(.{});
|
||||||
|
|
||||||
|
zgui.bulletText("nativefiledialog by Michael Labbe", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
zgui.bulletText("SDL ported by Carl Åstholm", .{});
|
||||||
|
{
|
||||||
|
zgui.indent(.{});
|
||||||
|
defer zgui.unindent(.{});
|
||||||
|
|
||||||
|
zgui.bulletText("SDL by Sam Lantinga", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
// zgui.bulletText("tomlz by Matthew Hall", .{});
|
||||||
|
zgui.bulletText("zba-gdbstub by Rekai Musuka", .{});
|
||||||
|
zgui.bulletText("zba-util by Rekai Musuka", .{});
|
||||||
|
zgui.bulletText("zgui ported by Michal Ziulek et al.", .{});
|
||||||
|
{
|
||||||
|
zgui.indent(.{});
|
||||||
|
defer zgui.unindent(.{});
|
||||||
|
|
||||||
|
zgui.bulletText("DearImGui by Omar Cornut", .{});
|
||||||
|
}
|
||||||
|
zgui.bulletText("zig-clap by Jimmi Holst Christensen", .{});
|
||||||
|
zgui.bulletText("zig-datetime by Jairus Martin", .{});
|
||||||
|
|
||||||
|
zgui.newLine();
|
||||||
|
zgui.bulletText("bitfield.zig by Hannes Bredberg et al.", .{});
|
||||||
|
zgui.bulletText("zigglgen ported by Carl Åstholm", .{});
|
||||||
|
{
|
||||||
|
zgui.indent(.{});
|
||||||
|
defer zgui.unindent(.{});
|
||||||
|
|
||||||
|
zgui.bulletText("OpenGL-Registry by The Khronos Group", .{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.win_stat.show_regs) {
|
||||||
|
_ = zgui.begin("Guest Registers", .{ .popen = &state.win_stat.show_regs });
|
||||||
|
defer zgui.end();
|
||||||
|
|
||||||
|
for (0..8) |i| {
|
||||||
|
zgui.text("R{}: 0x{X:0>8}", .{ i, cpu.r[i] });
|
||||||
|
|
||||||
|
zgui.sameLine(.{});
|
||||||
|
|
||||||
|
const padding = if (8 + i < 10) " " else "";
|
||||||
|
zgui.text("{s}R{}: 0x{X:0>8}", .{ padding, 8 + i, cpu.r[8 + i] });
|
||||||
|
}
|
||||||
|
|
||||||
|
zgui.separator();
|
||||||
|
|
||||||
|
widgets.psr("CPSR", cpu.cpsr);
|
||||||
|
widgets.psr("SPSR", cpu.spsr);
|
||||||
|
|
||||||
|
zgui.separator();
|
||||||
|
|
||||||
|
widgets.interrupts(" IE", bus_ptr.io.ie);
|
||||||
|
widgets.interrupts("IRQ", bus_ptr.io.irq);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.win_stat.show_perf) {
|
||||||
|
_ = zgui.begin("Performance", .{ .popen = &state.win_stat.show_perf });
|
||||||
|
defer zgui.end();
|
||||||
|
|
||||||
|
// const tmp = blk: {
|
||||||
|
// var buf: [histogram_len]u32 = undefined;
|
||||||
|
// const len = state.fps_hist.copy(&buf);
|
||||||
|
|
||||||
|
// break :blk .{ buf, len };
|
||||||
|
// };
|
||||||
|
// const values = tmp[0];
|
||||||
|
// const len = tmp[1];
|
||||||
|
|
||||||
|
// if (len == values.len) _ = state.fps_hist.pop();
|
||||||
|
|
||||||
|
// const sorted = blk: {
|
||||||
|
// var buf: @TypeOf(values) = undefined;
|
||||||
|
|
||||||
|
// @memcpy(buf[0..len], values[0..len]);
|
||||||
|
// std.mem.sort(u32, buf[0..len], {}, std.sort.asc(u32));
|
||||||
|
|
||||||
|
// break :blk buf;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const y_max: f64 = 2 * if (len != 0) @as(f64, @floatFromInt(sorted[len - 1])) else emu.frame_rate;
|
||||||
|
// const x_max: f64 = @floatFromInt(values.len);
|
||||||
|
|
||||||
|
// const y_args = .{ .flags = .{ .no_grid_lines = true } };
|
||||||
|
// const x_args = .{ .flags = .{ .no_grid_lines = true, .no_tick_labels = true, .no_tick_marks = true } };
|
||||||
|
|
||||||
|
// if (zgui.plot.beginPlot("Emulation FPS", .{ .w = 0.0, .flags = .{ .no_title = true, .no_frame = true } })) {
|
||||||
|
// defer zgui.plot.endPlot();
|
||||||
|
|
||||||
|
// zgui.plot.setupLegend(.{ .north = true, .east = true }, .{});
|
||||||
|
// zgui.plot.setupAxis(.x1, x_args);
|
||||||
|
// zgui.plot.setupAxis(.y1, y_args);
|
||||||
|
// zgui.plot.setupAxisLimits(.y1, .{ .min = 0.0, .max = y_max, .cond = .always });
|
||||||
|
// zgui.plot.setupAxisLimits(.x1, .{ .min = 0.0, .max = x_max, .cond = .always });
|
||||||
|
// zgui.plot.setupFinish();
|
||||||
|
|
||||||
|
// zgui.plot.plotLineValues("FPS", u32, .{ .v = values[0..len] });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const stats: struct { u32, u32, u32 } = blk: {
|
||||||
|
// if (len == 0) break :blk .{ 0, 0, 0 };
|
||||||
|
|
||||||
|
// const average: u32 = average: {
|
||||||
|
// var sum: u32 = 0;
|
||||||
|
// for (sorted[0..len]) |value| sum += value;
|
||||||
|
|
||||||
|
// break :average @intCast(sum / len);
|
||||||
|
// };
|
||||||
|
// const median = sorted[len / 2];
|
||||||
|
// const low = sorted[len / 100]; // 1% Low
|
||||||
|
|
||||||
|
// break :blk .{ average, median, low };
|
||||||
|
// };
|
||||||
|
|
||||||
|
// zgui.text("Average: {:0>3} fps", .{stats[0]});
|
||||||
|
// zgui.text(" Median: {:0>3} fps", .{stats[1]});
|
||||||
|
// zgui.text(" 1% Low: {:0>3} fps", .{stats[2]});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.win_stat.show_schedule) {
|
||||||
|
_ = zgui.begin("Schedule", .{ .popen = &state.win_stat.show_schedule });
|
||||||
|
defer zgui.end();
|
||||||
|
|
||||||
|
const scheduler = cpu.sched;
|
||||||
|
_ = scheduler;
|
||||||
|
|
||||||
|
// zgui.text("tick: {X:0>16}", .{scheduler.now()});
|
||||||
|
zgui.text("tick: (TODO: FIX THIS)", .{});
|
||||||
|
zgui.separator();
|
||||||
|
|
||||||
|
const sched_ptr: *Scheduler = @ptrCast(@alignCast(cpu.sched.ptr));
|
||||||
|
const Event = std.meta.Child(@TypeOf(sched_ptr.queue.items));
|
||||||
|
|
||||||
|
var items: [20]Event = undefined;
|
||||||
|
const len = @min(sched_ptr.queue.items.len, items.len);
|
||||||
|
|
||||||
|
@memcpy(items[0..len], sched_ptr.queue.items[0..len]);
|
||||||
|
std.mem.sort(Event, items[0..len], {}, widgets.eventDesc(Event));
|
||||||
|
|
||||||
|
for (items[0..len]) |event| {
|
||||||
|
// zgui.text("{X:0>16} | {?}", .{ event.tick, event.kind });
|
||||||
|
_ = event;
|
||||||
|
zgui.text("TODO: Fix This", .{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.win_stat.show_palette) {
|
||||||
|
_ = zgui.begin("Palette", .{ .popen = &state.win_stat.show_palette });
|
||||||
|
defer zgui.end();
|
||||||
|
|
||||||
|
widgets.paletteGrid(.Background, cpu);
|
||||||
|
|
||||||
|
zgui.sameLine(.{ .spacing = 20.0 });
|
||||||
|
|
||||||
|
widgets.paletteGrid(.Object, cpu);
|
||||||
|
}
|
||||||
|
|
||||||
|
// {
|
||||||
|
// zgui.showDemoWindow(null);
|
||||||
|
// }
|
||||||
|
|
||||||
|
return true; // request redraw
|
||||||
|
}
|
||||||
|
|
||||||
|
const widgets = struct {
|
||||||
|
const PaletteKind = enum { Background, Object };
|
||||||
|
|
||||||
|
fn paletteGrid(comptime kind: PaletteKind, cpu: *const Arm7tdmi) void {
|
||||||
|
_ = zgui.beginGroup();
|
||||||
|
defer zgui.endGroup();
|
||||||
|
|
||||||
|
const address: u32 = switch (kind) {
|
||||||
|
.Background => 0x0500_0000,
|
||||||
|
.Object => 0x0500_0200,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (0..0x100) |i| {
|
||||||
|
const offset: u32 = @truncate(i);
|
||||||
|
const bgr555 = cpu.bus.dbgRead(u16, address + offset * @sizeOf(u16));
|
||||||
|
widgets.colourSquare(bgr555);
|
||||||
|
|
||||||
|
if ((i + 1) % 0x10 != 0) zgui.sameLine(.{});
|
||||||
|
}
|
||||||
|
zgui.text(@tagName(kind), .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn colourSquare(bgr555: u16) void {
|
||||||
|
// FIXME: working with the packed struct enum is currently broken :pensive:
|
||||||
|
const ImguiColorEditFlags_NoInputs: u32 = 1 << 5;
|
||||||
|
const ImguiColorEditFlags_NoPicker: u32 = 1 << 2;
|
||||||
|
const flags: zgui.ColorEditFlags = @bitCast(ImguiColorEditFlags_NoInputs | ImguiColorEditFlags_NoPicker);
|
||||||
|
|
||||||
|
const b: f32 = @floatFromInt(bgr555 >> 10 & 0x1f);
|
||||||
|
const g: f32 = @floatFromInt(bgr555 >> 5 & 0x1F);
|
||||||
|
const r: f32 = @floatFromInt(bgr555 & 0x1F);
|
||||||
|
|
||||||
|
var col = [_]f32{ r / 31.0, g / 31.0, b / 31.0 };
|
||||||
|
|
||||||
|
_ = zgui.colorEdit3("", .{ .col = &col, .flags = flags });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn interrupts(comptime label: []const u8, int: anytype) void {
|
||||||
|
const h = 15.0;
|
||||||
|
const w = 9.0 * 2 + 3.5;
|
||||||
|
const ww = 9.0 * 3;
|
||||||
|
|
||||||
|
{
|
||||||
|
zgui.text(label ++ ":", .{});
|
||||||
|
|
||||||
|
zgui.sameLine(.{});
|
||||||
|
_ = zgui.selectable("VBL", .{ .w = w, .h = h, .selected = int.vblank.read() });
|
||||||
|
|
||||||
|
zgui.sameLine(.{});
|
||||||
|
_ = zgui.selectable("HBL", .{ .w = w, .h = h, .selected = int.hblank.read() });
|
||||||
|
|
||||||
|
zgui.sameLine(.{});
|
||||||
|
_ = zgui.selectable("VCT", .{ .w = w, .h = h, .selected = int.coincidence.read() });
|
||||||
|
|
||||||
|
{
|
||||||
|
zgui.sameLine(.{});
|
||||||
|
_ = zgui.selectable("TIM0", .{ .w = ww, .h = h, .selected = int.tim0.read() });
|
||||||
|
|
||||||
|
zgui.sameLine(.{});
|
||||||
|
_ = zgui.selectable("TIM1", .{ .w = ww, .h = h, .selected = int.tim1.read() });
|
||||||
|
|
||||||
|
zgui.sameLine(.{});
|
||||||
|
_ = zgui.selectable("TIM2", .{ .w = ww, .h = h, .selected = int.tim2.read() });
|
||||||
|
|
||||||
|
zgui.sameLine(.{});
|
||||||
|
_ = zgui.selectable("TIM3", .{ .w = ww, .h = h, .selected = int.tim3.read() });
|
||||||
|
}
|
||||||
|
|
||||||
|
zgui.sameLine(.{});
|
||||||
|
_ = zgui.selectable("SRL", .{ .w = w, .h = h, .selected = int.serial.read() });
|
||||||
|
|
||||||
|
{
|
||||||
|
zgui.sameLine(.{});
|
||||||
|
_ = zgui.selectable("DMA0", .{ .w = ww, .h = h, .selected = int.dma0.read() });
|
||||||
|
|
||||||
|
zgui.sameLine(.{});
|
||||||
|
_ = zgui.selectable("DMA1", .{ .w = ww, .h = h, .selected = int.dma1.read() });
|
||||||
|
|
||||||
|
zgui.sameLine(.{});
|
||||||
|
_ = zgui.selectable("DMA2", .{ .w = ww, .h = h, .selected = int.dma2.read() });
|
||||||
|
|
||||||
|
zgui.sameLine(.{});
|
||||||
|
_ = zgui.selectable("DMA3", .{ .w = ww, .h = h, .selected = int.dma3.read() });
|
||||||
|
}
|
||||||
|
|
||||||
|
zgui.sameLine(.{});
|
||||||
|
_ = zgui.selectable("KPD", .{ .w = w, .h = h, .selected = int.keypad.read() });
|
||||||
|
|
||||||
|
zgui.sameLine(.{});
|
||||||
|
_ = zgui.selectable("GPK", .{ .w = w, .h = h, .selected = int.game_pak.read() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn psr(comptime label: []const u8, register: anytype) void {
|
||||||
|
const Mode = @import("arm32").arm.Mode;
|
||||||
|
|
||||||
|
const maybe_mode = std.meta.intToEnum(Mode, register.mode.read()) catch null;
|
||||||
|
const mode = if (maybe_mode) |mode| mode.toString() else "???";
|
||||||
|
const w = 9.0;
|
||||||
|
const h = 15.0;
|
||||||
|
|
||||||
|
zgui.text(label ++ ": 0x{X:0>8}", .{register.raw});
|
||||||
|
|
||||||
|
zgui.sameLine(.{});
|
||||||
|
_ = zgui.selectable("N", .{ .w = w, .h = h, .selected = register.n.read() });
|
||||||
|
|
||||||
|
zgui.sameLine(.{});
|
||||||
|
_ = zgui.selectable("Z", .{ .w = w, .h = h, .selected = register.z.read() });
|
||||||
|
|
||||||
|
zgui.sameLine(.{});
|
||||||
|
_ = zgui.selectable("C", .{ .w = w, .h = h, .selected = register.c.read() });
|
||||||
|
|
||||||
|
zgui.sameLine(.{});
|
||||||
|
_ = zgui.selectable("V", .{ .w = w, .h = h, .selected = register.v.read() });
|
||||||
|
|
||||||
|
zgui.sameLine(.{});
|
||||||
|
zgui.text("{s}", .{mode});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eventDesc(comptime T: type) fn (void, T, T) bool {
|
||||||
|
return struct {
|
||||||
|
fn inner(_: void, left: T, right: T) bool {
|
||||||
|
return left.tick > right.tick;
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn handleTitle(title_opt: ?*const [12]u8) [12:0]u8 {
|
||||||
|
if (title_opt == null) return "[N/A Title]\x00".*; // No ROM present
|
||||||
|
const title = title_opt.?;
|
||||||
|
|
||||||
|
// ROM Title is an empty string (ImGui hates these)
|
||||||
|
if (title[0] == '\x00') return "[No Title]\x00\x00".*;
|
||||||
|
|
||||||
|
return title.* ++ [_:0]u8{};
|
||||||
|
}
|
||||||
28
src/lib.zig
Normal file
28
src/lib.zig
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
pub const c = @cImport({
|
||||||
|
@cDefine("SDL_DISABLE_OLD_NAMES", {});
|
||||||
|
@cDefine("SDL_MAIN_HANDLED", {});
|
||||||
|
|
||||||
|
@cInclude("SDL3/SDL.h");
|
||||||
|
@cInclude("SDL3/SDL_main.h");
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://github.com/castholm/zig-examples/blob/77a829c85b5ddbad673026d504626015db4093ac/opengl-sdl/main.zig#L200-L219
|
||||||
|
pub inline fn errify(value: anytype) error{sdl_error}!switch (@typeInfo(@TypeOf(value))) {
|
||||||
|
.bool => void,
|
||||||
|
.pointer, .optional => @TypeOf(value.?),
|
||||||
|
.int => |info| switch (info.signedness) {
|
||||||
|
.signed => @TypeOf(@max(0, value)),
|
||||||
|
.unsigned => @TypeOf(value),
|
||||||
|
},
|
||||||
|
else => @compileError("unerrifiable type: " ++ @typeName(@TypeOf(value))),
|
||||||
|
} {
|
||||||
|
return switch (@typeInfo(@TypeOf(value))) {
|
||||||
|
.bool => if (!value) error.sdl_error,
|
||||||
|
.pointer, .optional => value orelse error.sdl_error,
|
||||||
|
.int => |info| switch (info.signedness) {
|
||||||
|
.signed => if (value >= 0) @max(0, value) else error.sdl_error,
|
||||||
|
.unsigned => if (value != 0) value else error.sdl_error,
|
||||||
|
},
|
||||||
|
else => comptime unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
548
src/lib/fifo.zig
Normal file
548
src/lib/fifo.zig
Normal file
@@ -0,0 +1,548 @@
|
|||||||
|
// FIFO of fixed size items
|
||||||
|
// Usually used for e.g. byte buffers
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const math = std.math;
|
||||||
|
const mem = std.mem;
|
||||||
|
const Allocator = mem.Allocator;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
pub const LinearFifoBufferType = union(enum) {
|
||||||
|
/// The buffer is internal to the fifo; it is of the specified size.
|
||||||
|
static: usize,
|
||||||
|
|
||||||
|
/// The buffer is passed as a slice to the initialiser.
|
||||||
|
slice,
|
||||||
|
|
||||||
|
/// The buffer is managed dynamically using a `mem.Allocator`.
|
||||||
|
dynamic,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn LinearFifo(
|
||||||
|
comptime T: type,
|
||||||
|
comptime buffer_type: LinearFifoBufferType,
|
||||||
|
) type {
|
||||||
|
const autoalign = false;
|
||||||
|
|
||||||
|
const powers_of_two = switch (buffer_type) {
|
||||||
|
.static => std.math.isPowerOfTwo(buffer_type.static),
|
||||||
|
.slice => false, // Any size slice could be passed in
|
||||||
|
.dynamic => true, // This could be configurable in future
|
||||||
|
};
|
||||||
|
|
||||||
|
return struct {
|
||||||
|
allocator: if (buffer_type == .dynamic) Allocator else void,
|
||||||
|
buf: if (buffer_type == .static) [buffer_type.static]T else []T,
|
||||||
|
head: usize,
|
||||||
|
count: usize,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
pub const Reader = std.io.Reader(*Self, error{}, readFn);
|
||||||
|
pub const Writer = std.io.Writer(*Self, error{OutOfMemory}, appendWrite);
|
||||||
|
|
||||||
|
// Type of Self argument for slice operations.
|
||||||
|
// If buffer is inline (Static) then we need to ensure we haven't
|
||||||
|
// returned a slice into a copy on the stack
|
||||||
|
const SliceSelfArg = if (buffer_type == .static) *Self else Self;
|
||||||
|
|
||||||
|
pub const init = switch (buffer_type) {
|
||||||
|
.static => initStatic,
|
||||||
|
.slice => initSlice,
|
||||||
|
.dynamic => initDynamic,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn initStatic() Self {
|
||||||
|
comptime assert(buffer_type == .static);
|
||||||
|
return .{
|
||||||
|
.allocator = {},
|
||||||
|
.buf = undefined,
|
||||||
|
.head = 0,
|
||||||
|
.count = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initSlice(buf: []T) Self {
|
||||||
|
comptime assert(buffer_type == .slice);
|
||||||
|
return .{
|
||||||
|
.allocator = {},
|
||||||
|
.buf = buf,
|
||||||
|
.head = 0,
|
||||||
|
.count = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initDynamic(allocator: Allocator) Self {
|
||||||
|
comptime assert(buffer_type == .dynamic);
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.buf = &.{},
|
||||||
|
.head = 0,
|
||||||
|
.count = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: Self) void {
|
||||||
|
if (buffer_type == .dynamic) self.allocator.free(self.buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn realign(self: *Self) void {
|
||||||
|
if (self.buf.len - self.head >= self.count) {
|
||||||
|
mem.copyForwards(T, self.buf[0..self.count], self.buf[self.head..][0..self.count]);
|
||||||
|
self.head = 0;
|
||||||
|
} else {
|
||||||
|
var tmp: [4096 / 2 / @sizeOf(T)]T = undefined;
|
||||||
|
|
||||||
|
while (self.head != 0) {
|
||||||
|
const n = @min(self.head, tmp.len);
|
||||||
|
const m = self.buf.len - n;
|
||||||
|
@memcpy(tmp[0..n], self.buf[0..n]);
|
||||||
|
mem.copyForwards(T, self.buf[0..m], self.buf[n..][0..m]);
|
||||||
|
@memcpy(self.buf[m..][0..n], tmp[0..n]);
|
||||||
|
self.head -= n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{ // set unused area to undefined
|
||||||
|
const unused = mem.sliceAsBytes(self.buf[self.count..]);
|
||||||
|
@memset(unused, undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reduce allocated capacity to `size`.
|
||||||
|
pub fn shrink(self: *Self, size: usize) void {
|
||||||
|
assert(size >= self.count);
|
||||||
|
if (buffer_type == .dynamic) {
|
||||||
|
self.realign();
|
||||||
|
self.buf = self.allocator.realloc(self.buf, size) catch |e| switch (e) {
|
||||||
|
error.OutOfMemory => return, // no problem, capacity is still correct then.
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure that the buffer can fit at least `size` items
|
||||||
|
pub fn ensureTotalCapacity(self: *Self, size: usize) !void {
|
||||||
|
if (self.buf.len >= size) return;
|
||||||
|
if (buffer_type == .dynamic) {
|
||||||
|
self.realign();
|
||||||
|
const new_size = if (powers_of_two) math.ceilPowerOfTwo(usize, size) catch return error.OutOfMemory else size;
|
||||||
|
self.buf = try self.allocator.realloc(self.buf, new_size);
|
||||||
|
} else {
|
||||||
|
return error.OutOfMemory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes sure at least `size` items are unused
|
||||||
|
pub fn ensureUnusedCapacity(self: *Self, size: usize) error{OutOfMemory}!void {
|
||||||
|
if (self.writableLength() >= size) return;
|
||||||
|
|
||||||
|
return try self.ensureTotalCapacity(math.add(usize, self.count, size) catch return error.OutOfMemory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns number of items currently in fifo
|
||||||
|
pub fn readableLength(self: Self) usize {
|
||||||
|
return self.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a writable slice from the 'read' end of the fifo
|
||||||
|
fn readableSliceMut(self: SliceSelfArg, offset: usize) []T {
|
||||||
|
if (offset > self.count) return &[_]T{};
|
||||||
|
|
||||||
|
var start = self.head + offset;
|
||||||
|
if (start >= self.buf.len) {
|
||||||
|
start -= self.buf.len;
|
||||||
|
return self.buf[start .. start + (self.count - offset)];
|
||||||
|
} else {
|
||||||
|
const end = @min(self.head + self.count, self.buf.len);
|
||||||
|
return self.buf[start..end];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a readable slice from `offset`
|
||||||
|
pub fn readableSlice(self: SliceSelfArg, offset: usize) []const T {
|
||||||
|
return self.readableSliceMut(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readableSliceOfLen(self: *Self, len: usize) []const T {
|
||||||
|
assert(len <= self.count);
|
||||||
|
const buf = self.readableSlice(0);
|
||||||
|
if (buf.len >= len) {
|
||||||
|
return buf[0..len];
|
||||||
|
} else {
|
||||||
|
self.realign();
|
||||||
|
return self.readableSlice(0)[0..len];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Discard first `count` items in the fifo
|
||||||
|
pub fn discard(self: *Self, count: usize) void {
|
||||||
|
assert(count <= self.count);
|
||||||
|
{ // set old range to undefined. Note: may be wrapped around
|
||||||
|
const slice = self.readableSliceMut(0);
|
||||||
|
if (slice.len >= count) {
|
||||||
|
const unused = mem.sliceAsBytes(slice[0..count]);
|
||||||
|
@memset(unused, undefined);
|
||||||
|
} else {
|
||||||
|
const unused = mem.sliceAsBytes(slice[0..]);
|
||||||
|
@memset(unused, undefined);
|
||||||
|
const unused2 = mem.sliceAsBytes(self.readableSliceMut(slice.len)[0 .. count - slice.len]);
|
||||||
|
@memset(unused2, undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (autoalign and self.count == count) {
|
||||||
|
self.head = 0;
|
||||||
|
self.count = 0;
|
||||||
|
} else {
|
||||||
|
var head = self.head + count;
|
||||||
|
if (powers_of_two) {
|
||||||
|
// Note it is safe to do a wrapping subtract as
|
||||||
|
// bitwise & with all 1s is a noop
|
||||||
|
head &= self.buf.len -% 1;
|
||||||
|
} else {
|
||||||
|
head %= self.buf.len;
|
||||||
|
}
|
||||||
|
self.head = head;
|
||||||
|
self.count -= count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the next item from the fifo
|
||||||
|
pub fn readItem(self: *Self) ?T {
|
||||||
|
if (self.count == 0) return null;
|
||||||
|
|
||||||
|
const c = self.buf[self.head];
|
||||||
|
self.discard(1);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read data from the fifo into `dst`, returns number of items copied.
|
||||||
|
pub fn read(self: *Self, dst: []T) usize {
|
||||||
|
var dst_left = dst;
|
||||||
|
|
||||||
|
while (dst_left.len > 0) {
|
||||||
|
const slice = self.readableSlice(0);
|
||||||
|
if (slice.len == 0) break;
|
||||||
|
const n = @min(slice.len, dst_left.len);
|
||||||
|
@memcpy(dst_left[0..n], slice[0..n]);
|
||||||
|
self.discard(n);
|
||||||
|
dst_left = dst_left[n..];
|
||||||
|
}
|
||||||
|
|
||||||
|
return dst.len - dst_left.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as `read` except it returns an error union
|
||||||
|
/// The purpose of this function existing is to match `std.io.Reader` API.
|
||||||
|
fn readFn(self: *Self, dest: []u8) error{}!usize {
|
||||||
|
return self.read(dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reader(self: *Self) Reader {
|
||||||
|
return .{ .context = self };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns number of items available in fifo
|
||||||
|
pub fn writableLength(self: Self) usize {
|
||||||
|
return self.buf.len - self.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the first section of writable buffer.
|
||||||
|
/// Note that this may be of length 0
|
||||||
|
pub fn writableSlice(self: SliceSelfArg, offset: usize) []T {
|
||||||
|
if (offset > self.buf.len) return &[_]T{};
|
||||||
|
|
||||||
|
const tail = self.head + offset + self.count;
|
||||||
|
if (tail < self.buf.len) {
|
||||||
|
return self.buf[tail..];
|
||||||
|
} else {
|
||||||
|
return self.buf[tail - self.buf.len ..][0 .. self.writableLength() - offset];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a writable buffer of at least `size` items, allocating memory as needed.
|
||||||
|
/// Use `fifo.update` once you've written data to it.
|
||||||
|
pub fn writableWithSize(self: *Self, size: usize) ![]T {
|
||||||
|
try self.ensureUnusedCapacity(size);
|
||||||
|
|
||||||
|
// try to avoid realigning buffer
|
||||||
|
var slice = self.writableSlice(0);
|
||||||
|
if (slice.len < size) {
|
||||||
|
self.realign();
|
||||||
|
slice = self.writableSlice(0);
|
||||||
|
}
|
||||||
|
return slice;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the tail location of the buffer (usually follows use of writable/writableWithSize)
|
||||||
|
pub fn update(self: *Self, count: usize) void {
|
||||||
|
assert(self.count + count <= self.buf.len);
|
||||||
|
self.count += count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Appends the data in `src` to the fifo.
|
||||||
|
/// You must have ensured there is enough space.
|
||||||
|
pub fn writeAssumeCapacity(self: *Self, src: []const T) void {
|
||||||
|
assert(self.writableLength() >= src.len);
|
||||||
|
|
||||||
|
var src_left = src;
|
||||||
|
while (src_left.len > 0) {
|
||||||
|
const writable_slice = self.writableSlice(0);
|
||||||
|
assert(writable_slice.len != 0);
|
||||||
|
const n = @min(writable_slice.len, src_left.len);
|
||||||
|
@memcpy(writable_slice[0..n], src_left[0..n]);
|
||||||
|
self.update(n);
|
||||||
|
src_left = src_left[n..];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a single item to the fifo
|
||||||
|
pub fn writeItem(self: *Self, item: T) !void {
|
||||||
|
try self.ensureUnusedCapacity(1);
|
||||||
|
return self.writeItemAssumeCapacity(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn writeItemAssumeCapacity(self: *Self, item: T) void {
|
||||||
|
var tail = self.head + self.count;
|
||||||
|
if (powers_of_two) {
|
||||||
|
tail &= self.buf.len - 1;
|
||||||
|
} else {
|
||||||
|
tail %= self.buf.len;
|
||||||
|
}
|
||||||
|
self.buf[tail] = item;
|
||||||
|
self.update(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Appends the data in `src` to the fifo.
|
||||||
|
/// Allocates more memory as necessary
|
||||||
|
pub fn write(self: *Self, src: []const T) !void {
|
||||||
|
try self.ensureUnusedCapacity(src.len);
|
||||||
|
|
||||||
|
return self.writeAssumeCapacity(src);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as `write` except it returns the number of bytes written, which is always the same
|
||||||
|
/// as `bytes.len`. The purpose of this function existing is to match `std.io.Writer` API.
|
||||||
|
fn appendWrite(self: *Self, bytes: []const u8) error{OutOfMemory}!usize {
|
||||||
|
try self.write(bytes);
|
||||||
|
return bytes.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn writer(self: *Self) Writer {
|
||||||
|
return .{ .context = self };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make `count` items available before the current read location
|
||||||
|
fn rewind(self: *Self, count: usize) void {
|
||||||
|
assert(self.writableLength() >= count);
|
||||||
|
|
||||||
|
var head = self.head + (self.buf.len - count);
|
||||||
|
if (powers_of_two) {
|
||||||
|
head &= self.buf.len - 1;
|
||||||
|
} else {
|
||||||
|
head %= self.buf.len;
|
||||||
|
}
|
||||||
|
self.head = head;
|
||||||
|
self.count += count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Place data back into the read stream
|
||||||
|
pub fn unget(self: *Self, src: []const T) !void {
|
||||||
|
try self.ensureUnusedCapacity(src.len);
|
||||||
|
|
||||||
|
self.rewind(src.len);
|
||||||
|
|
||||||
|
const slice = self.readableSliceMut(0);
|
||||||
|
if (src.len < slice.len) {
|
||||||
|
@memcpy(slice[0..src.len], src);
|
||||||
|
} else {
|
||||||
|
@memcpy(slice, src[0..slice.len]);
|
||||||
|
const slice2 = self.readableSliceMut(slice.len);
|
||||||
|
@memcpy(slice2[0 .. src.len - slice.len], src[slice.len..]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the item at `offset`.
|
||||||
|
/// Asserts offset is within bounds.
|
||||||
|
pub fn peekItem(self: Self, offset: usize) T {
|
||||||
|
assert(offset < self.count);
|
||||||
|
|
||||||
|
var index = self.head + offset;
|
||||||
|
if (powers_of_two) {
|
||||||
|
index &= self.buf.len - 1;
|
||||||
|
} else {
|
||||||
|
index %= self.buf.len;
|
||||||
|
}
|
||||||
|
return self.buf[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pump data from a reader into a writer.
|
||||||
|
/// Stops when reader returns 0 bytes (EOF).
|
||||||
|
/// Buffer size must be set before calling; a buffer length of 0 is invalid.
|
||||||
|
pub fn pump(self: *Self, src_reader: anytype, dest_writer: anytype) !void {
|
||||||
|
assert(self.buf.len > 0);
|
||||||
|
while (true) {
|
||||||
|
if (self.writableLength() > 0) {
|
||||||
|
const n = try src_reader.read(self.writableSlice(0));
|
||||||
|
if (n == 0) break; // EOF
|
||||||
|
self.update(n);
|
||||||
|
}
|
||||||
|
self.discard(try dest_writer.write(self.readableSlice(0)));
|
||||||
|
}
|
||||||
|
// flush remaining data
|
||||||
|
while (self.readableLength() > 0) {
|
||||||
|
self.discard(try dest_writer.write(self.readableSlice(0)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toOwnedSlice(self: *Self) Allocator.Error![]T {
|
||||||
|
if (self.head != 0) self.realign();
|
||||||
|
assert(self.head == 0);
|
||||||
|
assert(self.count <= self.buf.len);
|
||||||
|
const allocator = self.allocator;
|
||||||
|
if (allocator.resize(self.buf, self.count)) {
|
||||||
|
const result = self.buf[0..self.count];
|
||||||
|
self.* = Self.init(allocator);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const new_memory = try allocator.dupe(T, self.buf[0..self.count]);
|
||||||
|
allocator.free(self.buf);
|
||||||
|
self.* = Self.init(allocator);
|
||||||
|
return new_memory;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test "LinearFifo(u8, .Dynamic) discard(0) from empty buffer should not error on overflow" {
|
||||||
|
var fifo = LinearFifo(u8, .dynamic).init(testing.allocator);
|
||||||
|
defer fifo.deinit();
|
||||||
|
|
||||||
|
// If overflow is not explicitly allowed this will crash in debug / safe mode
|
||||||
|
fifo.discard(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "LinearFifo(u8, .Dynamic)" {
|
||||||
|
var fifo = LinearFifo(u8, .dynamic).init(testing.allocator);
|
||||||
|
defer fifo.deinit();
|
||||||
|
|
||||||
|
try fifo.write("HELLO");
|
||||||
|
try testing.expectEqual(@as(usize, 5), fifo.readableLength());
|
||||||
|
try testing.expectEqualSlices(u8, "HELLO", fifo.readableSlice(0));
|
||||||
|
|
||||||
|
{
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < 5) : (i += 1) {
|
||||||
|
try fifo.write(&[_]u8{fifo.peekItem(i)});
|
||||||
|
}
|
||||||
|
try testing.expectEqual(@as(usize, 10), fifo.readableLength());
|
||||||
|
try testing.expectEqualSlices(u8, "HELLOHELLO", fifo.readableSlice(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
try testing.expectEqual(@as(u8, 'H'), fifo.readItem().?);
|
||||||
|
try testing.expectEqual(@as(u8, 'E'), fifo.readItem().?);
|
||||||
|
try testing.expectEqual(@as(u8, 'L'), fifo.readItem().?);
|
||||||
|
try testing.expectEqual(@as(u8, 'L'), fifo.readItem().?);
|
||||||
|
try testing.expectEqual(@as(u8, 'O'), fifo.readItem().?);
|
||||||
|
}
|
||||||
|
try testing.expectEqual(@as(usize, 5), fifo.readableLength());
|
||||||
|
|
||||||
|
{ // Writes that wrap around
|
||||||
|
try testing.expectEqual(@as(usize, 11), fifo.writableLength());
|
||||||
|
try testing.expectEqual(@as(usize, 6), fifo.writableSlice(0).len);
|
||||||
|
fifo.writeAssumeCapacity("6<chars<11");
|
||||||
|
try testing.expectEqualSlices(u8, "HELLO6<char", fifo.readableSlice(0));
|
||||||
|
try testing.expectEqualSlices(u8, "s<11", fifo.readableSlice(11));
|
||||||
|
try testing.expectEqualSlices(u8, "11", fifo.readableSlice(13));
|
||||||
|
try testing.expectEqualSlices(u8, "", fifo.readableSlice(15));
|
||||||
|
fifo.discard(11);
|
||||||
|
try testing.expectEqualSlices(u8, "s<11", fifo.readableSlice(0));
|
||||||
|
fifo.discard(4);
|
||||||
|
try testing.expectEqual(@as(usize, 0), fifo.readableLength());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const buf = try fifo.writableWithSize(12);
|
||||||
|
try testing.expectEqual(@as(usize, 12), buf.len);
|
||||||
|
var i: u8 = 0;
|
||||||
|
while (i < 10) : (i += 1) {
|
||||||
|
buf[i] = i + 'a';
|
||||||
|
}
|
||||||
|
fifo.update(10);
|
||||||
|
try testing.expectEqualSlices(u8, "abcdefghij", fifo.readableSlice(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
try fifo.unget("prependedstring");
|
||||||
|
var result: [30]u8 = undefined;
|
||||||
|
try testing.expectEqualSlices(u8, "prependedstringabcdefghij", result[0..fifo.read(&result)]);
|
||||||
|
try fifo.unget("b");
|
||||||
|
try fifo.unget("a");
|
||||||
|
try testing.expectEqualSlices(u8, "ab", result[0..fifo.read(&result)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fifo.shrink(0);
|
||||||
|
|
||||||
|
{
|
||||||
|
try fifo.writer().print("{s}, {s}!", .{ "Hello", "World" });
|
||||||
|
var result: [30]u8 = undefined;
|
||||||
|
try testing.expectEqualSlices(u8, "Hello, World!", result[0..fifo.read(&result)]);
|
||||||
|
try testing.expectEqual(@as(usize, 0), fifo.readableLength());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
try fifo.writer().writeAll("This is a test");
|
||||||
|
var result: [30]u8 = undefined;
|
||||||
|
try testing.expectEqualSlices(u8, "This", (try fifo.reader().readUntilDelimiterOrEof(&result, ' ')).?);
|
||||||
|
try testing.expectEqualSlices(u8, "is", (try fifo.reader().readUntilDelimiterOrEof(&result, ' ')).?);
|
||||||
|
try testing.expectEqualSlices(u8, "a", (try fifo.reader().readUntilDelimiterOrEof(&result, ' ')).?);
|
||||||
|
try testing.expectEqualSlices(u8, "test", (try fifo.reader().readUntilDelimiterOrEof(&result, ' ')).?);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
try fifo.ensureTotalCapacity(1);
|
||||||
|
var in_fbs = std.io.fixedBufferStream("pump test");
|
||||||
|
var out_buf: [50]u8 = undefined;
|
||||||
|
var out_fbs = std.io.fixedBufferStream(&out_buf);
|
||||||
|
try fifo.pump(in_fbs.reader(), out_fbs.writer());
|
||||||
|
try testing.expectEqualSlices(u8, in_fbs.buffer, out_fbs.getWritten());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test LinearFifo {
|
||||||
|
inline for ([_]type{ u1, u8, u16, u64 }) |T| {
|
||||||
|
inline for ([_]LinearFifoBufferType{ LinearFifoBufferType{ .static = 32 }, .slice, .dynamic }) |bt| {
|
||||||
|
const FifoType = LinearFifo(T, bt);
|
||||||
|
var buf: if (bt == .slice) [32]T else void = undefined;
|
||||||
|
var fifo = switch (bt) {
|
||||||
|
.static => FifoType.init(),
|
||||||
|
.slice => FifoType.init(buf[0..]),
|
||||||
|
.dynamic => FifoType.init(testing.allocator),
|
||||||
|
};
|
||||||
|
defer fifo.deinit();
|
||||||
|
|
||||||
|
try fifo.write(&[_]T{ 0, 1, 1, 0, 1 });
|
||||||
|
try testing.expectEqual(@as(usize, 5), fifo.readableLength());
|
||||||
|
|
||||||
|
{
|
||||||
|
try testing.expectEqual(@as(T, 0), fifo.readItem().?);
|
||||||
|
try testing.expectEqual(@as(T, 1), fifo.readItem().?);
|
||||||
|
try testing.expectEqual(@as(T, 1), fifo.readItem().?);
|
||||||
|
try testing.expectEqual(@as(T, 0), fifo.readItem().?);
|
||||||
|
try testing.expectEqual(@as(T, 1), fifo.readItem().?);
|
||||||
|
try testing.expectEqual(@as(usize, 0), fifo.readableLength());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
try fifo.writeItem(1);
|
||||||
|
try fifo.writeItem(1);
|
||||||
|
try fifo.writeItem(1);
|
||||||
|
try testing.expectEqual(@as(usize, 3), fifo.readableLength());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var readBuf: [3]T = undefined;
|
||||||
|
const n = fifo.read(&readBuf);
|
||||||
|
try testing.expectEqual(@as(usize, 3), n); // NOTE: It should be the number of items.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
336
src/main.zig
336
src/main.zig
@@ -1,160 +1,238 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const SDL = @import("sdl2");
|
const builtin = @import("builtin");
|
||||||
|
const known_folders = @import("known_folders");
|
||||||
|
const clap = @import("clap");
|
||||||
|
|
||||||
const emu = @import("emu.zig");
|
const config = @import("config.zig");
|
||||||
const Bus = @import("Bus.zig");
|
const emu = @import("core/emu.zig");
|
||||||
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
|
||||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
|
||||||
|
|
||||||
const Timer = std.time.Timer;
|
const Synchro = @import("core/emu.zig").Synchro;
|
||||||
const Thread = std.Thread;
|
const Gui = @import("platform.zig").Gui;
|
||||||
const Atomic = std.atomic.Atomic;
|
const Bus = @import("core/Bus.zig");
|
||||||
const File = std.fs.File;
|
const Scheduler = @import("core/scheduler.zig").Scheduler;
|
||||||
|
const FilePaths = @import("util.zig").FilePaths;
|
||||||
|
const FpsTracker = @import("util.zig").FpsTracker;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const window_scale = 3;
|
const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||||
const gba_width = @import("ppu.zig").width;
|
const IBus = @import("arm32").Bus;
|
||||||
const gba_height = @import("ppu.zig").height;
|
const IScheduler = @import("arm32").Scheduler;
|
||||||
const buf_pitch = @import("ppu.zig").buf_pitch;
|
|
||||||
|
|
||||||
pub const enable_logging: bool = false;
|
const log = std.log.scoped(.Cli);
|
||||||
const is_binary: bool = false;
|
pub const log_level = if (builtin.mode != .Debug) .info else std.log.default_level;
|
||||||
|
|
||||||
pub fn main() anyerror!void {
|
// CLI Arguments + Help Text
|
||||||
// Allocator for Emulator + CLI
|
const params = clap.parseParamsComptime(
|
||||||
|
\\-h, --help Display this help and exit.
|
||||||
|
\\-s, --skip Skip BIOS.
|
||||||
|
\\-b, --bios <str> Optional path to a GBA BIOS ROM.
|
||||||
|
\\ --gdb Run ZBA from the context of a GDB Server
|
||||||
|
\\<str> Path to the GBA GamePak ROM.
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
|
||||||
|
pub fn main() void {
|
||||||
|
// Main Allocator for ZBA
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
const alloc = gpa.allocator();
|
defer std.debug.assert(gpa.deinit() == .ok);
|
||||||
defer std.debug.assert(!gpa.deinit());
|
|
||||||
|
|
||||||
// Handle CLI Arguments
|
const allocator = gpa.allocator();
|
||||||
const args = try std.process.argsAlloc(alloc);
|
|
||||||
defer std.process.argsFree(alloc, args);
|
|
||||||
|
|
||||||
const zba_args: []const []const u8 = args[1..];
|
// 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 });
|
||||||
|
|
||||||
if (zba_args.len == 0) {
|
break :blk path;
|
||||||
std.log.err("Expected PATH to Gameboy Advance ROM as a CLI argument", .{});
|
};
|
||||||
return;
|
defer allocator.free(data_path);
|
||||||
} else if (zba_args.len > 1) {
|
|
||||||
std.log.err("Too many CLI arguments were provided", .{});
|
// Determine the Config Directory
|
||||||
return;
|
const config_path = blk: {
|
||||||
|
const result = known_folders.getPath(allocator, .roaming_configuration);
|
||||||
|
const option = result catch |e| exitln("interreupted while determining the config folder: {}", .{e});
|
||||||
|
const path = option orelse exitln("no valid config folder found", .{});
|
||||||
|
ensureConfigDirExists(path) catch |e| exitln("failed to create required folder \"{s}\": {}", .{ path, e });
|
||||||
|
|
||||||
|
break :blk path;
|
||||||
|
};
|
||||||
|
defer allocator.free(config_path);
|
||||||
|
|
||||||
|
// Parse CLI
|
||||||
|
|
||||||
|
const result = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{ .allocator = allocator }) catch |e| exitln("failed to parse cli: {}", .{e});
|
||||||
|
defer result.deinit();
|
||||||
|
|
||||||
|
// TODO: Move config file to XDG Config directory?
|
||||||
|
const cfg_file_path = configFilePath(allocator, config_path) catch |e| exitln("failed to ready config file for access: {}", .{e});
|
||||||
|
defer allocator.free(cfg_file_path);
|
||||||
|
|
||||||
|
config.load(allocator, cfg_file_path) catch |e| exitln("failed to load config file: {}", .{e});
|
||||||
|
|
||||||
|
var paths = handleArguments(allocator, data_path, &result) catch |e| exitln("failed to handle cli arguments: {}", .{e});
|
||||||
|
defer paths.deinit(allocator);
|
||||||
|
|
||||||
|
// if paths.bios is null, then we want to see if it's in the data directory
|
||||||
|
if (paths.bios == null) blk: {
|
||||||
|
const bios_path = std.mem.join(allocator, "/", &.{ data_path, "zba", "gba_bios.bin" }) catch |e| exitln("failed to allocate backup bios dir path: {}", .{e});
|
||||||
|
defer allocator.free(bios_path);
|
||||||
|
|
||||||
|
_ = std.fs.cwd().statFile(bios_path) catch |e| switch (e) {
|
||||||
|
error.FileNotFound => { // ZBA will crash on attempt to read BIOS but that's fine
|
||||||
|
log.err("file located at {s} was not found", .{bios_path});
|
||||||
|
break :blk;
|
||||||
|
},
|
||||||
|
else => exitln("error when checking \"{s}\": {}", .{ bios_path, e }),
|
||||||
|
};
|
||||||
|
|
||||||
|
paths.bios = allocator.dupe(u8, bios_path) catch |e| exitln("failed to duplicate path to bios: {}", .{e});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Emulator
|
const log_file = switch (config.config().debug.cpu_trace) {
|
||||||
var scheduler = Scheduler.init(alloc);
|
true => std.fs.cwd().createFile("zba.log", .{}) catch |e| exitln("failed to create trace log file: {}", .{e}),
|
||||||
defer scheduler.deinit();
|
false => null,
|
||||||
|
};
|
||||||
var bus = try Bus.init(alloc, &scheduler, zba_args[0]);
|
|
||||||
defer bus.deinit();
|
|
||||||
|
|
||||||
var cpu = Arm7tdmi.init(&scheduler, &bus);
|
|
||||||
cpu.fastBoot();
|
|
||||||
|
|
||||||
var log_file: ?File = undefined;
|
|
||||||
if (enable_logging) {
|
|
||||||
const file_name: []const u8 = if (is_binary) "zba.bin" else "zba.log";
|
|
||||||
const file = try std.fs.cwd().createFile(file_name, .{});
|
|
||||||
cpu.useLogger(&file, is_binary);
|
|
||||||
|
|
||||||
log_file = file;
|
|
||||||
}
|
|
||||||
defer if (log_file) |file| file.close();
|
defer if (log_file) |file| file.close();
|
||||||
|
|
||||||
// Init Atomics
|
// TODO: Take Emulator Init Code out of main.zig
|
||||||
var quit = Atomic(bool).init(false);
|
var scheduler = Scheduler.init(allocator);
|
||||||
var pause = Atomic(bool).init(false);
|
defer scheduler.deinit();
|
||||||
|
|
||||||
// Create Emulator Thread
|
var bus: Bus = undefined;
|
||||||
const emu_thread = try Thread.spawn(.{}, emu.runEmuThread, .{ &quit, &pause, &scheduler, &cpu, &bus });
|
|
||||||
defer emu_thread.join();
|
|
||||||
|
|
||||||
// Initialize SDL
|
const ischeduler = IScheduler.init(&scheduler);
|
||||||
const status = SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO | SDL.SDL_INIT_GAMECONTROLLER);
|
const ibus = IBus.init(&bus);
|
||||||
if (status < 0) sdlPanic();
|
|
||||||
defer SDL.SDL_Quit();
|
|
||||||
|
|
||||||
var window = SDL.SDL_CreateWindow(
|
var cpu = Arm7tdmi.init(ischeduler, ibus);
|
||||||
"ZBA",
|
|
||||||
SDL.SDL_WINDOWPOS_CENTERED,
|
|
||||||
SDL.SDL_WINDOWPOS_CENTERED,
|
|
||||||
gba_width * window_scale,
|
|
||||||
gba_height * window_scale,
|
|
||||||
SDL.SDL_WINDOW_SHOWN,
|
|
||||||
) orelse sdlPanic();
|
|
||||||
defer SDL.SDL_DestroyWindow(window);
|
|
||||||
|
|
||||||
var renderer = SDL.SDL_CreateRenderer(window, -1, SDL.SDL_RENDERER_ACCELERATED) orelse sdlPanic();
|
bus.init(allocator, &scheduler, &cpu, paths) catch |e| exitln("failed to init zba bus: {}", .{e});
|
||||||
defer SDL.SDL_DestroyRenderer(renderer);
|
defer bus.deinit();
|
||||||
|
|
||||||
const texture = SDL.SDL_CreateTexture(renderer, SDL.SDL_PIXELFORMAT_BGR555, SDL.SDL_TEXTUREACCESS_STREAMING, 240, 160) orelse sdlPanic();
|
if (config.config().guest.skip_bios or result.args.skip != 0 or paths.bios == null) {
|
||||||
defer SDL.SDL_DestroyTexture(texture);
|
@import("core/cpu_util.zig").fastBoot(&cpu);
|
||||||
|
|
||||||
// Init FPS Timer
|
|
||||||
// var timer = Timer.start() catch unreachable;
|
|
||||||
// var title_buf: [0x30]u8 = [_]u8{0x00} ** 0x30;
|
|
||||||
|
|
||||||
emu_loop: while (true) {
|
|
||||||
var event: SDL.SDL_Event = undefined;
|
|
||||||
if (SDL.SDL_PollEvent(&event) != 0) {
|
|
||||||
// Pause Emulation Thread during Input Writing
|
|
||||||
pause.store(true, .Unordered);
|
|
||||||
|
|
||||||
switch (event.type) {
|
|
||||||
SDL.SDL_QUIT => break :emu_loop,
|
|
||||||
SDL.SDL_KEYDOWN => {
|
|
||||||
const key_code = event.key.keysym.sym;
|
|
||||||
|
|
||||||
switch (key_code) {
|
|
||||||
SDL.SDLK_UP => bus.io.keyinput.up.unset(),
|
|
||||||
SDL.SDLK_DOWN => bus.io.keyinput.down.unset(),
|
|
||||||
SDL.SDLK_LEFT => bus.io.keyinput.left.unset(),
|
|
||||||
SDL.SDLK_RIGHT => bus.io.keyinput.right.unset(),
|
|
||||||
SDL.SDLK_x => bus.io.keyinput.a.unset(),
|
|
||||||
SDL.SDLK_z => bus.io.keyinput.b.unset(),
|
|
||||||
SDL.SDLK_a => bus.io.keyinput.shoulder_l.unset(),
|
|
||||||
SDL.SDLK_s => bus.io.keyinput.shoulder_r.unset(),
|
|
||||||
SDL.SDLK_RETURN => bus.io.keyinput.start.unset(),
|
|
||||||
SDL.SDLK_RSHIFT => bus.io.keyinput.select.unset(),
|
|
||||||
else => {},
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
SDL.SDL_KEYUP => {
|
|
||||||
const key_code = event.key.keysym.sym;
|
|
||||||
|
|
||||||
switch (key_code) {
|
const title_ptr = if (paths.rom != null) &bus.pak.title else null;
|
||||||
SDL.SDLK_UP => bus.io.keyinput.up.set(),
|
|
||||||
SDL.SDLK_DOWN => bus.io.keyinput.down.set(),
|
// TODO: Just copy the title instead of grabbing a pointer to it
|
||||||
SDL.SDLK_LEFT => bus.io.keyinput.left.set(),
|
var gui = Gui.init(allocator, &bus.apu, title_ptr) catch |e| exitln("failed to init gui: {}", .{e});
|
||||||
SDL.SDLK_RIGHT => bus.io.keyinput.right.set(),
|
defer gui.deinit();
|
||||||
SDL.SDLK_x => bus.io.keyinput.a.set(),
|
|
||||||
SDL.SDLK_z => bus.io.keyinput.b.set(),
|
var sync = Synchro.init(allocator) catch |e| exitln("failed to allocate sync types: {}", .{e});
|
||||||
SDL.SDLK_a => bus.io.keyinput.shoulder_l.set(),
|
defer sync.deinit(allocator);
|
||||||
SDL.SDLK_s => bus.io.keyinput.shoulder_r.set(),
|
|
||||||
SDL.SDLK_RETURN => bus.io.keyinput.start.set(),
|
if (result.args.gdb != 0) {
|
||||||
SDL.SDLK_RSHIFT => bus.io.keyinput.select.set(),
|
const Server = @import("gdbstub").Server;
|
||||||
else => {},
|
const EmuThing = @import("core/emu.zig").EmuThing;
|
||||||
}
|
|
||||||
},
|
var wrapper = EmuThing.init(&cpu, &scheduler);
|
||||||
else => {},
|
var emulator = wrapper.interface();
|
||||||
|
defer emulator.deinit(allocator);
|
||||||
|
|
||||||
|
log.info("Ready to connect", .{});
|
||||||
|
|
||||||
|
var server = Server.init(
|
||||||
|
emulator,
|
||||||
|
.{ .memory_map = EmuThing.map, .target = EmuThing.target },
|
||||||
|
) catch |e| exitln("failed to init gdb server: {}", .{e});
|
||||||
|
defer server.deinit(allocator);
|
||||||
|
|
||||||
|
log.info("Starting GDB Server Thread", .{});
|
||||||
|
|
||||||
|
const thread = std.Thread.spawn(.{}, Server.run, .{ &server, allocator, &sync.should_quit }) catch |e| exitln("gdb server thread crashed: {}", .{e});
|
||||||
|
defer thread.join();
|
||||||
|
|
||||||
|
gui.run(.{
|
||||||
|
.cpu = &cpu,
|
||||||
|
.scheduler = &scheduler,
|
||||||
|
.sync = &sync,
|
||||||
|
}) catch |e| exitln("main thread panicked: {}", .{e});
|
||||||
|
} else {
|
||||||
|
var tracker = FpsTracker.init();
|
||||||
|
|
||||||
|
const thread = std.Thread.spawn(.{}, emu.run, .{ &cpu, &scheduler, &tracker, &sync }) catch |e| exitln("emu thread panicked: {}", .{e});
|
||||||
|
defer thread.join();
|
||||||
|
|
||||||
|
gui.run(.{
|
||||||
|
.cpu = &cpu,
|
||||||
|
.scheduler = &scheduler,
|
||||||
|
.tracker = &tracker,
|
||||||
|
.sync = &sync,
|
||||||
|
}) catch |e| exitln("main thread panicked: {}", .{e});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Is it OK just to copy the Emulator's Frame Buffer to SDL?
|
fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) !FilePaths {
|
||||||
const buf_ptr = bus.ppu.frame_buf.ptr;
|
const rom_path = try romPath(allocator, result);
|
||||||
_ = SDL.SDL_UpdateTexture(texture, null, buf_ptr, buf_pitch);
|
errdefer if (rom_path) |path| allocator.free(path);
|
||||||
_ = SDL.SDL_RenderCopy(renderer, texture, null, null);
|
|
||||||
SDL.SDL_RenderPresent(renderer);
|
|
||||||
|
|
||||||
// const fps = std.time.ns_per_s / timer.lap();
|
const bios_path: ?[]const u8 = if (result.args.bios) |path| try allocator.dupe(u8, path) else null;
|
||||||
// const title = std.fmt.bufPrint(&title_buf, "ZBA FPS: {d}", .{fps}) catch unreachable;
|
errdefer if (bios_path) |path| allocator.free(path);
|
||||||
// SDL.SDL_SetWindowTitle(window, title.ptr);
|
|
||||||
|
|
||||||
pause.store(false, .Unordered);
|
const save_path = try std.fs.path.join(allocator, &[_][]const u8{ data_path, "zba", "save" });
|
||||||
|
|
||||||
|
log.info("ROM path: {?s}", .{rom_path});
|
||||||
|
log.info("BIOS path: {?s}", .{bios_path});
|
||||||
|
log.info("Save path: {s}", .{save_path});
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.rom = rom_path,
|
||||||
|
.bios = bios_path,
|
||||||
|
.save = save_path,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
quit.store(true, .Unordered); // Terminate Emulator Thread
|
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();
|
||||||
|
|
||||||
|
// FIXME(2025-09-22): re-enable
|
||||||
|
// try config_file.writeAll(@embedFile("example.toml"));
|
||||||
|
try config_file.writeAll("");
|
||||||
|
};
|
||||||
|
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sdlPanic() noreturn {
|
fn ensureDataDirsExist(data_path: []const u8) !void {
|
||||||
const str = @as(?[*:0]const u8, SDL.SDL_GetError()) orelse "unknown error";
|
var dir = try std.fs.openDirAbsolute(data_path, .{});
|
||||||
@panic(std.mem.sliceTo(str, 0));
|
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(allocator: Allocator, result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) !?[]const u8 {
|
||||||
|
return switch (result.positionals.len) {
|
||||||
|
0 => null,
|
||||||
|
1 => if (result.positionals[0]) |path| try allocator.dupe(u8, path) else null,
|
||||||
|
else => exitln("ZBA received too many positional arguments.", .{}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exitln(comptime format: []const u8, args: anytype) noreturn {
|
||||||
|
var buf: [1024]u8 = undefined;
|
||||||
|
var stderr = std.fs.File.stderr().writer(&buf).interface;
|
||||||
|
|
||||||
|
stderr.print(format, args) catch {}; // Just exit already...
|
||||||
|
stderr.writeByte('\n') catch {};
|
||||||
|
std.process.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
406
src/platform.zig
Normal file
406
src/platform.zig
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const gl = @import("gl");
|
||||||
|
const zgui = @import("zgui");
|
||||||
|
const c = @import("lib.zig").c;
|
||||||
|
|
||||||
|
const emu = @import("core/emu.zig");
|
||||||
|
const config = @import("config.zig");
|
||||||
|
const imgui = @import("imgui.zig");
|
||||||
|
|
||||||
|
const Apu = @import("core/apu.zig").Apu;
|
||||||
|
const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||||
|
const Bus = @import("core/Bus.zig");
|
||||||
|
const Scheduler = @import("core/scheduler.zig").Scheduler;
|
||||||
|
const FpsTracker = @import("util.zig").FpsTracker;
|
||||||
|
const Synchro = @import("core/emu.zig").Synchro;
|
||||||
|
const KeyInput = @import("core/bus/io.zig").KeyInput;
|
||||||
|
|
||||||
|
const gba_width = @import("core/ppu.zig").width;
|
||||||
|
const gba_height = @import("core/ppu.zig").height;
|
||||||
|
|
||||||
|
const GLsizei = gl.sizei;
|
||||||
|
const SDL_GLContext = *c.SDL_GLContextState;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
pub const Dimensions = struct { width: u32, height: u32 };
|
||||||
|
const default_dim: Dimensions = .{ .width = 1280, .height = 720 };
|
||||||
|
|
||||||
|
pub const sample_rate = 1 << 15;
|
||||||
|
// pub const sample_format = SDL.AUDIO_U16;
|
||||||
|
|
||||||
|
const window_title = "ZBA";
|
||||||
|
|
||||||
|
const errify = @import("lib.zig").errify;
|
||||||
|
|
||||||
|
var gl_procs: gl.ProcTable = undefined;
|
||||||
|
|
||||||
|
pub const Gui = struct {
|
||||||
|
const Self = @This();
|
||||||
|
const log = std.log.scoped(.Gui);
|
||||||
|
|
||||||
|
window: *c.SDL_Window,
|
||||||
|
ctx: SDL_GLContext,
|
||||||
|
audio: Audio,
|
||||||
|
|
||||||
|
state: imgui.State,
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator, apu: *Apu, title_opt: ?*const [12]u8) !Self {
|
||||||
|
c.SDL_SetMainReady();
|
||||||
|
|
||||||
|
try errify(c.SDL_Init(c.SDL_INIT_VIDEO | c.SDL_INIT_AUDIO | c.SDL_INIT_EVENTS));
|
||||||
|
|
||||||
|
try errify(c.SDL_SetAppMetadata(window_title, "0.1.0", "moe.paoda.zba"));
|
||||||
|
try errify(c.SDL_GL_SetAttribute(c.SDL_GL_CONTEXT_MAJOR_VERSION, gl.info.version_major));
|
||||||
|
try errify(c.SDL_GL_SetAttribute(c.SDL_GL_CONTEXT_MINOR_VERSION, gl.info.version_minor));
|
||||||
|
try errify(c.SDL_GL_SetAttribute(c.SDL_GL_CONTEXT_PROFILE_MASK, c.SDL_GL_CONTEXT_PROFILE_CORE));
|
||||||
|
try errify(c.SDL_GL_SetAttribute(c.SDL_GL_CONTEXT_FLAGS, c.SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG));
|
||||||
|
|
||||||
|
const window: *c.SDL_Window = try errify(c.SDL_CreateWindow(window_title, default_dim.width, default_dim.height, c.SDL_WINDOW_OPENGL | c.SDL_WINDOW_RESIZABLE));
|
||||||
|
errdefer c.SDL_DestroyWindow(window);
|
||||||
|
|
||||||
|
const gl_ctx = try errify(c.SDL_GL_CreateContext(window));
|
||||||
|
errdefer errify(c.SDL_GL_DestroyContext(gl_ctx)) catch {};
|
||||||
|
|
||||||
|
try errify(c.SDL_GL_MakeCurrent(window, gl_ctx));
|
||||||
|
errdefer errify(c.SDL_GL_MakeCurrent(window, null)) catch {};
|
||||||
|
|
||||||
|
if (!gl_procs.init(c.SDL_GL_GetProcAddress)) return error.gl_init_failed;
|
||||||
|
|
||||||
|
gl.makeProcTableCurrent(&gl_procs);
|
||||||
|
errdefer gl.makeProcTableCurrent(null);
|
||||||
|
|
||||||
|
try errify(c.SDL_GL_SetSwapInterval(@intFromBool(config.config().host.vsync)));
|
||||||
|
|
||||||
|
zgui.init(allocator);
|
||||||
|
zgui.plot.init();
|
||||||
|
zgui.backend.init(window, gl_ctx);
|
||||||
|
|
||||||
|
// zgui.io.setIniFilename(null);
|
||||||
|
|
||||||
|
return Self{
|
||||||
|
.window = window,
|
||||||
|
.ctx = gl_ctx,
|
||||||
|
.audio = try Audio.init(apu),
|
||||||
|
|
||||||
|
.allocator = allocator,
|
||||||
|
.state = try imgui.State.init(allocator, title_opt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.audio.deinit();
|
||||||
|
self.state.deinit(self.allocator);
|
||||||
|
|
||||||
|
zgui.backend.deinit();
|
||||||
|
zgui.plot.deinit();
|
||||||
|
zgui.deinit();
|
||||||
|
|
||||||
|
errify(c.SDL_GL_DestroyContext(self.ctx)) catch {};
|
||||||
|
c.SDL_DestroyWindow(self.window);
|
||||||
|
c.SDL_Quit();
|
||||||
|
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RunOptions = struct {
|
||||||
|
sync: *Synchro,
|
||||||
|
tracker: ?*FpsTracker = null,
|
||||||
|
cpu: *Arm7tdmi,
|
||||||
|
scheduler: *Scheduler,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn run(self: *Self, opt: RunOptions) !void {
|
||||||
|
const cpu = opt.cpu;
|
||||||
|
const tracker = opt.tracker;
|
||||||
|
const sync = opt.sync;
|
||||||
|
|
||||||
|
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
|
||||||
|
|
||||||
|
const vao_id = opengl_impl.vao();
|
||||||
|
defer gl.DeleteVertexArrays(1, vao_id[0..]);
|
||||||
|
|
||||||
|
const emu_tex = opengl_impl.screenTex(bus_ptr.ppu.framebuf.get(.Renderer));
|
||||||
|
defer gl.DeleteTextures(1, emu_tex[0..]);
|
||||||
|
|
||||||
|
const out_tex = opengl_impl.outTex();
|
||||||
|
defer gl.DeleteTextures(1, out_tex[0..]);
|
||||||
|
|
||||||
|
const fbo_id = try opengl_impl.frameBuffer(out_tex[0]);
|
||||||
|
defer gl.DeleteFramebuffers(1, fbo_id[0..]);
|
||||||
|
|
||||||
|
const prog_id = try opengl_impl.program(); // Dynamic Shaders?
|
||||||
|
defer gl.DeleteProgram(prog_id);
|
||||||
|
|
||||||
|
var win_dim: Dimensions = default_dim;
|
||||||
|
|
||||||
|
emu_loop: while (true) {
|
||||||
|
// Outside of `SDL.SDL_QUIT` below, the DearImgui UI might signal that the program
|
||||||
|
// should exit, in which case we should also handle this
|
||||||
|
if (self.state.should_quit or sync.should_quit.load(.monotonic)) break :emu_loop;
|
||||||
|
|
||||||
|
var event: c.SDL_Event = undefined;
|
||||||
|
|
||||||
|
while (c.SDL_PollEvent(&event)) {
|
||||||
|
_ = zgui.backend.processEvent(&event);
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
c.SDL_EVENT_QUIT => break :emu_loop,
|
||||||
|
c.SDL_EVENT_KEY_DOWN => {
|
||||||
|
// TODO: Make use of compare_and_xor?
|
||||||
|
var keyinput: KeyInput = .{ .raw = 0x0000 };
|
||||||
|
|
||||||
|
switch (event.key.scancode) {
|
||||||
|
c.SDL_SCANCODE_UP => keyinput.up.write(true),
|
||||||
|
c.SDL_SCANCODE_DOWN => keyinput.down.write(true),
|
||||||
|
c.SDL_SCANCODE_LEFT => keyinput.left.write(true),
|
||||||
|
c.SDL_SCANCODE_RIGHT => keyinput.right.write(true),
|
||||||
|
c.SDL_SCANCODE_X => keyinput.a.write(true),
|
||||||
|
c.SDL_SCANCODE_Z => keyinput.b.write(true),
|
||||||
|
c.SDL_SCANCODE_A => keyinput.shoulder_l.write(true),
|
||||||
|
c.SDL_SCANCODE_S => keyinput.shoulder_r.write(true),
|
||||||
|
c.SDL_SCANCODE_RETURN => keyinput.start.write(true),
|
||||||
|
c.SDL_SCANCODE_RSHIFT => keyinput.select.write(true),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
bus_ptr.io.keyinput.fetchAnd(~keyinput.raw, .monotonic);
|
||||||
|
},
|
||||||
|
c.SDL_EVENT_KEY_UP => {
|
||||||
|
// FIXME(paoda): merge with above?
|
||||||
|
// TODO: Make use of compare_and_xor?
|
||||||
|
var keyinput: KeyInput = .{ .raw = 0x0000 };
|
||||||
|
|
||||||
|
switch (event.key.scancode) {
|
||||||
|
c.SDL_SCANCODE_UP => keyinput.up.write(true),
|
||||||
|
c.SDL_SCANCODE_DOWN => keyinput.down.write(true),
|
||||||
|
c.SDL_SCANCODE_LEFT => keyinput.left.write(true),
|
||||||
|
c.SDL_SCANCODE_RIGHT => keyinput.right.write(true),
|
||||||
|
c.SDL_SCANCODE_X => keyinput.a.write(true),
|
||||||
|
c.SDL_SCANCODE_Z => keyinput.b.write(true),
|
||||||
|
c.SDL_SCANCODE_A => keyinput.shoulder_l.write(true),
|
||||||
|
c.SDL_SCANCODE_S => keyinput.shoulder_r.write(true),
|
||||||
|
c.SDL_SCANCODE_RETURN => keyinput.start.write(true),
|
||||||
|
c.SDL_SCANCODE_RSHIFT => keyinput.select.write(true),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
bus_ptr.io.keyinput.fetchOr(keyinput.raw, .monotonic);
|
||||||
|
},
|
||||||
|
c.SDL_EVENT_WINDOW_RESIZED => {
|
||||||
|
log.debug("window resized to: {}x{}", .{ event.window.data1, event.window.data2 });
|
||||||
|
|
||||||
|
win_dim.width = @intCast(event.window.data1);
|
||||||
|
win_dim.height = @intCast(event.window.data2);
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var zgui_redraw: bool = false;
|
||||||
|
|
||||||
|
switch (self.state.emulation) {
|
||||||
|
.Transition => |inner| switch (inner) {
|
||||||
|
.Active => {
|
||||||
|
sync.paused.store(false, .monotonic);
|
||||||
|
if (!config.config().host.mute) try errify(c.SDL_PauseAudioDevice(c.SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK));
|
||||||
|
|
||||||
|
self.state.emulation = .Active;
|
||||||
|
},
|
||||||
|
.Inactive => {
|
||||||
|
// Assert that double pausing is impossible
|
||||||
|
try errify(c.SDL_ResumeAudioDevice(c.SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK));
|
||||||
|
sync.paused.store(true, .monotonic);
|
||||||
|
|
||||||
|
self.state.emulation = .Inactive;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
.Active => {
|
||||||
|
// Add FPS count to the histogram
|
||||||
|
if (tracker) |t| self.state.fps_hist.push(t.value()) catch {};
|
||||||
|
|
||||||
|
// Draw GBA Screen to Texture
|
||||||
|
{
|
||||||
|
gl.BindFramebuffer(gl.FRAMEBUFFER, fbo_id[0]);
|
||||||
|
defer gl.BindFramebuffer(gl.FRAMEBUFFER, 0);
|
||||||
|
|
||||||
|
gl.Viewport(0, 0, gba_width, gba_height);
|
||||||
|
opengl_impl.drawScreen(emu_tex[0], prog_id, vao_id[0], bus_ptr.ppu.framebuf.get(.Renderer));
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: We only really care about locking the audio device (and therefore writing silence)
|
||||||
|
// since if nfd-zig is used the emu may be paused for way too long. Perhaps we should try and limit
|
||||||
|
// spurious calls to SDL_LockAudioDevice?
|
||||||
|
|
||||||
|
try errify(c.SDL_LockAudioStream(self.audio.stream));
|
||||||
|
defer errify(c.SDL_UnlockAudioStream(self.audio.stream)) catch @panic("TODO: FIXME");
|
||||||
|
|
||||||
|
zgui_redraw = imgui.draw(&self.state, sync, win_dim, cpu, out_tex[0]);
|
||||||
|
},
|
||||||
|
.Inactive => zgui_redraw = imgui.draw(&self.state, sync, win_dim, cpu, out_tex[0]),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zgui_redraw) {
|
||||||
|
// Background Colour
|
||||||
|
const size = zgui.io.getDisplaySize();
|
||||||
|
gl.Viewport(0, 0, @intFromFloat(size[0]), @intFromFloat(size[1]));
|
||||||
|
gl.ClearColor(0, 0, 0, 1.0);
|
||||||
|
gl.Clear(gl.COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
zgui.backend.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
try errify(c.SDL_GL_SwapWindow(self.window));
|
||||||
|
}
|
||||||
|
|
||||||
|
sync.should_quit.store(true, .monotonic);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Audio = struct {
|
||||||
|
const Self = @This();
|
||||||
|
const log = std.log.scoped(.PlatformAudio);
|
||||||
|
|
||||||
|
stream: *c.SDL_AudioStream,
|
||||||
|
|
||||||
|
fn init(apu: *Apu) !Self {
|
||||||
|
var desired: c.SDL_AudioSpec = std.mem.zeroes(c.SDL_AudioSpec);
|
||||||
|
desired.freq = sample_rate;
|
||||||
|
desired.format = c.SDL_AUDIO_S16LE;
|
||||||
|
desired.channels = 2;
|
||||||
|
|
||||||
|
log.info("Host Sample Rate: {}Hz, Host Format: SDL_AUDIO_S16LE", .{sample_rate});
|
||||||
|
|
||||||
|
const stream = try errify(c.SDL_OpenAudioDeviceStream(c.SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desired, null, null));
|
||||||
|
errdefer c.SDL_DestroyAudioStream(stream);
|
||||||
|
|
||||||
|
apu.stream = stream;
|
||||||
|
return .{ .stream = stream };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinit(self: *Self) void {
|
||||||
|
c.SDL_DestroyAudioStream(self.stream);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const opengl_impl = struct {
|
||||||
|
fn drawScreen(tex_id: gl.uint, prog_id: gl.uint, vao_id: gl.uint, buf: []const u8) void {
|
||||||
|
gl.BindTexture(gl.TEXTURE_2D, tex_id);
|
||||||
|
defer gl.BindTexture(gl.TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
gl.TexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gba_width, gba_height, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, buf.ptr);
|
||||||
|
|
||||||
|
// Bind VAO
|
||||||
|
gl.BindVertexArray(vao_id);
|
||||||
|
defer gl.BindVertexArray(0);
|
||||||
|
|
||||||
|
// Use compiled frag + vertex shader
|
||||||
|
gl.UseProgram(prog_id);
|
||||||
|
defer gl.UseProgram(0);
|
||||||
|
|
||||||
|
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn program() !gl.uint {
|
||||||
|
const vert_shader: [1][*]const u8 = .{@embedFile("shader/pixelbuf.vert")};
|
||||||
|
const frag_shader: [1][*]const u8 = .{@embedFile("shader/pixelbuf.frag")};
|
||||||
|
|
||||||
|
const vs = gl.CreateShader(gl.VERTEX_SHADER);
|
||||||
|
defer gl.DeleteShader(vs);
|
||||||
|
|
||||||
|
gl.ShaderSource(vs, 1, vert_shader[0..], null);
|
||||||
|
gl.CompileShader(vs);
|
||||||
|
|
||||||
|
if (!shader.didCompile(vs)) return error.VertexCompileError;
|
||||||
|
|
||||||
|
const fs = gl.CreateShader(gl.FRAGMENT_SHADER);
|
||||||
|
defer gl.DeleteShader(fs);
|
||||||
|
|
||||||
|
gl.ShaderSource(fs, 1, frag_shader[0..], null);
|
||||||
|
gl.CompileShader(fs);
|
||||||
|
|
||||||
|
if (!shader.didCompile(fs)) return error.FragmentCompileError;
|
||||||
|
|
||||||
|
const prog = gl.CreateProgram();
|
||||||
|
gl.AttachShader(prog, vs);
|
||||||
|
gl.AttachShader(prog, fs);
|
||||||
|
gl.LinkProgram(prog);
|
||||||
|
|
||||||
|
return prog;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vao() [1]gl.uint {
|
||||||
|
var vao_id: [1]gl.uint = undefined;
|
||||||
|
gl.GenVertexArrays(1, vao_id[0..]);
|
||||||
|
|
||||||
|
return vao_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn screenTex(buf: []const u8) [1]gl.uint {
|
||||||
|
var tex_id: [1]gl.uint = undefined;
|
||||||
|
gl.GenTextures(1, tex_id[0..]);
|
||||||
|
|
||||||
|
gl.BindTexture(gl.TEXTURE_2D, tex_id[0]);
|
||||||
|
defer gl.BindTexture(gl.TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||||
|
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||||
|
|
||||||
|
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gba_width, gba_height, 0, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, buf.ptr);
|
||||||
|
|
||||||
|
return tex_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn outTex() [1]gl.uint {
|
||||||
|
var tex_id: [1]gl.uint = undefined;
|
||||||
|
gl.GenTextures(1, tex_id[0..]);
|
||||||
|
|
||||||
|
gl.BindTexture(gl.TEXTURE_2D, tex_id[0]);
|
||||||
|
defer gl.BindTexture(gl.TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||||
|
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||||
|
|
||||||
|
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gba_width, gba_height, 0, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, null);
|
||||||
|
|
||||||
|
return tex_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frameBuffer(tex_id: gl.uint) ![1]gl.uint {
|
||||||
|
var fbo_id: [1]gl.uint = undefined;
|
||||||
|
gl.GenFramebuffers(1, fbo_id[0..]);
|
||||||
|
|
||||||
|
gl.BindFramebuffer(gl.FRAMEBUFFER, fbo_id[0]);
|
||||||
|
defer gl.BindFramebuffer(gl.FRAMEBUFFER, 0);
|
||||||
|
|
||||||
|
gl.FramebufferTexture(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, tex_id, 0);
|
||||||
|
gl.DrawBuffers(1, &.{gl.COLOR_ATTACHMENT0});
|
||||||
|
|
||||||
|
if (gl.CheckFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE)
|
||||||
|
return error.FrameBufferObejctInitFailed;
|
||||||
|
|
||||||
|
return fbo_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shader = struct {
|
||||||
|
const log = std.log.scoped(.shader);
|
||||||
|
|
||||||
|
fn didCompile(id: gl.uint) bool {
|
||||||
|
var success: [1]gl.int = undefined;
|
||||||
|
gl.GetShaderiv(id, gl.COMPILE_STATUS, success[0..]);
|
||||||
|
|
||||||
|
if (success[0] == 0) err(id);
|
||||||
|
|
||||||
|
return success[0] == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn err(id: gl.uint) void {
|
||||||
|
const buf_len = 512;
|
||||||
|
var error_msg: [buf_len]u8 = undefined;
|
||||||
|
|
||||||
|
gl.GetShaderInfoLog(id, buf_len, null, &error_msg);
|
||||||
|
log.err("{s}", .{std.mem.sliceTo(&error_msg, 0)});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
160
src/ppu.zig
160
src/ppu.zig
@@ -1,160 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const EventKind = @import("scheduler.zig").EventKind;
|
|
||||||
const Io = @import("bus/io.zig").Io;
|
|
||||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
pub const width = 240;
|
|
||||||
pub const height = 160;
|
|
||||||
pub const buf_pitch = width * @sizeOf(u16);
|
|
||||||
const buf_len = buf_pitch * height;
|
|
||||||
|
|
||||||
pub const Ppu = struct {
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
vram: Vram,
|
|
||||||
palette: Palette,
|
|
||||||
sched: *Scheduler,
|
|
||||||
frame_buf: []u8,
|
|
||||||
alloc: Allocator,
|
|
||||||
|
|
||||||
pub fn init(alloc: Allocator, sched: *Scheduler) !Self {
|
|
||||||
// Queue first Hblank
|
|
||||||
sched.push(.Draw, sched.tick + (240 * 4));
|
|
||||||
|
|
||||||
return Self{
|
|
||||||
.vram = try Vram.init(alloc),
|
|
||||||
.palette = try Palette.init(alloc),
|
|
||||||
.sched = sched,
|
|
||||||
.frame_buf = try alloc.alloc(u8, buf_len),
|
|
||||||
.alloc = alloc,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: Self) void {
|
|
||||||
self.alloc.free(self.frame_buf);
|
|
||||||
self.vram.deinit();
|
|
||||||
self.palette.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn drawScanline(self: *Self, io: *const Io) void {
|
|
||||||
const bg_mode = io.dispcnt.bg_mode.read();
|
|
||||||
const scanline = io.vcount.scanline.read();
|
|
||||||
|
|
||||||
switch (bg_mode) {
|
|
||||||
0x3 => {
|
|
||||||
const start = buf_pitch * @as(usize, scanline);
|
|
||||||
const end = start + buf_pitch;
|
|
||||||
|
|
||||||
std.mem.copy(u8, self.frame_buf[start..end], self.vram.buf[start..end]);
|
|
||||||
},
|
|
||||||
0x4 => {
|
|
||||||
const select = io.dispcnt.frame_select.read();
|
|
||||||
const vram_start = width * @as(usize, scanline);
|
|
||||||
const buf_start = vram_start * @sizeOf(u16);
|
|
||||||
|
|
||||||
const start = vram_start + if (select) 0xA000 else @as(usize, 0);
|
|
||||||
const end = start + width; // Each Entry is only a byte long
|
|
||||||
|
|
||||||
// Render Current Scanline
|
|
||||||
for (self.vram.buf[start..end]) |byte, i| {
|
|
||||||
const id = byte * 2;
|
|
||||||
const j = i * @sizeOf(u16);
|
|
||||||
|
|
||||||
self.frame_buf[buf_start + j + 1] = self.palette.buf[id + 1];
|
|
||||||
self.frame_buf[buf_start + j] = self.palette.buf[id];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
else => {}, // std.debug.panic("[PPU] TODO: Implement BG Mode {}", .{bg_mode}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Palette = struct {
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
buf: []u8,
|
|
||||||
alloc: Allocator,
|
|
||||||
|
|
||||||
fn init(alloc: Allocator) !Self {
|
|
||||||
return Self{
|
|
||||||
.buf = try alloc.alloc(u8, 0x400),
|
|
||||||
.alloc = alloc,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deinit(self: Self) void {
|
|
||||||
self.alloc.free(self.buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get32(self: *const Self, idx: usize) u32 {
|
|
||||||
return (@as(u32, self.get16(idx + 2)) << 16) | @as(u32, self.get16(idx));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set32(self: *Self, idx: usize, word: u32) void {
|
|
||||||
self.set16(idx + 2, @truncate(u16, word >> 16));
|
|
||||||
self.set16(idx, @truncate(u16, word));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get16(self: *const Self, idx: usize) u16 {
|
|
||||||
return (@as(u16, self.buf[idx + 1]) << 8) | @as(u16, self.buf[idx]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set16(self: *Self, idx: usize, halfword: u16) void {
|
|
||||||
self.buf[idx + 1] = @truncate(u8, halfword >> 8);
|
|
||||||
self.buf[idx] = @truncate(u8, halfword);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get8(self: *const Self, idx: usize) u8 {
|
|
||||||
return self.buf[idx];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Vram = struct {
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
buf: []u8,
|
|
||||||
alloc: Allocator,
|
|
||||||
|
|
||||||
fn init(alloc: Allocator) !Self {
|
|
||||||
// In Modes 3 and 4, parts of the VRAM are copied to the
|
|
||||||
// frame buffer, therefore we want to zero-initialize Vram
|
|
||||||
//
|
|
||||||
// some programs like Armwrestler assume that VRAM is zeroed-out.
|
|
||||||
const black = std.mem.zeroes([0x18000]u8);
|
|
||||||
const buf = try alloc.alloc(u8, 0x18000);
|
|
||||||
std.mem.copy(u8, buf, &black);
|
|
||||||
|
|
||||||
return Self{
|
|
||||||
.buf = buf,
|
|
||||||
.alloc = alloc,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deinit(self: Self) void {
|
|
||||||
self.alloc.free(self.buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get32(self: *const Self, idx: usize) u32 {
|
|
||||||
return (@as(u32, self.get16(idx + 2)) << 16) | @as(u32, self.get16(idx));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set32(self: *Self, idx: usize, word: u32) void {
|
|
||||||
self.set16(idx + 2, @truncate(u16, word >> 16));
|
|
||||||
self.set16(idx, @truncate(u16, word));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get16(self: *const Self, idx: usize) u16 {
|
|
||||||
return (@as(u16, self.buf[idx + 1]) << 8) | @as(u16, self.buf[idx]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set16(self: *Self, idx: usize, halfword: u16) void {
|
|
||||||
self.buf[idx + 1] = @truncate(u8, halfword >> 8);
|
|
||||||
self.buf[idx] = @truncate(u8, halfword);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get8(self: *const Self, idx: usize) u8 {
|
|
||||||
return self.buf[idx];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("Bus.zig");
|
|
||||||
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
|
||||||
|
|
||||||
const Order = std.math.Order;
|
|
||||||
const PriorityQueue = std.PriorityQueue;
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
pub const Scheduler = struct {
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
tick: u64,
|
|
||||||
queue: PriorityQueue(Event, void, lessThan),
|
|
||||||
|
|
||||||
pub fn init(alloc: Allocator) Self {
|
|
||||||
var sched = Self{ .tick = 0, .queue = PriorityQueue(Event, void, lessThan).init(alloc, {}) };
|
|
||||||
sched.push(.HeatDeath, std.math.maxInt(u64));
|
|
||||||
|
|
||||||
return sched;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: Self) void {
|
|
||||||
self.queue.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handleEvent(self: *Self, _: *Arm7tdmi, bus: *Bus) void {
|
|
||||||
const should_handle = if (self.queue.peek()) |e| self.tick >= e.tick else false;
|
|
||||||
|
|
||||||
if (should_handle) {
|
|
||||||
const event = self.queue.remove();
|
|
||||||
// std.log.info("[Scheduler] Handle {} at {} ticks", .{ event.kind, self.tick });
|
|
||||||
|
|
||||||
switch (event.kind) {
|
|
||||||
.HeatDeath => {
|
|
||||||
std.debug.panic("[Scheduler] Somehow, a u64 overflowed", .{});
|
|
||||||
},
|
|
||||||
.HBlank => {
|
|
||||||
// The End of a Hblank (During Draw or Vblank)
|
|
||||||
const old_scanline = bus.io.vcount.scanline.read();
|
|
||||||
const scanline = (old_scanline + 1) % 228;
|
|
||||||
|
|
||||||
bus.io.vcount.scanline.write(scanline);
|
|
||||||
bus.io.dispstat.hblank.unset();
|
|
||||||
|
|
||||||
if (scanline < 160) {
|
|
||||||
// Transitioning to another Draw
|
|
||||||
self.push(.Draw, self.tick + (240 * 4));
|
|
||||||
} else {
|
|
||||||
// Transitioning to a Vblank
|
|
||||||
if (scanline < 227) bus.io.dispstat.vblank.set() else bus.io.dispstat.vblank.unset();
|
|
||||||
|
|
||||||
self.push(.VBlank, self.tick + (240 * 4));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.Draw => {
|
|
||||||
// The end of a Draw
|
|
||||||
bus.ppu.drawScanline(&bus.io);
|
|
||||||
|
|
||||||
// Transitioning to a Hblank
|
|
||||||
bus.io.dispstat.hblank.set();
|
|
||||||
self.push(.HBlank, self.tick + (68 * 4));
|
|
||||||
},
|
|
||||||
.VBlank => {
|
|
||||||
// The end of a Vblank
|
|
||||||
self.push(.HBlank, self.tick + (68 * 4));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn push(self: *Self, kind: EventKind, end: u64) void {
|
|
||||||
self.queue.add(.{ .kind = kind, .tick = end }) catch unreachable;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn nextTimestamp(self: *Self) u64 {
|
|
||||||
if (self.queue.peek()) |e| {
|
|
||||||
return e.tick;
|
|
||||||
} else unreachable;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Event = struct {
|
|
||||||
kind: EventKind,
|
|
||||||
tick: u64,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn lessThan(_: void, a: Event, b: Event) Order {
|
|
||||||
return std.math.order(a.tick, b.tick);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const EventKind = enum {
|
|
||||||
HeatDeath,
|
|
||||||
HBlank,
|
|
||||||
VBlank,
|
|
||||||
Draw,
|
|
||||||
};
|
|
||||||
24
src/shader/pixelbuf.frag
Normal file
24
src/shader/pixelbuf.frag
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#version 330 core
|
||||||
|
out vec4 frag_color;
|
||||||
|
|
||||||
|
in vec2 uv;
|
||||||
|
|
||||||
|
uniform sampler2D screen;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// https://near.sh/video/color-emulation
|
||||||
|
// Thanks to Talarubi + Near for the Colour Correction
|
||||||
|
// Thanks to fleur + mattrb for the Shader Impl
|
||||||
|
|
||||||
|
vec4 color = texture(screen, uv);
|
||||||
|
color.rgb = pow(color.rgb, vec3(4.0)); // LCD Gamma
|
||||||
|
|
||||||
|
frag_color = vec4(
|
||||||
|
pow(vec3(
|
||||||
|
0 * color.b + 50 * color.g + 255 * color.r,
|
||||||
|
30 * color.b + 230 * color.g + 10 * color.r,
|
||||||
|
220 * color.b + 10 * color.g + 50 * color.r
|
||||||
|
) / 255, vec3(1.0 / 2.2)), // Out Gamma
|
||||||
|
1.0);
|
||||||
|
}
|
||||||
|
|
||||||
10
src/shader/pixelbuf.vert
Normal file
10
src/shader/pixelbuf.vert
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#version 330 core
|
||||||
|
out vec2 uv;
|
||||||
|
|
||||||
|
const vec2 pos[3] = vec2[3](vec2(-1.0f, -1.0f), vec2(-1.0f, 3.0f), vec2(3.0f, -1.0f));
|
||||||
|
const vec2 uvs[3] = vec2[3](vec2( 0.0f, 0.0f), vec2( 0.0f, 2.0f), vec2(2.0f, 0.0f));
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
uv = uvs[gl_VertexID];
|
||||||
|
gl_Position = vec4(pos[gl_VertexID], 0.0, 1.0);
|
||||||
|
}
|
||||||
330
src/util.zig
330
src/util.zig
@@ -1,29 +1,325 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const config = @import("config.zig");
|
||||||
|
|
||||||
pub fn signExtend(comptime T: type, comptime bits: usize, value: anytype) T {
|
const Log2Int = std.math.Log2Int;
|
||||||
const ValT = comptime @TypeOf(value);
|
const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||||
comptime std.debug.assert(isInteger(ValT));
|
|
||||||
comptime std.debug.assert(isSigned(ValT));
|
|
||||||
|
|
||||||
const value_bits = @typeInfo(ValT).Int.bits;
|
const Allocator = std.mem.Allocator;
|
||||||
comptime std.debug.assert(value_bits >= bits);
|
|
||||||
|
|
||||||
const bit_diff = value_bits - bits;
|
pub const FpsTracker = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
// (1 << bits) -1 is a mask that will take values like 0x100 and make them 0xFF
|
fps: u32,
|
||||||
// value & mask so that only the relevant bits are sign extended
|
count: std.atomic.Value(u32),
|
||||||
// therefore, value & ((1 << bits) - 1) is the isolation of the relevant bits
|
timer: std.time.Timer,
|
||||||
return ((value & ((1 << bits) - 1)) << bit_diff) >> bit_diff;
|
|
||||||
|
pub fn init() Self {
|
||||||
|
return .{
|
||||||
|
.fps = 0,
|
||||||
|
.count = std.atomic.Value(u32).init(0),
|
||||||
|
.timer = std.time.Timer.start() catch unreachable,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn u32SignExtend(comptime bits: usize, value: u32) u32 {
|
pub fn tick(self: *Self) void {
|
||||||
return @bitCast(u32, signExtend(i32, bits, @bitCast(i32, value)));
|
_ = self.count.fetchAdd(1, .monotonic);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn isInteger(comptime T: type) bool {
|
pub fn value(self: *Self) u32 {
|
||||||
return @typeInfo(T) == .Int;
|
if (self.timer.read() >= std.time.ns_per_s) {
|
||||||
|
self.fps = self.count.swap(0, .monotonic);
|
||||||
|
self.timer.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn isSigned(comptime T: type) bool {
|
return self.fps;
|
||||||
return @typeInfo(T).Int.signedness == .signed;
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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 fn deinit(self: @This(), allocator: Allocator) void {
|
||||||
|
if (self.rom) |path| allocator.free(path);
|
||||||
|
if (self.bios) |path| allocator.free(path);
|
||||||
|
allocator.free(self.save);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const io = struct {
|
||||||
|
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 {
|
||||||
|
@branchHint(.cold);
|
||||||
|
|
||||||
|
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 {
|
||||||
|
@branchHint(.cold);
|
||||||
|
|
||||||
|
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();
|
||||||
|
const FmtArgTuple = std.meta.Tuple(&.{ u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32 });
|
||||||
|
|
||||||
|
buf: std.io.BufferedWriter(4096 << 2, std.fs.File.Writer),
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const audio = struct {
|
||||||
|
const _io = @import("core/bus/io.zig");
|
||||||
|
|
||||||
|
const ToneSweep = @import("core/apu/ToneSweep.zig");
|
||||||
|
const Tone = @import("core/apu/Tone.zig");
|
||||||
|
const Wave = @import("core/apu/Wave.zig");
|
||||||
|
const Noise = @import("core/apu/Noise.zig");
|
||||||
|
|
||||||
|
pub const length = struct {
|
||||||
|
const FrameSequencer = @import("core/apu.zig").FrameSequencer;
|
||||||
|
|
||||||
|
/// Update State of Ch1, Ch2 and Ch3 length timer
|
||||||
|
pub fn update(comptime T: type, self: *T, fs: *const FrameSequencer, nrx34: _io.Frequency) void {
|
||||||
|
comptime std.debug.assert(T == ToneSweep or T == Tone or T == Wave);
|
||||||
|
|
||||||
|
// Write to NRx4 when FS's next step is not one that clocks the length counter
|
||||||
|
if (!fs.isLengthNext()) {
|
||||||
|
// If length_enable was disabled but is now enabled and length timer is not 0 already,
|
||||||
|
// decrement the length timer
|
||||||
|
|
||||||
|
if (!self.freq.length_enable.read() and nrx34.length_enable.read() and self.len_dev.timer != 0) {
|
||||||
|
self.len_dev.timer -= 1;
|
||||||
|
|
||||||
|
// If Length Timer is now 0 and trigger is clear, disable the channel
|
||||||
|
if (self.len_dev.timer == 0 and !nrx34.trigger.read()) self.enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const ch4 = struct {
|
||||||
|
/// update state of ch4 length timer
|
||||||
|
pub fn update(self: *Noise, fs: *const FrameSequencer, nr44: _io.NoiseControl) void {
|
||||||
|
// Write to NRx4 when FS's next step is not one that clocks the length counter
|
||||||
|
if (!fs.isLengthNext()) {
|
||||||
|
// If length_enable was disabled but is now enabled and length timer is not 0 already,
|
||||||
|
// decrement the length timer
|
||||||
|
|
||||||
|
if (!self.cnt.length_enable.read() and nr44.length_enable.read() and self.len_dev.timer != 0) {
|
||||||
|
self.len_dev.timer -= 1;
|
||||||
|
|
||||||
|
// If Length Timer is now 0 and trigger is clear, disable the channel
|
||||||
|
if (self.len_dev.timer == 0 and !nr44.trigger.read()) self.enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Sets a quarter (8) of the bits of the u32 `left` to the value of u8 `right`
|
||||||
|
pub inline fn setQuart(left: u32, addr: u8, right: u8) u32 {
|
||||||
|
const offset: u2 = @truncate(addr);
|
||||||
|
|
||||||
|
return switch (offset) {
|
||||||
|
0b00 => (left & 0xFFFF_FF00) | right,
|
||||||
|
0b01 => (left & 0xFFFF_00FF) | @as(u32, right) << 8,
|
||||||
|
0b10 => (left & 0xFF00_FFFF) | @as(u32, right) << 16,
|
||||||
|
0b11 => (left & 0x00FF_FFFF) | @as(u32, right) << 24,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the correct shift offset for an aligned/unaligned u8 read
|
||||||
|
///
|
||||||
|
/// TODO: Support u16 reads of u32 values?
|
||||||
|
pub inline fn getHalf(byte: u8) u4 {
|
||||||
|
return @as(u4, @truncate(byte & 1)) << 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn setHalf(comptime T: type, left: T, addr: u8, right: HalfInt(T)) T {
|
||||||
|
const offset: u1 = @truncate(addr >> if (T == u32) 1 else 0);
|
||||||
|
|
||||||
|
return switch (T) {
|
||||||
|
u32 => switch (offset) {
|
||||||
|
0b0 => (left & 0xFFFF_0000) | right,
|
||||||
|
0b1 => (left & 0x0000_FFFF) | @as(u32, right) << 16,
|
||||||
|
},
|
||||||
|
u16 => switch (offset) {
|
||||||
|
0b0 => (left & 0xFF00) | right,
|
||||||
|
0b1 => (left & 0x00FF) | @as(u16, right) << 8,
|
||||||
|
},
|
||||||
|
else => @compileError("unsupported type"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Integer type which corresponds to T with exactly half the amount of bits
|
||||||
|
fn HalfInt(comptime T: type) type {
|
||||||
|
const type_info = @typeInfo(T);
|
||||||
|
comptime std.debug.assert(type_info == .int); // Type must be an integer
|
||||||
|
comptime std.debug.assert(type_info.int.bits % 2 == 0); // Type must have an even amount of bits
|
||||||
|
|
||||||
|
return std.meta.Int(type_info.int.signedness, type_info.int.bits >> 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Double Buffering Implementation
|
||||||
|
pub const FrameBuffer = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
layers: [2][]u8,
|
||||||
|
buf: []u8,
|
||||||
|
current: u1 = 0,
|
||||||
|
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
// TODO: Rename
|
||||||
|
const Device = enum { Emulator, Renderer };
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator, comptime len: comptime_int) !Self {
|
||||||
|
const buf = try allocator.alloc(u8, len * 2);
|
||||||
|
@memset(buf, 0);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
// Front and Back Framebuffers
|
||||||
|
.layers = [_][]u8{ buf[0..][0..len], buf[len..][0..len] },
|
||||||
|
.buf = buf,
|
||||||
|
|
||||||
|
.allocator = allocator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
@memset(self.buf, 0);
|
||||||
|
self.current = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.allocator.free(self.buf);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn swap(self: *Self) void {
|
||||||
|
self.current = ~self.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(self: *Self, comptime dev: Device) []u8 {
|
||||||
|
return self.layers[if (dev == .Emulator) self.current else ~self.current];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const RingBuffer = @import("zba_util").RingBuffer;
|
||||||
|
|
||||||
|
// TODO: Lock Free Queue?
|
||||||
|
pub fn Queue(comptime T: type) type {
|
||||||
|
return struct {
|
||||||
|
inner: RingBuffer(T),
|
||||||
|
mtx: std.Thread.Mutex = .{},
|
||||||
|
|
||||||
|
pub fn init(buf: []T) @This() {
|
||||||
|
return .{ .inner = RingBuffer(T).init(buf) };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(self: *@This(), value: T) !void {
|
||||||
|
self.mtx.lock();
|
||||||
|
defer self.mtx.unlock();
|
||||||
|
|
||||||
|
try self.inner.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pop(self: *@This()) ?T {
|
||||||
|
self.mtx.lock();
|
||||||
|
defer self.mtx.unlock();
|
||||||
|
|
||||||
|
return self.inner.pop();
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user