Compare commits

..

469 Commits

Author SHA1 Message Date
Rekai Nyangadzayi Musuka 1f4e01bd32 chore: resolve bulid error after rebase on main 2022-09-23 07:23:06 -03:00
Rekai Nyangadzayi Musuka f9ba5f9b3a chore: dont allocate not-small ?Sprite array on stack
use memset like most other allocations in this emu
2022-09-23 07:23:06 -03:00
Rekai Nyangadzayi Musuka 764ceff7a8 chore: move FrameBuffer struct to util.zig 2022-09-23 07:23:06 -03:00
Rekai Nyangadzayi Musuka e8fe2a5dec chore: move OAM, PALRAM and VRAM structs to separate files 2022-09-23 07:23:06 -03:00
Rekai Nyangadzayi Musuka b659c829fb fix: 8-bit writes to WIN PPU registers
Advance Wars depends on these registers similar to Mario Kart's 8-bit
writes to Affine Background registers:
2022-09-23 07:23:06 -03:00
Rekai Nyangadzayi Musuka 0bde97c6cd chore: refactor window 2022-09-23 07:23:06 -03:00
Rekai Nyangadzayi Musuka d921255c8e chore: crude background window impl (no affine) 2022-09-23 07:23:06 -03:00
Rekai Nyangadzayi Musuka aa1db42b0f chore: rename function (misspelt until now somehow) 2022-09-23 07:23:06 -03:00
Rekai Nyangadzayi Musuka c697dec716 chore: update dependencies 2022-09-23 07:21:46 -03:00
Rekai Nyangadzayi Musuka 92cfc763c0 chore: move util.zig 2022-09-19 16:07:19 -03:00
Rekai Nyangadzayi Musuka e192c6712f chore: disable audio sync by default
forgot SDL2 AudioStream doesn't work well for my use-case
2022-09-18 09:20:01 -03:00
Rekai Nyangadzayi Musuka 3466bf6c0a chore: change default settings 2022-09-18 06:30:39 -03:00
Rekai Nyangadzayi Musuka fbe3de0eb3 chore: reimpl util.escape
should make use of stdlib when I can
2022-09-18 06:23:30 -03:00
Rekai Nyangadzayi Musuka 4af144fca2 fix: Detect FRAM ROMs 2022-09-18 06:19:05 -03:00
Rekai Nyangadzayi Musuka 9a8aaba1ab chore: improve util and Gui API 2022-09-18 05:55:15 -03:00
Rekai Nyangadzayi Musuka fa3b9c21b9 chore: move Gpio and Clock structs to separate file 2022-09-18 00:37:45 -03:00
Rekai Nyangadzayi Musuka d3efa432fa Merge pull request 'Implement RTC' (#1) from rtc into main
Reviewed-on: #1
2022-09-17 23:36:34 +00:00
Rekai Nyangadzayi Musuka 50adb5fbac feat: add option to force-enable RTC 2022-09-17 20:27:17 -03:00
Rekai Nyangadzayi Musuka 19d78b9292 feat: auto-detect RTC in commercial ROMS 2022-09-17 20:23:49 -03:00
Rekai Nyangadzayi Musuka a2e702c366 fix: account for lateness in RTC scheduler event 2022-09-17 09:07:31 -03:00
Rekai Nyangadzayi Musuka 12c138364d fix: RTC day is 6 bits wide, not 3 2022-09-16 10:59:41 -03:00
Rekai Nyangadzayi Musuka 7783c11fac feat: put RTC Sync on Scheduler
TODO: Database to see what games have what GPIO devices
2022-09-16 10:39:02 -03:00
Rekai Nyangadzayi Musuka 3fc3366c8a chore: import datetime library + default time for RTC 2022-09-16 10:39:02 -03:00
Rekai Nyangadzayi Musuka d6b182f245 fix: ignore RTC Time/DateTime writes
this falls in-line with better emulators
2022-09-16 10:39:02 -03:00
Rekai Nyangadzayi Musuka 3857c44e68 chore: use Clock.Writer for Command parsing, delete Clock.Command 2022-09-16 10:39:02 -03:00
Rekai Nyangadzayi Musuka 089c5fa025 feat: implement RTC Read/Writes 2022-09-16 10:39:02 -03:00
Rekai Nyangadzayi Musuka c977f3f965 feat: implement force irqs for GPIO/RTC 2022-09-16 10:38:51 -03:00
Rekai Nyangadzayi Musuka 92417025e9 fix: properly resovle stack UAF 2022-09-16 02:10:41 -03:00
Rekai Nyangadzayi Musuka 1c52c0bf91 chore: shorten `orelse @panic` to `.?` 2022-09-16 02:10:41 -03:00
Rekai Nyangadzayi Musuka 617f7f4690 fix: update GpioData extern union
u4's are no longer supported in extern unions :\
2022-09-16 02:10:41 -03:00
Rekai Nyangadzayi Musuka 434a0dfac9 tmp: incomplete impl of GPIO + RTC 2022-09-16 02:10:41 -03:00
Rekai Nyangadzayi Musuka 4ec8dab460 chore: Guilty Gear X expects these I/O Registers 2022-09-14 11:38:26 -03:00
Rekai Nyangadzayi Musuka 59c9ff910e feat: implement open bus for unmapped i/o 2022-09-12 23:18:29 -03:00
Rekai Nyangadzayi Musuka 0027d3f8a3 chore: comment open bus impl 2022-09-11 07:38:55 -03:00
Rekai Nyangadzayi Musuka 9f45888910 chore: update dependencies 2022-09-11 06:59:10 -03:00
Rekai Nyangadzayi Musuka bf442d5a40 chore: Update README.md 2022-09-10 07:34:52 -03:00
Rekai Nyangadzayi Musuka 65cfc97f28 feat: reimplement audio sync
APU will now drop samples if the Audio Queue is already full, therefore
creating a "sped-up" effect when the emulator runs faster than 100%
2022-09-08 20:38:42 -03:00
Rekai Nyangadzayi Musuka fa862f095a chore: move arm/thumb lut idx functions 2022-09-06 23:58:24 -03:00
Rekai Nyangadzayi Musuka f3c05b6fe6 chore: update dependencies 2022-09-05 22:52:07 -03:00
Rekai Nyangadzayi Musuka 3fb7f2f814 chore: better conform to zig idioms 2022-09-03 18:30:48 -03:00
Rekai Nyangadzayi Musuka 59669ba3a5 chore: rename arm7tdmi variables to just cpu
Less verbose, specifying arm7tdmi doesn't really do much when there's
no other CPU in the system
2022-09-03 17:56:37 -03:00
Rekai Nyangadzayi Musuka 6a798d2c9d chore: allocate sprite array on heap
Each Sprite optional is 10 bytes meaning I'm allocating 1.28Kb on the
stack which isn't necessary.
2022-08-29 01:07:25 -05:00
Rekai Nyangadzayi Musuka 5f8c6833f4 chore: improve init/deinit methods 2022-08-29 01:07:25 -05:00
Rekai Nyangadzayi Musuka aa52bb5917 chore: reorganize some code 2022-08-26 14:13:49 -05:00
Rekai Nyangadzayi Musuka e57f918856 chore: pass the allocator as an argument more often
As of right now, I think the only cases where I shouldn't explicitly pass an allocator
are in read/write functions and deinits
2022-08-26 13:54:38 -05:00
Rekai Nyangadzayi Musuka e5b7441740 fix: resolve use-afer-free in backup.zig
This worked fine on stage1, and works fine in debug in stage3.
However, stage3 ReleaseSafe would panic due to what I assume must
have been an undefined behaviour optimization.

While I'm happy that I was quickly made aware of the issue thanks to
the safety checks in ReleaseSafe I do wish that this issue showed itself
in Debug, since I *am* using the GPA
2022-08-26 13:04:09 -05:00
Rekai Nyangadzayi Musuka 2ab8769b7a feat: Get ZBA working on Zig's new stage2/stage3 compiler 2022-08-21 12:28:31 -05:00
Rekai Nyangadzayi Musuka 3c3c0d32dd chore: move window scale const to emu.zig 2022-08-08 11:03:23 +02:00
Rekai Nyangadzayi Musuka 739db99c83 fix: reimpl debug reads w/out throwing away *const Self 2022-08-07 05:11:29 -05:00
Rekai Nyangadzayi Musuka 5a18b1dcc7 chore: update dependencies: 2022-08-06 08:28:30 -05:00
Rekai Nyangadzayi Musuka 2c8616f610 feat: reimplement cpu logging 2022-07-27 14:50:28 -03:00
Rekai Nyangadzayi Musuka 53eec5c3ff chore: don't init bus in Arm7tdmi init 2022-07-27 13:44:24 -03:00
Rekai Nyangadzayi Musuka c397b7069d feat: move arm instr decoding to module 2022-07-27 13:23:29 -03:00
Rekai Nyangadzayi Musuka 9d037fdc3e feat: move thumb instr decoding to module 2022-07-27 13:10:58 -03:00
Rekai Nyangadzayi Musuka 53191b0eeb chore: change directory structure 2022-07-22 21:11:19 -03:00
Rekai Nyangadzayi Musuka c7c4a90948 fix: reimplement halt fast-forwarding 2022-07-21 11:25:49 -03:00
Rekai Nyangadzayi Musuka 03ded099d2 chore: move audio sync, video sync variables 2022-07-21 11:05:49 -03:00
Rekai Nyangadzayi Musuka 5d9a57c7eb chore: update README.md 2022-07-12 18:28:11 -03:00
Rekai Nyangadzayi Musuka 769fad8996 chore: update SDL.zig 2022-07-12 18:11:32 -03:00
Rekai Nyangadzayi Musuka 3b3eb52c48 feat: impl WININ, WINOUT, WIN{N}H and WIN{N}V 2022-06-29 14:56:48 -03:00
Rekai Nyangadzayi Musuka d798aea6ea fix: force align DMA transfers 2022-06-29 04:31:02 -03:00
Rekai Nyangadzayi Musuka 5d37c212e2 fix: resolve bugs in VRAM unpredictable read/writes 2022-06-29 03:59:14 -03:00
Rekai Nyangadzayi Musuka 887bd89668 fix: don't start HDMA in vblank 2022-06-23 05:45:52 -03:00
Rekai Nyangadzayi Musuka 81c669fe64 feat: implement brightness increase/decrease 2022-06-23 04:50:04 -03:00
Rekai Nyangadzayi Musuka 265234ee97 feat: implement object blending 2022-06-23 02:49:56 -03:00
Rekai Nyangadzayi Musuka 02534c5c19 feat: implement background alpha blending 2022-06-19 22:10:56 -03:00
Rekai Nyangadzayi Musuka 5e4fb7b952 feat: implement BLDCNT, BLDALPHA, BLDY 2022-06-19 01:27:14 -03:00
Rekai Nyangadzayi Musuka 7e15e83d38 chore: update README 2022-06-19 00:02:33 -03:00
Rekai Nyangadzayi Musuka c9ea80e03b chore: rename + remove some code 2022-06-18 22:01:17 -03:00
Rekai Nyangadzayi Musuka 4cd722e447 fix: properly fire DMA IRQs
This resolves Sound DMA Timing issues present in DOOM
2022-06-18 21:13:12 -03:00
Rekai Nyangadzayi Musuka adfb23fab4 chore: rename Dma.active to Dma.in_progress 2022-06-18 19:15:34 -03:00
Rekai Nyangadzayi Musuka 5bbbdc3469 chore: rewrite info log message 2022-06-18 18:46:40 -03:00
Rekai Nyangadzayi Musuka 47adc0c5ae feat: implement NR10 obscure behaviour 2022-06-18 18:16:29 -03:00
Rekai Nyangadzayi Musuka 601b0b2aae feat: handle all I/O when using Cult-Of-GBA BIOS 2022-06-18 17:50:11 -03:00
Rekai Nyangadzayi Musuka 3becd790cf chore: 32-bit reads for PSG audio 2022-06-18 17:35:52 -03:00
Rekai Nyangadzayi Musuka 460f8308a7 chore: implement more than just 1 cycle per mem access 2022-06-16 22:35:42 -03:00
Rekai Nyangadzayi Musuka cc8c1c1e21 fix: implement register reads for Yoshi's Island 2022-06-16 02:32:31 -03:00
Rekai Nyangadzayi Musuka f5e401a4ee fix: reimplement DMA ticking 2022-06-16 01:46:37 -03:00
Rekai Nyangadzayi Musuka dba8873f76 chore(cpu): add inline fn isHalted() 2022-06-16 00:49:37 -03:00
Rekai Nyangadzayi Musuka db08edbdb9 chore: attempt to debug Rhythm Heaven 2022-06-16 00:03:51 -03:00
Rekai Nyangadzayi Musuka 35dba63b94 fix: impl BG?{X,Y} RefPoint write behaviour outside of Vblank
With this fix Mode 7-like games now properly render their backgrounds
2022-06-15 01:34:34 -03:00
Rekai Nyangadzayi Musuka a753912cb5 chore: change priority of some logs 2022-06-15 01:18:45 -03:00
Rekai Nyangadzayi Musuka 7441af9582 chore: mess with debug statements + mask APU I/O reads 2022-06-15 01:08:43 -03:00
Rekai Nyangadzayi Musuka 708f64035f chore: move timer, apu and dma i/o addr matching outside of io.zig 2022-06-15 00:05:36 -03:00
Rekai Nyangadzayi Musuka e4bbd33a49 chore: separate render code for affine sprites 2022-06-04 20:07:20 -03:00
Rekai Nyangadzayi Musuka 25d13722f7 chore: reimplement object rendering
TODO: implement affine sprites
2022-06-04 13:55:31 -03:00
Rekai Nyangadzayi Musuka 4eb0d469b3 chore: small changes to normal background drawing code 2022-06-04 10:21:03 -03:00
Rekai Nyangadzayi Musuka ce2271100b feat: implement affine backgrounds 2022-06-04 09:49:34 -03:00
Rekai Nyangadzayi Musuka e226a59a2f chore: stub 8-bit window registers 2022-06-04 09:48:59 -03:00
Rekai Nyangadzayi Musuka 7ff5f3b8e7 chore: remove code that pretends to remove DC offset 2022-06-04 09:47:58 -03:00
Rekai Nyangadzayi Musuka b6f5517c89 fix: replace affine bg register bitfields with signed integers 2022-06-03 19:26:52 -03:00
Rekai Nyangadzayi Musuka 2dc3864dca chore: use stdlib endian-aware integer read/write functions 2022-06-03 13:26:55 -03:00
Rekai Nyangadzayi Musuka deff74d804 chore: update zig version in README.md 2022-05-28 15:28:52 -03:00
Rekai Nyangadzayi Musuka 4f93f3e454 chore: update SDL.zig 2022-05-28 15:25:37 -03:00
Rekai Nyangadzayi Musuka 38afb567b9 chore: misc style improvements 2022-05-27 22:09:15 -03:00
Rekai Nyangadzayi Musuka 4006888629 chore: rename method in FpsTracker 2022-05-27 21:50:16 -03:00
Rekai Nyangadzayi Musuka e7faa9713b chore: update README.md 2022-05-27 21:27:19 -03:00
Rekai Nyangadzayi Musuka 413ff02ad9 fix(backup): resolve banking issue in flash impl 2022-05-27 21:17:50 -03:00
Rekai Nyangadzayi Musuka 517ed7a835 chore: remove awful ptr casts in backup.zig and bios.zig 2022-05-27 20:24:09 -03:00
Rekai Nyangadzayi Musuka d4bc1f2cd0 feat: pass jsmolka's bios.gba 2022-05-27 20:08:09 -03:00
Rekai Nyangadzayi Musuka c6ce810afe fix: play right samples in right channel 2022-05-27 18:47:34 -03:00
Rekai Nyangadzayi Musuka 1170673447 fix: resolve issue when handling event sooner than expected 2022-05-26 17:11:02 -03:00
Rekai Nyangadzayi Musuka c007bf4d8e fix: remove DC offset from audio output 2022-05-26 17:10:10 -03:00
Rekai Nyangadzayi Musuka 697ec46cf1 chore: add debug keybinds for scheduler capacity + event count 2022-05-25 15:13:57 -03:00
Rekai Nyangadzayi Musuka 41ee32b118 perf: don't check scheduler every iteration of runFrame loop
~20fps gain in Pokemon Emerald, nice
2022-05-25 14:17:44 -03:00
Rekai Nyangadzayi Musuka 8f5d054195 chore: simplify 4bpp palette code 2022-05-25 10:10:57 -03:00
Rekai Nyangadzayi Musuka c907552864 perf: convert BGR555 to RGBA8888 at compile-time, access w/ lookup table
Compile speed isn't slowed down by that much + there's a ~20fps gain in
Pokemon emerald, though this isn't anything exact
2022-05-24 13:55:50 -03:00
Rekai Nyangadzayi Musuka ff3f79801c chore: modify type signature of util.sext 2022-05-23 14:48:52 -03:00
Rekai Nyangadzayi Musuka f130d1991c chore: cleanup main 2022-05-23 12:50:01 -03:00
Rekai Nyangadzayi Musuka 24a8905c29 chore: emu audio sync code to emu.zig 2022-05-23 12:05:57 -03:00
Rekai Nyangadzayi Musuka e70fe73899 chore: redo apu sampling 2022-05-23 11:25:28 -03:00
Rekai Nyangadzayi Musuka a2d2a84850 chore: implement apu u16 reads 2022-05-21 15:09:32 -03:00
Rekai Nyangadzayi Musuka 109561310e fix: clean up frequency timer implementations 2022-05-21 14:21:50 -03:00
Rekai Nyangadzayi Musuka 5164aa961d Revert "fix: resolve off-by-one errors when scheduling freq timer expirations"
This reverts commit c9b0030b4b.
2022-05-21 13:46:46 -03:00
Rekai Nyangadzayi Musuka c9b0030b4b fix: resolve off-by-one errors when scheduling freq timer expirations 2022-05-21 13:34:14 -03:00
Rekai Nyangadzayi Musuka af2ad6c924 chore: improve APU accuracy + scheduler refactoring 2022-05-20 16:01:12 -03:00
Rekai Nyangadzayi Musuka e7777737b3 chore: update SDL.zig 2022-05-20 12:34:22 -03:00
Rekai Nyangadzayi Musuka c40cc2ba30 feat: stub Affine BG registers 2022-05-18 15:50:40 -03:00
Rekai Nyangadzayi Musuka 8dddb865cc fix: resolve out-of-bounds error with 8bpp tall / horizontal sprites
Boot ROM is now enabled by default as well
2022-05-17 12:16:30 -03:00
Rekai Nyangadzayi Musuka 2e821ab79c chore: improve audio accuracy 2022-05-17 11:28:05 -03:00
Rekai Nyangadzayi Musuka a667269d26 chore: reintroduce thread sleeping + simplify fps counter 2022-05-17 08:55:23 -03:00
Rekai Nyangadzayi Musuka daf977ef06 feat: implement double buffering 2022-05-17 06:53:37 -03:00
Rekai Nyangadzayi Musuka 660c8a2d62 chore: clean up DMA code 2022-05-05 22:36:11 -03:00
Rekai Nyangadzayi Musuka 9d590b099a feat: handle DMA IRQs (maybe?) 2022-05-05 22:04:59 -03:00
Rekai Nyangadzayi Musuka d5443d9c2f chore: contain Timers in a tuple rather than a struct 2022-05-05 20:09:00 -03:00
Rekai Nyangadzayi Musuka f0ce39230b chore: contain DMA Controllers in a tuple rather than a struct 2022-05-05 19:53:12 -03:00
Rekai Nyangadzayi Musuka c0e026b9a8 chore: update git submodules 2022-05-05 18:21:59 -03:00
Rekai Nyangadzayi Musuka 208e88e869 chore: resolve incorrect memory mirror in VRAM
+ stub GPIO registers on ROM Write
2022-05-05 16:44:48 -03:00
Rekai Nyangadzayi Musuka 5df023fb41 chore: stub a few I/O registers 2022-05-03 22:41:05 -03:00
Rekai Nyangadzayi Musuka 46ac1542a6 chore: allow 8-bit IO to BG0CNT and BG1CNT
BG0CNT and and BG1CNT now work properly in mario kart
2022-05-01 20:41:00 -03:00
Rekai Nyangadzayi Musuka c2f55e0bfb chore: define affine sprite attributes 2022-05-01 19:15:56 -03:00
Rekai Nyangadzayi Musuka 12f9bb51c1 feat: stub mode 1 and 2 2022-05-01 18:53:11 -03:00
Rekai Nyangadzayi Musuka 41558c9103 feat: implement mode 5
I wonder which obscure game makes heavy use of this mode
2022-05-01 18:10:52 -03:00
Rekai Nyangadzayi Musuka 68012f84d3 chore: comment ARM MSR code + Audio issues 2022-04-30 22:17:34 -05:00
Rekai Nyangadzayi Musuka 640b1f7c5d chore: pass destoer's cond_invalid test 2022-04-30 20:42:47 -05:00
Rekai Nyangadzayi Musuka f2f4bb205a chore: misc print message improvements 2022-04-29 12:41:05 -05:00
Rekai Nyangadzayi Musuka 6c88a0aec2 chore: improvements to APU accuracy 2022-04-29 12:19:31 -05:00
Rekai Nyangadzayi Musuka 002287ecfe fix: incorrect order-of-operations in ARM BL impl 2022-04-27 23:15:39 -05:00
Rekai Nyangadzayi Musuka a87b46898b chore: special case saving for ROMS without titles 2022-04-27 18:08:44 -05:00
Rekai Nyangadzayi Musuka 417810581b chore: update README 2022-04-26 10:52:56 -05:00
Rekai Nyangadzayi Musuka bc2950916f chore: update most recent zig version 2022-04-26 09:57:26 -05:00
Rekai Nyangadzayi Musuka d9c9105449 feat: pass DenSinH's eeprom-test 2022-04-25 17:20:43 -05:00
Rekai Nyangadzayi Musuka 05a432f1c1 feat: implement EEPROM 2022-04-25 16:23:24 -05:00
Rekai Nyangadzayi Musuka f4a48d536c chore: implement I/O regsister for Minish Cap 2022-04-25 08:01:34 -05:00
Rekai Nyangadzayi Musuka 81db06d2fc chore: change default window scale to 4x 2022-04-24 08:33:28 -04:00
Rekai Nyangadzayi Musuka 1812fb8008 chore: write more debug log messages for unimplemented registers 2022-04-22 22:19:26 -03:00
Rekai Nyangadzayi Musuka 762494453f chore: only sync to audio for now 2022-04-22 20:56:52 -03:00
Rekai Nyangadzayi Musuka e3553bcbd6 feat: panic on unimplemented I/O in ReleaseSafe/Debug but not ReleaseFast 2022-04-22 20:56:52 -03:00
Rekai Nyangadzayi Musuka 9cce4d9859 chore: misc improvements 2022-04-21 10:15:52 -03:00
Rekai Nyangadzayi Musuka 75ba9a4bf9 fix: improper lifetime for *Arm7tdmi ptr in Bus
*Arm7tdmi ptr is now assigned one scope up so that it lives as least
as long as Bus does
2022-04-21 07:56:17 -03:00
Rekai Nyangadzayi Musuka 85e8ca9146 feat: implement Noise
Kirby & The Amazing Mirror crashes only in ReleaseSafe / ReleaseBug.

TODO: Figure out why
2022-04-21 02:40:02 -03:00
Rekai Nyangadzayi Musuka 2f07c18f0b feat: implement ch3 2022-04-21 00:21:55 -03:00
Rekai Nyangadzayi Musuka ed3d275974 feat: implement ch2 2022-04-20 21:33:46 -03:00
Rekai Nyangadzayi Musuka 0184ec3e5e feat: implement ch1
TODO: It's really loud
2022-04-20 20:52:50 -03:00
Rekai Nyangadzayi Musuka 97a689ab55 chore: broken impl of ch1 2022-04-20 09:39:12 -03:00
Rekai Nyangadzayi Musuka c3611a0f00 feat: add audio resampler
Also implement extremely naive audio sync
2022-04-20 06:27:06 -03:00
Rekai Nyangadzayi Musuka d270ec711f chore: calculate apu sample rate a bit better 2022-04-20 02:36:32 -03:00
Rekai Nyangadzayi Musuka 172a59aefb feat: schedule audio sampling on scheduler
DMA sound in games like Pokemon Emerald, Chobits, Love Hina, and Kirby:
Nightmare in Dream Land sound great save for conerns about resampling
2022-04-14 05:58:32 -03:00
Rekai Nyangadzayi Musuka 92cabd248b chore: improve timer behaviour 2022-04-14 01:58:40 -03:00
Rekai Nyangadzayi Musuka 6b09250a56 chore: move some init code to functions 2022-04-14 00:52:21 -03:00
Rekai Nyangadzayi Musuka f6d746e810 feat: impelemt THUMB open bus 2022-04-14 00:21:49 -03:00
Rekai Nyangadzayi Musuka 9b9b6c0d6f feat: implement ARM read open bus 2022-04-13 23:28:14 -03:00
Rekai Nyangadzayi Musuka 6d5c30ac25 fix: remove accidental rotation in ldrsh instructions 2022-04-13 22:59:32 -03:00
Rekai Nyangadzayi Musuka c1b74d556a chore: move log statement 2022-04-13 21:45:15 -03:00
Rekai Nyangadzayi Musuka dfe94fb931 chore: remove magic numbers 2022-04-13 21:39:35 -03:00
Rekai Nyangadzayi Musuka ffbb31c767 chore: remove unnecessary 32MB allocation 2022-04-13 21:25:41 -03:00
Rekai Nyangadzayi Musuka 714209565b chore: define more I/O read/writes 2022-04-12 00:50:44 -03:00
Rekai Nyangadzayi Musuka 643cd13952 chore: update README 2022-04-11 23:14:44 -03:00
Rekai Nyangadzayi Musuka 2c763e9772 feat: pass jsmolka memory.gba 2022-04-11 22:52:17 -03:00
Rekai Nyangadzayi Musuka ad1f5ea8b8 chore: ignore instead of logging errors for perf reasons 2022-04-10 23:10:06 -03:00
Rekai Nyangadzayi Musuka 76b4d56ca6 feat: Initial Implementation of DMA Audio 2022-04-10 04:50:09 -03:00
Rekai Nyangadzayi Musuka c100d64fcb chore: tick scheduler on memory access 2022-04-09 19:43:27 -03:00
Rekai Nyangadzayi Musuka 5da84aff36 chore: log error on open bus in page 0x00 and 0x01 2022-04-09 18:01:17 -03:00
Rekai Nyangadzayi Musuka 76789aa8bc chore: rewrite I/O read/writes 2022-04-08 17:07:36 -03:00
Rekai Nyangadzayi Musuka 80e714e2eb chore: reimplement bus read/writes 2022-04-08 16:48:43 -03:00
Rekai Nyangadzayi Musuka 37a360ec07 fix: force align reads/writes in memory bus rather than in CPU 2022-04-08 15:17:31 -03:00
Rekai Nyangadzayi Musuka a976a5769e fix: pass none.gba and kind of sram.gba from jsmolka test suite 2022-04-08 14:38:35 -03:00
Rekai Nyangadzayi Musuka 6df55c2d86 feat: implement GamePak out-of-bounds reads 2022-04-08 02:34:08 -03:00
Rekai Nyangadzayi Musuka a1008738d2 chore: run zigfmt 2022-04-08 02:13:58 -03:00
Rekai Nyangadzayi Musuka 11a034658a chore: change implementation of rotr 2022-04-08 02:13:41 -03:00
Rekai Nyangadzayi Musuka aac01b0bfe chore: rewrite read/write methods for remainig Bus devices 2022-04-08 02:08:26 -03:00
Rekai Nyangadzayi Musuka 5310c12669 chore: mirror VRAM 2022-04-08 01:10:12 -03:00
Rekai Nyangadzayi Musuka 9b9de11e0c chore: write generic read/write for VRAM 2022-04-08 00:44:52 -03:00
Rekai Nyangadzayi Musuka f8018854be Merge branch 'main' of ssh://musuka.dev:2222/paoda/zba 2022-04-07 17:23:22 -03:00
Rekai Nyangadzayi Musuka fae4b430ab chore: update dependencies 2022-04-07 17:23:07 -03:00
Rekai Nyangadzayi Musuka 1bb3659df6 chore: update README 2022-03-29 18:52:09 -03:00
Rekai Nyangadzayi Musuka 3046e6243a chore: don't assume 1cpi when stepping by a frame 2022-03-29 09:06:26 -03:00
Rekai Nyangadzayi Musuka e127669549 Revert "chore: tick on memory access instead of 1cpi"
This reverts commit 7f555095f2.
2022-03-29 08:58:57 -03:00
Rekai Nyangadzayi Musuka 7f555095f2 chore: tick on memory access instead of 1cpi 2022-03-29 08:50:12 -03:00
Rekai Nyangadzayi Musuka 29da7b294e feat: implement Flash backup cartrige kinds 2022-03-28 19:41:22 -03:00
Rekai Nyangadzayi Musuka bf7b533b3c chore: stub more apu I/O addresses 2022-03-28 19:40:47 -03:00
Rekai Nyangadzayi Musuka 37c039fb92 fix: don't create un-needed save file
If we don't know if we support a game's save type yet, avoid
creating a file for it.
2022-03-22 15:03:05 -03:00
Rekai Nyangadzayi Musuka 4c172cff70 fix: account for subset of disallowed chars in save file names 2022-03-22 14:55:08 -03:00
Rekai Nyangadzayi Musuka bd54cba8a0 feat: implement SRAM saving and loading 2022-03-22 14:41:18 -03:00
Rekai Nyangadzayi Musuka da4bb17782 chore: properly deallocate OAM buffer 2022-03-22 11:41:17 -03:00
Rekai Nyangadzayi Musuka 5dd69500ca fix: speed percentage in title is now accurate
We now properly account for full speed being 59.97Hz not, 59Hz or 60Hz
2022-03-22 10:39:42 -03:00
Rekai Nyangadzayi Musuka eff25a0ab2 chore: make some variables const 2022-03-19 02:00:53 -03:00
Rekai Nyangadzayi Musuka 1901a471e4 feat: minor performance improvements 2022-03-18 09:49:49 -03:00
Rekai Nyangadzayi Musuka 3d61c0dba4 feat: switch from BGR555 to RGBA8888 2022-03-18 07:52:54 -03:00
Rekai Nyangadzayi Musuka 39ab363afa fix: improve perf of instructions w/ rotr 2022-03-16 22:56:37 -03:00
Rekai Nyangadzayi Musuka 1921218c7b fix: improve frame limiting and fps counting 2022-03-16 21:25:32 -03:00
Rekai Nyangadzayi Musuka 40968f0990 fix: implement proper SRAM mirroring and stub Flash 2022-03-15 21:54:55 -03:00
Rekai Nyangadzayi Musuka 04d54ec97a chore: move DMA and Timers from io to bus 2022-03-15 08:25:26 -03:00
Rekai Nyangadzayi Musuka 1fd80c1c23 feat: define APU registers 2022-03-15 08:09:07 -03:00
Rekai Nyangadzayi Musuka 48679fa4ca fix: move code in scheduler to ppu 2022-03-15 08:09:07 -03:00
Rekai Nyangadzayi Musuka bdea19f280 chore: create different types of emuloops 2022-03-15 03:46:33 -03:00
Rekai Nyangadzayi Musuka 5579643d65 fix: resolve relative sprite priority issues 2022-03-15 00:37:29 -03:00
Rekai Nyangadzayi Musuka c6e6b42869 chore: improve accuracy of frame limiter 2022-03-14 20:38:29 -03:00
Rekai Nyangadzayi Musuka 3623362f72 chore: improve accuracy of thread sleep in emu thread 2022-03-14 08:54:48 -03:00
Rekai Nyangadzayi Musuka c538079ad4 feat: implement video sync 2022-03-14 05:16:02 -03:00
Rekai Nyangadzayi Musuka 8e3f48837d chore: organize io switch statements 2022-03-13 07:50:19 -03:00
Rekai Nyangadzayi Musuka 025f295c08 fix: mirror SRAM
SRAM is mirrored in 64K chunks
TODO: According to GBATEK SRAM chips are 32K and mirrored
2022-03-13 05:53:32 -03:00
Rekai Nyangadzayi Musuka 017ec407f5 chore: don't panic on unknown bus and io writes/reads
This will lead to emulation bugs due to devices I've yet to implement but by
doing this a lot of games become playable "by default" such as Doom or
Kirby: Nightmare in Dream Land.

When implementing feature and/or debuggin make sure to set:
panic_on_und_bus and panic_on_und_io to true so that the emu crashes
on unknown reads/writes
2022-03-13 05:39:09 -03:00
Rekai Nyangadzayi Musuka 4a76611fca feat: implement Timers 2022-03-13 05:35:01 -03:00
Rekai Nyangadzayi Musuka cb10dfbdfd fix: implement sprite coord overflow behaviour 2022-03-12 03:46:41 -04:00
Rekai Nyangadzayi Musuka 0c50ef1e6d fix: resolve issues with sprite mirroring 2022-03-11 23:52:56 -04:00
Rekai Nyangadzayi Musuka 05e67da181 feat: Implement MVP of Mode 0 Sprites 2022-03-11 01:43:47 -04:00
Rekai Nyangadzayi Musuka f1df3d6615 chore: clean up io 2022-03-04 22:55:04 -04:00
Rekai Nyangadzayi Musuka c5b4b51ae0 feat: fix tile flipping issue 2022-03-03 03:08:35 -04:00
Rekai Nyangadzayi Musuka 2e4854c2ff chore: add some type definitions for sprites 2022-03-03 02:10:33 -04:00
Rekai Nyangadzayi Musuka 85f0b13f4a feat: improve DMA Transfer support 2022-03-03 02:10:29 -04:00
Rekai Nyangadzayi Musuka dccd00782b chore(ppu): resolve integer overflow regression 2022-03-02 23:15:10 -04:00
Rekai Nyangadzayi Musuka 3d8c944bcc feat(ppu): implement bg priority and transparency 2022-03-02 01:39:05 -04:00
Rekai Nyangadzayi Musuka 90302d1c52 chore: update README.md 2022-03-01 21:30:29 -04:00
Rekai Nyangadzayi Musuka cb4d3a9a51 chore: replace unnecessarily complex sign extension implementation 2022-02-28 20:38:50 -04:00
Rekai Nyangadzayi Musuka ddb68a7952 feat: pass beeg yoshi 2022-02-28 18:24:24 -04:00
Rekai Nyangadzayi Musuka 97de5d1a96 fix: palette id is a u16 not a u8 2022-02-28 17:32:10 -04:00
Rekai Nyangadzayi Musuka d6ef53fd67 feat: DMA Transfer MVP 2022-02-28 12:34:00 -06:00
Rekai Nyangadzayi Musuka b65f833b28 feat(ppu): implement transparency + backdrop in mode 0 2022-02-26 18:33:16 -06:00
Rekai Nyangadzayi Musuka ac0486be1b chore(io): replace some bitfields with enums 2022-02-24 17:20:23 -06:00
Rekai Nyangadzayi Musuka 441ebc38c7 fix: better emulate behaviour of IO reads 2022-02-24 17:20:20 -06:00
Rekai Nyangadzayi Musuka be2dfb379a chore: document select unimplmented I/O registers
These registers are written to / read from Kirby: Nightmare in Dream Land
2022-02-22 17:14:30 -06:00
Rekai Nyangadzayi Musuka bc66be6c06 feat: impelement a barebones SRAM 2022-02-22 17:14:26 -06:00
Rekai Nyangadzayi Musuka 5368ff912d feat: pass retAddr.gba 2022-02-21 15:34:46 -06:00
Rekai Nyangadzayi Musuka c2cf2d2965 feat: implement Hblank and Vcount Interrupts
Also implemented unique behaviour when writing to IF
2022-02-21 14:45:47 -06:00
Rekai Nyangadzayi Musuka e5ab8b51a9 chore: improve Bus log + panic messages 2022-02-19 11:48:43 -05:00
Rekai Nyangadzayi Musuka c767e88e8d chore: improve io.zig 2022-02-19 11:48:17 -05:00
Rekai Nyangadzayi Musuka 9e2e8c3d1a feat: implement mirroring for IWRAM EWRAM, OAM and PALRAM
Also realized I confused IWRAM and EWRAM. This is also fixed
TODO: Implemnt Mirroring for VRRAM
2022-02-19 10:08:31 -05:00
Rekai Nyangadzayi Musuka e426f2459e fix: resolve integer overflow in BG0 Drawing 2022-02-19 06:55:30 -04:00
Rekai Nyangadzayi Musuka 3746cf6025 chore: don't panic on 32-bit I/O 2022-02-19 06:45:39 -04:00
Rekai Nyangadzayi Musuka f6c8d7ca07 chore: stub CPU HALTing 2022-02-17 00:27:34 -04:00
Rekai Nyangadzayi Musuka 07343efdf3 chore: correct logic errors in map size 1 and 3 2022-02-16 23:49:08 -04:00
Rekai Nyangadzayi Musuka 4018f3875b chore: properly write to VOFS and HOFS in 32-bit bus 2022-02-16 23:23:41 -04:00
Rekai Nyangadzayi Musuka 034f2e8d1d feat: implement hofs and vofs on io bus 2022-02-16 04:29:04 -04:00
Rekai Nyangadzayi Musuka d275a4890f feat: implement scrolling 2022-02-16 03:37:25 -04:00
Rekai Nyangadzayi Musuka ce97a52868 feat: add support for multiple BGs in Mode 0 2022-02-16 03:27:06 -04:00
Rekai Nyangadzayi Musuka d2d4667f7b feat: document mode 0 2022-02-16 03:05:19 -04:00
Rekai Nyangadzayi Musuka 5835b509e4 feat: Mode 0 MVP 2022-02-16 02:27:15 -04:00
Rekai Nyangadzayi Musuka 338122ed43 chore: use zig slices for fun 2022-02-13 05:28:56 -04:00
Rekai Nyangadzayi Musuka e5a76a3c02 chore: give DISPCNT DISPSTAT and VCOUNT to PPU struct 2022-02-13 04:28:15 -04:00
Rekai Nyangadzayi Musuka 31fa06ac4a chore: give io read/write functions access to the entire Bus 2022-02-13 04:13:06 -04:00
Rekai Nyangadzayi Musuka ec25a9aae4 feat: implement BG Scrolling Registers 2022-02-13 04:04:10 -04:00
Rekai Nyangadzayi Musuka b238a3e8f3 feat: impelemnt BG0,1,2CNT and IF 2022-02-13 03:23:09 -04:00
Rekai Nyangadzayi Musuka aca7fc9a60 feat: implement OAM 2022-02-13 02:30:02 -04:00
Rekai Nyangadzayi Musuka d2740e30d9 chore: squash bugs preventing swi_demo.gba from working 2022-02-13 02:29:53 -04:00
Rekai Nyangadzayi Musuka 8ab7a178c1 chore(cpu): reimplement bank switching logic 2022-02-12 04:33:32 -04:00
Rekai Nyangadzayi Musuka d897c2fdcc fix: don't mask away MSB in THUMB.5 add 2022-02-12 03:23:55 -04:00
Rekai Nyangadzayi Musuka 783706193b fix: properly decode format 11 instructions 2022-02-12 03:13:38 -04:00
Rekai Nyangadzayi Musuka b93bd53529 chore: make use of scoped logging 2022-02-11 01:33:33 -04:00
Rekai Nyangadzayi Musuka f9013cf9db Merge branch 'main' of ssh://musuka.dev:2222/paoda/zba 2022-02-10 23:02:35 -04:00
Rekai Nyangadzayi Musuka eaac49cebb chore: update README 2022-02-10 21:21:34 -04:00
Rekai Nyangadzayi Musuka ee27053db3 chore: remove TODOs and some useless imports 2022-02-06 19:07:23 -04:00
Rekai Nyangadzayi Musuka 7441dd151c fix: improper condition check and initialization of register 2022-02-06 18:41:16 -04:00
Rekai Nyangadzayi Musuka bbd4447734 fix(cpu): force align thumb and arm block data transfers 2022-02-06 17:08:12 -04:00
Rekai Nyangadzayi Musuka 225c0f7d55 feat: pass arm.gba 2022-02-06 05:06:25 -04:00
Rekai Nyangadzayi Musuka fcde905ae1 chore: reimplement ARM LDM/STM 2022-02-06 04:34:45 -04:00
Rekai Nyangadzayi Musuka 798987eba0 chore: improve arm ldm/stm 2022-02-05 23:29:34 -04:00
Rekai Nyangadzayi Musuka adfd501fc4 fix(cpu): force-align SWP reads and writes 2022-02-05 23:18:23 -04:00
Rekai Nyangadzayi Musuka 9581e3b3cb fix: force-align ARM STRH reads 2022-02-05 23:09:13 -04:00
Rekai Nyangadzayi Musuka 1b9ab1f1d7 fix: implement the same LDRSH logic as THUMB LDRSH 2022-02-05 23:09:02 -04:00
Rekai Nyangadzayi Musuka c52dc5adb1 fix: PC is 12 ahead when it is rd in str and strb 2022-02-05 21:42:04 -04:00
Rekai Nyangadzayi Musuka 7bfb87a859 fix: listen to my past self
By deleting this line I go from test 234 to test 355 in arm.gba
2022-02-05 21:35:26 -04:00
Rekai Nyangadzayi Musuka aec189ac6a chore: update SDL.zig 2022-02-05 21:07:15 -04:00
Rekai Nyangadzayi Musuka 0aece06107 chore: dont use std.mem.bytesToValue
the stdlib accounts for endianness, which isn't something we want.
2022-02-05 21:05:08 -04:00
Rekai Nyangadzayi Musuka 2842345111 chore: remove unnecessary @as calls 2022-02-05 21:01:39 -04:00
Rekai Nyangadzayi Musuka aa6f3c7a92 feat: pass thumb.gba 2022-02-05 20:39:15 -04:00
Rekai Nyangadzayi Musuka 3ae24d6977 chore: account for empty rlist in THUMB LDM/STM 2022-02-05 18:03:39 -04:00
Rekai Nyangadzayi Musuka 0a22730479 fix(cpu): handle edge case in LDRSH 2022-02-05 17:12:25 -04:00
Rekai Nyangadzayi Musuka 166bc6fc6d chore: specify which compiler this project is built with 2022-02-05 16:28:06 -04:00
Rekai Nyangadzayi Musuka bf4207ba8c chore: reorganize util.zig 2022-02-05 15:55:12 -04:00
Rekai Nyangadzayi Musuka 78080b4682 fix: zero initialize all allocated memory 2022-02-05 15:54:53 -04:00
Rekai Nyangadzayi Musuka 9159270e87 chore: don't commit *.sh files 2022-02-05 15:53:30 -04:00
Rekai Nyangadzayi Musuka 428eff1468 Revert "fix: allow for 32-bit reads to KEYINPUT"
This reverts commit 3a51707280.
2022-02-05 14:52:49 -04:00
Rekai Nyangadzayi Musuka 5ec8d4b0a5 fix: resolve decoding mixup in THUMB format 8 instructions 2022-02-05 14:50:34 -04:00
Rekai Nyangadzayi Musuka 3a51707280 fix: allow for 32-bit reads to KEYINPUT 2022-02-05 13:47:05 -04:00
Rekai Nyangadzayi Musuka b4d20fb264 chore: refactor ARMv4 decoding 2022-02-05 13:46:55 -04:00
Rekai Nyangadzayi Musuka 746158043d chore: add more debug information to CPU panic method 2022-02-05 13:46:24 -04:00
Rekai Nyangadzayi Musuka 25300c8a9f chore: give more descriptive panic messages when changing mode fails 2022-02-04 16:54:57 -04:00
Rekai Nyangadzayi Musuka 27d0ba8c7e chore: clean up THUMB instruction decoding 2022-02-04 15:57:46 -04:00
Rekai Nyangadzayi Musuka 2f74b61f2e feat: parse cartridge header 2022-02-04 05:54:06 -04:00
Rekai Nyangadzayi Musuka b233981a34 feat: rename ARM and THUMB SWI functions 2022-02-04 04:34:47 -04:00
Rekai Nyangadzayi Musuka 1b8db0c427 chore: group THUMB and select ARM instructions together (same file) 2022-02-04 04:18:20 -04:00
Rekai Nyangadzayi Musuka 3e4f9eddb2 feat: integrate zig-clap with ZBA 2022-02-04 03:12:35 -04:00
Rekai Nyangadzayi Musuka 6ab4610a81 fix(cpu): properly decode format 7 and 8 2022-02-03 01:29:18 -04:00
Rekai Nyangadzayi Musuka 91384a7c68 fix(cpu): resolve edge cases in THUMB Format 5 2022-02-03 00:55:57 -04:00
Rekai Nyangadzayi Musuka c6bb4bf8e1 fix(cpu): allow for select values to overflow
FuzzARM found these operations which panicked, when they should
have overflowed. These are now fixed

n = 8000
2022-02-02 22:49:33 -04:00
Rekai Nyangadzayi Musuka 800ed6f1a7 feat(cpu): implement format 13
While bugs do exist, at this point all THUMB and ARMv4 instructions
have been implemented! Yay!
2022-02-02 22:31:21 -04:00
Rekai Nyangadzayi Musuka 027e4fb57b feat(cpu): implement THUMB format 17 2022-02-02 22:31:08 -04:00
Rekai Nyangadzayi Musuka 1378c809e6 feat(cpu): implement THUMB format11 2022-02-02 22:30:46 -04:00
Rekai Nyangadzayi Musuka 33399e9517 chore: update to latest zig nightly 2022-02-02 21:26:12 -04:00
Rekai Nyangadzayi Musuka 99492a6782 chore: progress towards passing ldr/str thumb in armwrestler 2022-02-02 21:14:46 -04:00
Rekai Nyangadzayi Musuka 8b574efe85 fix(cpu): properly negate in NEG 2022-02-02 20:12:20 -04:00
Rekai Nyangadzayi Musuka 9fd03d2a92 fix(cpu): reimplement THUMB offset shifts 2022-02-02 20:12:07 -04:00
Rekai Nyangadzayi Musuka 9affe01da8 fix(cpu): op == 0b00 decodes to add in format 5 2022-02-02 18:58:06 -04:00
Rekai Nyangadzayi Musuka 784bc81a4a fix(cpu): account for overflow in THUMB alu MUL 2022-02-02 18:57:33 -04:00
Rekai Nyangadzayi Musuka 045c98de1f chore: use if-else when decoding THUMB instructions 2022-02-02 18:48:47 -04:00
Rekai Nyangadzayi Musuka c2901ee0d8 fix(cpu): account for rn in rlist in block data transfer 2022-02-02 17:35:33 -04:00
Rekai Nyangadzayi Musuka d95efa5b12 feat: implement LDM/STM behaviour when S is set 2022-02-02 16:12:47 -04:00
Rekai Nyangadzayi Musuka 237beb9caa feat(cpu): Pass all LDR/STR ARMwrestler tests 2022-02-02 14:07:18 -04:00
Rekai Nyangadzayi Musuka 30bad76e44 feat(cpu): decode and implement all necessary ARM CPU instructions 2022-02-02 12:06:41 -04:00
Rekai Nyangadzayi Musuka c34c2ee6eb feat(cpu): implement ARM SWP and SWPB 2022-02-02 08:44:33 -04:00
Rekai Nyangadzayi Musuka 6c7934be70 fix: resolve off by n * 2 when accessing Palette during BG Mode 4 2022-02-01 22:56:53 -04:00
Rekai Nyangadzayi Musuka 48017b45f5 feat(cpu): Implement Multiply Long ARM instructions 2022-02-01 22:09:38 -04:00
Rekai Nyangadzayi Musuka 28c81f79ae fix: no buttons are pressed by default 2022-02-01 20:52:01 -04:00
Rekai Nyangadzayi Musuka a80600156d feat(cpu): implement format 18 THUMB instructions 2022-02-01 19:12:01 -04:00
Rekai Nyangadzayi Musuka 0d7600ed7a chore: more detailed panic message 2022-02-01 19:11:56 -04:00
Rekai Nyangadzayi Musuka ca41f6a85c feat(cpu): implement format 10 THUMB instructions 2022-02-01 17:56:11 -04:00
Rekai Nyangadzayi Musuka 85927a943f feat(cpu): implement SWP 2022-02-01 16:30:55 -04:00
Rekai Nyangadzayi Musuka b27bf4a85c fix(cpu): perform MUL with u64s, throw away upper 32 bits 2022-02-01 16:15:08 -04:00
Rekai Nyangadzayi Musuka b07eb22b86 feat: implement keyboard input 2022-02-01 16:11:59 -04:00
Rekai Nyangadzayi Musuka f6e4b4931f chore: don't panic on unsupported BG mode 2022-01-30 02:43:11 -04:00
Rekai Nyangadzayi Musuka e35d81eeb8 chore: tempoarily disable fps counter 2022-01-30 02:42:01 -04:00
Rekai Nyangadzayi Musuka 8c248ffb11 chore: zero-initialize VRAM 2022-01-30 02:39:16 -04:00
Rekai Nyangadzayi Musuka b0332e6eb8 chore: stub KeyInput I/O register 2022-01-30 02:38:29 -04:00
Rekai Nyangadzayi Musuka dd632975f8 fix(cpu): properly decode multiply instructions 2022-01-30 02:16:12 -04:00
Rekai Nyangadzayi Musuka a459d4b433 feat(cpu): implement ARM multiply instructions 2022-01-30 02:04:24 -04:00
Rekai Nyangadzayi Musuka 6c008ce950 fix: allow 32-bit writes to DISPCNT 2022-01-30 01:42:54 -04:00
Rekai Nyangadzayi Musuka 8d1df7ae43 fix(cpu): properly decode ldm stm thumb instructions 2022-01-30 01:12:34 -04:00
Rekai Nyangadzayi Musuka 6ffaf12804 fix(cpu): properly decode THUMB PUSH and POP at comptime 2022-01-30 00:16:13 -04:00
Rekai Nyangadzayi Musuka dc6931639f fix(cpu): don't ignore 11th bit of THUMB BL offset 2022-01-29 23:53:40 -04:00
Rekai Nyangadzayi Musuka e18f10126e feat(cpu): implement thumb push / pop and stub format 13 thumb instrs 2022-01-29 23:22:10 -04:00
Rekai Nyangadzayi Musuka 0598ba402d feat(cpu): implement THUMB format 9 loads / stores 2022-01-29 22:34:40 -04:00
Rekai Nyangadzayi Musuka b8a9aaee86 fix(cpu): resolve issues with unexpected PC value in THUMB 2022-01-29 22:07:36 -04:00
Rekai Nyangadzayi Musuka 00058f6094 feat(cpu): implement THUMB ldmia stmia 2022-01-29 21:10:14 -04:00
Rekai Nyangadzayi Musuka 2dde47318c chore: implement THUMB format 4 instructions 2022-01-29 20:42:13 -04:00
Rekai Nyangadzayi Musuka ae4023e51c chore: dedup code in THUMB instructions 2022-01-29 20:05:27 -04:00
Rekai Nyangadzayi Musuka bce067557f chore: refactor and genericize ARM data processing calculations 2022-01-29 19:40:58 -04:00
Rekai Nyangadzayi Musuka e0acabf050 chore: relocate barrel_shifter zig file 2022-01-29 18:52:16 -04:00
Rekai Nyangadzayi Musuka 599e068c7e feat(cpu): implement format2 THUMB instructions 2022-01-29 18:46:27 -04:00
Rekai Nyangadzayi Musuka 4ca65caef0 feat(cpu): implement format19 THUMB instructions 2022-01-29 18:25:50 -04:00
Rekai Nyangadzayi Musuka 0c49bf2288 chore: account for THUMB BL instruction when mimicking mGBA logs 2022-01-29 18:14:00 -04:00
Rekai Nyangadzayi Musuka 44dbdba48c feat(cpu): implement format16 THUMB instructions 2022-01-29 17:44:04 -04:00
Rekai Nyangadzayi Musuka d85e0c8d05 feat(cpu): implement format 1 THUMB instructions 2022-01-29 17:29:30 -04:00
Rekai Nyangadzayi Musuka 995633e9e8 fix: dont close file handle early 2022-01-29 01:18:45 -04:00
Rekai Nyangadzayi Musuka cfbd292edc feat(cpu): implement format 6 THUMB instructions 2022-01-29 01:18:41 -04:00
Rekai Nyangadzayi Musuka 95efb3f35d chore: rename title 2022-01-28 23:27:03 -04:00
Rekai Nyangadzayi Musuka 6a6dccf4d8 chore: refactor GBA Display Timings
This change should reflect that the Hblank bit of DISPSTAT is toggled on all scanlines
while also ensuring that the Vblank bit is set on all Vblank scanlines
2022-01-28 22:58:19 -04:00
Rekai Nyangadzayi Musuka ad1db4dc2e chore: move a single statement lol 2022-01-28 22:57:48 -04:00
Rekai Nyangadzayi Musuka 19359f7ee4 chore: mark indexing methods as inline 2022-01-28 17:11:29 -04:00
Rekai Nyangadzayi Musuka 24f0922f86 feat: create emulator thread 2022-01-28 16:33:38 -04:00
Rekai Nyangadzayi Musuka b1cc985230 chore: disable logging by default 2022-01-25 18:20:30 -04:00
Rekai Nyangadzayi Musuka e5c8f0ce07 chore: revert fastboot changes 2022-01-25 18:20:01 -04:00
Rekai Nyangadzayi Musuka fbc5b309b0 chore: binary logging + file logging + DP chanes + fastBoot changes 2022-01-25 18:18:52 -04:00
Rekai Nyangadzayi Musuka 899a9ead76 chore: ignore .bin files 2022-01-25 12:58:25 -04:00
Rekai Nyangadzayi Musuka 540fbf739a chore: rename skipBios to fastBoot 2022-01-25 11:15:17 -04:00
Rekai Nyangadzayi Musuka 0546b1c308 chore: set correct values for select banked registers on fast boot 2022-01-25 11:14:15 -04:00
Rekai Nyangadzayi Musuka 997dc1314c feat(cpu): implement SWI 2022-01-25 10:34:21 -04:00
Rekai Nyangadzayi Musuka 1456d0f317 chore(bios): allow reading from BIOS 2022-01-25 10:32:28 -04:00
Rekai Nyangadzayi Musuka 6257418405 fix(cpu): interim solution to weird program counter behaviour on illegal tst instruction 2022-01-25 09:23:32 -04:00
Rekai Nyangadzayi Musuka 985fefb9f6 chore(cpu): implement behaviour for undefined test instruction 2022-01-25 08:05:42 -04:00
Rekai Nyangadzayi Musuka 95dd3e3df8 fix(cpu): fix PC offset when barrel shifter and bit 4 of DP is set 2022-01-24 17:52:01 -04:00
Rekai Nyangadzayi Musuka 038c0a9283 chore: remove reccomended extension 2022-01-23 23:13:16 -04:00
Rekai Nyangadzayi Musuka 702ff288d8 fix(cpu): implement S set + rd == 15 case for data processing 2022-01-19 07:46:49 -04:00
Rekai Nyangadzayi Musuka bf36a23722 feat(cpu): implement banked registers 2022-01-19 07:29:49 -04:00
Rekai Nyangadzayi Musuka fc5a3460dd fix(cpu): improve MRS and MSR instructions 2022-01-18 20:17:00 -04:00
Rekai Nyangadzayi Musuka 6177927049 feat(cpu): implement CMN 2022-01-18 15:09:25 -04:00
Rekai Nyangadzayi Musuka 903b75c7c4 fix(barrel_shifter): fix PC being 1 word ahead in barrel shifter 2022-01-18 15:08:29 -04:00
Rekai Nyangadzayi Musuka 8d786cbe25 feat(cpu): Implement RSC 2022-01-18 14:46:57 -04:00
Rekai Nyangadzayi Musuka 212bc9e11d feat(cpu): implement RSB 2022-01-18 14:36:03 -04:00
Rekai Nyangadzayi Musuka 63a57ac954 feat(cpu): implement BIC 2022-01-18 14:28:47 -04:00
Rekai Nyangadzayi Musuka 85dae5e1d7 feat(cpu): implement EOR 2022-01-18 14:27:07 -04:00
Rekai Nyangadzayi Musuka 6189bf0315 feat(cpu): implement ADD 2022-01-18 14:25:29 -04:00
Rekai Nyangadzayi Musuka 2f3213f693 feat(cpu): implement fix for ADC and implement SBC 2022-01-18 14:20:01 -04:00
Rekai Nyangadzayi Musuka a62cd9aa40 chore(barrel_shifter): remove panic from ASR 2022-01-18 14:19:58 -04:00
Rekai Nyangadzayi Musuka 25c57a4cc7 fix(barrel_shifter): should not modify cpsr when amount == 0 2022-01-18 13:30:41 -04:00
Rekai Nyangadzayi Musuka a7a44c4463 chore(cpu): refactor the barrel shifter once again 2022-01-17 15:55:55 -04:00
Rekai Nyangadzayi Musuka d4d2fedfbe feat(cpu): implement ADC
ADC interacting w/ the Barrel Shifter is not working though
2022-01-17 14:29:34 -04:00
Rekai Nyangadzayi Musuka 483e149b32 feat(cpu): implement RRX for Barrel Shifter 2022-01-17 14:19:40 -04:00
Rekai Nyangadzayi Musuka 85ffdf44f5 feat(cpu): implement SUB in THUMB format 3 2022-01-17 11:36:02 -04:00
Rekai Nyangadzayi Musuka 9098a55ae3 feat(cpu): implement ARM SUB in data processing 2022-01-17 11:35:41 -04:00
Rekai Nyangadzayi Musuka c0d956ea95 feat(cpu): implement MVN 2022-01-17 11:30:59 -04:00
Rekai Nyangadzayi Musuka 1025500407 chore(cpu): refactor barrel shifter 2022-01-17 11:17:04 -04:00
Rekai Nyangadzayi Musuka d05a924420 fix(cpu): use barrel shifter in data processing immediates 2022-01-17 11:02:34 -04:00
Rekai Nyangadzayi Musuka 2a416fb2c6 feat(cpu): implement format 12 thumb instructions 2022-01-17 10:07:50 -04:00
Rekai Nyangadzayi Musuka ea5f0ce552 feat(cpu): implement some already decoded format 3 instructions 2022-01-17 09:29:11 -04:00
Rekai Nyangadzayi Musuka e55d2dc323 feat(cpu): implement THUMB format 5 instructions 2022-01-17 09:28:46 -04:00
Rekai Nyangadzayi Musuka 3037407ebe chore: mgba log now supports printing THUMB instructions 2022-01-17 07:18:44 -04:00
Rekai Nyangadzayi Musuka 1915d98bdd feat(cpu): implement like 1 THUMB instruction 2022-01-16 12:46:59 -04:00
Rekai Nyangadzayi Musuka 4606a1ab25 chore: distinguish between undefined ARM and THUMB instr 2022-01-14 05:30:32 -04:00
Rekai Nyangadzayi Musuka 0cf052838d chore(cpu): lay groundwork for THUMB instruction decoding and execution 2022-01-14 05:23:16 -04:00
Rekai Nyangadzayi Musuka ae37b1218b chore(cpu): refactor ARM functions to make room for THUMB 2022-01-14 04:26:09 -04:00
Rekai Nyangadzayi Musuka 070322064d fix(cpu): fix conditions for GT cond 2022-01-14 04:19:54 -04:00
Rekai Nyangadzayi Musuka 37bd6758fb fix(cpu): fix imm value calculation in MSR 2022-01-14 04:08:04 -04:00
Rekai Nyangadzayi Musuka 7f6ab626d9 fix(cpu): resolve off-by-one error when executing LDM 2022-01-14 03:43:03 -04:00
Rekai Nyangadzayi Musuka 77dba68a0b feat(cpu): implement branch and exchange
If I want to continue with armwrestler, I'll have to implement
THUMB instructions now
2022-01-12 07:20:24 -04:00
Rekai Nyangadzayi Musuka 7adc7c8802 fix(cpu): make Data Processing instructions r15-aware 2022-01-12 07:20:24 -04:00
Rekai Nyangadzayi Musuka 229f7c3388 fix(cpu): make LDRH and STRH aware of r15 2022-01-12 07:20:21 -04:00
Rekai Nyangadzayi Musuka 5812b9713c fix(cpu): account for r15 in LDR and STR instructions 2022-01-12 06:16:59 -04:00
Rekai Nyangadzayi Musuka 98c5803208 fix(cpu): flip two branches in PSR Transfer execution 2022-01-12 06:16:34 -04:00
Rekai Nyangadzayi Musuka 74abd3df4d feat(cpu): implement MSR and MRS 2022-01-12 04:48:57 -04:00
Rekai Nyangadzayi Musuka 7531af7f2b feat(cpu): stub PSR Transfer instructions 2022-01-12 03:40:51 -04:00
Rekai Nyangadzayi Musuka 1c173eb4b8 chore(io): implement IE and IME 2022-01-12 02:19:26 -04:00
Rekai Nyangadzayi Musuka 769c67b9d4 chore: remove some magic constants 2022-01-12 00:46:20 -04:00
Rekai Nyangadzayi Musuka 3596caf106 Merge branch 'main' of ssh://musuka.dev:2222/paoda/zba 2022-01-11 02:36:37 -04:00
Rekai Nyangadzayi Musuka 3be084cb82 chore: ignores for building on windows 2022-01-11 01:42:26 -04:00
Rekai Nyangadzayi Musuka c1be53bcb2 fix(bus): remove accidental recursion 2022-01-10 21:25:45 -04:00
Rekai Nyangadzayi Musuka 072a66cfdb fix(cpu): write results of ORR to destination register 2022-01-10 10:56:41 -04:00
Rekai Nyangadzayi Musuka ed3bdd90fb feat(cpu): implement TEQ 2022-01-10 08:09:02 -04:00
Rekai Nyangadzayi Musuka e9c1c94cae feat(cpu): Implement ORR 2022-01-10 08:06:00 -04:00
Rekai Nyangadzayi Musuka 0f08ad05be feat(bus): implement IWRAM and EWRAM 2022-01-10 07:59:21 -04:00
Rekai Nyangadzayi Musuka fd5006b29d fix(ppu): properly access Mode 4 palette 2022-01-10 07:23:54 -04:00
Rekai Nyangadzayi Musuka 22b95b2a74 feat(cpu): refactor LDM/STM 2022-01-10 06:51:32 -04:00
Rekai Nyangadzayi Musuka 7d79a0bee2 feat(cpu): implement LDM/STM 2022-01-10 06:27:36 -04:00
Rekai Nyangadzayi Musuka 6c0651ca08 chore(io): DISPSTAT bits 3 and 4 better match GBATEK documentation 2022-01-10 06:26:42 -04:00
Rekai Nyangadzayi Musuka 0d8c5e6882 fix(cpu): fix off-by-word bug in BL 2022-01-10 06:26:02 -04:00
Rekai Nyangadzayi Musuka 89a8fe403b feat(bus): have VCOUNT be addressable on the bus 2022-01-10 03:35:28 -04:00
Rekai Nyangadzayi Musuka 7c5d2d2389 feat(ppu): implement Mode 4
Implementation is not tested. Pending on LDM and STM so that I can
run beeg.gba
2022-01-10 03:35:24 -04:00
Rekai Nyangadzayi Musuka 2467b94dbd chore(io): rename some io bitfield fields 2022-01-10 02:13:25 -04:00
Rekai Nyangadzayi Musuka 0d4c850218 chore: remove premature inlines 2022-01-10 01:24:14 -04:00
Rekai Nyangadzayi Musuka bbe2ecfa53 chore: add FPS counter 2022-01-10 01:22:55 -04:00
Rekai Nyangadzayi Musuka c54145ce3c chore: improve code clarity 2022-01-09 23:34:33 -04:00
Rekai Nyangadzayi Musuka ead6d1ce49 feat(ppu): improve timings + implement BG mode 3 bitmap 2022-01-09 22:16:34 -04:00
Rekai Nyangadzayi Musuka 581285a434 fix: allocate framebuf on heap 2022-01-08 20:30:57 -04:00
Rekai Nyangadzayi Musuka 0d203543ca chore: add code for heap alloc of white texture 2022-01-08 18:52:11 -04:00
Rekai Nyangadzayi Musuka eb6c00f0ac chore(gui): switch from RGBA8888 to BGR5555 to match BG Mode 3 2022-01-08 04:54:39 -04:00
Rekai Nyangadzayi Musuka da7f21f47e feat: draw white texture using SDL2 2022-01-07 22:46:17 -04:00
Rekai Nyangadzayi Musuka 8fb666624f fix(ppu): deallocate palette RAM on cleanup 2022-01-07 22:27:08 -04:00
Rekai Nyangadzayi Musuka 568c374131 chore: code cleanup 2022-01-07 20:00:42 -04:00
Rekai Nyangadzayi Musuka 910745f442 chore(bus): refactor bus.zig 2022-01-07 19:49:58 -04:00
Rekai Nyangadzayi Musuka f8c6af3247 chore: refactor instruction exec code 2022-01-07 19:44:48 -04:00
Rekai Nyangadzayi Musuka a407671de2 chore(io): alias @This() to Self in io.zig 2022-01-07 19:34:54 -04:00
Rekai Nyangadzayi Musuka e9ec124e33 chore: refactor bios.zig and pak.zig 2022-01-07 19:33:49 -04:00
Rekai Nyangadzayi Musuka 9f64804763 fix: by convention deinit() should not take pointers to self 2022-01-07 19:16:23 -04:00
Rekai Nyangadzayi Musuka c6123d8a6d feat: implement PPU Timings in Scheduler 2022-01-05 21:18:33 -04:00
Rekai Nyangadzayi Musuka f709458638 feat(sched): add HBlank and VBlank events to the scheduler 2022-01-05 17:34:59 -05:00
Rekai Nyangadzayi Musuka 5037b8f0cc feat: implement S (when rd != 15) for several data processing instructions 2022-01-05 15:45:52 -05:00
Rekai Nyangadzayi Musuka 28a70d0112 feat: implement dedicated Barrel Shifter SHL and SHR 2022-01-05 13:58:11 -05:00
Rekai Nyangadzayi Musuka 7473ffedc7 chore: stub TST 2022-01-04 04:08:02 -06:00
Rekai Nyangadzayi Musuka 172f3e8efe chore: comment-out logging by default 2022-01-04 03:58:11 -06:00
Rekai Nyangadzayi Musuka 28bb410dfd fix(cpu): improve LDR/STR write-back logic 2022-01-04 03:55:41 -06:00
Rekai Nyangadzayi Musuka 5ea888f68c feat(bus): implement Palette RAM and DISPSTAT 2022-01-04 03:29:56 -06:00
Rekai Nyangadzayi Musuka 8b9a80b279 fix(bus): restrict Game ROM and VRAM to a 16-bit bus 2022-01-04 03:08:12 -06:00
Rekai Nyangadzayi Musuka ed9c1413b1 fix(cpu): properly implement SUB/CMP CSPSR carry bit condition 2022-01-04 03:08:08 -06:00
Rekai Nyangadzayi Musuka 8cabcd8901 fix(cpu): resolve reversed if statement + write back on W = 0 2022-01-04 01:57:37 -06:00
Rekai Nyangadzayi Musuka 8d8cedea59 chore: add mgba compatible (minus disasm) log function 2022-01-04 01:11:53 -06:00
Rekai Nyangadzayi Musuka 0f827fca96 chore: rename CPSR u32 from val to raw 2022-01-03 22:25:11 -06:00
Rekai Nyangadzayi Musuka 1fefd4de5c chore: remove print statements 2022-01-03 21:30:08 -06:00
Rekai Nyangadzayi Musuka 3aa680ab8c chore: remove all memory leaks 2022-01-03 20:08:55 -06:00
Rekai Nyangadzayi Musuka 8257a3899a feat(ppu): implement VRAM 2022-01-03 19:52:10 -06:00
Rekai Nyangadzayi Musuka 1d4ba2e2b3 fix(emu): prevent infinite loop when advancing scheduler 2022-01-03 19:51:55 -06:00
Rekai Nyangadzayi Musuka c9f0e1632c fix(io): fix DISPCNT is at wrong IO address 2022-01-03 17:49:15 -06:00
Rekai Nyangadzayi Musuka 44d52d8137 feat(cpu): properly implement STR STRH and STRB 2022-01-03 17:48:43 -06:00
Rekai Nyangadzayi Musuka dee0e113d8 feat(cpu): implement skipBios method 2022-01-02 14:58:39 -06:00
Rekai Nyangadzayi Musuka eb37d73cb2 chore: panic on read from BIOS
GBA Bios requires a lot of implemented features, so we're ignoring it
for now
2022-01-02 14:57:59 -06:00
Rekai Nyangadzayi Musuka 1c42d1795a feat(bus): add Io Struct
Also, add more information to all panic messages
2022-01-02 14:40:49 -06:00
Rekai Nyangadzayi Musuka 01d6399dfb chore: rename consturctors to fit convention 2022-01-02 13:58:57 -06:00
Rekai Nyangadzayi Musuka f09f814dc3 chore: move bitfield library to lib director
I'd presonally prefer to use a git submodule here but It doesn't quite
seem like git submodules are possible for individual files. I'll have to
check with FlorenceOS every once and a while to ensure that there are no
lingering soundness issues with the library.

Thanks to @N00byEdge for this wonderful library!
2022-01-02 13:19:09 -06:00
Rekai Nyangadzayi Musuka de9045fba3 chore: use bitfield library 2022-01-02 13:01:11 -06:00
Rekai Nyangadzayi Musuka e144261e07 feat(bus): emu is now able to read from user-provided BIOS 2022-01-02 03:16:03 -06:00
Rekai Nyangadzayi Musuka 65c3dd722c feat(bus): implement Gameboy Advance MMIO 2022-01-02 02:36:06 -06:00
Rekai Nyangadzayi Musuka b63eb2dabc feat: implement ROM CLI argument 2022-01-01 23:37:21 -06:00
Rekai Nyangadzayi Musuka 52e367d24a fix(cpu): purposely overflow when calculating PC during branch 2022-01-01 21:57:52 -06:00
Rekai Nyangadzayi Musuka cc7e42efd8 feat(cpu): implement condition field behaviour 2022-01-01 21:56:58 -06:00
Rekai Nyangadzayi Musuka c40a1af534 chore: conform to zig style guides 2022-01-01 21:08:47 -06:00
Rekai Nyangadzayi Musuka f2cc0721c7 chore: run zig fmt 2022-01-01 03:42:20 -06:00
Rekai Nyangadzayi Musuka 92a06e49c3 chore(cpu): iron out some false assumptions 2022-01-01 03:41:50 -06:00
Rekai Nyangadzayi Musuka c660ca8922 feat: implement LDR STR 2021-12-29 17:16:32 -06:00
Rekai Nyangadzayi Musuka 7cc3f40a85 chore: run zig fmt 2021-12-29 15:13:50 -06:00
Rekai Nyangadzayi Musuka ff7bf4eaa7 chore: add reccomended vscode extensions 2021-12-29 15:11:30 -06:00
54 changed files with 2480 additions and 7816 deletions

4
.gitignore vendored
View File

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

3
.gitmodules vendored
View File

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

View File

@ -1,29 +1,14 @@
# 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
- [ ] Affine Sprites
- [ ] Windowing (see [this branch](https://git.musuka.dev/paoda/zba/src/branch/window))
- [ ] Audio Resampler (Having issues with SDL2's)
- [ ] Immediate Mode GUI
- [ ] Refactoring for easy-ish perf boosts
An in-progress Game Boy Advance Emulator written in Zig ⚡!
## Tests
- [x] [jsmolka's GBA Test Collection](https://github.com/jsmolka/gba-tests)
- [ ] [jsmolka's GBA Test Collection](https://github.com/jsmolka/gba-tests)
- [x] `arm.gba` and `thumb.gba`
- [x] `flash64.gba`, `flash128.gba`, `none.gba`, and `sram.gba`
- [x] `hello.gba`, `shades.gba`, and `stripes.gba`
- [x] `memory.gba`
- [x] `bios.gba`
- [x] `nes.gba`
- [ ] `nes.gba`
- [ ] [DenSinH's GBA ROMs](https://github.com/DenSinH/GBARoms)
- [x] `eeprom-test` and `flash-test`
- [x] `midikey2freq`
@ -50,20 +35,18 @@ features and the set of possible improvements would be in memory timing or in UI
* [ARM7TDMI Data Sheet](https://www.dca.fee.unicamp.br/cursos/EA871/references/ARM/ARM7TDMIDataSheet.pdf)
## Compiling
Most recently built on Zig [0.10.0-dev.4474+b41b35f57](https://github.com/ziglang/zig/tree/b41b35f57)
Most recently built on Zig [0.10.0-dev.3900+ab4b26d8a](https://github.com/ziglang/zig/tree/ab4b26d8a)
### Dependencies
* [SDL.zig](https://github.com/MasterQ32/SDL.zig)
* [SDL2](https://www.libsdl.org/download-2.0.php)
* [zig-clap](https://github.com/Hejsil/zig-clap)
* [known-folders](https://github.com/ziglibs/known-folders)
* [zig-toml](https://github.com/aeronavery/zig-toml)
* [zig-datetime](https://github.com/frmdstryr/zig-datetime)
* [`bitfields.zig`](https://github.com/FlorenceOS/Florence/blob/aaa5a9e568/lib/util/bitfields.zig)
* [`bitfields.zig`](https://github.com/FlorenceOS/Florence/blob/aaa5a9e568197ad24780ec9adb421217530d4466/lib/util/bitfields.zig)
`bitfields.zig` from [FlorenceOS](https://github.com/FlorenceOS) is included under `lib/util/bitfield.zig`.
Use `git submodule update --init` from the project root to pull the git submodules `SDL.zig`, `zig-clap`, `known-folders`, `zig-toml` and `zig-datetime`
Use `git submodule update --init` from the project root to pull the git submodules `SDL.zig`, `zig-clap`, and `known-folders`
Be sure to provide SDL2 using:
* Linux: Your distro's package manager

View File

@ -13,9 +13,6 @@ pub fn build(b: *std.build.Builder) void {
const mode = b.standardReleaseOptions();
const exe = b.addExecutable("zba", "src/main.zig");
exe.setMainPkgPath("."); // Necessary so that src/main.zig can embed example.toml
exe.setTarget(target);
// Known Folders (%APPDATA%, XDG, etc.)
exe.addPackagePath("known_folders", "lib/known-folders/known-folders.zig");
@ -29,17 +26,13 @@ pub fn build(b: *std.build.Builder) void {
// Argument Parsing Library
exe.addPackagePath("clap", "lib/zig-clap/clap.zig");
// TOML Library
exe.addPackagePath("toml", "lib/zig-toml/src/toml.zig");
// OpenGL 3.3 Bindings
exe.addPackagePath("gl", "lib/gl.zig");
// Zig SDL Bindings: https://github.com/MasterQ32/SDL.zig
const sdk = Sdk.init(b);
sdk.link(exe, .dynamic);
exe.addPackage(sdk.getNativePackage("sdl2"));
exe.setTarget(target);
exe.setBuildMode(mode);
exe.install();

View File

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

5028
lib/gl.zig

File diff suppressed because it is too large Load Diff

@ -1 +1 @@
Subproject commit e5d09c4b2d121025ad7195b2de704451e6306807
Subproject commit 4f4196fc3bc95c4bd3b12ce2e4a5f1050742cd3c

@ -1 +0,0 @@
Subproject commit 5dfa919e03b446c66b295c04bef9bdecabd4276f

View File

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

View File

@ -46,7 +46,7 @@ iwram: Iwram,
ewram: Ewram,
io: Io,
cpu: *Arm7tdmi,
cpu: ?*Arm7tdmi,
sched: *Scheduler,
pub fn init(self: *Self, allocator: Allocator, sched: *Scheduler, cpu: *Arm7tdmi, paths: FilePaths) !void {
@ -82,9 +82,9 @@ pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
// General Internal Memory
0x00 => blk: {
if (address < Bios.size)
break :blk self.bios.dbgRead(T, self.cpu.r[15], aligned_addr);
break :blk self.bios.dbgRead(T, self.cpu.?.r[15], aligned_addr);
break :blk self.openBus(T, address);
break :blk self.readOpenBus(T, address);
},
0x02 => self.ewram.read(T, aligned_addr),
0x03 => self.iwram.read(T, aligned_addr),
@ -109,63 +109,48 @@ pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
break :blk @as(T, value) * multiplier;
},
else => self.openBus(T, address),
else => self.readOpenBus(T, address),
};
}
fn readIo(self: *const Self, comptime T: type, unaligned_address: u32) T {
const maybe_value = io.read(self, T, forceAlign(T, unaligned_address));
return if (maybe_value) |value| value else self.openBus(T, unaligned_address);
return if (maybe_value) |value| value else self.readOpenBus(T, unaligned_address);
}
fn openBus(self: *const Self, comptime T: type, address: u32) T {
const r15 = self.cpu.r[15];
fn readOpenBus(self: *const Self, comptime T: type, address: u32) T {
const r15 = self.cpu.?.r[15];
const word = blk: {
// If Arm, get the most recently fetched instruction (PC + 8)
if (!self.cpu.cpsr.t.read()) break :blk self.cpu.pipe.stage[1].?;
// If u32 Open Bus, read recently fetched opcode (PC + 8)
if (!self.cpu.?.cpsr.t.read()) break :blk self.dbgRead(u32, r15 + 4);
const page = @truncate(u8, r15 >> 24);
// PC + 2 = stage[0]
// PC + 4 = stage[1]
// PC + 6 = Need a Debug Read for this?
switch (page) {
// EWRAM, PALRAM, VRAM, and Game ROM (16-bit)
0x02, 0x05, 0x06, 0x08...0x0D => {
const halfword: u32 = @truncate(u16, self.cpu.pipe.stage[1].?);
break :blk halfword << 16 | halfword;
},
// (PC + 4)
const halfword = self.dbgRead(u16, r15 + 2);
break :blk @as(u32, 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;
const offset: u32 = if (address & 3 == 0b00) 2 else 0;
// TODO: What to do on PC + 6?
const high: u32 = if (aligned) self.dbgRead(u16, r15 + 4) else @truncate(u16, self.cpu.pipe.stage[1].?);
const low: u32 = @truncate(u16, self.cpu.pipe.stage[@boolToInt(aligned)].?);
break :blk high << 16 | low;
break :blk @as(u32, self.dbgRead(u16, r15 + 2 + offset)) << 16 | self.dbgRead(u16, r15 + offset);
},
// IWRAM (16-bit but special)
0x03 => {
// Aligned: (PC + 2) | (PC + 4)
// Unaligned: (PC + 4) | (PC + 2)
const aligned = address & 3 == 0b00;
const offset: u32 = if (address & 3 == 0b00) 2 else 0;
const high: u32 = @truncate(u16, self.cpu.pipe.stage[1 - @boolToInt(aligned)].?);
const low: u32 = @truncate(u16, self.cpu.pipe.stage[@boolToInt(aligned)].?);
break :blk high << 16 | low;
},
else => {
log.err("THUMB open bus read from 0x{X:0>2} page @0x{X:0>8}", .{ page, address });
@panic("invariant most-likely broken");
break :blk @as(u32, self.dbgRead(u16, r15 + 2 - offset)) << 16 | self.dbgRead(u16, r15 + offset);
},
else => unreachable,
}
};
@ -182,9 +167,9 @@ pub fn read(self: *Self, comptime T: type, address: u32) T {
// General Internal Memory
0x00 => blk: {
if (address < Bios.size)
break :blk self.bios.read(T, self.cpu.r[15], aligned_addr);
break :blk self.bios.read(T, self.cpu.?.r[15], aligned_addr);
break :blk self.openBus(T, address);
break :blk self.readOpenBus(T, address);
},
0x02 => self.ewram.read(T, aligned_addr),
0x03 => self.iwram.read(T, aligned_addr),
@ -209,7 +194,7 @@ pub fn read(self: *Self, comptime T: type, address: u32) T {
break :blk @as(T, value) * multiplier;
},
else => self.openBus(T, address),
else => self.readOpenBus(T, address),
};
}

File diff suppressed because it is too large Load Diff

View File

@ -1,142 +0,0 @@
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;
self.envelope.raw = 0;
self.poly.raw = 0;
self.cnt.raw = 0;
self.sample = 0;
self.enabled = false;
}
pub fn tick(self: *Self, comptime kind: Tick) void {
switch (kind) {
.Length => self.len_dev.tick(self.cnt.length_enable.read(), &self.enabled),
.Envelope => self.env_dev.tick(self.envelope),
.Sweep => @compileError("Channel 4 does not implement Sweep"),
}
}
/// NR41, NR42
pub fn sound4CntL(self: *const Self) u16 {
return @as(u16, self.envelope.raw) << 8;
}
/// NR41, NR42
pub fn setSound4CntL(self: *Self, value: u16) void {
self.setNr41(@truncate(u8, value));
self.setNr42(@truncate(u8, value >> 8));
}
/// NR41
pub fn setNr41(self: *Self, len: u8) void {
self.len = @truncate(u6, len);
self.len_dev.timer = @as(u7, 64) - @truncate(u6, len);
}
/// NR42
pub fn setNr42(self: *Self, value: u8) void {
self.envelope.raw = value;
if (!self.isDacEnabled()) self.enabled = false;
}
/// NR43, NR44
pub fn sound4CntH(self: *const Self) u16 {
return @as(u16, self.poly.raw & 0x40) << 8 | self.cnt.raw;
}
/// NR43, NR44
pub fn setSound4CntH(self: *Self, fs: *const FrameSequencer, value: u16) void {
self.poly.raw = @truncate(u8, value);
self.setNr44(fs, @truncate(u8, value >> 8));
}
/// NR44
pub fn setNr44(self: *Self, fs: *const FrameSequencer, byte: u8) void {
var new: io.NoiseControl = .{ .raw = byte };
if (new.trigger.read()) {
self.enabled = true;
if (self.len_dev.timer == 0) {
self.len_dev.timer =
if (!fs.isLengthNext() and new.length_enable.read()) 63 else 64;
}
// Update The Frequency Timer
self.lfsr.reload(self.poly);
self.lfsr.shift = 0x7FFF;
// Update Envelope and Volume
self.env_dev.timer = self.envelope.period.read();
if (fs.isEnvelopeNext() and self.env_dev.timer != 0b111) self.env_dev.timer += 1;
self.env_dev.vol = self.envelope.init_vol.read();
self.enabled = self.isDacEnabled();
}
util.audio.length.ch4.update(self, fs, new);
self.cnt = new;
}
pub fn onNoiseEvent(self: *Self, late: u64) void {
self.lfsr.onLfsrTimerExpire(self.poly, late);
self.sample = 0;
if (!self.isDacEnabled()) return;
self.sample = if (self.enabled) self.lfsr.sample() * @as(i8, self.env_dev.vol) else 0;
}
fn isDacEnabled(self: *const Self) bool {
return self.envelope.raw & 0xF8 != 0x00;
}

View File

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

View File

@ -1,184 +0,0 @@
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;
self.sweep_dev.calc_performed = false;
self.duty.raw = 0;
self.envelope.raw = 0;
self.freq.raw = 0;
self.sample = 0;
self.enabled = false;
}
pub fn tick(self: *Self, comptime kind: Tick) void {
switch (kind) {
.Length => self.len_dev.tick(self.freq.length_enable.read(), &self.enabled),
.Envelope => self.env_dev.tick(self.envelope),
.Sweep => self.sweep_dev.tick(self),
}
}
pub fn onToneSweepEvent(self: *Self, late: u64) void {
self.square.onSquareTimerExpire(Self, self.freq, late);
self.sample = 0;
if (!self.isDacEnabled()) return;
self.sample = if (self.enabled) self.square.sample(self.duty) * @as(i8, self.env_dev.vol) else 0;
}
/// NR10, NR11, NR12
pub fn setSound1Cnt(self: *Self, value: u32) void {
self.setSound1CntL(@truncate(u8, value));
self.setSound1CntH(@truncate(u16, value >> 16));
}
/// NR10
pub fn sound1CntL(self: *const Self) u8 {
return self.sweep.raw & 0x7F;
}
/// NR10
pub fn setSound1CntL(self: *Self, value: u8) void {
const new = io.Sweep{ .raw = value };
if (self.sweep.direction.read() and !new.direction.read()) {
// Sweep Negate bit has been cleared
// If At least 1 Sweep Calculation has been made since
// the last trigger, the channel is immediately disabled
if (self.sweep_dev.calc_performed) self.enabled = false;
}
self.sweep.raw = value;
}
/// NR11, NR12
pub fn sound1CntH(self: *const Self) u16 {
return @as(u16, self.envelope.raw) << 8 | (self.duty.raw & 0xC0);
}
/// NR11, NR12
pub fn setSound1CntH(self: *Self, value: u16) void {
self.setNr11(@truncate(u8, value));
self.setNr12(@truncate(u8, value >> 8));
}
/// NR11
pub fn setNr11(self: *Self, value: u8) void {
self.duty.raw = value;
self.len_dev.timer = @as(u7, 64) - @truncate(u6, value);
}
/// NR12
pub fn setNr12(self: *Self, value: u8) void {
self.envelope.raw = value;
if (!self.isDacEnabled()) self.enabled = false;
}
/// NR13, NR14
pub fn sound1CntX(self: *const Self) u16 {
return self.freq.raw & 0x4000;
}
/// NR13, NR14
pub fn setSound1CntX(self: *Self, fs: *const FrameSequencer, value: u16) void {
self.setNr13(@truncate(u8, value));
self.setNr14(fs, @truncate(u8, value >> 8));
}
/// NR13
pub fn setNr13(self: *Self, byte: u8) void {
self.freq.raw = (self.freq.raw & 0xFF00) | byte;
}
/// NR14
pub fn setNr14(self: *Self, fs: *const FrameSequencer, byte: u8) void {
var new: io.Frequency = .{ .raw = (@as(u16, byte) << 8) | (self.freq.raw & 0xFF) };
if (new.trigger.read()) {
self.enabled = true;
if (self.len_dev.timer == 0) {
self.len_dev.timer =
if (!fs.isLengthNext() and new.length_enable.read()) 63 else 64;
}
self.square.reload(Self, self.freq.frequency.read());
// Reload Envelope period and timer
self.env_dev.timer = self.envelope.period.read();
if (fs.isEnvelopeNext() and self.env_dev.timer != 0b111) self.env_dev.timer += 1;
self.env_dev.vol = self.envelope.init_vol.read();
// Sweep Trigger Behaviour
const sw_period = self.sweep.period.read();
const sw_shift = self.sweep.shift.read();
self.sweep_dev.calc_performed = false;
self.sweep_dev.shadow = self.freq.frequency.read();
self.sweep_dev.timer = if (sw_period == 0) 8 else sw_period;
self.sweep_dev.enabled = sw_period != 0 or sw_shift != 0;
if (sw_shift != 0) _ = self.sweep_dev.calculate(self.sweep, &self.enabled);
self.enabled = self.isDacEnabled();
}
util.audio.length.update(Self, self, fs, new);
self.freq = new;
}
fn isDacEnabled(self: *const Self) bool {
return self.envelope.raw & 0xF8 != 0;
}

View File

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

View File

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

View File

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

View File

@ -1,52 +0,0 @@
const io = @import("../../bus/io.zig");
const ToneSweep = @import("../ToneSweep.zig");
const Self = @This();
timer: u8,
enabled: bool,
shadow: u11,
calc_performed: bool,
pub fn create() Self {
return .{
.timer = 0,
.enabled = false,
.shadow = 0,
.calc_performed = false,
};
}
pub fn 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.calc_performed) self.calc_performed = true;
if (self.enabled and period != 0) {
const new_freq = self.calculate(ch1.sweep, &ch1.enabled);
if (new_freq <= 0x7FF and ch1.sweep.shift.read() != 0) {
ch1.freq.frequency.write(@truncate(u11, new_freq));
self.shadow = @truncate(u11, new_freq);
_ = 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) shadow - shadow_shifted else shadow + shadow_shifted;
if (freq > 0x7FF) ch_enable.* = false;
return freq;
}

View File

@ -1,59 +0,0 @@
const io = @import("../../bus/io.zig");
/// Linear Feedback Shift Register
const Scheduler = @import("../../scheduler.zig").Scheduler;
const FrameSequencer = @import("../../apu.zig").FrameSequencer;
const Noise = @import("../Noise.zig");
const Self = @This();
pub const interval: u64 = (1 << 24) / (1 << 22);
shift: u15,
timer: u16,
sched: *Scheduler,
pub fn create(sched: *Scheduler) Self {
return .{
.shift = 0,
.timer = 0,
.sched = sched,
};
}
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, clogging up the Scheduler
pub fn onLfsrTimerExpire(self: *Self, poly: io.PolyCounter, late: u64) void {
// Obscure: "Using a noise channel clock shift of 14 or 15
// results in the LFSR receiving no clocks."
if (poly.shift.read() >= 14) return;
const div = Self.divisor(poly.div_ratio.read());
const timer = div << poly.shift.read();
const tmp = (self.shift & 1) ^ ((self.shift & 2) >> 1);
self.shift = (self.shift >> 1) | (tmp << 14);
if (poly.width.read())
self.shift = (self.shift & ~@as(u15, 0x40)) | tmp << 6;
self.sched.push(.{ .ApuChannel = 3 }, @as(u64, timer) * interval -| late);
}
fn divisor(code: u3) u16 {
if (code == 0) return 8;
return @as(u16, code) << 4;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,4 @@
const std = @import("std");
const config = @import("../../config.zig");
const Bit = @import("bitfield").Bit;
const Bitfield = @import("bitfield").Bitfield;
const DateTime = @import("datetime").datetime.Datetime;
@ -10,6 +8,7 @@ const Backup = @import("backup.zig").Backup;
const Gpio = @import("gpio.zig").Gpio;
const Allocator = std.mem.Allocator;
const force_rtc = @import("../emu.zig").force_rtc;
const log = std.log.scoped(.GamePak);
const Self = @This();
@ -20,11 +19,78 @@ allocator: Allocator,
backup: Backup,
gpio: *Gpio,
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, rom_path: []const u8, save_path: ?[]const u8) !Self {
const file = try std.fs.cwd().openFile(rom_path, .{});
defer file.close();
const file_buf = try file.readToEndAlloc(allocator, try file.getEndPos());
const title = file_buf[0xA0..0xAC].*;
const kind = Backup.guessKind(file_buf);
const device = if (force_rtc) .Rtc else guessDevice(file_buf);
logHeader(file_buf, &title);
return .{
.buf = file_buf,
.allocator = allocator,
.title = title,
.backup = try Backup.init(allocator, kind, title, save_path),
.gpio = try Gpio.init(allocator, cpu, device),
};
}
/// Searches the ROM to see if it can determine whether the ROM it's searching uses
/// any GPIO device, like a RTC for example.
fn guessDevice(buf: []const u8) Gpio.Device.Kind {
// Try to Guess if ROM uses RTC
const needle = "RTC_V"; // I was told SIIRTC_V, though Pokemen Firered (USA) is a false negative
var i: usize = 0;
while ((i + needle.len) < buf.len) : (i += 1) {
if (std.mem.eql(u8, needle, buf[i..(i + needle.len)])) return .Rtc;
}
// TODO: Detect other GPIO devices
return .None;
}
fn logHeader(buf: []const u8, title: *const [12]u8) void {
const code = buf[0xAC..0xB0];
const maker = buf[0xB0..0xB2];
const version = buf[0xBC];
log.info("Title: {s}", .{title});
if (version != 0) log.info("Version: {}", .{version});
log.info("Game Code: {s}", .{code});
if (lookupMaker(maker)) |c| log.info("Maker: {s}", .{c}) else log.info("Maker Code: {s}", .{maker});
}
fn lookupMaker(slice: *const [2]u8) ?[]const u8 {
const id = @as(u16, slice[1]) << 8 | @as(u16, slice[0]);
return switch (id) {
0x3130 => "Nintendo",
else => null,
};
}
inline fn isLarge(self: *const Self) bool {
return self.buf.len > 0x100_0000;
}
pub fn deinit(self: *Self) void {
self.backup.deinit();
self.gpio.deinit(self.allocator);
self.allocator.destroy(self.gpio);
self.allocator.free(self.buf);
self.* = undefined;
}
pub fn read(self: *Self, comptime T: type, address: u32) T {
const addr = address & 0x1FF_FFFF;
if (self.backup.kind == .Eeprom) {
if (self.buf.len > 0x100_0000) { // Large
if (self.isLarge()) {
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
// * Backup type is EEPROM
// * Large ROM (Size is greater than 16MB)
@ -76,19 +142,11 @@ pub fn read(self: *Self, comptime T: type, address: u32) T {
};
}
inline fn get(self: *const Self, i: u32) u8 {
@setRuntimeSafety(false);
if (i < self.buf.len) return self.buf[i];
const lhs = i >> 1 & 0xFFFF;
return @truncate(u8, lhs >> 8 * @truncate(u5, i & 1));
}
pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
const addr = address & 0x1FF_FFFF;
if (self.backup.kind == .Eeprom) {
if (self.buf.len > 0x100_0000) { // Large
if (self.isLarge()) {
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
// * Backup type is EEPROM
// * Large ROM (Size is greater than 16MB)
@ -103,35 +161,6 @@ pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
}
}
if (self.gpio.cnt == 1) {
// GPIO Can be read from
// We assume that this will only be true when a ROM actually does want something from GPIO
switch (T) {
u32 => switch (address) {
// 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)),
@ -146,7 +175,7 @@ pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value
if (self.backup.kind == .Eeprom) {
const bit = @truncate(u1, value);
if (self.buf.len > 0x100_0000) { // Large
if (self.isLarge()) {
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
// * Backup type is EEPROM
// * Large ROM (Size is greater than 16MB)
@ -184,59 +213,12 @@ pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value
}
}
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, rom_path: []const u8, save_path: ?[]const u8) !Self {
const file = try std.fs.cwd().openFile(rom_path, .{});
defer file.close();
fn get(self: *const Self, i: u32) u8 {
@setRuntimeSafety(false);
if (i < self.buf.len) return self.buf[i];
const file_buf = try file.readToEndAlloc(allocator, try file.getEndPos());
const title = file_buf[0xA0..0xAC].*;
const kind = Backup.guess(file_buf);
const device = if (config.config().guest.force_rtc) .Rtc else guessDevice(file_buf);
logHeader(file_buf, &title);
return .{
.buf = file_buf,
.allocator = allocator,
.title = title,
.backup = try Backup.init(allocator, kind, title, save_path),
.gpio = try Gpio.init(allocator, cpu, device),
};
}
pub fn deinit(self: *Self) void {
self.backup.deinit();
self.gpio.deinit(self.allocator);
self.allocator.destroy(self.gpio);
self.allocator.free(self.buf);
self.* = undefined;
}
/// Searches the ROM to see if it can determine whether the ROM it's searching uses
/// any GPIO device, like a RTC for example.
fn guessDevice(buf: []const u8) Gpio.Device.Kind {
// Try to Guess if ROM uses RTC
const needle = "RTC_V"; // I was told SIIRTC_V, though Pokemen Firered (USA) is a false negative
var i: usize = 0;
while ((i + needle.len) < buf.len) : (i += 1) {
if (std.mem.eql(u8, needle, buf[i..(i + needle.len)])) return .Rtc;
}
// TODO: Detect other GPIO devices
return .None;
}
fn logHeader(buf: []const u8, title: *const [12]u8) void {
const code = buf[0xAC..0xB0];
const maker = buf[0xB0..0xB2];
const version = buf[0xBC];
log.info("Title: {s}", .{title});
if (version != 0) log.info("Version: {}", .{version});
log.info("Game Code: {s}", .{code});
log.info("Maker Code: {s}", .{maker});
const lhs = i >> 1 & 0xFFFF;
return @truncate(u8, lhs >> 8 * @truncate(u5, i & 1));
}
test "OOB Access" {

View File

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

View File

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

View File

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

View File

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

View File

@ -8,9 +8,6 @@ const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
pub const DmaTuple = std.meta.Tuple(&[_]type{ DmaController(0), DmaController(1), DmaController(2), DmaController(3) });
const log = std.log.scoped(.DmaTransfer);
const setHi = util.setHi;
const setLo = util.setLo;
pub fn create() DmaTuple {
return .{ DmaController(0).init(), DmaController(1).init(), DmaController(2).init(), DmaController(3).init() };
}
@ -43,48 +40,48 @@ pub fn write(comptime T: type, dma: *DmaTuple, addr: u32, value: T) void {
switch (T) {
u32 => switch (byte) {
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),
0xB0 => dma.*[0].setSad(value),
0xB4 => dma.*[0].setDad(value),
0xB8 => dma.*[0].setCnt(value),
0xBC => dma.*[1].setSad(value),
0xC0 => dma.*[1].setDad(value),
0xC4 => dma.*[1].setCnt(value),
0xC8 => dma.*[2].setSad(value),
0xCC => dma.*[2].setDad(value),
0xD0 => dma.*[2].setCnt(value),
0xD4 => dma.*[3].setSad(value),
0xD8 => dma.*[3].setDad(value),
0xDC => dma.*[3].setCnt(value),
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
},
u16 => switch (byte) {
0xB0 => dma.*[0].setDmasad(setLo(u32, dma.*[0].sad, value)),
0xB2 => dma.*[0].setDmasad(setHi(u32, dma.*[0].sad, value)),
0xB4 => dma.*[0].setDmadad(setLo(u32, dma.*[0].dad, value)),
0xB6 => dma.*[0].setDmadad(setHi(u32, dma.*[0].dad, value)),
0xB8 => dma.*[0].setDmacntL(value),
0xBA => dma.*[0].setDmacntH(value),
0xB0 => dma.*[0].setSad(setU32L(dma.*[0].sad, value)),
0xB2 => dma.*[0].setSad(setU32H(dma.*[0].sad, value)),
0xB4 => dma.*[0].setDad(setU32L(dma.*[0].dad, value)),
0xB6 => dma.*[0].setDad(setU32H(dma.*[0].dad, value)),
0xB8 => dma.*[0].setCntL(value),
0xBA => dma.*[0].setCntH(value),
0xBC => dma.*[1].setDmasad(setLo(u32, dma.*[1].sad, value)),
0xBE => dma.*[1].setDmasad(setHi(u32, dma.*[1].sad, value)),
0xC0 => dma.*[1].setDmadad(setLo(u32, dma.*[1].dad, value)),
0xC2 => dma.*[1].setDmadad(setHi(u32, dma.*[1].dad, value)),
0xC4 => dma.*[1].setDmacntL(value),
0xC6 => dma.*[1].setDmacntH(value),
0xBC => dma.*[1].setSad(setU32L(dma.*[1].sad, value)),
0xBE => dma.*[1].setSad(setU32H(dma.*[1].sad, value)),
0xC0 => dma.*[1].setDad(setU32L(dma.*[1].dad, value)),
0xC2 => dma.*[1].setDad(setU32H(dma.*[1].dad, value)),
0xC4 => dma.*[1].setCntL(value),
0xC6 => dma.*[1].setCntH(value),
0xC8 => dma.*[2].setDmasad(setLo(u32, dma.*[2].sad, value)),
0xCA => dma.*[2].setDmasad(setHi(u32, dma.*[2].sad, value)),
0xCC => dma.*[2].setDmadad(setLo(u32, dma.*[2].dad, value)),
0xCE => dma.*[2].setDmadad(setHi(u32, dma.*[2].dad, value)),
0xD0 => dma.*[2].setDmacntL(value),
0xD2 => dma.*[2].setDmacntH(value),
0xC8 => dma.*[2].setSad(setU32L(dma.*[2].sad, value)),
0xCA => dma.*[2].setSad(setU32H(dma.*[2].sad, value)),
0xCC => dma.*[2].setDad(setU32L(dma.*[2].dad, value)),
0xCE => dma.*[2].setDad(setU32H(dma.*[2].dad, value)),
0xD0 => dma.*[2].setCntL(value),
0xD2 => dma.*[2].setCntH(value),
0xD4 => dma.*[3].setDmasad(setLo(u32, dma.*[3].sad, value)),
0xD6 => dma.*[3].setDmasad(setHi(u32, dma.*[3].sad, value)),
0xD8 => dma.*[3].setDmadad(setLo(u32, dma.*[3].dad, value)),
0xDA => dma.*[3].setDmadad(setHi(u32, dma.*[3].dad, value)),
0xDC => dma.*[3].setDmacntL(value),
0xDE => dma.*[3].setDmacntH(value),
0xD4 => dma.*[3].setSad(setU32L(dma.*[3].sad, value)),
0xD6 => dma.*[3].setSad(setU32H(dma.*[3].sad, value)),
0xD8 => dma.*[3].setDad(setU32L(dma.*[3].dad, value)),
0xDA => dma.*[3].setDad(setU32H(dma.*[3].dad, value)),
0xDC => dma.*[3].setCntL(value),
0xDE => dma.*[3].setCntH(value),
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
},
u8 => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
@ -113,12 +110,15 @@ fn DmaController(comptime id: u2) type {
cnt: DmaControl,
/// Internal. Currrent Source Address
sad_latch: u32,
_sad: u32,
/// Internal. Current Destination Address
dad_latch: u32,
_dad: u32,
/// Internal. Word Count
_word_count: if (id == 3) u16 else u14,
// Internal. FIFO Word Count
_fifo_word_count: u8,
/// 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
@ -132,32 +132,33 @@ fn DmaController(comptime id: u2) type {
.cnt = .{ .raw = 0x000 },
// Internals
.sad_latch = 0,
.dad_latch = 0,
._sad = 0,
._dad = 0,
._word_count = 0,
._fifo_word_count = 4,
.in_progress = false,
};
}
pub fn setDmasad(self: *Self, addr: u32) void {
pub fn setSad(self: *Self, addr: u32) void {
self.sad = addr & sad_mask;
}
pub fn setDmadad(self: *Self, addr: u32) void {
pub fn setDad(self: *Self, addr: u32) void {
self.dad = addr & dad_mask;
}
pub fn setDmacntL(self: *Self, halfword: u16) void {
pub fn setCntL(self: *Self, halfword: u16) void {
self.word_count = @truncate(@TypeOf(self.word_count), halfword);
}
pub fn setDmacntH(self: *Self, halfword: u16) void {
pub fn setCntH(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._sad = self.sad;
self._dad = self.dad;
self._word_count = if (self.word_count == 0) std.math.maxInt(@TypeOf(self._word_count)) else self.word_count;
// Only a Start Timing of 00 has a DMA Transfer immediately begin
@ -167,15 +168,15 @@ fn DmaController(comptime id: u2) type {
self.cnt.raw = halfword;
}
pub fn setDmacnt(self: *Self, word: u32) void {
self.setDmacntL(@truncate(u16, word));
self.setDmacntH(@truncate(u16, word >> 16));
pub fn setCnt(self: *Self, word: u32) void {
self.setCntL(@truncate(u16, word));
self.setCntH(@truncate(u16, word >> 16));
}
pub fn step(self: *Self, cpu: *Arm7tdmi) void {
const is_fifo = (id == 1 or id == 2) and self.cnt.start_timing.read() == 0b11;
const sad_adj = @intToEnum(Adjustment, self.cnt.sad_adj.read());
const dad_adj = if (is_fifo) .Fixed else @intToEnum(Adjustment, self.cnt.dad_adj.read());
const sad_adj = Self.adjustment(self.cnt.sad_adj.read());
const dad_adj = if (is_fifo) .Fixed else Self.adjustment(self.cnt.dad_adj.read());
const transfer_type = is_fifo or self.cnt.transfer_type.read();
const offset: u32 = if (transfer_type) @sizeOf(u32) else @sizeOf(u16);
@ -183,22 +184,22 @@ fn DmaController(comptime id: u2) type {
const mask = if (transfer_type) ~@as(u32, 3) else ~@as(u32, 1);
if (transfer_type) {
cpu.bus.write(u32, self.dad_latch & mask, cpu.bus.read(u32, self.sad_latch & mask));
cpu.bus.write(u32, self._dad & mask, cpu.bus.read(u32, self._sad & mask));
} else {
cpu.bus.write(u16, self.dad_latch & mask, cpu.bus.read(u16, self.sad_latch & mask));
cpu.bus.write(u16, self._dad & mask, cpu.bus.read(u16, self._sad & mask));
}
switch (sad_adj) {
.Increment => self.sad_latch +%= offset,
.Decrement => self.sad_latch -%= offset,
// FIXME: Is just ignoring this ok?
.Increment => self._sad +%= offset,
.Decrement => self._sad -%= offset,
// TODO: Is just ignoring this ok?
.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,
.Increment, .IncrementReload => self._dad +%= offset,
.Decrement => self._dad -%= offset,
.Fixed => {},
}
@ -226,7 +227,7 @@ fn DmaController(comptime id: u2) type {
}
}
fn poll(self: *Self, comptime kind: DmaKind) void {
pub fn pollBlanking(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
@ -242,11 +243,11 @@ fn DmaController(comptime id: u2) type {
// Reload internal DAD latch if we are in IncrementRelaod
if (self.in_progress) {
self._word_count = if (self.word_count == 0) std.math.maxInt(@TypeOf(self._word_count)) else self.word_count;
if (@intToEnum(Adjustment, self.cnt.dad_adj.read()) == .IncrementReload) self.dad_latch = self.dad;
if (Self.adjustment(self.cnt.dad_adj.read()) == .IncrementReload) self._dad = self.dad;
}
}
pub fn requestAudio(self: *Self, _: u32) void {
pub fn requestSoundDma(self: *Self, _: u32) void {
comptime std.debug.assert(id == 1 or id == 2);
if (self.in_progress) return; // APU must wait their turn
@ -258,19 +259,23 @@ fn DmaController(comptime id: u2) type {
// We Assume DMACNT_L is set to 4
// FIXME: Safe to just assume whatever DAD is set to is the FIFO Address?
// self.dad_latch = fifo_addr;
// self._dad = fifo_addr;
self.cnt.repeat.set();
self._word_count = 4;
self.in_progress = true;
}
fn adjustment(idx: u2) Adjustment {
return std.meta.intToEnum(Adjustment, idx) catch unreachable;
}
};
}
pub fn onBlanking(bus: *Bus, comptime kind: DmaKind) void {
bus.dma[0].poll(kind);
bus.dma[1].poll(kind);
bus.dma[2].poll(kind);
bus.dma[3].poll(kind);
bus.dma[0].pollBlanking(kind);
bus.dma[1].pollBlanking(kind);
bus.dma[2].pollBlanking(kind);
bus.dma[3].pollBlanking(kind);
}
const Adjustment = enum(u2) {
@ -286,3 +291,11 @@ const DmaKind = enum(u2) {
VBlank,
Special,
};
fn setU32L(left: u32, right: u16) u32 {
return (left & 0xFFFF_0000) | right;
}
fn setU32H(left: u32, right: u16) u32 {
return (left & 0x0000_FFFF) | (@as(u32, right) << 16);
}

View File

@ -68,8 +68,6 @@ pub const Gpio = struct {
log.info("Device: {}", .{kind});
const self = try allocator.create(Self);
errdefer allocator.destroy(self);
self.* = .{
.data = 0b0000,
.direction = 0b1111, // TODO: What is GPIO DIrection set to by default?
@ -290,7 +288,7 @@ pub const Clock = struct {
cpu.sched.push(.RealTimeClock, 1 << 24); // Every Second
}
pub fn onClockUpdate(self: *Self, late: u64) void {
pub fn updateTime(self: *Self, late: u64) void {
self.cpu.sched.push(.RealTimeClock, (1 << 24) -| late); // Reschedule
const now = DateTime.now();

View File

@ -11,9 +11,6 @@ const Bus = @import("../Bus.zig");
const DmaController = @import("dma.zig").DmaController;
const Scheduler = @import("../scheduler.zig").Scheduler;
const setHi = util.setLo;
const setLo = util.setHi;
const log = std.log.scoped(.@"I/O");
pub const Io = struct {
@ -236,18 +233,18 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
0x0400_0022 => bus.ppu.aff_bg[0].pb = @bitCast(i16, value),
0x0400_0024 => bus.ppu.aff_bg[0].pc = @bitCast(i16, value),
0x0400_0026 => bus.ppu.aff_bg[0].pd = @bitCast(i16, value),
0x0400_0028 => bus.ppu.aff_bg[0].x = @bitCast(i32, setLo(u32, @bitCast(u32, bus.ppu.aff_bg[0].x), value)),
0x0400_002A => bus.ppu.aff_bg[0].x = @bitCast(i32, setHi(u32, @bitCast(u32, bus.ppu.aff_bg[0].x), value)),
0x0400_002C => bus.ppu.aff_bg[0].y = @bitCast(i32, setLo(u32, @bitCast(u32, bus.ppu.aff_bg[0].y), value)),
0x0400_002E => bus.ppu.aff_bg[0].y = @bitCast(i32, setHi(u32, @bitCast(u32, bus.ppu.aff_bg[0].y), value)),
0x0400_0028 => bus.ppu.aff_bg[0].x = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[0].x) & 0xFFFF_0000 | value),
0x0400_002A => bus.ppu.aff_bg[0].x = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[0].x) & 0x0000_FFFF | (@as(u32, value) << 16)),
0x0400_002C => bus.ppu.aff_bg[0].y = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[0].y) & 0xFFFF_0000 | value),
0x0400_002E => bus.ppu.aff_bg[0].y = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[0].y) & 0x0000_FFFF | (@as(u32, value) << 16)),
0x0400_0030 => bus.ppu.aff_bg[1].pa = @bitCast(i16, value),
0x0400_0032 => bus.ppu.aff_bg[1].pb = @bitCast(i16, value),
0x0400_0034 => bus.ppu.aff_bg[1].pc = @bitCast(i16, value),
0x0400_0036 => bus.ppu.aff_bg[1].pd = @bitCast(i16, value),
0x0400_0038 => bus.ppu.aff_bg[1].x = @bitCast(i32, setLo(u32, @bitCast(u32, bus.ppu.aff_bg[1].x), value)),
0x0400_003A => bus.ppu.aff_bg[1].x = @bitCast(i32, setHi(u32, @bitCast(u32, bus.ppu.aff_bg[1].x), value)),
0x0400_003C => bus.ppu.aff_bg[1].y = @bitCast(i32, setLo(u32, @bitCast(u32, bus.ppu.aff_bg[1].y), value)),
0x0400_003E => bus.ppu.aff_bg[1].y = @bitCast(i32, setHi(u32, @bitCast(u32, bus.ppu.aff_bg[1].y), value)),
0x0400_0038 => bus.ppu.aff_bg[1].x = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[1].x) & 0xFFFF_0000 | value),
0x0400_003A => bus.ppu.aff_bg[1].x = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[1].x) & 0x0000_FFFF | (@as(u32, value) << 16)),
0x0400_003C => bus.ppu.aff_bg[1].y = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[1].y) & 0xFFFF_0000 | value),
0x0400_003E => bus.ppu.aff_bg[1].y = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[1].y) & 0x0000_FFFF | (@as(u32, value) << 16)),
0x0400_0040 => bus.ppu.win.h[0].raw = value,
0x0400_0042 => bus.ppu.win.h[1].raw = value,
0x0400_0044 => bus.ppu.win.v[0].raw = value,
@ -299,26 +296,24 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
},
u8 => switch (address) {
// Display
0x0400_0004 => bus.ppu.dispstat.raw = setLo(u16, bus.ppu.dispstat.raw, value),
0x0400_0005 => bus.ppu.dispstat.raw = setHi(u16, bus.ppu.dispstat.raw, value),
0x0400_0008 => bus.ppu.bg[0].cnt.raw = setLo(u16, bus.ppu.bg[0].cnt.raw, value),
0x0400_0009 => bus.ppu.bg[0].cnt.raw = setHi(u16, bus.ppu.bg[0].cnt.raw, value),
0x0400_000A => bus.ppu.bg[1].cnt.raw = setLo(u16, bus.ppu.bg[1].cnt.raw, value),
0x0400_000B => bus.ppu.bg[1].cnt.raw = setHi(u16, bus.ppu.bg[1].cnt.raw, value),
0x0400_0040 => bus.ppu.win.h[0].raw = setLo(u16, bus.ppu.win.h[0].raw, value),
0x0400_0041 => bus.ppu.win.h[0].raw = setHi(u16, bus.ppu.win.h[0].raw, value),
0x0400_0042 => bus.ppu.win.h[1].raw = setLo(u16, bus.ppu.win.h[1].raw, value),
0x0400_0043 => bus.ppu.win.h[1].raw = setHi(u16, bus.ppu.win.h[1].raw, value),
0x0400_0044 => bus.ppu.win.v[0].raw = setLo(u16, bus.ppu.win.v[0].raw, value),
0x0400_0045 => bus.ppu.win.v[0].raw = setHi(u16, bus.ppu.win.v[0].raw, value),
0x0400_0046 => bus.ppu.win.v[1].raw = setLo(u16, bus.ppu.win.v[1].raw, value),
0x0400_0047 => bus.ppu.win.v[1].raw = setHi(u16, bus.ppu.win.v[1].raw, value),
0x0400_0048 => bus.ppu.win.in.raw = setLo(u16, bus.ppu.win.in.raw, value),
0x0400_0049 => bus.ppu.win.in.raw = setHi(u16, bus.ppu.win.in.raw, value),
0x0400_004A => bus.ppu.win.out.raw = setLo(u16, bus.ppu.win.out.raw, value),
0x0400_0054 => bus.ppu.bldy.raw = setLo(u16, bus.ppu.bldy.raw, value),
0x0400_0004 => bus.ppu.dispstat.raw = (bus.ppu.dispstat.raw & 0xFF00) | value,
0x0400_0005 => bus.ppu.dispstat.raw = (@as(u16, value) << 8) | (bus.ppu.dispstat.raw & 0xFF),
0x0400_0008 => bus.ppu.bg[0].cnt.raw = (bus.ppu.bg[0].cnt.raw & 0xFF00) | value,
0x0400_0009 => bus.ppu.bg[0].cnt.raw = (@as(u16, value) << 8) | (bus.ppu.bg[0].cnt.raw & 0xFF),
0x0400_000A => bus.ppu.bg[1].cnt.raw = (bus.ppu.bg[1].cnt.raw & 0xFF00) | value,
0x0400_000B => bus.ppu.bg[1].cnt.raw = (@as(u16, value) << 8) | (bus.ppu.bg[1].cnt.raw & 0xFF),
0x0400_0040 => bus.ppu.win.h[0].set(.Lo, value),
0x0400_0041 => bus.ppu.win.h[0].set(.Hi, value),
0x0400_0042 => bus.ppu.win.h[1].set(.Lo, value),
0x0400_0043 => bus.ppu.win.h[1].set(.Hi, value),
0x0400_0044 => bus.ppu.win.v[0].set(.Lo, value),
0x0400_0045 => bus.ppu.win.v[0].set(.Hi, value),
0x0400_0046 => bus.ppu.win.v[1].set(.Lo, value),
0x0400_0047 => bus.ppu.win.v[1].set(.Hi, value),
0x0400_0048 => bus.ppu.win.setInL(value),
0x0400_0049 => bus.ppu.win.setInH(value),
0x0400_004A => bus.ppu.win.setOutL(value),
0x0400_0054 => bus.ppu.bldy.raw = (bus.ppu.bldy.raw & 0xFF00) | value,
// Sound
0x0400_0060...0x0400_00A7 => apu.write(T, &bus.apu, address, value),
@ -481,6 +476,13 @@ pub const WinH = extern union {
x2: Bitfield(u16, 0, 8),
x1: Bitfield(u16, 8, 8),
raw: u16,
pub fn set(self: *Self, comptime K: u8WriteKind, value: u8) void {
self.raw = switch (K) {
.Hi => (@as(u16, value) << 8) | self.raw & 0xFF,
.Lo => (self.raw & 0xFF00) | value,
};
}
};
/// Write-only

View File

@ -19,20 +19,20 @@ pub fn read(comptime T: type, tim: *const TimerTuple, addr: u32) ?T {
return switch (T) {
u32 => switch (nybble) {
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(),
0x0 => @as(T, tim.*[0].cnt.raw) << 16 | tim.*[0].getCntL(),
0x4 => @as(T, tim.*[1].cnt.raw) << 16 | tim.*[1].getCntL(),
0x8 => @as(T, tim.*[2].cnt.raw) << 16 | tim.*[2].getCntL(),
0xC => @as(T, tim.*[3].cnt.raw) << 16 | tim.*[3].getCntL(),
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
},
u16 => switch (nybble) {
0x0 => tim.*[0].timcntL(),
0x0 => tim.*[0].getCntL(),
0x2 => tim.*[0].cnt.raw,
0x4 => tim.*[1].timcntL(),
0x4 => tim.*[1].getCntL(),
0x6 => tim.*[1].cnt.raw,
0x8 => tim.*[2].timcntL(),
0x8 => tim.*[2].getCntL(),
0xA => tim.*[2].cnt.raw,
0xC => tim.*[3].timcntL(),
0xC => tim.*[3].getCntL(),
0xE => tim.*[3].cnt.raw,
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
},
@ -46,21 +46,21 @@ pub fn write(comptime T: type, tim: *TimerTuple, addr: u32, value: T) void {
return switch (T) {
u32 => switch (nybble) {
0x0 => tim.*[0].setTimcnt(value),
0x4 => tim.*[1].setTimcnt(value),
0x8 => tim.*[2].setTimcnt(value),
0xC => tim.*[3].setTimcnt(value),
0x0 => tim.*[0].setCnt(value),
0x4 => tim.*[1].setCnt(value),
0x8 => tim.*[2].setCnt(value),
0xC => tim.*[3].setCnt(value),
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
},
u16 => switch (nybble) {
0x0 => tim.*[0].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),
0x0 => tim.*[0].setCntL(value),
0x2 => tim.*[0].setCntH(value),
0x4 => tim.*[1].setCntL(value),
0x6 => tim.*[1].setCntH(value),
0x8 => tim.*[2].setCntL(value),
0xA => tim.*[2].setCntH(value),
0xC => tim.*[3].setCntL(value),
0xE => tim.*[3].setCntH(value),
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
},
u8 => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
@ -72,13 +72,13 @@ fn Timer(comptime id: u2) type {
return struct {
const Self = @This();
/// Read Only, Internal. Please use self.timcntL()
/// Read Only, Internal. Please use self.getCntL()
_counter: u16,
/// Write Only, Internal. Please use self.setTimcntL()
/// Write Only, Internal. Please use self.setCntL()
_reload: u16,
/// Write Only, Internal. Please use self.setTimcntH()
/// Write Only, Internal. Please use self.setCntH()
cnt: TimerControl,
/// Internal.
@ -97,26 +97,26 @@ fn Timer(comptime id: u2) type {
};
}
/// TIMCNT_L Getter
pub fn timcntL(self: *const Self) u16 {
/// TIMCNT_L
pub fn getCntL(self: *const Self) u16 {
if (self.cnt.cascade.read() or !self.cnt.enabled.read()) return self._counter;
return self._counter +% @truncate(u16, (self.sched.now() - self._start_timestamp) / self.frequency());
}
/// TIMCNT_L Setter
pub fn setTimcntL(self: *Self, halfword: u16) void {
/// TIMCNT_L
pub fn setCntL(self: *Self, halfword: u16) void {
self._reload = halfword;
}
/// TIMCNT_L & TIMCNT_H
pub fn setTimcnt(self: *Self, word: u32) void {
self.setTimcntL(@truncate(u16, word));
self.setTimcntH(@truncate(u16, word >> 16));
pub fn setCnt(self: *Self, word: u32) void {
self.setCntL(@truncate(u16, word));
self.setCntH(@truncate(u16, word >> 16));
}
/// TIMCNT_H
pub fn setTimcntH(self: *Self, halfword: u16) void {
pub fn setCntH(self: *Self, halfword: u16) void {
const new = TimerControl{ .raw = halfword };
// If Timer happens to be enabled, It will either be resheduled or disabled
@ -132,12 +132,12 @@ fn Timer(comptime id: u2) type {
if (!self.cnt.enabled.read() and new.enabled.read()) self._counter = self._reload;
// If Timer is enabled and we're not cascading, we need to schedule an overflow event
if (new.enabled.read() and !new.cascade.read()) self.rescheduleTimerExpire(0);
if (new.enabled.read() and !new.cascade.read()) self.scheduleOverflow(0);
self.cnt.raw = halfword;
}
pub fn onTimerExpire(self: *Self, cpu: *Arm7tdmi, late: u64) void {
pub fn handleOverflow(self: *Self, cpu: *Arm7tdmi, late: u64) void {
// Fire IRQ if enabled
const io = &cpu.bus.io;
@ -154,22 +154,22 @@ fn Timer(comptime id: u2) type {
// DMA Sound Things
if (id == 0 or id == 1) {
cpu.bus.apu.onDmaAudioSampleRequest(cpu, id);
cpu.bus.apu.handleTimerOverflow(cpu, id);
}
// Perform Cascade Behaviour
switch (id) {
0 => if (cpu.bus.tim[1].cnt.cascade.read()) {
cpu.bus.tim[1]._counter +%= 1;
if (cpu.bus.tim[1]._counter == 0) cpu.bus.tim[1].onTimerExpire(cpu, late);
if (cpu.bus.tim[1]._counter == 0) cpu.bus.tim[1].handleOverflow(cpu, late);
},
1 => if (cpu.bus.tim[2].cnt.cascade.read()) {
cpu.bus.tim[2]._counter +%= 1;
if (cpu.bus.tim[2]._counter == 0) cpu.bus.tim[2].onTimerExpire(cpu, late);
if (cpu.bus.tim[2]._counter == 0) cpu.bus.tim[2].handleOverflow(cpu, late);
},
2 => if (cpu.bus.tim[3].cnt.cascade.read()) {
cpu.bus.tim[3]._counter +%= 1;
if (cpu.bus.tim[3]._counter == 0) cpu.bus.tim[3].onTimerExpire(cpu, late);
if (cpu.bus.tim[3]._counter == 0) cpu.bus.tim[3].handleOverflow(cpu, late);
},
3 => {}, // There is no Timer for TIM3 to "cascade" to,
}
@ -177,11 +177,11 @@ fn Timer(comptime id: u2) type {
// Reschedule Timer if we're not cascading
if (!self.cnt.cascade.read()) {
self._counter = self._reload;
self.rescheduleTimerExpire(late);
self.scheduleOverflow(late);
}
}
fn rescheduleTimerExpire(self: *Self, late: u64) void {
fn scheduleOverflow(self: *Self, late: u64) void {
const when = (@as(u64, 0x10000) - self._counter) * self.frequency();
self._start_timestamp = self.sched.now();

View File

@ -236,13 +236,13 @@ pub const thumb = struct {
}
};
const cpu_logging = @import("emu.zig").cpu_logging;
const log = std.log.scoped(.Arm7Tdmi);
pub const Arm7tdmi = struct {
const Self = @This();
r: [16]u32,
pipe: Pipeline,
sched: *Scheduler,
bus: *Bus,
cpsr: PSR,
@ -263,7 +263,6 @@ pub const Arm7tdmi = struct {
pub fn init(sched: *Scheduler, bus: *Bus, log_file: ?std.fs.File) Self {
return Self{
.r = [_]u32{0x00} ** 16,
.pipe = Pipeline.init(),
.sched = sched,
.bus = bus,
.cpsr = .{ .raw = 0x0000_001F },
@ -412,43 +411,29 @@ pub const Arm7tdmi = struct {
self.cpsr.mode.write(@enumToInt(next));
}
/// Advances state so that the BIOS is skipped
///
/// Note: This accesses the CPU's bus ptr so it only may be called
/// once the Bus has been properly initialized
///
/// TODO: Make above notice impossible to do in code
pub fn fastBoot(self: *Self) void {
self.r = std.mem.zeroes([16]u32);
// self.r[0] = 0x08000000;
// self.r[1] = 0x000000EA;
self.r[0] = 0x08000000;
self.r[1] = 0x000000EA;
self.r[13] = 0x0300_7F00;
self.r[15] = 0x0800_0000;
self.banked_r[bankedIdx(.Irq, .R13)] = 0x0300_7FA0;
self.banked_r[bankedIdx(.Supervisor, .R13)] = 0x0300_7FE0;
// self.cpsr.raw = 0x6000001F;
self.cpsr.raw = 0x0000_001F;
self.bus.bios.addr_latch = 0x0000_00DC + 8;
self.cpsr.raw = 0x6000001F;
}
pub fn step(self: *Self) void {
defer {
if (!self.pipe.flushed) self.r[15] += if (self.cpsr.t.read()) 2 else @as(u32, 4);
self.pipe.flushed = false;
}
if (self.cpsr.t.read()) {
const opcode = @truncate(u16, self.pipe.step(self, u16) orelse return);
if (self.logger) |*trace| trace.mgbaLog(self, opcode);
const opcode = self.fetch(u16);
if (cpu_logging) self.logger.?.mgbaLog(self, opcode);
thumb.lut[thumb.idx(opcode)](self, self.bus, opcode);
} else {
const opcode = self.pipe.step(self, u32) orelse return;
if (self.logger) |*trace| trace.mgbaLog(self, opcode);
const opcode = self.fetch(u32);
if (cpu_logging) self.logger.?.mgbaLog(self, opcode);
if (checkCond(self.cpsr, @truncate(u4, opcode >> 28))) {
arm.lut[arm.idx(opcode)](self, self.bus, opcode);
@ -488,41 +473,42 @@ pub const Arm7tdmi = struct {
pub fn handleInterrupt(self: *Self) void {
const should_handle = self.bus.io.ie.raw & self.bus.io.irq.raw;
// Return if IME is disabled, CPSR I is set or there is nothing to handle
if (!self.bus.io.ime or self.cpsr.i.read() or should_handle == 0) return;
// If Pipeline isn't full, we have a bug
std.debug.assert(self.pipe.isFull());
// log.debug("Handling Interrupt!", .{});
if (should_handle != 0) {
self.bus.io.haltcnt = .Execute;
// log.debug("An Interrupt was Fired!", .{});
// FIXME: This seems weird, but retAddr.gba suggests I need to make these changes
const ret_addr = self.r[15] - if (self.cpsr.t.read()) 0 else @as(u32, 4);
const new_spsr = self.cpsr.raw;
// Either IME is not true or I in CPSR is true
// Don't handle interrupts
if (!self.bus.io.ime or self.cpsr.i.read()) return;
// log.debug("An interrupt was Handled!", .{});
// retAddr.gba says r15 on it's own is off by -04h in both ARM and THUMB mode
const r15 = self.r[15] + 4;
const cpsr = self.cpsr.raw;
self.changeMode(.Irq);
self.cpsr.t.write(false);
self.cpsr.i.write(true);
self.r[14] = ret_addr;
self.spsr.raw = new_spsr;
self.r[15] = 0x0000_0018;
self.pipe.reload(self);
self.r[14] = r15;
self.spsr.raw = cpsr;
self.r[15] = 0x000_0018;
}
}
inline fn fetch(self: *Self, comptime T: type, address: u32) T {
inline fn fetch(self: *Self, comptime T: type) T {
comptime std.debug.assert(T == u32 or T == u16); // Opcode may be 32-bit (ARM) or 16-bit (THUMB)
defer self.r[15] += if (T == u32) 4 else 2;
// Bus.read will advance the scheduler. There are different timings for CPU fetches,
// so we want to undo what Bus.read will apply. We can do this by caching the current tick
// This is very dumb.
//
// FIXME: Please rework this
// FIXME: You better hope this is optimized out
const tick_cache = self.sched.tick;
defer self.sched.tick = tick_cache + Bus.fetch_timings[@boolToInt(T == u32)][@truncate(u4, address >> 24)];
defer self.sched.tick = tick_cache + Bus.fetch_timings[@boolToInt(T == u32)][@truncate(u4, self.r[15] >> 24)];
return self.bus.read(T, address);
return self.bus.read(T, self.r[15]);
}
pub fn fakePC(self: *const Self) u32 {
return self.r[15] + 4;
}
pub fn panic(self: *const Self, comptime format: []const u8, args: anytype) noreturn {
@ -539,8 +525,6 @@ pub const Arm7tdmi = struct {
std.debug.print("spsr: 0x{X:0>8} ", .{self.spsr.raw});
prettyPrintPsr(&self.spsr);
std.debug.print("pipeline: {??X:0>8}\n", .{self.pipe.stage});
if (self.cpsr.t.read()) {
const opcode = self.bus.dbgRead(u16, self.r[15] - 4);
const id = thumb.idx(opcode);
@ -604,7 +588,7 @@ pub const Arm7tdmi = struct {
const r12 = self.r[12];
const r13 = self.r[13];
const r14 = self.r[14];
const r15 = self.r[15] -| if (self.cpsr.t.read()) 2 else @as(u32, 4);
const r15 = self.r[15];
const c_psr = self.cpsr.raw;
@ -612,7 +596,7 @@ pub const Arm7tdmi = struct {
if (self.cpsr.t.read()) {
if (opcode >> 11 == 0x1E) {
// Instruction 1 of a BL Opcode, print in ARM mode
const other_half = self.bus.debugRead(u16, self.r[15] - 2);
const other_half = self.bus.dbgRead(u16, self.r[15]);
const bl_opcode = @as(u32, opcode) << 16 | other_half;
log_str = try std.fmt.bufPrint(&buf, arm_fmt, .{ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, c_psr, bl_opcode });
@ -648,49 +632,6 @@ pub fn checkCond(cpsr: PSR, cond: u4) bool {
};
}
const Pipeline = struct {
const Self = @This();
stage: [2]?u32,
flushed: bool,
fn init() Self {
return .{
.stage = [_]?u32{null} ** 2,
.flushed = false,
};
}
pub fn isFull(self: *const Self) bool {
return self.stage[0] != null and self.stage[1] != null;
}
pub fn step(self: *Self, cpu: *Arm7tdmi, comptime T: type) ?u32 {
comptime std.debug.assert(T == u32 or T == u16);
// FIXME: https://github.com/ziglang/zig/issues/12642
var opcode = self.stage[0];
self.stage[0] = self.stage[1];
self.stage[1] = cpu.fetch(T, cpu.r[15]);
return opcode;
}
pub fn reload(self: *Self, cpu: *Arm7tdmi) void {
if (cpu.cpsr.t.read()) {
self.stage[0] = cpu.fetch(u16, cpu.r[15]);
self.stage[1] = cpu.fetch(u16, cpu.r[15] + 2);
cpu.r[15] += 4;
} else {
self.stage[0] = cpu.fetch(u32, cpu.r[15]);
self.stage[1] = cpu.fetch(u32, cpu.r[15] + 4);
cpu.r[15] += 8;
}
self.flushed = true;
}
};
pub const PSR = extern union {
mode: Bitfield(u32, 0, 5),
t: Bit(u32, 5),

View File

@ -55,10 +55,8 @@ pub fn blockDataTransfer(comptime P: bool, comptime U: bool, comptime S: bool, c
if (L) {
cpu.r[15] = bus.read(u32, und_addr);
cpu.pipe.reload(cpu);
} else {
// FIXME: Should r15 on write be +12 ahead?
bus.write(u32, und_addr, cpu.r[15] + 4);
bus.write(u32, und_addr, cpu.r[15] + 8);
}
cpu.r[rn] = if (U) cpu.r[rn] + 0x40 else cpu.r[rn] - 0x40;
@ -88,23 +86,17 @@ pub fn blockDataTransfer(comptime P: bool, comptime U: bool, comptime S: bool, c
cpu.setUserModeRegister(i, bus.read(u32, address));
} else {
const value = bus.read(u32, address);
cpu.r[i] = value;
if (i == 0xF) {
cpu.r[i] &= ~@as(u32, 3); // Align r15
cpu.pipe.reload(cpu);
if (S) cpu.setCpsr(cpu.spsr.raw);
}
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.write(u32, address, value + if (i == 0xF) 4 else @as(u32, 0)); // PC is already 8 ahead to make 12
bus.write(u32, address, value + if (i == 0xF) 8 else @as(u32, 0)); // PC is already 4 ahead to make 12
} else {
bus.write(u32, address, cpu.r[i] + if (i == 0xF) 4 else @as(u32, 0));
bus.write(u32, address, cpu.r[i] + if (i == 0xF) 8 else @as(u32, 0));
}
}
}

View File

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

View File

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

View File

@ -15,8 +15,20 @@ pub fn halfAndSignedDataTransfer(comptime P: bool, comptime U: bool, comptime I:
const rm = opcode & 0xF;
const imm_offset_high = opcode >> 8 & 0xF;
const base = cpu.r[rn] + if (!L and rn == 0xF) 4 else @as(u32, 0);
const offset = if (I) imm_offset_high << 4 | rm else cpu.r[rm];
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;

View File

@ -14,10 +14,15 @@ pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool,
const rn = opcode >> 16 & 0xF;
const rd = opcode >> 12 & 0xF;
// rn is r15 and L is not set, the PC is 12 ahead
const base = cpu.r[rn] + if (!L and rn == 0xF) 4 else @as(u32, 0);
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.immediate(false, cpu, opcode) else opcode & 0xFFF;
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;
@ -35,26 +40,18 @@ pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool,
} else {
if (B) {
// STRB
const value = cpu.r[rd] + if (rd == 0xF) 4 else @as(u32, 0); // PC is 12 ahead
const value = if (rd == 0xF) cpu.r[rd] + 8 else cpu.r[rd];
bus.write(u8, address, @truncate(u8, value));
} else {
// STR
const value = cpu.r[rd] + if (rd == 0xF) 4 else @as(u32, 0);
const value = if (rd == 0xF) cpu.r[rd] + 8 else cpu.r[rd];
bus.write(u32, address, value);
}
}
address = modified_base;
if (W and P or !P) {
cpu.r[rn] = address;
if (rn == 0xF) cpu.pipe.reload(cpu);
}
if (L) {
// This emulates the LDR rd == rn behaviour
cpu.r[rd] = result;
if (rd == 0xF) cpu.pipe.reload(cpu);
}
if (W and P or !P) cpu.r[rn] = address;
if (L) cpu.r[rd] = result; // This emulates the LDR rd == rn behaviour
}
}.inner;
}

View File

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

View File

@ -5,33 +5,37 @@ const CPSR = @import("../cpu.zig").PSR;
const rotr = @import("../../util.zig").rotr;
pub fn exec(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
pub fn execute(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
var result: u32 = undefined;
if (opcode >> 4 & 1 == 1) {
result = register(S, cpu, opcode);
result = registerShift(S, cpu, opcode);
} else {
result = immediate(S, cpu, opcode);
result = immShift(S, cpu, opcode);
}
return result;
}
fn register(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
fn registerShift(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
const rs_idx = opcode >> 8 & 0xF;
const rm = cpu.r[opcode & 0xF];
const rs = @truncate(u8, cpu.r[rs_idx]);
const rm_idx = opcode & 0xF;
const rm = if (rm_idx == 0xF) cpu.fakePC() else cpu.r[rm_idx];
return switch (@truncate(u2, opcode >> 5)) {
0b00 => lsl(S, &cpu.cpsr, rm, rs),
0b01 => lsr(S, &cpu.cpsr, rm, rs),
0b10 => asr(S, &cpu.cpsr, rm, rs),
0b11 => ror(S, &cpu.cpsr, rm, rs),
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 immediate(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
pub fn immShift(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
const amount = @truncate(u8, opcode >> 7 & 0x1F);
const rm = cpu.r[opcode & 0xF];
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) {
@ -60,17 +64,17 @@ pub fn immediate(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
}
} else {
switch (@truncate(u2, opcode >> 5)) {
0b00 => result = lsl(S, &cpu.cpsr, rm, amount),
0b01 => result = lsr(S, &cpu.cpsr, rm, amount),
0b10 => result = asr(S, &cpu.cpsr, rm, amount),
0b11 => result = ror(S, &cpu.cpsr, rm, amount),
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 lsl(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
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;
@ -97,7 +101,7 @@ pub fn lsl(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
return result;
}
pub fn lsr(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u32) u32 {
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;
@ -121,7 +125,7 @@ pub fn lsr(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u32) u32 {
return result;
}
pub fn asr(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
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;
@ -138,7 +142,7 @@ pub fn asr(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
return result;
}
pub fn ror(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
pub fn rotateRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
const result = rotr(u32, rm, total_amount);
if (S and total_amount != 0) {

View File

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

View File

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

View File

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

View File

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

View File

@ -12,9 +12,7 @@ pub fn fmt6(comptime rd: u3) InstrFn {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
// LDR
const offset = (opcode & 0xFF) << 2;
// Bit 1 of the PC intentionally ignored
cpu.r[rd] = bus.read(u32, (cpu.r[15] & ~@as(u32, 2)) + offset);
cpu.r[rd] = bus.read(u32, (cpu.r[15] + 2 & 0xFFFF_FFFD) + offset);
}
}.inner;
}

View File

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

View File

@ -1,6 +1,5 @@
const std = @import("std");
const SDL = @import("sdl2");
const config = @import("../config.zig");
const Bus = @import("Bus.zig");
const Scheduler = @import("scheduler.zig").Scheduler;
@ -13,6 +12,14 @@ const Thread = std.Thread;
const Atomic = std.atomic.Atomic;
const Allocator = std.mem.Allocator;
// TODO: Move these to a TOML File
const sync_audio = false; // Enable Audio Sync
const sync_video: RunKind = .LimitedFPS; // Configure Video Sync
pub const win_scale = 4; // 1x, 2x, 3x, etc. Window Scaling
pub const cpu_logging = false; // Enable detailed CPU logging
pub const allow_unhandled_io = true; // Only relevant in Debug Builds
pub const force_rtc = false;
// 228 Lines which consist of 308 dots (which are 4 cycles long)
const cycles_per_frame: u64 = 228 * (308 * 4); //280896
const clock_rate: u64 = 1 << 24; // 16.78MHz
@ -33,57 +40,18 @@ const RunKind = enum {
UnlimitedFPS,
Limited,
LimitedFPS,
LimitedBusy,
};
pub fn run(quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: *FpsTracker) void {
const audio_sync = config.config().guest.audio_sync;
if (audio_sync) log.info("Audio sync enabled", .{});
pub fn run(quit: *Atomic(bool), fps: *FpsTracker, sched: *Scheduler, cpu: *Arm7tdmi) void {
if (sync_audio) log.info("Audio sync enabled", .{});
if (config.config().guest.video_sync) {
inner(.LimitedFPS, audio_sync, quit, scheduler, cpu, tracker);
} else {
inner(.UnlimitedFPS, audio_sync, quit, scheduler, cpu, tracker);
}
}
fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: ?*FpsTracker) void {
if (kind == .UnlimitedFPS or kind == .LimitedFPS) {
std.debug.assert(tracker != null);
log.info("FPS tracking enabled", .{});
}
switch (kind) {
.Unlimited, .UnlimitedFPS => {
log.info("Emulation w/out video sync", .{});
while (!quit.load(.SeqCst)) {
runFrame(scheduler, cpu);
audioSync(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
if (kind == .UnlimitedFPS) tracker.?.tick();
}
},
.Limited, .LimitedFPS => {
log.info("Emulation w/ video sync", .{});
var timer = Timer.start() catch @panic("failed to initalize std.timer.Timer");
var wake_time: u64 = frame_period;
while (!quit.load(.SeqCst)) {
runFrame(scheduler, cpu);
const new_wake_time = videoSync(&timer, wake_time);
// Spin to make up the difference of OS scheduler innacuracies
// If we happen to also be syncing to audio, we choose to spin on
// the amount of time needed for audio to catch up rather than
// our expected wake-up time
audioSync(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
if (!audio_sync) spinLoop(&timer, wake_time);
wake_time = new_wake_time;
if (kind == .LimitedFPS) tracker.?.tick();
}
},
switch (sync_video) {
.Unlimited => runUnsynchronized(quit, sched, cpu, null),
.Limited => runSynchronized(quit, sched, cpu, null),
.UnlimitedFPS => runUnsynchronized(quit, sched, cpu, fps),
.LimitedFPS => runSynchronized(quit, sched, cpu, fps),
.LimitedBusy => runBusyLoop(quit, sched, cpu),
}
}
@ -104,7 +72,7 @@ pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
}
}
fn audioSync(audio_sync: bool, stream: *SDL.SDL_AudioStream, is_buffer_full: *bool) void {
fn syncToAudio(stream: *SDL.SDL_AudioStream, is_buffer_full: *bool) void {
const sample_size = 2 * @sizeOf(u16);
const max_buf_size: c_int = 0x400;
@ -117,20 +85,90 @@ fn audioSync(audio_sync: bool, stream: *SDL.SDL_AudioStream, is_buffer_full: *bo
while (true) {
still_full = SDL.SDL_AudioStreamAvailable(stream) > sample_size * max_buf_size >> 1;
if (!audio_sync or !still_full) break;
if (!sync_audio or !still_full) break;
}
}
fn videoSync(timer: *Timer, wake_time: u64) u64 {
pub fn runUnsynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*FpsTracker) void {
log.info("Emulation thread w/out video sync", .{});
if (fps) |tracker| {
log.info("FPS Tracking Enabled", .{});
while (!quit.load(.SeqCst)) {
runFrame(sched, cpu);
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
tracker.tick();
}
} else {
while (!quit.load(.SeqCst)) {
runFrame(sched, cpu);
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
}
}
}
pub fn runSynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*FpsTracker) void {
log.info("Emulation thread w/ video sync", .{});
var timer = Timer.start() catch std.debug.panic("Failed to initialize std.timer.Timer", .{});
var wake_time: u64 = frame_period;
if (fps) |tracker| {
log.info("FPS Tracking Enabled", .{});
while (!quit.load(.SeqCst)) {
runFrame(sched, cpu);
const new_wake_time = blockOnVideo(&timer, wake_time);
// Spin to make up the difference of OS scheduler innacuracies
// If we happen to also be syncing to audio, we choose to spin on
// the amount of time needed for audio to catch up rather than
// our expected wake-up time
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
if (!sync_audio) spinLoop(&timer, wake_time);
wake_time = new_wake_time;
tracker.tick();
}
} else {
while (!quit.load(.SeqCst)) {
runFrame(sched, cpu);
const new_wake_time = blockOnVideo(&timer, wake_time);
// see above comment
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
if (!sync_audio) spinLoop(&timer, wake_time);
wake_time = new_wake_time;
}
}
}
inline fn blockOnVideo(timer: *Timer, wake_time: u64) u64 {
// Use the OS scheduler to put the emulation thread to sleep
const recalculated = sleep(timer, wake_time);
const maybe_recalc_wake_time = 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;
return if (maybe_recalc_wake_time) |recalc| recalc else wake_time + frame_period;
}
pub fn runBusyLoop(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi) void {
log.info("Emulation thread with video sync using busy loop", .{});
var timer = Timer.start() catch unreachable;
var wake_time: u64 = frame_period;
while (!quit.load(.SeqCst)) {
runFrame(sched, cpu);
spinLoop(&timer, wake_time);
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
// Update to the new wake time
wake_time += frame_period;
}
}
// TODO: Better sleep impl?
fn sleep(timer: *Timer, wake_time: u64) ?u64 {
// const step = std.time.ns_per_ms * 10; // 10ms
const timestamp = timer.read();

View File

@ -13,7 +13,10 @@ const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
const FrameBuffer = @import("../util.zig").FrameBuffer;
const Allocator = std.mem.Allocator;
const log = std.log.scoped(.PPU);
const log = std.log.scoped(.Ppu);
/// This is used to generate byuu / Talurabi's Color Correction algorithm
const COLOUR_LUT = genColourLut();
pub const width = 240;
pub const height = 160;
@ -394,7 +397,7 @@ pub const Ppu = struct {
const maybe_btm = self.scanline.btm()[i];
const bgr555 = self.getBgr555(maybe_top, maybe_btm);
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
}
// Reset Current Scanline Pixel Buffer and list of fetched sprites
@ -421,7 +424,7 @@ pub const Ppu = struct {
const maybe_btm = self.scanline.btm()[i];
const bgr555 = self.getBgr555(maybe_top, maybe_btm);
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
}
// Reset Current Scanline Pixel Buffer and list of fetched sprites
@ -447,7 +450,7 @@ pub const Ppu = struct {
const maybe_btm = self.scanline.btm()[i];
const bgr555 = self.getBgr555(maybe_top, maybe_btm);
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
}
// Reset Current Scanline Pixel Buffer and list of fetched sprites
@ -462,7 +465,7 @@ pub const Ppu = struct {
var i: usize = 0;
while (i < width) : (i += 1) {
const bgr555 = self.vram.read(u16, vram_base + i * @sizeOf(u16));
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
}
},
0x4 => {
@ -473,7 +476,7 @@ pub const Ppu = struct {
// Render Current Scanline
for (self.vram.buf[vram_base .. vram_base + width]) |byte, i| {
const bgr555 = self.palette.read(u16, @as(u16, byte) * @sizeOf(u16));
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
}
},
0x5 => {
@ -490,7 +493,7 @@ pub const Ppu = struct {
const bgr555 =
if (scanline < m5_height and i < m5_width) self.vram.read(u16, vram_base + i * @sizeOf(u16)) else self.palette.backdrop();
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
}
},
else => std.debug.panic("[PPU] TODO: Implement BG Mode {}", .{bg_mode}),
@ -651,7 +654,7 @@ pub const Ppu = struct {
};
}
pub fn onHdrawEnd(self: *Self, cpu: *Arm7tdmi, late: u64) void {
pub fn handleHDrawEnd(self: *Self, cpu: *Arm7tdmi, late: u64) void {
// Transitioning to a Hblank
if (self.dispstat.hblank_irq.read()) {
cpu.bus.io.irq.hblank.set();
@ -667,7 +670,7 @@ pub const Ppu = struct {
self.sched.push(.HBlank, 68 * 4 -| late);
}
pub fn onHblankEnd(self: *Self, cpu: *Arm7tdmi, late: u64) void {
pub fn handleHBlankEnd(self: *Self, cpu: *Arm7tdmi, late: u64) void {
// The End of a Hblank (During Draw or Vblank)
const old_scanline = self.vcount.scanline.read();
const scanline = (old_scanline + 1) % 228;
@ -764,6 +767,18 @@ const Window = struct {
self.in.raw = @truncate(u16, value);
self.out.raw = @truncate(u16, value >> 16);
}
pub fn setInL(self: *Self, value: u8) void {
self.in.raw = (self.in.raw & 0xFF00) | value;
}
pub fn setInH(self: *Self, value: u8) void {
self.in.raw = (self.in.raw & 0x00FF) | (@as(u16, value) << 8);
}
pub fn setOutL(self: *Self, value: u8) void {
self.out.raw = (self.out.raw & 0xFF00) | value;
}
};
const Background = struct {
@ -1019,7 +1034,7 @@ fn spriteDimensions(shape: u2, size: u2) [2]u8 {
};
}
inline fn rgba888(bgr555: u16) u32 {
fn toRgba8888(bgr555: u16) u32 {
const b = @as(u32, bgr555 >> 10 & 0x1F);
const g = @as(u32, bgr555 >> 5 & 0x1F);
const r = @as(u32, bgr555 & 0x1F);
@ -1027,6 +1042,39 @@ inline fn rgba888(bgr555: u16) u32 {
return (r << 3 | r >> 2) << 24 | (g << 3 | g >> 2) << 16 | (b << 3 | b >> 2) << 8 | 0xFF;
}
fn genColourLut() [0x8000]u32 {
return comptime {
@setEvalBranchQuota(0x10001);
var lut: [0x8000]u32 = undefined;
for (lut) |*px, i| px.* = toRgba8888(i);
return lut;
};
}
// FIXME: The implementation is incorrect and using it in the LUT crashes the compiler (OOM)
/// Implementation courtesy of byuu and Talarubi at https://near.sh/articles/video/color-emulation
fn toRgba8888Talarubi(bgr555: u16) u32 {
@setRuntimeSafety(false);
const lcd_gamma: f64 = 4;
const out_gamma: f64 = 2.2;
const b = @as(u32, bgr555 >> 10 & 0x1F);
const g = @as(u32, bgr555 >> 5 & 0x1F);
const r = @as(u32, bgr555 & 0x1F);
const lb = std.math.pow(f64, @intToFloat(f64, b << 3 | b >> 2) / 31, lcd_gamma);
const lg = std.math.pow(f64, @intToFloat(f64, g << 3 | g >> 2) / 31, lcd_gamma);
const lr = std.math.pow(f64, @intToFloat(f64, r << 3 | r >> 2) / 31, lcd_gamma);
const out_b = std.math.pow(f64, (220 * lb + 10 * lg + 50 * lr) / 255, 1 / out_gamma);
const out_g = std.math.pow(f64, (30 * lb + 230 * lg + 10 * lr) / 255, 1 / out_gamma);
const out_r = std.math.pow(f64, (0 * lb + 50 * lg + 255 * lr) / 255, 1 / out_gamma);
return @floatToInt(u32, out_r) << 24 | @floatToInt(u32, out_g) << 16 | @floatToInt(u32, out_b) << 8 | 0xFF;
}
fn alphaBlend(top: u16, btm: u16, bldalpha: io.BldAlpha) u16 {
const eva: u16 = bldalpha.eva.read();
const evb: u16 = bldalpha.evb.read();

View File

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

View File

@ -1,10 +1,9 @@
const std = @import("std");
const builtin = @import("builtin");
const known_folders = @import("known_folders");
const clap = @import("clap");
const config = @import("config.zig");
const Gui = @import("platform.zig").Gui;
const Bus = @import("core/Bus.zig");
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
@ -15,14 +14,16 @@ const Allocator = std.mem.Allocator;
const log = std.log.scoped(.Cli);
const width = @import("core/ppu.zig").width;
const height = @import("core/ppu.zig").height;
const cpu_logging = @import("core/emu.zig").cpu_logging;
pub const log_level = if (builtin.mode != .Debug) .info else std.log.default_level;
// TODO: Reimpl Logging
// CLI Arguments + Help Text
const params = clap.parseParamsComptime(
\\-h, --help Display this help and exit.
\\-s, --skip Skip BIOS.
\\-b, --bios <str> Optional path to a GBA BIOS ROM.
\\<str> Path to the GBA GamePak ROM.
\\<str> Path to the GBA GamePak ROM
\\
);
@ -30,36 +31,16 @@ pub fn main() anyerror!void {
// Main Allocator for ZBA
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer std.debug.assert(!gpa.deinit());
const allocator = gpa.allocator();
// Determine the Data Directory (stores saves, config file, etc.)
const data_path = blk: {
const result = known_folders.getPath(allocator, .data);
const option = result catch |e| exitln("interrupted while attempting to find a data directory: {}", .{e});
const path = option orelse exitln("no valid data directory could be found", .{});
ensureDirectoriesExist(path) catch |e| exitln("failed to create directories under \"{s}\": {}", .{ path, e });
break :blk path;
};
defer allocator.free(data_path);
// Parse CLI
const result = clap.parse(clap.Help, &params, clap.parsers.default, .{}) catch |e| exitln("failed to parse cli: {}", .{e});
// Handle CLI Input
const result = try clap.parse(clap.Help, &params, clap.parsers.default, .{});
defer result.deinit();
// TODO: Move config file to XDG Config directory?
const config_path = configFilePath(allocator, data_path) catch |e| exitln("failed to determine the config file path for ZBA: {}", .{e});
defer allocator.free(config_path);
config.load(allocator, config_path) catch |e| exitln("failed to read config file: {}", .{e});
const paths = handleArguments(allocator, data_path, &result) catch |e| exitln("failed to handle cli arguments: {}", .{e});
const paths = try handleArguments(allocator, &result);
defer if (paths.save) |path| allocator.free(path);
const log_file = if (config.config().debug.cpu_trace) blk: {
break :blk std.fs.cwd().createFile("zba.log", .{}) catch |e| exitln("failed to create trace log file: {}", .{e});
} else null;
const log_file: ?std.fs.File = if (cpu_logging) try std.fs.cwd().createFile("zba.log", .{}) else null;
defer if (log_file) |file| file.close();
// TODO: Take Emulator Init Code out of main.zig
@ -68,78 +49,54 @@ pub fn main() anyerror!void {
var bus: Bus = undefined;
var cpu = Arm7tdmi.init(&scheduler, &bus, log_file);
if (paths.bios == null) cpu.fastBoot();
bus.init(allocator, &scheduler, &cpu, paths) catch |e| exitln("failed to init zba bus: {}", .{e});
try bus.init(allocator, &scheduler, &cpu, paths);
defer bus.deinit();
if (config.config().guest.skip_bios or result.args.skip or paths.bios == null) {
cpu.fastBoot();
}
var gui = Gui.init(&bus.pak.title, &bus.apu, width, height);
defer gui.deinit();
gui.run(&cpu, &scheduler) catch |e| exitln("failed to run gui thread: {}", .{e});
try gui.run(&cpu, &scheduler);
}
pub fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const clap.Result(clap.Help, &params, clap.parsers.default)) !FilePaths {
const rom_path = romPath(result);
fn getSavePath(allocator: Allocator) !?[]const u8 {
const save_subpath = "zba" ++ [_]u8{std.fs.path.sep} ++ "save";
const maybe_data_path = try known_folders.getPath(allocator, .data);
defer if (maybe_data_path) |path| allocator.free(path);
const save_path = if (maybe_data_path) |base| try std.fs.path.join(allocator, &[_][]const u8{ base, "zba", "save" }) else null;
if (save_path) |_| {
// If we've determined what our save path should be, ensure the prereq directories
// are present so that we can successfully write to the path when necessary
const maybe_data_dir = try known_folders.open(allocator, .data, .{});
if (maybe_data_dir) |data_dir| try data_dir.makePath(save_subpath);
}
return save_path;
}
fn getRomPath(result: *const clap.Result(clap.Help, &params, clap.parsers.default)) ![]const u8 {
return switch (result.positionals.len) {
1 => result.positionals[0],
0 => std.debug.panic("ZBA requires a positional path to a GamePak ROM.\n", .{}),
else => std.debug.panic("ZBA received too many arguments.\n", .{}),
};
}
pub fn handleArguments(allocator: Allocator, result: *const clap.Result(clap.Help, &params, clap.parsers.default)) !FilePaths {
const rom_path = try getRomPath(result);
log.info("ROM path: {s}", .{rom_path});
const bios_path = result.args.bios;
if (bios_path) |path| log.info("BIOS path: {s}", .{path}) else log.warn("No BIOS provided", .{});
if (bios_path) |path| log.info("BIOS path: {s}", .{path}) else log.info("No BIOS provided", .{});
const save_path = try getSavePath(allocator);
if (save_path) |path| log.info("Save path: {s}", .{path});
const save_path = try std.fs.path.join(allocator, &[_][]const u8{ data_path, "zba", "save" });
log.info("Save path: {s}", .{save_path});
return .{
return FilePaths{
.rom = rom_path,
.bios = bios_path,
.save = save_path,
};
}
fn configFilePath(allocator: Allocator, data_path: []const u8) ![]const u8 {
const path = try std.fs.path.join(allocator, &[_][]const u8{ data_path, "zba", "config.toml" });
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 = try std.fs.createFileAbsolute(path, .{});
defer config_file.close();
try config_file.writeAll(@embedFile("../example.toml"));
};
return path;
}
fn ensureDirectoriesExist(data_path: []const u8) !void {
var dir = try std.fs.openDirAbsolute(data_path, .{});
defer dir.close();
// We want to make sure: %APPDATA%/zba and %APPDATA%/zba/save exist
// (~/.local/share/zba/save for linux, ??? for macOS)
// Will recursively create directories
try dir.makePath("zba" ++ [_]u8{std.fs.path.sep} ++ "save");
}
fn romPath(result: *const clap.Result(clap.Help, &params, clap.parsers.default)) []const u8 {
return switch (result.positionals.len) {
1 => result.positionals[0],
0 => exitln("ZBA requires a path to a GamePak ROM", .{}),
else => exitln("ZBA received too many positional arguments.", .{}),
};
}
fn exitln(comptime format: []const u8, args: anytype) noreturn {
const stderr = std.io.getStdErr().writer();
stderr.print(format, args) catch {}; // Just exit already...
stderr.writeByte('\n') catch {};
std.os.exit(1);
}

View File

@ -1,8 +1,6 @@
const std = @import("std");
const SDL = @import("sdl2");
const gl = @import("gl");
const emu = @import("core/emu.zig");
const config = @import("config.zig");
const Apu = @import("core/apu.zig").Apu;
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
@ -12,156 +10,61 @@ const FpsTracker = @import("util.zig").FpsTracker;
const span = @import("util.zig").span;
const pitch = @import("core/ppu.zig").framebuf_pitch;
const gba_width = @import("core/ppu.zig").width;
const gba_height = @import("core/ppu.zig").height;
const scale = @import("core/emu.zig").win_scale;
const default_title: []const u8 = "ZBA";
pub const Gui = struct {
const Self = @This();
const SDL_GLContext = *anyopaque; // SDL.SDL_GLContext is a ?*anyopaque
const log = std.log.scoped(.Gui);
// zig fmt: off
const vertices: [32]f32 = [_]f32{
// Positions // Colours // Texture Coords
1.0, -1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, // Top Right
1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, // Bottom Right
-1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, // Bottom Left
-1.0, -1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, // Top Left
};
const indices: [6]u32 = [_]u32{
0, 1, 3, // First Triangle
1, 2, 3, // Second Triangle
};
// zig fmt: on
window: *SDL.SDL_Window,
ctx: SDL_GLContext,
title: []const u8,
renderer: *SDL.SDL_Renderer,
texture: *SDL.SDL_Texture,
audio: Audio,
program_id: gl.GLuint,
pub fn init(title: *const [12]u8, apu: *Apu, width: i32, height: i32) Self {
if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO) < 0) panic();
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_PROFILE_MASK, SDL.SDL_GL_CONTEXT_PROFILE_CORE) < 0) panic();
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic();
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic();
const win_scale = @intCast(c_int, config.config().host.win_scale);
const ret = SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO | SDL.SDL_INIT_GAMECONTROLLER);
if (ret < 0) panic();
const window = SDL.SDL_CreateWindow(
default_title.ptr,
SDL.SDL_WINDOWPOS_CENTERED,
SDL.SDL_WINDOWPOS_CENTERED,
@as(c_int, width * win_scale),
@as(c_int, height * win_scale),
SDL.SDL_WINDOW_OPENGL | SDL.SDL_WINDOW_SHOWN,
@as(c_int, width * scale),
@as(c_int, height * scale),
SDL.SDL_WINDOW_SHOWN,
) orelse panic();
const ctx = SDL.SDL_GL_CreateContext(window) orelse panic();
if (SDL.SDL_GL_MakeCurrent(window, ctx) < 0) panic();
const renderer = SDL.SDL_CreateRenderer(window, -1, SDL.SDL_RENDERER_ACCELERATED | SDL.SDL_RENDERER_PRESENTVSYNC) orelse panic();
gl.load(ctx, Self.glGetProcAddress) catch @panic("gl.load failed");
if (SDL.SDL_GL_SetSwapInterval(@boolToInt(config.config().host.vsync)) < 0) panic();
const program_id = compileShaders();
const texture = SDL.SDL_CreateTexture(
renderer,
SDL.SDL_PIXELFORMAT_RGBA8888,
SDL.SDL_TEXTUREACCESS_STREAMING,
@as(c_int, width),
@as(c_int, height),
) orelse panic();
return Self{
.window = window,
.title = span(title),
.ctx = ctx,
.program_id = program_id,
.renderer = renderer,
.texture = texture,
.audio = Audio.init(apu),
};
}
fn compileShaders() gl.GLuint {
// TODO: Panic on Shader Compiler Failure + Error Message
const vert_shader = @embedFile("shader/pixelbuf.vert");
const frag_shader = @embedFile("shader/pixelbuf.frag");
const vs = gl.createShader(gl.VERTEX_SHADER);
defer gl.deleteShader(vs);
gl.shaderSource(vs, 1, &[_][*c]const u8{vert_shader}, 0);
gl.compileShader(vs);
const fs = gl.createShader(gl.FRAGMENT_SHADER);
defer gl.deleteShader(fs);
gl.shaderSource(fs, 1, &[_][*c]const u8{frag_shader}, 0);
gl.compileShader(fs);
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
return program;
}
// Returns the VAO ID since it's used in run()
fn generateBuffers() [3]c_uint {
var vao_id: c_uint = undefined;
var vbo_id: c_uint = undefined;
var ebo_id: c_uint = undefined;
gl.genVertexArrays(1, &vao_id);
gl.genBuffers(1, &vbo_id);
gl.genBuffers(1, &ebo_id);
gl.bindVertexArray(vao_id);
gl.bindBuffer(gl.ARRAY_BUFFER, vbo_id);
gl.bufferData(gl.ARRAY_BUFFER, @sizeOf(@TypeOf(vertices)), &vertices, gl.STATIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo_id);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, @sizeOf(@TypeOf(indices)), &indices, gl.STATIC_DRAW);
// Position
gl.vertexAttribPointer(0, 3, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, 0)); // lmao
gl.enableVertexAttribArray(0);
// Colour
gl.vertexAttribPointer(1, 3, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, (3 * @sizeOf(f32))));
gl.enableVertexAttribArray(1);
// Texture Coord
gl.vertexAttribPointer(2, 2, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, (6 * @sizeOf(f32))));
gl.enableVertexAttribArray(2);
return .{ vao_id, vbo_id, ebo_id };
}
fn generateTexture(buf: []const u8) c_uint {
var tex_id: c_uint = undefined;
gl.genTextures(1, &tex_id);
gl.bindTexture(gl.TEXTURE_2D, tex_id);
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gba_width, gba_height, 0, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, buf.ptr);
// gl.generateMipmap(gl.TEXTURE_2D); // TODO: Remove?
return tex_id;
}
pub fn run(self: *Self, cpu: *Arm7tdmi, scheduler: *Scheduler) !void {
var quit = std.atomic.Atomic(bool).init(false);
var tracker = FpsTracker.init();
var frame_rate = FpsTracker.init();
const thread = try std.Thread.spawn(.{}, emu.run, .{ &quit, scheduler, cpu, &tracker });
const thread = try std.Thread.spawn(.{}, emu.run, .{ &quit, &frame_rate, scheduler, cpu });
defer thread.join();
var title_buf: [0x100]u8 = [_]u8{0} ** 0x100;
const vao_id = Self.generateBuffers()[0];
_ = Self.generateTexture(cpu.bus.ppu.framebuf.get(.Renderer));
emu_loop: while (true) {
var event: SDL.SDL_Event = undefined;
while (SDL.SDL_PollEvent(&event) != 0) {
@ -220,14 +123,11 @@ pub const Gui = struct {
// Emulator has an internal Double Buffer
const framebuf = cpu.bus.ppu.framebuf.get(.Renderer);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gba_width, gba_height, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, framebuf.ptr);
_ = SDL.SDL_UpdateTexture(self.texture, null, framebuf.ptr, pitch);
_ = SDL.SDL_RenderCopy(self.renderer, self.texture, null, null);
SDL.SDL_RenderPresent(self.renderer);
gl.useProgram(self.program_id);
gl.bindVertexArray(vao_id);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, null);
SDL.SDL_GL_SwapWindow(self.window);
const dyn_title = std.fmt.bufPrint(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, tracker.value() }) catch unreachable;
const dyn_title = std.fmt.bufPrint(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, frame_rate.value() }) catch unreachable;
SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr);
}
@ -236,18 +136,12 @@ pub const Gui = struct {
pub fn deinit(self: *Self) void {
self.audio.deinit();
// TODO: Buffer deletions
gl.deleteProgram(self.program_id);
SDL.SDL_GL_DeleteContext(self.ctx);
SDL.SDL_DestroyTexture(self.texture);
SDL.SDL_DestroyRenderer(self.renderer);
SDL.SDL_DestroyWindow(self.window);
SDL.SDL_Quit();
self.* = undefined;
}
fn glGetProcAddress(ctx: SDL.SDL_GLContext, proc: [:0]const u8) ?*anyopaque {
_ = ctx;
return SDL.SDL_GL_GetProcAddress(proc.ptr);
}
};
const Audio = struct {
@ -282,16 +176,10 @@ const Audio = struct {
export fn callback(userdata: ?*anyopaque, stream: [*c]u8, len: c_int) void {
const apu = @ptrCast(*Apu, @alignCast(@alignOf(*Apu), userdata));
// TODO: Find a better way to mute this
if (!config.config().host.mute) {
_ = SDL.SDL_AudioStreamGet(apu.stream, stream, len);
} else {
// FIXME: I don't think this hack to remove DC Offset is acceptable :thinking:
std.mem.set(u8, stream[0..@intCast(usize, len)], 0x40);
}
// If we don't write anything, play silence otherwise garbage will be played
// FIXME: I don't think this hack to remove DC Offset is acceptable :thinking:
// if (written == 0) std.mem.set(u8, stream[0..@intCast(usize, len)], 0x40);
}
};

View File

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

View File

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

View File

@ -1,12 +1,12 @@
const std = @import("std");
const builtin = @import("builtin");
const config = @import("config.zig");
const Log2Int = std.math.Log2Int;
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
const Allocator = std.mem.Allocator;
const allow_unhandled_io = @import("core/emu.zig").allow_unhandled_io;
// Sign-Extend value of type `T` to type `U`
pub fn sext(comptime T: type, comptime U: type, value: T) T {
// U must have less bits than T
@ -146,10 +146,8 @@ pub const io = struct {
}
pub fn undef(comptime T: type, log: anytype, comptime format: []const u8, args: anytype) ?T {
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", .{});
if (builtin.mode == .Debug and !allow_unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{});
return null;
}
@ -157,13 +155,22 @@ pub const io = struct {
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", .{});
if (builtin.mode == .Debug and !allow_unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{});
}
};
};
pub fn readUndefined(log: anytype, comptime format: []const u8, args: anytype) u8 {
log.warn(format, args);
if (builtin.mode == .Debug) std.debug.panic("TODO: Implement I/O Register", .{});
return 0;
}
pub fn writeUndefined(log: anytype, comptime format: []const u8, args: anytype) void {
log.warn(format, args);
if (builtin.mode == .Debug) std.debug.panic("TODO: Implement I/O Register", .{});
}
pub const Logger = struct {
const Self = @This();
@ -179,7 +186,6 @@ pub const Logger = struct {
pub fn print(self: *Self, comptime format: []const u8, args: anytype) !void {
try self.buf.writer().print(format, args);
try self.buf.flush(); // FIXME: On panics, whatever is in the buffer isn't written to file
}
pub fn mgbaLog(self: *Self, cpu: *const Arm7tdmi, opcode: u32) void {
@ -219,92 +225,14 @@ pub const Logger = struct {
cpu.r[12],
cpu.r[13],
cpu.r[14],
cpu.r[15] - if (cpu.cpsr.t.read()) 2 else @as(u32, 4),
cpu.r[15],
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 the high bits of an integer to a value
pub inline fn setHi(comptime T: type, left: T, right: HalfInt(T)) T {
return switch (T) {
u32 => (left & 0xFFFF_0000) | right,
u16 => (left & 0xFF00) | right,
u8 => (left & 0xF0) | right,
else => @compileError("unsupported type"),
};
}
/// sets the low bits of an integer to a value
pub inline fn setLo(comptime T: type, left: T, right: HalfInt(T)) T {
return switch (T) {
u32 => (left & 0x0000_FFFF) | @as(u32, right) << 16,
u16 => (left & 0x00FF) | @as(u16, right) << 8,
u8 => (left & 0x0F) | @as(u8, right) << 4,
else => @compileError("unsupported type"),
};
}
/// The Integer type which corresponds to T with exactly half the amount of bits
fn HalfInt(comptime T: type) type {
const type_info = @typeInfo(T);
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
// Double Buffering Implementation
pub const FrameBuffer = struct {
const Self = @This();