525 Commits

Author SHA1 Message Date
976a87e888 chore: dont allocate not-small ?Sprite array on stack
use memset like most other allocations in this emu
2022-10-21 05:38:20 -03:00
c753e0a0e7 chore: move FrameBuffer struct to util.zig 2022-10-21 05:38:20 -03:00
00f75f0d39 chore: move OAM, PALRAM and VRAM structs to separate files 2022-10-21 05:38:20 -03:00
4f9cb7d8a3 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-10-21 05:38:20 -03:00
6c2dfa9d05 chore: refactor window 2022-10-21 05:38:20 -03:00
06addcfc71 chore: crude background window impl (no affine) 2022-10-21 05:38:20 -03:00
7bb44894a7 chore: rename function (misspelt until now somehow) 2022-10-21 05:38:20 -03:00
98fc82c17a chore(config): change defaults in example.toml 2022-10-21 05:37:46 -03:00
9436888b28 chore(gitignore): update .gitignore
sometimes I'm left with a src/zig-cache, and I want to
just make sure that I never-ever end up commiting that
2022-10-21 05:13:13 -03:00
583ed54ee3 feat: write default config.toml if it doesn't exist
also resolves panic on missing /zba or /zba/save directory by ensuring
those directories exist as soon as we know the data directory
2022-10-21 05:13:13 -03:00
7944cd7abc chore: update README 2022-10-21 05:13:13 -03:00
ffd94c96ce Merge pull request 'Configure SDL2 to use OpenGL' (#4) from opengl into main
Reviewed-on: #4
2022-10-21 05:13:13 -03:00
4fe612da61 fix(opengl): properly control whether vsync is enabled 2022-10-21 05:13:12 -03:00
4262688fa8 chore(ppu): remove BGR555 -> RGBA888 LUT
LUT probably couldn't fit in CPU cache anyways.

TODO: Consider whether LUTs for separate channels (size 32 * 3 * 3
instead of std.math.maxInt(u15))
2022-10-21 05:13:12 -03:00
35d685f032 chore: replace OpenGL 4.5 bindings with OpenGL 3.3 2022-10-21 05:13:12 -03:00
5b9d3d0960 chore: remove unnecessary ptr cast 2022-10-21 05:13:12 -03:00
44b2e9be88 chore(gpio): add missing errdefer 2022-10-21 05:13:11 -03:00
4b7ef52a7a feat: implement better Colour Emulation 2022-10-21 05:13:11 -03:00
5103fdf646 chore(main): report errors slightly better 2022-10-21 05:13:11 -03:00
acddaf76aa fix: lower required OpenGL version + resolve offset bug 2022-10-21 05:13:11 -03:00
3da3b69e13 feat: use opengl
TODO:
- Texture isn't scaling properly
- I need to reverse the colours in the frag shader
2022-10-21 05:13:11 -03:00
40062d4dfe chore(config): add log message 2022-10-21 05:13:10 -03:00
1c91b497bc feat(config): add option to skip BIOS 2022-10-21 05:13:10 -03:00
e3286a9872 feat(cli): Add option to skip BIOS 2022-10-21 05:13:10 -03:00
ddb0e2f928 chore: Update README.md 2022-10-21 05:13:10 -03:00
d927ee6c90 Merge pull request 'Draft: Implement Instruction Pipeline' (#3) from pipeline into main
Reviewed-on: #3
2022-10-21 05:13:10 -03:00
011f105bee feat(config): add config option to mute ZBA 2022-10-21 05:13:09 -03:00
2c88f9bbce fix(bios): set addr_latch even if bios is skipped 2022-10-21 05:13:09 -03:00
30c5fafec1 chore(config): add example config file 2022-10-21 05:13:09 -03:00
3b144e581a fix(bus): make open bus impl aware of CPU pipeline 2022-10-21 05:13:09 -03:00
0117a52f4d style(bus): cpu ptr doesn't need to be optional 2022-10-21 05:13:09 -03:00
3cf7c83269 style: code cleanup 2022-10-21 05:13:09 -03:00
85d4690c56 fix: resolve timing regressions
make sure to use fetch timings when fetching instructions
2022-10-21 05:13:08 -03:00
130d40bc7f fix: rename Pipline to Pipeline 2022-10-21 05:13:08 -03:00
2c928eafec feat: working pipeline implementation 2022-10-21 05:13:08 -03:00
dcbeeee7cc chore: refactor ARM/THUMB data processing instructions 2022-10-21 05:13:08 -03:00
84ccb7224b fix: don't flush pipeline when reloading CPSR in ARM Data Processing 2022-10-21 05:13:08 -03:00
dd4bb4ff03 chore: don't write to CPSR + swap with SPSR at the same time 2022-10-21 05:13:07 -03:00
ecbfe67b27 chore: update README.md 2022-10-21 05:13:07 -03:00
e29c20019d fix: advance r15, even when the pipeline is reloaded from the scheduler
The PC would fall behind whenever an IRQ was called because the pipeline
was reloaded (+8 to PC), however that was never actually done by any code

Now, the PC is always incremented when the pipeline is reloaded
2022-10-21 05:13:07 -03:00
d8c397248f chore: dump pipeline state on cpu panic 2022-10-21 05:13:07 -03:00
64aed01869 fix: reimpl THUMB.5 instructions
pipeline branch now passes arm.gba and thumb.gba again

(TODO: Stop rewriting my commits away)
2022-10-21 05:13:07 -03:00
29f2a0288f fix: impl workaround for stage2 miscompilation 2022-10-21 05:13:07 -03:00
27ada16377 chore: instantly refill the pipeline on flush
I believe this to be necessary in order to get hardware interrupts
working.

thumb.gba test 108 fails but I'm committing anyways (despite the
regression) because this is kind of rebase/merge hell and I have
something that at least sort of works rn
2022-10-21 05:13:06 -03:00
ad1cec58e7 fix: reimpl handleInterrupt code 2022-10-21 05:13:06 -03:00
97d9edab93 feat: implement basic pipeline
passes arm.gba, thumb.gb and armwrestler, fails in actual games
TODO: run FuzzARM debug specific titles
2022-10-21 05:13:06 -03:00
39f56e6d8a feat: resolve off-by-{word, halfword} errors when printing debug info 2022-10-21 05:13:06 -03:00
f0014e7179 feat: reimplement cpu logging 2022-10-21 05:13:06 -03:00
8a804ea05e Merge pull request 'Add TOML Support' (#2) from toml into main
Reviewed-on: #2
2022-10-21 05:13:05 -03:00
efce1a6dce chore(emu): refactor code 2022-10-21 05:13:05 -03:00
8b9ab6f4b5 feat(config): add support for (and read from) TOML config file 2022-10-21 05:13:05 -03:00
d21c860eb5 feat: parse config.toml in data folder
Also took the chance to rework parts of the logic that determines
ZBA's save path
2022-10-21 05:13:05 -03:00
85e670a1d7 chore: add zig-toml dependency 2022-10-21 05:13:05 -03:00
4f823217bd chore: update dependencies 2022-10-21 05:13:04 -03:00
207a99edbe style: improve code quality 2022-10-21 05:13:04 -03:00
3c5b30dece feat: rewrite device ticks 2022-10-21 05:13:04 -03:00
739d38533a style(scheduler): rename scheduler event handlers 2022-10-21 05:13:04 -03:00
ae6b8e2f03 style: code refactoring 2022-10-21 05:13:04 -03:00
208f4b522d style(apu): split apu.zig into multiple files + refactor 2022-10-21 05:13:03 -03:00
bd54700103 style(backup): refactor code 2022-10-21 05:13:03 -03:00
08d27520e0 style(flash): move flash code into it's own file 2022-10-21 05:13:03 -03:00
bfe97c671e style(eeprom): move eeprom code to it's own file 2022-10-21 05:13:03 -03:00
9baadadba2 style(bus): refactor several hardware abstractions 2022-10-21 05:13:03 -03:00
68a87e0a54 chore: SDL2.zig expects target to be set before link() is called 2022-10-21 05:13:02 -03:00
1d7dfe71ca chore: update dependencies 2022-10-21 05:13:02 -03:00
1acc5e35e9 chore: move util.zig 2022-10-21 05:13:02 -03:00
df73cdbecc chore: disable audio sync by default
forgot SDL2 AudioStream doesn't work well for my use-case
2022-10-21 05:13:02 -03:00
6738dfac85 chore: change default settings 2022-10-21 05:13:02 -03:00
11985f4019 chore: reimpl util.escape
should make use of stdlib when I can
2022-10-21 05:13:02 -03:00
7cad3aca13 fix: Detect FRAM ROMs 2022-10-21 05:13:01 -03:00
2bbc12cd1a chore: improve util and Gui API 2022-10-21 05:13:01 -03:00
270db2b5ff chore: move Gpio and Clock structs to separate file 2022-10-21 05:13:01 -03:00
bce46418cd Merge pull request 'Implement RTC' (#1) from rtc into main
Reviewed-on: #1
2022-10-21 05:13:01 -03:00
2d9b03a725 feat: add option to force-enable RTC 2022-10-21 05:13:01 -03:00
c34752ac65 feat: auto-detect RTC in commercial ROMS 2022-10-21 05:13:00 -03:00
60680a36e2 fix: account for lateness in RTC scheduler event 2022-10-21 05:13:00 -03:00
4111bb5e4f fix: RTC day is 6 bits wide, not 3 2022-10-21 05:13:00 -03:00
612f5fe30e feat: put RTC Sync on Scheduler
TODO: Database to see what games have what GPIO devices
2022-10-21 05:13:00 -03:00
d9776e99d3 chore: import datetime library + default time for RTC 2022-10-21 05:13:00 -03:00
960efcd428 fix: ignore RTC Time/DateTime writes
this falls in-line with better emulators
2022-10-21 05:13:00 -03:00
b07dc8484d chore: use Clock.Writer for Command parsing, delete Clock.Command 2022-10-21 05:12:59 -03:00
ebcae80a9d feat: implement RTC Read/Writes 2022-10-21 05:12:59 -03:00
ff8ea79620 feat: implement force irqs for GPIO/RTC 2022-10-21 05:12:59 -03:00
fe19b19fc7 fix: properly resovle stack UAF 2022-10-21 05:12:59 -03:00
e709c2030c chore: shorten orelse @panic to .? 2022-10-21 05:12:59 -03:00
5725bbbe35 fix: update GpioData extern union
u4's are no longer supported in extern unions :\
2022-10-21 05:12:59 -03:00
8553ab6e6d chore: Guilty Gear X expects these I/O Registers 2022-10-21 05:12:58 -03:00
372bfdc5f6 tmp: incomplete impl of GPIO + RTC 2022-10-21 05:12:58 -03:00
fad5c9e632 feat: implement open bus for unmapped i/o 2022-10-21 05:12:58 -03:00
5fb5247d0e chore: comment open bus impl 2022-10-21 05:12:58 -03:00
75be6aa82a chore: update dependencies 2022-10-21 05:12:58 -03:00
0b8cc30d4d chore: Update README.md 2022-10-21 05:12:58 -03:00
d2a50cf9d2 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-10-21 05:12:57 -03:00
b2386a6a2b chore: move arm/thumb lut idx functions 2022-10-21 05:12:57 -03:00
22fbe380eb chore: update dependencies 2022-10-21 05:12:57 -03:00
f2b27f31f4 chore: better conform to zig idioms 2022-10-21 05:12:57 -03:00
867025b1ec 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-10-21 05:12:57 -03:00
9b0f54b111 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-10-21 05:12:56 -03:00
4cf58f1faa chore: improve init/deinit methods 2022-10-21 05:12:56 -03:00
1a56f957c1 chore: reorganize some code 2022-10-21 05:12:56 -03:00
1a4a2a56a3 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-10-21 05:12:56 -03:00
c701156ce6 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-10-21 05:12:56 -03:00
4b6897aedf feat: Get ZBA working on Zig's new stage2/stage3 compiler 2022-10-21 05:12:55 -03:00
1471288969 chore: move window scale const to emu.zig 2022-10-21 05:12:55 -03:00
7488fd7fd5 fix: reimpl debug reads w/out throwing away *const Self 2022-10-21 05:12:55 -03:00
20056eff2c chore: update dependencies: 2022-10-21 05:12:55 -03:00
3f760fccaf feat: reimplement cpu logging 2022-10-21 05:12:55 -03:00
f833de765c chore: don't init bus in Arm7tdmi init 2022-10-21 05:12:55 -03:00
aa19ef5f71 feat: move arm instr decoding to module 2022-10-21 05:12:54 -03:00
f0284107f9 feat: move thumb instr decoding to module 2022-10-21 05:12:54 -03:00
91c94fe528 chore: change directory structure 2022-10-21 05:12:54 -03:00
45fc49b216 fix: reimplement halt fast-forwarding 2022-10-21 05:12:54 -03:00
0939d6d7bc chore: move audio sync, video sync variables 2022-10-21 05:12:53 -03:00
e7b5410509 chore: update README.md 2022-10-21 05:12:53 -03:00
2758e511ea chore: update SDL.zig 2022-10-21 05:12:53 -03:00
46ee21f464 feat: impl WININ, WINOUT, WIN{N}H and WIN{N}V 2022-10-21 05:12:53 -03:00
0287c9a260 fix: force align DMA transfers 2022-10-21 05:12:53 -03:00
665767c250 fix: resolve bugs in VRAM unpredictable read/writes 2022-10-21 05:12:53 -03:00
0fd8a13a93 fix: don't start HDMA in vblank 2022-10-21 05:12:52 -03:00
125b931d0c feat: implement brightness increase/decrease 2022-10-21 05:12:52 -03:00
27259c97db feat: implement object blending 2022-10-21 05:12:52 -03:00
9479838614 feat: implement background alpha blending 2022-10-21 05:12:52 -03:00
8a203ff05f feat: implement BLDCNT, BLDALPHA, BLDY 2022-10-21 05:12:52 -03:00
39f71730e0 chore: update README 2022-10-21 05:12:52 -03:00
33f993c19d chore: rename + remove some code 2022-10-21 05:12:51 -03:00
18ec16eb6d fix: properly fire DMA IRQs
This resolves Sound DMA Timing issues present in DOOM
2022-10-21 05:12:51 -03:00
bf558922f9 chore: rename Dma.active to Dma.in_progress 2022-10-21 05:12:51 -03:00
e87bda7584 chore: rewrite info log message 2022-10-21 05:12:51 -03:00
87dc70436c feat: implement NR10 obscure behaviour 2022-10-21 05:12:51 -03:00
5dd78177f4 feat: handle all I/O when using Cult-Of-GBA BIOS 2022-10-21 05:12:51 -03:00
203af4c471 chore: 32-bit reads for PSG audio 2022-10-21 05:12:50 -03:00
98223d9e5a chore: implement more than just 1 cycle per mem access 2022-10-21 05:12:50 -03:00
e8cc0dfabb fix: implement register reads for Yoshi's Island 2022-10-21 05:12:50 -03:00
c8585a6f9a fix: reimplement DMA ticking 2022-10-21 05:12:50 -03:00
7d79361aca chore(cpu): add inline fn isHalted() 2022-10-21 05:12:50 -03:00
d1d32e465c chore: attempt to debug Rhythm Heaven 2022-10-21 05:12:50 -03:00
42c6b21124 fix: impl BG?{X,Y} RefPoint write behaviour outside of Vblank
With this fix Mode 7-like games now properly render their backgrounds
2022-10-21 05:12:49 -03:00
44e1dacb7b chore: change priority of some logs 2022-10-21 05:12:49 -03:00
eeea8a6327 chore: mess with debug statements + mask APU I/O reads 2022-10-21 05:12:49 -03:00
0a5df26c31 chore: move timer, apu and dma i/o addr matching outside of io.zig 2022-10-21 05:12:49 -03:00
d3bc58d71c chore: separate render code for affine sprites 2022-10-21 05:12:49 -03:00
8d32d9788e chore: reimplement object rendering
TODO: implement affine sprites
2022-10-21 05:12:48 -03:00
b18f488b01 chore: small changes to normal background drawing code 2022-10-21 05:12:48 -03:00
4b4242df2a feat: implement affine backgrounds 2022-10-21 05:12:48 -03:00
e2533dedeb chore: stub 8-bit window registers 2022-10-21 05:12:48 -03:00
89619596ad chore: remove code that pretends to remove DC offset 2022-10-21 05:12:48 -03:00
02fee16561 fix: replace affine bg register bitfields with signed integers 2022-10-21 05:12:48 -03:00
bf8521eb5e chore: use stdlib endian-aware integer read/write functions 2022-10-21 05:12:47 -03:00
299244a37a chore: update zig version in README.md 2022-10-21 05:12:47 -03:00
9990fa3513 chore: update SDL.zig 2022-10-21 05:12:47 -03:00
db064d2321 chore: misc style improvements 2022-10-21 05:12:47 -03:00
e0523aea63 chore: rename method in FpsTracker 2022-10-21 05:12:47 -03:00
5982fdea98 chore: update README.md 2022-10-21 05:12:46 -03:00
c18be62b11 fix(backup): resolve banking issue in flash impl 2022-10-21 05:12:46 -03:00
9f69b122d0 chore: remove awful ptr casts in backup.zig and bios.zig 2022-10-21 05:12:46 -03:00
295aa139f6 feat: pass jsmolka's bios.gba 2022-10-21 05:12:46 -03:00
2502cc5bf0 fix: play right samples in right channel 2022-10-21 05:12:46 -03:00
fc7f2a2959 fix: resolve issue when handling event sooner than expected 2022-10-21 05:12:46 -03:00
9d839a0328 fix: remove DC offset from audio output 2022-10-21 05:12:45 -03:00
55dada243e chore: add debug keybinds for scheduler capacity + event count 2022-10-21 05:12:45 -03:00
78e7c0bc3f perf: don't check scheduler every iteration of runFrame loop
~20fps gain in Pokemon Emerald, nice
2022-10-21 05:12:45 -03:00
9134456229 chore: simplify 4bpp palette code 2022-10-21 05:12:45 -03:00
0ef71ecb49 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-10-21 05:12:45 -03:00
3d18685d36 chore: modify type signature of util.sext 2022-10-21 05:12:45 -03:00
f194bee4eb chore: cleanup main 2022-10-21 05:12:44 -03:00
f373d8e17c chore: emu audio sync code to emu.zig 2022-10-21 05:12:44 -03:00
1b7d15e7d2 chore: redo apu sampling 2022-10-21 05:12:44 -03:00
89d8c08cd1 chore: implement apu u16 reads 2022-10-21 05:12:44 -03:00
3cd02a44cf fix: clean up frequency timer implementations 2022-10-21 05:12:44 -03:00
87eb0cc808 Revert "fix: resolve off-by-one errors when scheduling freq timer expirations"
This reverts commit c9b0030b4b.
2022-10-21 05:12:43 -03:00
8313210ddc fix: resolve off-by-one errors when scheduling freq timer expirations 2022-10-21 05:12:43 -03:00
2664f5cf20 chore: improve APU accuracy + scheduler refactoring 2022-10-21 05:12:43 -03:00
47518268a6 chore: update SDL.zig 2022-10-21 05:12:43 -03:00
29ee225c1f feat: stub Affine BG registers 2022-10-21 05:12:43 -03:00
ff6d2517be fix: resolve out-of-bounds error with 8bpp tall / horizontal sprites
Boot ROM is now enabled by default as well
2022-10-21 05:12:43 -03:00
242bf08cf2 chore: improve audio accuracy 2022-10-21 05:12:42 -03:00
996de65688 chore: reintroduce thread sleeping + simplify fps counter 2022-10-21 05:12:42 -03:00
b97b66927f feat: implement double buffering 2022-10-21 05:12:42 -03:00
628d4cfb68 chore: clean up DMA code 2022-10-21 05:12:42 -03:00
ff002f18c6 feat: handle DMA IRQs (maybe?) 2022-10-21 05:12:42 -03:00
5d5d3827fb chore: contain Timers in a tuple rather than a struct 2022-10-21 05:12:42 -03:00
8826242bf3 chore: contain DMA Controllers in a tuple rather than a struct 2022-10-21 05:12:41 -03:00
4bb1aa06f7 chore: update git submodules 2022-10-21 05:12:41 -03:00
570ff8536c chore: resolve incorrect memory mirror in VRAM
+ stub GPIO registers on ROM Write
2022-10-21 05:12:41 -03:00
2c0d2b64a2 chore: stub a few I/O registers 2022-10-21 05:12:41 -03:00
1750c0a26e chore: allow 8-bit IO to BG0CNT and BG1CNT
BG0CNT and and BG1CNT now work properly in mario kart
2022-10-21 05:12:41 -03:00
a102d68e99 chore: define affine sprite attributes 2022-10-21 05:12:41 -03:00
61483b93e8 feat: stub mode 1 and 2 2022-10-21 05:12:40 -03:00
192d7645eb feat: implement mode 5
I wonder which obscure game makes heavy use of this mode
2022-10-21 05:12:40 -03:00
f1c68fb0de chore: comment ARM MSR code + Audio issues 2022-10-21 05:12:40 -03:00
cb74bfd280 chore: pass destoer's cond_invalid test 2022-10-21 05:12:40 -03:00
0d1717538b chore: misc print message improvements 2022-10-21 05:12:40 -03:00
896ae0935a chore: improvements to APU accuracy 2022-10-21 05:12:39 -03:00
1a23073424 fix: incorrect order-of-operations in ARM BL impl 2022-10-21 05:12:39 -03:00
36f3b0d381 chore: special case saving for ROMS without titles 2022-10-21 05:12:39 -03:00
74335cacbf chore: update README 2022-10-21 05:12:39 -03:00
1ee2dd9c5e chore: update most recent zig version 2022-10-21 05:12:39 -03:00
c2f3790dc3 feat: pass DenSinH's eeprom-test 2022-10-21 05:12:39 -03:00
746c28004f feat: implement EEPROM 2022-10-21 05:12:38 -03:00
084d4b28dd chore: implement I/O regsister for Minish Cap 2022-10-21 05:12:38 -03:00
ed6e83b121 chore: change default window scale to 4x 2022-10-21 05:12:38 -03:00
b827ba3a1c chore: write more debug log messages for unimplemented registers 2022-10-21 05:12:38 -03:00
0f343f0b74 chore: only sync to audio for now 2022-10-21 05:12:38 -03:00
7a670c6ed9 feat: panic on unimplemented I/O in ReleaseSafe/Debug but not ReleaseFast 2022-10-21 05:12:38 -03:00
e690f88cda chore: misc improvements 2022-10-21 05:12:37 -03:00
c7f537959b 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-10-21 05:12:37 -03:00
f074b703b3 feat: implement Noise
Kirby & The Amazing Mirror crashes only in ReleaseSafe / ReleaseBug.

TODO: Figure out why
2022-10-21 05:12:37 -03:00
f8159645e0 feat: implement ch3 2022-10-21 05:12:37 -03:00
cccb83a926 feat: implement ch2 2022-10-21 05:12:37 -03:00
af8fe66c43 feat: implement ch1
TODO: It's really loud
2022-10-21 05:12:36 -03:00
1f23aff22c chore: broken impl of ch1 2022-10-21 05:12:36 -03:00
e580b78020 feat: add audio resampler
Also implement extremely naive audio sync
2022-10-21 05:12:36 -03:00
a87a5ce273 chore: calculate apu sample rate a bit better 2022-10-21 05:12:36 -03:00
af1887e0a6 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-10-21 05:12:36 -03:00
14bb2f6fbe chore: improve timer behaviour 2022-10-21 05:12:36 -03:00
d144973acf chore: move some init code to functions 2022-10-21 05:12:35 -03:00
eecd78a008 feat: impelemt THUMB open bus 2022-10-21 05:12:35 -03:00
c03c142b14 feat: implement ARM read open bus 2022-10-21 05:12:35 -03:00
40f3600de2 fix: remove accidental rotation in ldrsh instructions 2022-10-21 05:12:35 -03:00
c08e331f77 chore: move log statement 2022-10-21 05:12:35 -03:00
a92989ed24 chore: remove magic numbers 2022-10-21 05:12:35 -03:00
6c61eb3537 chore: remove unnecessary 32MB allocation 2022-10-21 05:12:34 -03:00
05c1274ec1 chore: define more I/O read/writes 2022-10-21 05:12:34 -03:00
f5bc78ae22 chore: update README 2022-10-21 05:12:34 -03:00
27ad8772ea feat: pass jsmolka memory.gba 2022-10-21 05:12:34 -03:00
135987745c chore: ignore instead of logging errors for perf reasons 2022-10-21 05:12:34 -03:00
f039c891c7 feat: Initial Implementation of DMA Audio 2022-10-21 05:12:33 -03:00
e69f4cfafe chore: tick scheduler on memory access 2022-10-21 05:12:33 -03:00
a4020400da chore: log error on open bus in page 0x00 and 0x01 2022-10-21 05:12:33 -03:00
d4aac22e34 chore: rewrite I/O read/writes 2022-10-21 05:12:33 -03:00
601e717850 chore: reimplement bus read/writes 2022-10-21 05:12:33 -03:00
886b9abf3d fix: force align reads/writes in memory bus rather than in CPU 2022-10-21 05:12:32 -03:00
9c87b9820e fix: pass none.gba and kind of sram.gba from jsmolka test suite 2022-10-21 05:12:32 -03:00
1fe332a44f feat: implement GamePak out-of-bounds reads 2022-10-21 05:12:32 -03:00
b6d2084c96 chore: run zigfmt 2022-10-21 05:12:32 -03:00
ed896d976d chore: change implementation of rotr 2022-10-21 05:12:32 -03:00
677eecad41 chore: rewrite read/write methods for remainig Bus devices 2022-10-21 05:12:32 -03:00
74b6fa2ecc chore: mirror VRAM 2022-10-21 05:12:31 -03:00
effb6315e9 chore: write generic read/write for VRAM 2022-10-21 05:12:31 -03:00
3c8390a248 Merge branch 'main' of ssh://musuka.dev:2222/paoda/zba 2022-10-21 05:12:31 -03:00
8337a6dcd9 chore: update dependencies 2022-10-21 05:12:31 -03:00
ee194b03d0 chore: update README 2022-10-21 05:12:31 -03:00
2d8fa9c2f7 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-10-21 05:12:30 -03:00
b76481b34c chore: don't assume 1cpi when stepping by a frame 2022-10-21 05:12:30 -03:00
147f6ac9ec Revert "chore: tick on memory access instead of 1cpi"
This reverts commit 7f555095f2.
2022-10-21 05:12:30 -03:00
84273cbdad chore: tick on memory access instead of 1cpi 2022-10-21 05:12:30 -03:00
885f92beeb feat: implement Flash backup cartrige kinds 2022-10-21 05:12:30 -03:00
f12800f2d0 chore: stub more apu I/O addresses 2022-10-21 05:12:30 -03:00
a93b335dea fix: account for subset of disallowed chars in save file names 2022-10-21 05:12:29 -03:00
ad9463dcb9 feat: implement SRAM saving and loading 2022-10-21 05:12:29 -03:00
242627199b chore: properly deallocate OAM buffer 2022-10-21 05:12:29 -03:00
22a8f67d81 fix: speed percentage in title is now accurate
We now properly account for full speed being 59.97Hz not, 59Hz or 60Hz
2022-10-21 05:12:29 -03:00
4ee0eed2e5 chore: make some variables const 2022-10-21 05:12:29 -03:00
20f39176c6 feat: minor performance improvements 2022-10-21 05:12:29 -03:00
ed792d71d3 feat: switch from BGR555 to RGBA8888 2022-10-21 05:12:28 -03:00
c3ae727ed1 fix: improve perf of instructions w/ rotr 2022-10-21 05:12:28 -03:00
0116dcdbe1 fix: improve frame limiting and fps counting 2022-10-21 05:12:28 -03:00
5ecbcc9f33 fix: implement proper SRAM mirroring and stub Flash 2022-10-21 05:12:28 -03:00
2d16e4a4e6 chore: move DMA and Timers from io to bus 2022-10-21 05:12:28 -03:00
bb9dc45e0c feat: define APU registers 2022-10-21 05:12:27 -03:00
9dcecc0d58 fix: move code in scheduler to ppu 2022-10-21 05:12:27 -03:00
92e63f5dd6 chore: create different types of emuloops 2022-10-21 05:12:27 -03:00
74cda6a1d0 fix: resolve relative sprite priority issues 2022-10-21 05:12:27 -03:00
d7354cca33 chore: improve accuracy of frame limiter 2022-10-21 05:12:27 -03:00
7684cf0f4a chore: improve accuracy of thread sleep in emu thread 2022-10-21 05:12:27 -03:00
3994c2c7c8 feat: implement video sync 2022-10-21 05:12:26 -03:00
a768d28e7c chore: organize io switch statements 2022-10-21 05:12:26 -03:00
efd99f16e0 fix: mirror SRAM
SRAM is mirrored in 64K chunks
TODO: According to GBATEK SRAM chips are 32K and mirrored
2022-10-21 05:12:26 -03:00
61d6288fec 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-10-21 05:12:26 -03:00
e3e45cd129 feat: implement Timers 2022-10-21 05:12:26 -03:00
15191aedca fix: implement sprite coord overflow behaviour 2022-10-21 05:12:26 -03:00
63486f13f2 fix: resolve issues with sprite mirroring 2022-10-21 05:12:25 -03:00
17455e40d1 feat: Implement MVP of Mode 0 Sprites 2022-10-21 05:12:25 -03:00
d54e593276 chore: clean up io 2022-10-21 05:12:25 -03:00
c6a544a824 feat: fix tile flipping issue 2022-10-21 05:12:25 -03:00
e3ae3635bc chore: add some type definitions for sprites 2022-10-21 05:12:25 -03:00
6723bfb364 feat: improve DMA Transfer support 2022-10-21 05:12:25 -03:00
12c628e82c chore(ppu): resolve integer overflow regression 2022-10-21 05:12:24 -03:00
5f9abf69d3 feat(ppu): implement bg priority and transparency 2022-10-21 05:12:24 -03:00
351a687a2d chore: update README.md 2022-10-21 05:12:24 -03:00
68b0601a42 chore: replace unnecessarily complex sign extension implementation 2022-10-21 05:12:24 -03:00
9455ffe837 feat: pass beeg yoshi 2022-10-21 05:12:24 -03:00
02d2ff3e0c fix: palette id is a u16 not a u8 2022-10-21 05:12:24 -03:00
02572dd15c feat: DMA Transfer MVP 2022-10-21 05:12:23 -03:00
6d253cc74e feat(ppu): implement transparency + backdrop in mode 0 2022-10-21 05:12:23 -03:00
4885a86833 chore(io): replace some bitfields with enums 2022-10-21 05:12:23 -03:00
fd9ffb20b4 fix: better emulate behaviour of IO reads 2022-10-21 05:12:23 -03:00
56ef2b077a chore: document select unimplmented I/O registers
These registers are written to / read from Kirby: Nightmare in Dream Land
2022-10-21 05:12:23 -03:00
d4c7cfdf8b feat: impelement a barebones SRAM 2022-10-21 05:12:23 -03:00
4d75d156e5 feat: pass retAddr.gba 2022-10-21 05:12:22 -03:00
d30a4d7ee5 feat: implement Hblank and Vcount Interrupts
Also implemented unique behaviour when writing to IF
2022-10-21 05:12:22 -03:00
d1fce8ba75 chore: improve Bus log + panic messages 2022-10-21 05:12:22 -03:00
0f74e4fcf9 chore: improve io.zig 2022-10-21 05:12:22 -03:00
4afcbd0957 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-10-21 05:12:22 -03:00
50dc31447d fix: resolve integer overflow in BG0 Drawing 2022-10-21 05:12:22 -03:00
acf1a10f91 chore: don't panic on 32-bit I/O 2022-10-21 05:12:21 -03:00
606f9b959a chore: stub CPU HALTing 2022-10-21 05:12:21 -03:00
f19b42baf3 chore: correct logic errors in map size 1 and 3 2022-10-21 05:12:21 -03:00
2a3d0c8b0d chore: properly write to VOFS and HOFS in 32-bit bus 2022-10-21 05:12:21 -03:00
4405fa6bbf feat: implement hofs and vofs on io bus 2022-10-21 05:12:21 -03:00
e2b2bf882a feat: implement scrolling 2022-10-21 05:12:21 -03:00
08e4eb1bf1 feat: add support for multiple BGs in Mode 0 2022-10-21 05:12:20 -03:00
f4b176a813 feat: document mode 0 2022-10-21 05:12:20 -03:00
efc7d817db feat: Mode 0 MVP 2022-10-21 05:12:20 -03:00
1b17b1eb0c chore: use zig slices for fun 2022-10-21 05:12:20 -03:00
fab6d4c2a2 chore: give DISPCNT DISPSTAT and VCOUNT to PPU struct 2022-10-21 05:12:20 -03:00
223a3403c0 chore: give io read/write functions access to the entire Bus 2022-10-21 05:12:20 -03:00
dfd0d064de feat: implement BG Scrolling Registers 2022-10-21 05:12:19 -03:00
0c4882e658 feat: impelemnt BG0,1,2CNT and IF 2022-10-21 05:12:19 -03:00
bfdad9fa32 feat: implement OAM 2022-10-21 05:12:19 -03:00
37fd8dab84 chore: squash bugs preventing swi_demo.gba from working 2022-10-21 05:12:19 -03:00
c143aefb01 chore(cpu): reimplement bank switching logic 2022-10-21 05:12:19 -03:00
05bf245b5a fix: don't mask away MSB in THUMB.5 add 2022-10-21 05:12:19 -03:00
f9e7128061 fix: properly decode format 11 instructions 2022-10-21 05:12:18 -03:00
603e4b6fdf chore: make use of scoped logging 2022-10-21 05:12:18 -03:00
9ed37340cc Merge branch 'main' of ssh://musuka.dev:2222/paoda/zba 2022-10-21 05:12:18 -03:00
599a1f2973 chore: remove TODOs and some useless imports 2022-10-21 05:12:18 -03:00
22424ca69c fix: improper condition check and initialization of register 2022-10-21 05:12:18 -03:00
67a785cc22 fix(cpu): force align thumb and arm block data transfers 2022-10-21 05:12:18 -03:00
4a4663607e chore: update README 2022-10-21 05:12:17 -03:00
4eb3842606 feat: pass arm.gba 2022-10-21 05:12:17 -03:00
1ee8b51b2b chore: reimplement ARM LDM/STM 2022-10-21 05:12:17 -03:00
130310e5cc chore: improve arm ldm/stm 2022-10-21 05:12:17 -03:00
e933d7e5c7 fix(cpu): force-align SWP reads and writes 2022-10-21 05:12:17 -03:00
44e8b5b882 fix: force-align ARM STRH reads 2022-10-21 05:12:17 -03:00
28361e8b7d fix: implement the same LDRSH logic as THUMB LDRSH 2022-10-21 05:12:16 -03:00
75921d6413 fix: PC is 12 ahead when it is rd in str and strb 2022-10-21 05:12:16 -03:00
17226d8f75 fix: listen to my past self
By deleting this line I go from test 234 to test 355 in arm.gba
2022-10-21 05:12:16 -03:00
0166999446 chore: update SDL.zig 2022-10-21 05:12:16 -03:00
2cb1bf834a chore: dont use std.mem.bytesToValue
the stdlib accounts for endianness, which isn't something we want.
2022-10-21 05:12:16 -03:00
8006ca31e6 chore: remove unnecessary @as calls 2022-10-21 05:12:16 -03:00
82b92b4733 feat: pass thumb.gba 2022-10-21 05:12:15 -03:00
bf42d3ae2f chore: account for empty rlist in THUMB LDM/STM 2022-10-21 05:12:15 -03:00
f63ae76931 fix(cpu): handle edge case in LDRSH 2022-10-21 05:12:15 -03:00
feded4ac25 chore: specify which compiler this project is built with 2022-10-21 05:12:15 -03:00
f046787523 chore: reorganize util.zig 2022-10-21 05:12:15 -03:00
bbd73550e8 fix: zero initialize all allocated memory 2022-10-21 05:12:15 -03:00
fce560dd89 chore: don't commit *.sh files 2022-10-21 05:12:14 -03:00
4776dc0788 Revert "fix: allow for 32-bit reads to KEYINPUT"
This reverts commit 3a51707280.
2022-10-21 05:12:14 -03:00
d8bd6da563 fix: resolve decoding mixup in THUMB format 8 instructions 2022-10-21 05:12:14 -03:00
b569a32170 fix: allow for 32-bit reads to KEYINPUT 2022-10-21 05:12:14 -03:00
06e20666bd chore: refactor ARMv4 decoding 2022-10-21 05:12:14 -03:00
36687c5c67 chore: add more debug information to CPU panic method 2022-10-21 05:12:14 -03:00
dd9b20030a chore: give more descriptive panic messages when changing mode fails 2022-10-21 05:12:13 -03:00
c0db2a987b chore: clean up THUMB instruction decoding 2022-10-21 05:12:13 -03:00
c3ff2ed6c1 feat: parse cartridge header 2022-10-21 05:12:13 -03:00
271f42cf0e feat: rename ARM and THUMB SWI functions 2022-10-21 05:12:13 -03:00
01e15584da chore: group THUMB and select ARM instructions together (same file) 2022-10-21 05:12:13 -03:00
17b91db2ef feat: integrate zig-clap with ZBA 2022-10-21 05:12:13 -03:00
3e786d02ac fix(cpu): properly decode format 7 and 8 2022-10-21 05:12:12 -03:00
c8f2db69df fix(cpu): resolve edge cases in THUMB Format 5 2022-10-21 05:12:12 -03:00
b4e0682801 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-10-21 05:12:12 -03:00
af10c1b076 feat(cpu): implement format 13
While bugs do exist, at this point all THUMB and ARMv4 instructions
have been implemented! Yay!
2022-10-21 05:12:12 -03:00
e6a0eab667 feat(cpu): implement THUMB format 17 2022-10-21 05:12:12 -03:00
523b9d2736 feat(cpu): implement THUMB format11 2022-10-21 05:12:12 -03:00
011d2f2f2a chore: update to latest zig nightly 2022-10-21 05:12:11 -03:00
c37546d273 chore: progress towards passing ldr/str thumb in armwrestler 2022-10-21 05:12:11 -03:00
fbedebb938 fix(cpu): properly negate in NEG 2022-10-21 05:12:11 -03:00
1773a3acc8 fix(cpu): reimplement THUMB offset shifts 2022-10-21 05:12:11 -03:00
058c02150c fix(cpu): op == 0b00 decodes to add in format 5 2022-10-21 05:12:11 -03:00
8d841ead50 fix(cpu): account for overflow in THUMB alu MUL 2022-10-21 05:12:10 -03:00
152dafbdf7 chore: use if-else when decoding THUMB instructions 2022-10-21 05:12:10 -03:00
7dbd2fc556 fix(cpu): account for rn in rlist in block data transfer 2022-10-21 05:12:10 -03:00
85e0924669 feat: implement LDM/STM behaviour when S is set 2022-10-21 05:12:10 -03:00
97919f646d feat(cpu): Pass all LDR/STR ARMwrestler tests 2022-10-21 05:12:10 -03:00
696cc1b359 feat(cpu): decode and implement all necessary ARM CPU instructions 2022-10-21 05:12:10 -03:00
151de2eab4 feat(cpu): implement ARM SWP and SWPB 2022-10-21 05:12:10 -03:00
e7f6464564 fix: resolve off by n * 2 when accessing Palette during BG Mode 4 2022-10-21 05:12:09 -03:00
da681c946e feat(cpu): Implement Multiply Long ARM instructions 2022-10-21 05:12:09 -03:00
e0e43eece5 fix: no buttons are pressed by default 2022-10-21 05:12:09 -03:00
7013389288 feat(cpu): implement format 18 THUMB instructions 2022-10-21 05:12:09 -03:00
443520ecae chore: more detailed panic message 2022-10-21 05:12:09 -03:00
96d7285111 feat(cpu): implement format 10 THUMB instructions 2022-10-21 05:12:08 -03:00
7e6fc44191 feat(cpu): implement SWP 2022-10-21 05:12:08 -03:00
9cb4ebaa7f fix(cpu): perform MUL with u64s, throw away upper 32 bits 2022-10-21 05:12:08 -03:00
3e4d7e7ed8 feat: implement keyboard input 2022-10-21 05:12:08 -03:00
3a6951d93d chore: don't panic on unsupported BG mode 2022-10-21 05:12:08 -03:00
391096872e chore: tempoarily disable fps counter 2022-10-21 05:12:08 -03:00
eebf6fcae4 chore: zero-initialize VRAM 2022-10-21 05:12:07 -03:00
8b7223cf35 chore: stub KeyInput I/O register 2022-10-21 05:12:07 -03:00
e1fec48a0e fix(cpu): properly decode multiply instructions 2022-10-21 05:12:07 -03:00
0778ee8dd7 feat(cpu): implement ARM multiply instructions 2022-10-21 05:12:07 -03:00
14d5160674 fix: allow 32-bit writes to DISPCNT 2022-10-21 05:12:07 -03:00
eabf787305 fix(cpu): properly decode ldm stm thumb instructions 2022-10-21 05:12:07 -03:00
980e4ff5dd fix(cpu): properly decode THUMB PUSH and POP at comptime 2022-10-21 05:12:06 -03:00
1ac193c506 fix(cpu): don't ignore 11th bit of THUMB BL offset 2022-10-21 05:12:06 -03:00
d6ed071bc6 feat(cpu): implement thumb push / pop and stub format 13 thumb instrs 2022-10-21 05:12:06 -03:00
a3d53d40fb feat(cpu): implement THUMB format 9 loads / stores 2022-10-21 05:12:06 -03:00
a17dfbe41f fix(cpu): resolve issues with unexpected PC value in THUMB 2022-10-21 05:12:06 -03:00
b9a81baa47 feat(cpu): implement THUMB ldmia stmia 2022-10-21 05:12:06 -03:00
97b236225e chore: implement THUMB format 4 instructions 2022-10-21 05:12:05 -03:00
8113146b86 chore: dedup code in THUMB instructions 2022-10-21 05:12:05 -03:00
e6625113db chore: refactor and genericize ARM data processing calculations 2022-10-21 05:12:05 -03:00
2643504eb5 chore: relocate barrel_shifter zig file 2022-10-21 05:12:05 -03:00
f7518d1bab feat(cpu): implement format2 THUMB instructions 2022-10-21 05:12:05 -03:00
800ca798cd feat(cpu): implement format19 THUMB instructions 2022-10-21 05:12:05 -03:00
d714ffb4f9 chore: account for THUMB BL instruction when mimicking mGBA logs 2022-10-21 05:12:04 -03:00
7bc186a03c feat(cpu): implement format16 THUMB instructions 2022-10-21 05:12:04 -03:00
b94b87d186 feat(cpu): implement format 1 THUMB instructions 2022-10-21 05:12:04 -03:00
0289be60ef fix: dont close file handle early 2022-10-21 05:12:04 -03:00
93922b65e3 feat(cpu): implement format 6 THUMB instructions 2022-10-21 05:12:04 -03:00
c6608748c6 chore: rename title 2022-10-21 05:12:04 -03:00
8e383d55d7 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-10-21 05:12:03 -03:00
8926026b5b chore: move a single statement lol 2022-10-21 05:12:03 -03:00
464479b986 chore: mark indexing methods as inline 2022-10-21 05:12:03 -03:00
de1c84914c feat: create emulator thread 2022-10-21 05:12:03 -03:00
8e7766e694 chore: disable logging by default 2022-10-21 05:12:03 -03:00
4858dbc5dc chore: revert fastboot changes 2022-10-21 05:12:03 -03:00
c4e131b92d chore: binary logging + file logging + DP chanes + fastBoot changes 2022-10-21 05:12:02 -03:00
90e0d9139c chore: ignore .bin files 2022-10-21 05:12:02 -03:00
2e54be76d6 chore: rename skipBios to fastBoot 2022-10-21 05:12:02 -03:00
7914268702 chore: set correct values for select banked registers on fast boot 2022-10-21 05:12:02 -03:00
4bdb85834c feat(cpu): implement SWI 2022-10-21 05:12:02 -03:00
f89a37936f chore(bios): allow reading from BIOS 2022-10-21 05:12:02 -03:00
8bb7ea6be6 fix(cpu): interim solution to weird program counter behaviour on illegal tst instruction 2022-10-21 05:12:01 -03:00
60a1f7fa99 chore(cpu): implement behaviour for undefined test instruction 2022-10-21 05:12:01 -03:00
b3b8182f85 fix(cpu): fix PC offset when barrel shifter and bit 4 of DP is set 2022-10-21 05:12:01 -03:00
19094b492e chore: remove reccomended extension 2022-10-21 05:12:01 -03:00
56e660714c fix(cpu): implement S set + rd == 15 case for data processing 2022-10-21 05:12:01 -03:00
eb632056a2 feat(cpu): implement banked registers 2022-10-21 05:12:01 -03:00
fbc9de0335 fix(cpu): improve MRS and MSR instructions 2022-10-21 05:12:01 -03:00
5c7539cd26 feat(cpu): implement CMN 2022-10-21 05:12:00 -03:00
7c20e5fdb5 fix(barrel_shifter): fix PC being 1 word ahead in barrel shifter 2022-10-21 05:12:00 -03:00
f79e7126ee feat(cpu): Implement RSC 2022-10-21 05:12:00 -03:00
15e92bc6af feat(cpu): implement RSB 2022-10-21 05:12:00 -03:00
47fc96fe00 feat(cpu): implement BIC 2022-10-21 05:12:00 -03:00
4ac5ad42c6 feat(cpu): implement EOR 2022-10-21 05:12:00 -03:00
c93153672f feat(cpu): implement ADD 2022-10-21 05:11:59 -03:00
a46dd448f4 feat(cpu): implement fix for ADC and implement SBC 2022-10-21 05:11:59 -03:00
01f75112ce chore(barrel_shifter): remove panic from ASR 2022-10-21 05:11:59 -03:00
051b98bc02 fix(barrel_shifter): should not modify cpsr when amount == 0 2022-10-21 05:11:59 -03:00
5d0bc1b335 chore(cpu): refactor the barrel shifter once again 2022-10-21 05:11:59 -03:00
43d011538e feat(cpu): implement ADC
ADC interacting w/ the Barrel Shifter is not working though
2022-10-21 05:11:59 -03:00
f3ad5e90ff feat(cpu): implement RRX for Barrel Shifter 2022-10-21 05:11:58 -03:00
9b867c02e0 feat(cpu): implement SUB in THUMB format 3 2022-10-21 05:11:58 -03:00
2ba09868ba feat(cpu): implement ARM SUB in data processing 2022-10-21 05:11:58 -03:00
9394754593 feat(cpu): implement MVN 2022-10-21 05:11:58 -03:00
41bab3d6ba chore(cpu): refactor barrel shifter 2022-10-21 05:11:58 -03:00
99b686b2d7 fix(cpu): use barrel shifter in data processing immediates 2022-10-21 05:11:58 -03:00
daad98bbfe feat(cpu): implement format 12 thumb instructions 2022-10-21 05:11:57 -03:00
2fb01577af feat(cpu): implement some already decoded format 3 instructions 2022-10-21 05:11:57 -03:00
96d21f27a5 feat(cpu): implement THUMB format 5 instructions 2022-10-21 05:11:57 -03:00
793110f315 chore: mgba log now supports printing THUMB instructions 2022-10-21 05:11:57 -03:00
5ed5c5d52d feat(cpu): implement like 1 THUMB instruction 2022-10-21 05:11:57 -03:00
01385ee46b chore: distinguish between undefined ARM and THUMB instr 2022-10-21 05:11:57 -03:00
0eba3aca1f chore(cpu): lay groundwork for THUMB instruction decoding and execution 2022-10-21 05:11:57 -03:00
83a5370196 chore(cpu): refactor ARM functions to make room for THUMB 2022-10-21 05:11:56 -03:00
b0b6247f06 fix(cpu): fix conditions for GT cond 2022-10-21 05:11:56 -03:00
2a33716166 fix(cpu): fix imm value calculation in MSR 2022-10-21 05:11:56 -03:00
9b26454c72 fix(cpu): resolve off-by-one error when executing LDM 2022-10-21 05:11:56 -03:00
97b933d9ea feat(cpu): implement branch and exchange
If I want to continue with armwrestler, I'll have to implement
THUMB instructions now
2022-10-21 05:11:56 -03:00
ff70aadfdb fix(cpu): make Data Processing instructions r15-aware 2022-10-21 05:11:55 -03:00
ae53f92d40 fix(cpu): make LDRH and STRH aware of r15 2022-10-21 05:11:55 -03:00
f51e1d3154 fix(cpu): account for r15 in LDR and STR instructions 2022-10-21 05:11:55 -03:00
a21f94569f fix(cpu): flip two branches in PSR Transfer execution 2022-10-21 05:11:55 -03:00
b9255bffe7 feat(cpu): implement MSR and MRS 2022-10-21 05:11:55 -03:00
e1f8400343 feat(cpu): stub PSR Transfer instructions 2022-10-21 05:11:55 -03:00
52493831cc chore(io): implement IE and IME 2022-10-21 05:11:54 -03:00
00ba7afac4 chore: remove some magic constants 2022-10-21 05:11:54 -03:00
c703352ac3 Merge branch 'main' of ssh://musuka.dev:2222/paoda/zba 2022-10-21 05:11:54 -03:00
97e663febf fix(bus): remove accidental recursion 2022-10-21 05:11:54 -03:00
3891b54f42 chore: ignores for building on windows 2022-10-21 05:11:54 -03:00
9a5959e46c fix(cpu): write results of ORR to destination register 2022-10-21 05:11:54 -03:00
780c717409 feat(cpu): implement TEQ 2022-10-21 05:11:53 -03:00
34c6df344d feat(cpu): Implement ORR 2022-10-21 05:11:53 -03:00
670347d4a0 feat(bus): implement IWRAM and EWRAM 2022-10-21 05:11:53 -03:00
e0c4b3b407 fix(ppu): properly access Mode 4 palette 2022-10-21 05:11:53 -03:00
cffffab8ea feat(cpu): refactor LDM/STM 2022-10-21 05:11:53 -03:00
527bd2889e feat(cpu): implement LDM/STM 2022-10-21 05:11:53 -03:00
15dc4ce03a chore(io): DISPSTAT bits 3 and 4 better match GBATEK documentation 2022-10-21 05:11:52 -03:00
4f629227ab fix(cpu): fix off-by-word bug in BL 2022-10-21 05:11:52 -03:00
caa799853e feat(bus): have VCOUNT be addressable on the bus 2022-10-21 05:11:52 -03:00
3590215d33 feat(ppu): implement Mode 4
Implementation is not tested. Pending on LDM and STM so that I can
run beeg.gba
2022-10-21 05:11:52 -03:00
d6aafc61bd chore(io): rename some io bitfield fields 2022-10-21 05:11:52 -03:00
357211a4cc chore: remove premature inlines 2022-10-21 05:11:52 -03:00
c4c4332485 chore: add FPS counter 2022-10-21 05:11:51 -03:00
2cec8d9f70 chore: improve code clarity 2022-10-21 05:11:51 -03:00
8348d7c4bc feat(ppu): improve timings + implement BG mode 3 bitmap 2022-10-21 05:11:51 -03:00
c33068d831 fix: allocate framebuf on heap 2022-10-21 05:11:51 -03:00
f28b963f9d chore: add code for heap alloc of white texture 2022-10-21 05:11:51 -03:00
7fc77f993f chore(gui): switch from RGBA8888 to BGR5555 to match BG Mode 3 2022-10-21 05:11:51 -03:00
0e0b21d8c3 feat: draw white texture using SDL2 2022-10-21 05:11:50 -03:00
f3ad0eb3aa fix(ppu): deallocate palette RAM on cleanup 2022-10-21 05:11:50 -03:00
036b861b05 chore: code cleanup 2022-10-21 05:11:50 -03:00
880546468c chore(bus): refactor bus.zig 2022-10-21 05:11:50 -03:00
1a9c9ba4cb chore: refactor instruction exec code 2022-10-21 05:11:50 -03:00
a9e7140a88 chore(io): alias @This() to Self in io.zig 2022-10-21 05:11:50 -03:00
808633deb7 chore: refactor bios.zig and pak.zig 2022-10-21 05:11:49 -03:00
ee4fcd926b fix: by convention deinit() should not take pointers to self 2022-10-21 05:11:49 -03:00
9d1229fe0c feat: implement PPU Timings in Scheduler 2022-10-21 05:11:49 -03:00
d54c8df7b3 feat(sched): add HBlank and VBlank events to the scheduler 2022-10-21 05:11:49 -03:00
d495f5b4c5 feat: implement S (when rd != 15) for several data processing instructions 2022-10-21 05:11:49 -03:00
788bef188d feat: implement dedicated Barrel Shifter SHL and SHR 2022-10-21 05:11:49 -03:00
bff9be03cc chore: stub TST 2022-10-21 05:11:48 -03:00
44424e0687 chore: comment-out logging by default 2022-10-21 05:11:48 -03:00
4b43dcd256 fix(cpu): improve LDR/STR write-back logic 2022-10-21 05:11:48 -03:00
47805fb60c feat(bus): implement Palette RAM and DISPSTAT 2022-10-21 05:11:48 -03:00
abe2fc431e fix(bus): restrict Game ROM and VRAM to a 16-bit bus 2022-10-21 05:11:48 -03:00
46c694d95a fix(cpu): properly implement SUB/CMP CSPSR carry bit condition 2022-10-21 05:11:48 -03:00
faced77161 fix(cpu): resolve reversed if statement + write back on W = 0 2022-10-21 05:11:47 -03:00
32f0b9d71c chore: add mgba compatible (minus disasm) log function 2022-10-21 05:11:47 -03:00
bbdcc0a8c2 chore: rename CPSR u32 from val to raw 2022-10-21 05:11:47 -03:00
67bf975034 chore: remove print statements 2022-10-21 05:11:47 -03:00
da7300a78c chore: remove all memory leaks 2022-10-21 05:11:47 -03:00
5c5179a553 feat(ppu): implement VRAM 2022-10-21 05:11:47 -03:00
dcf78d0f76 fix(emu): prevent infinite loop when advancing scheduler 2022-10-21 05:11:46 -03:00
4836ea3bcf fix(io): fix DISPCNT is at wrong IO address 2022-10-21 05:11:46 -03:00
182392bf1c feat(cpu): properly implement STR STRH and STRB 2022-10-21 05:11:46 -03:00
c6de540c8a feat(cpu): implement skipBios method 2022-10-21 05:11:46 -03:00
82fb5e7a93 chore: panic on read from BIOS
GBA Bios requires a lot of implemented features, so we're ignoring it
for now
2022-10-21 05:11:46 -03:00
cbcc6282df feat(bus): add Io Struct
Also, add more information to all panic messages
2022-10-21 05:11:46 -03:00
614ac4a262 chore: rename consturctors to fit convention 2022-10-21 05:11:45 -03:00
80d49e03d6 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-10-21 05:11:45 -03:00
7016fcdb79 chore: use bitfield library 2022-10-21 05:11:45 -03:00
f030889d6c feat(bus): emu is now able to read from user-provided BIOS 2022-10-21 05:11:45 -03:00
d50aff30c9 feat(bus): implement Gameboy Advance MMIO 2022-10-21 05:11:45 -03:00
5cbfae677a feat: implement ROM CLI argument 2022-10-21 05:11:45 -03:00
d660babecd fix(cpu): purposely overflow when calculating PC during branch 2022-10-21 05:11:44 -03:00
cb06c20864 feat(cpu): implement condition field behaviour 2022-10-21 05:11:44 -03:00
c98e8d384a chore: conform to zig style guides 2022-10-21 05:11:44 -03:00
446eeba094 chore: run zig fmt 2022-10-21 05:11:44 -03:00
e841bf44ca chore(cpu): iron out some false assumptions 2022-10-21 05:11:44 -03:00
1991bd8525 feat: implement LDR STR 2022-10-21 05:11:44 -03:00
6c6d7d463d chore: run zig fmt 2022-10-21 05:11:43 -03:00
acdf13cb7b chore: add reccomended vscode extensions 2022-10-21 05:11:43 -03:00
71 changed files with 5811 additions and 5904 deletions

View File

@@ -1,59 +0,0 @@
name: Nightly
on:
push:
paths:
- "**.zig"
- "dl_sdl2.ps1"
branches:
- main
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest] # TODO: Figure out Apple Silicon macOS
runs-on: ${{matrix.os}}
steps:
- uses: goto-bus-stop/setup-zig@v2
with:
version: 0.13.0
- run: |
git config --global core.autocrlf false
- uses: actions/checkout@v3
with:
submodules: recursive
- name: prepare-linux
if: runner.os == 'Linux'
run: |
sudo apt update
sudo apt install libgtk-3-dev libsdl2-dev
- name: prepare-windows
if: runner.os == 'Windows'
run: |
.\dl_sdl2.ps1
- name: prepare-macos
if: runner.os == 'macOS'
run: |
brew install sdl2
- name: build
run: zig build -Doptimize=ReleaseSafe -Dcpu=baseline
- name: upload
uses: actions/upload-artifact@v3
with:
name: zba-${{matrix.os}}
path: zig-out
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: goto-bus-stop/setup-zig@v2
with:
version: 0.13.0
- run: zig fmt --check {src,lib}/**/*.zig build.zig build.zig.zon

5
.gitignore vendored
View File

@@ -1,7 +1,6 @@
/.vscode /.vscode
/bin /bin
**/zig-cache **/zig-cache
**/.zig-cache
**/zig-out **/zig-out
/docs /docs
**/*.log **/*.log
@@ -13,7 +12,3 @@
# Any Custom Scripts for Debugging purposes # Any Custom Scripts for Debugging purposes
*.sh *.sh
# Dear ImGui
**/imgui.ini

14
.gitmodules vendored
View File

@@ -1,3 +1,15 @@
[submodule "lib/SDL.zig"] [submodule "lib/SDL.zig"]
path = lib/SDL.zig path = lib/SDL.zig
url = https://github.com/paoda/SDL.zig url = https://github.com/MasterQ32/SDL.zig
[submodule "lib/zig-clap"]
path = lib/zig-clap
url = https://github.com/Hejsil/zig-clap
[submodule "lib/known-folders"]
path = lib/known-folders
url = https://github.com/ziglibs/known-folders
[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

8
.vscode/extensions.json vendored Normal file
View File

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

130
README.md
View File

@@ -1,96 +1,86 @@
# ZBA (working title) # ZBA (working title)
A Game Boy Advance Emulator written in Zig ⚡! A Game Boy Advance Emulator written in Zig ⚡!
![ZBA running リズム天国](assets/screenshot.png)
## Scope ## 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).
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:
This is a simple (read: incomplete) for-fun long-term project. I hope to get "mostly there", which to me means that I'm not missing any major hardware features and the set of possible improvements would be in memory timing or in UI/UX. With respect to that goal, here's what's outstanding:
### TODO ### TODO
- [ ] Affine Sprites
- [x] Affine Sprites
- [ ] Windowing (see [this branch](https://git.musuka.dev/paoda/zba/src/branch/window)) - [ ] Windowing (see [this branch](https://git.musuka.dev/paoda/zba/src/branch/window))
- [ ] Audio Resampler (Having issues with SDL2's) - [ ] Audio Resampler (Having issues with SDL2's)
- [ ] Immediate Mode GUI
- [ ] Refactoring for easy-ish perf boosts - [ ] Refactoring for easy-ish perf boosts
## Usage ## Tests
- [x] [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`
- [ ] [DenSinH's GBA ROMs](https://github.com/DenSinH/GBARoms)
- [x] `eeprom-test` and `flash-test`
- [x] `midikey2freq`
- [ ] `swi-tests-random`
- [ ] [destoer's GBA Tests](https://github.com/destoer/gba_tests)
- [x] `cond_invalid.gba`
- [x] `dma_priority.gba`
- [x] `hello_world.gba`
- [x] `if_ack.gba`
- [ ] `line_timing.gba`
- [ ] `lyc_midline.gba`
- [ ] `window_midframe.gba`
- [x] [ladystarbreeze's GBA Test Collection](https://github.com/ladystarbreeze/GBA-Test-Collection)
- [x] `retAddr.gba`
- [x] `helloWorld.gba`
- [x] `helloAudio.gba`
- [x] [`armwrestler-gba-fixed.gba`](https://github.com/destoer/armwrestler-gba-fixed)
- [x] [FuzzARM](https://github.com/DenSinH/FuzzARM)
ZBA supports both a CLI and a GUI. If running from the terminal, try using `zba --help` to see what you can do. If you want to use the GUI, feel free to just run `zba` without any arguments. ## Resources
* [GBATEK](https://problemkaputt.de/gbatek.htm)
ZBA does not feature any BIOS HLE, so providing one will be necessary if a ROM makes use of it. Need one? Why not try using the open-source [Cult-Of-GBA BIOS](https://github.com/Cult-of-GBA/BIOS) written by [fleroviux](https://github.com/fleroviux) and [DenSinH](https://github.com/DenSinH)? * [TONC](https://coranac.com/tonc/text/toc.htm)
* [ARM Architecture Reference Manual](https://www.intel.com/content/dam/www/programmable/us/en/pdfs/literature/third-party/ddi0100e_arm_arm.pdf)
Finally it's worth noting that ZBA uses a TOML config file it'll store in your OS's data directory. See `example.toml` to learn about the defaults and what exactly you can mess around with. * [ARM7TDMI Data Sheet](https://www.dca.fee.unicamp.br/cursos/EA871/references/ARM/ARM7TDMIDataSheet.pdf)
## Compiling ## Compiling
Most recently built on Zig [0.10.0-dev.4474+b41b35f57](https://github.com/ziglang/zig/tree/b41b35f57)
Most recently built on Zig [v0.11.0](https://github.com/ziglang/zig/tree/0.11.0)
### Dependencies ### Dependencies
* [SDL.zig](https://github.com/MasterQ32/SDL.zig)
* [SDL2](https://www.libsdl.org/download-2.0.php)
* [zig-clap](https://github.com/Hejsil/zig-clap)
* [known-folders](https://github.com/ziglibs/known-folders)
* [zig-toml](https://github.com/aeronavery/zig-toml)
* [zig-datetime](https://github.com/frmdstryr/zig-datetime)
* [`bitfields.zig`](https://github.com/FlorenceOS/Florence/blob/aaa5a9e568/lib/util/bitfields.zig)
Dependency | Source `bitfields.zig` from [FlorenceOS](https://github.com/FlorenceOS) is included under `lib/util/bitfield.zig`.
--- | ---
known-folders | <https://github.com/ziglibs/known-folders>
nfd-zig | <https://github.com/fabioarnold/nfd-zig>
SDL.zig | <https://github.com/MasterQ32/SDL.zig>
tomlz | <https://github.com/mattyhall/tomlz>
zba-gdbstub | <https://github.com/paoda/zba-gdbstub>
zba-util | <https://git.musuka.dev/paoda/zba-util>
zgui | <https://github.com/michal-z/zig-gamedev/tree/main/libs/zgui>
zig-clap | <https://github.com/Hejsil/zig-clap>
zig-datetime | <https://github.com/frmdstryr/zig-datetime>
`bitfield.zig` | [https://github.com/FlorenceOS/Florence](https://github.com/FlorenceOS/Florence/blob/aaa5a9e568/lib/util/bitfields.zig)
`gl.zig` | <https://github.com/MasterQ32/zig-opengl>
Use `git submodule update --init` from the project root to pull the git relevant git submodules Use `git submodule update --init` from the project root to pull the git submodules `SDL.zig`, `zig-clap`, `known-folders`, `zig-toml` and `zig-datetime`
Be sure to provide SDL2 using: Be sure to provide SDL2 using:
* Linux: Your distro's package manager
- Linux: Your distro's package manager * MacOS: ¯\\\_(ツ)_/¯
- macOS: ¯\\\_(ツ)_/¯ (try [this formula](https://formulae.brew.sh/formula/sdl2)?) * Windows: [`vcpkg`](https://github.com/Microsoft/vcpkg) (install `sdl2:x64-windows`)
- Windows: [`vcpkg`](https://github.com/Microsoft/vcpkg) (install `sdl2:x64-windows`)
`SDL.zig` will provide a helpful compile error if the zig compiler is unable to find SDL2. `SDL.zig` will provide a helpful compile error if the zig compiler is unable to find SDL2.
Once you've got all the dependencies, execute `zig build -Doptimize=ReleaseSafe`. The executable will be under `zig-out/bin` and the shared libraries (if enabled) under `zig-out/lib`. If working with shared libraries on windows, be sure to add all artifacts to the same directory. On Unix, you'll want to make use of `LD_PRELOAD`. Once you've got all the dependencies, execute `zig build -Drelease-fast`. The executable is located at `zig-out/bin/`.
## Controls ## Controls
Key | Button
Key | Button | | Key | Button --- | ---
--- | --- | --- | --- | --- <kbd>X</kbd> | A
<kbd>A</kbd> | L | | <kbd>S</kbd> | R <kbd>Z</kbd> | B
<kbd>X</kbd> | A | | <kbd>Z</kbd> | B <kbd>A</kbd> | L
<kbd>Return</kbd> | Start | | <kbd>RShift</kbd> | Select <kbd>S</kbd> | R
<kbd>Return</kbd> | Start
<kbd>RShift</kbd> | Select
Arrow Keys | D-Pad Arrow Keys | D-Pad
## Tests
GBA Tests | [jsmolka](https://github.com/jsmolka/) | gba_tests | [destoer](https://github.com/destoer/)
--- | --- | --- | ---
`arm.gba`, `thumb.gba` | PASS | `cond_invalid.gba` | PASS
`memory.gba`, `bios.gba` | PASS | `dma_priority.gba` | PASS
`flash64.gba`, `flash128.gba` | PASS | `hello_world.gba` | PASS
`sram.gba` | PASS | `if_ack.gba` | PASS
`none.gba` | PASS | `line_timing.gba` | FAIL
`hello.gba`, `shades.gba`, `stripes.gba` | PASS | `lyc_midline.gba` | FAIL
`nes.gba` | PASS | `window_midframe.gba` | FAIL
GBARoms | [DenSinH](https://github.com/DenSinH/) | GBA Test Collection | [ladystarbreeze](https://github.com/ladystarbreeze)
--- | --- | --- | ---
`eeprom-test`, `flash-test` | PASS | `retAddr.gba` | PASS
`midikey2freq` | PASS | `helloWorld.gba` | PASS
`swi-tests-random` | FAIL | `helloAudio.gba` | PASS
FuzzARM | [DenSinH](https://github.com/DenSinH/) | arm7wrestler GBA Fixed | [destoer](https://github.com/destoer)
--- | --- | --- | ---
`main.gba` | PASS | `armwrestler-gba-fixed.gba` | PASS
## Resources
- [GBATEK](https://problemkaputt.de/gbatek.htm)
- [TONC](https://coranac.com/tonc/text/toc.htm)
- [ARM Architecture Reference Manual](https://www.intel.com/content/dam/www/programmable/us/en/pdfs/literature/third-party/ddi0100e_arm_arm.pdf)
- [ARM7TDMI Data Sheet](https://www.dca.fee.unicamp.br/cursos/EA871/references/ARM/ARM7TDMIDataSheet.pdf)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -1,65 +1,60 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const Sdk = @import("lib/SDL.zig/Sdk.zig");
const sdl = @import("lib/SDL.zig/build.zig");
const SemVer = std.SemanticVersion;
const target_version = "0.13.0";
pub fn build(b: *std.Build) void {
const actual_version = builtin.zig_version;
if (comptime actual_version.order(SemVer.parse(target_version) catch unreachable) != .eq) {
@compileError("ZBA must be built with Zig v" ++ target_version ++ ".");
}
pub fn build(b: *std.build.Builder) void {
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
// for restricting supported target set are available.
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{ // Standard release options allow the person running `zig build` to select
.name = "zba", // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
.root_source_file = b.path("src/main.zig"), const mode = b.standardReleaseOptions();
.target = target,
.optimize = optimize,
});
const sdk = sdl.init(b, null, null); const exe = b.addExecutable("zba", "src/main.zig");
const zgui = b.dependency("zgui", .{ .shared = false, .with_implot = true, .backend = .sdl2_opengl3 }); exe.setMainPkgPath("."); // Necessary so that src/main.zig can embed example.toml
const imgui = zgui.artifact("imgui"); exe.setTarget(target);
exe.root_module.addImport("known_folders", b.dependency("known-folders", .{}).module("known-folders")); // https://github.com/ziglibs/known-folders // Known Folders (%APPDATA%, XDG, etc.)
exe.root_module.addImport("datetime", b.dependency("zig-datetime", .{}).module("zig-datetime")); // https://github.com/frmdstryr/zig-datetime exe.addPackagePath("known_folders", "lib/known-folders/known-folders.zig");
exe.root_module.addImport("clap", b.dependency("zig-clap", .{}).module("clap")); // https://github.com/Hejsil/zig-clap
exe.root_module.addImport("zba-util", b.dependency("zba-util", .{}).module("zba-util")); // https://git.musuka.dev/paoda/zba-util
exe.root_module.addImport("tomlz", b.dependency("tomlz", .{}).module("tomlz")); // https://github.com/mattyhall/tomlz
exe.root_module.addImport("arm32", b.dependency("arm32", .{}).module("arm32")); // https://git.musuka.dev/paoda/arm32
exe.root_module.addImport("gdbstub", b.dependency("zba-gdbstub", .{}).module("zba-gdbstub")); // https://git.musuka.dev/paoda/gdbstub
exe.root_module.addImport("nfd", b.dependency("nfd", .{}).module("nfd")); // https://github.com/fabioarnold/nfd-zig
exe.root_module.addImport("zgui", zgui.module("root")); // https://git.musuka.dev/paoda/zgui
exe.root_module.addImport("sdl2", sdk.getNativeModule()); // https://github.com/MasterQ32/SDL.zig
exe.root_module.addAnonymousImport("bitfield", .{ .root_source_file = b.path("lib/bitfield.zig") }); // https://github.com/FlorenceOS/ // DateTime Library
exe.root_module.addAnonymousImport("gl", .{ .root_source_file = b.path("lib/gl.zig") }); // https://github.com/MasterQ32/zig-opengl exe.addPackagePath("datetime", "lib/zig-datetime/src/main.zig");
exe.root_module.addAnonymousImport("example.toml", .{ .root_source_file = b.path("example.toml") });
sdk.link(exe, .dynamic, .SDL2); // Bitfield type from FlorenceOS: https://github.com/FlorenceOS/
sdk.link(imgui, .dynamic, .SDL2); // exe.addPackage(.{ .name = "bitfield", .path = .{ .path = "lib/util/bitfield.zig" } });
exe.linkLibrary(imgui); exe.addPackagePath("bitfield", "lib/util/bitfield.zig");
b.installArtifact(exe); // Argument Parsing Library
exe.addPackagePath("clap", "lib/zig-clap/clap.zig");
const run_cmd = b.addRunArtifact(exe); // 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.setBuildMode(mode);
exe.install();
const run_cmd = exe.run();
run_cmd.step.dependOn(b.getInstallStep()); run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| run_cmd.addArgs(args); if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app"); const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step); run_step.dependOn(&run_cmd.step);
const exe_tests = b.addTest(.{ const exe_tests = b.addTest("src/main.zig");
.root_source_file = b.path("src/main.zig"), exe_tests.setTarget(target);
.target = target, exe_tests.setBuildMode(mode);
.optimize = optimize,
});
const test_step = b.step("test", "Run unit tests"); const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&exe_tests.step); test_step.dependOn(&exe_tests.step);

View File

@@ -1,50 +0,0 @@
.{
.name = "zba",
.version = "0.1.0",
.paths = .{
"build.zig",
"build.zig.zon",
"lib/bitfield.zig",
"lib/gl.zig",
"src",
},
.minimum_zig_version = "0.13.0",
.dependencies = .{
.nfd = .{
.url = "git+https://github.com/paoda/nfd-zig#ad81729d33da30d5f4fd23718debec48245121ca",
.hash = "1220a679380847513262c8c5c474d4a415f9ecc4921c8c6aefbdbdce66cf2aa19ceb",
},
.@"known-folders" = .{
.url = "git+https://github.com/ziglibs/known-folders#1cceeb70e77dec941a4178160ff6c8d05a74de6f",
.hash = "12205f5e7505c96573f6fc5144592ec38942fb0a326d692f9cddc0c7dd38f9028f29",
},
.@"zig-datetime" = .{
.url = "git+https://github.com/frmdstryr/zig-datetime#70aebf28fb3e137cd84123a9349d157a74708721",
.hash = "122077215ce36e125a490e59ec1748ffd4f6ba00d4d14f7308978e5360711d72d77f",
},
.@"zig-clap" = .{
.url = "git+https://github.com/Hejsil/zig-clap#c0193e9247335a6c1688b946325060289405de2a",
.hash = "12207ee987ce045596cb992cfb15b0d6d9456e50d4721c3061c69dabc2962053644d",
},
.@"zba-util" = .{
.url = "git+https://git.musuka.dev/paoda/zba-util#bf0e744047ce1ec90172dbcc0c72bfcc29a063e3",
.hash = "1220d044ecfbeacc3b3cebeff131d587e24167d61435a3cb96dffd4d4521bb06aed0",
},
.@"zba-gdbstub" = .{
.url = "git+https://git.musuka.dev/paoda/zba-gdbstub#9a50607d5f48293f950a4e823344f2bc24582a5a",
.hash = "1220ac267744ed2a735f03c4620d7c6210fbd36d7bfb2b376ddc3436faebadee0f61",
},
.tomlz = .{
.url = "git+https://github.com/paoda/tomlz#9a16dd53927ef2012478b6494bafb4475e44f4c9",
.hash = "12204f922cab84980e36b5c058d354ec0ee169bda401c8e0e80a463580349b476569",
},
.arm32 = .{
.url = "git+https://git.musuka.dev/paoda/arm32#814d081ea0983bc48841a6baad7158c157b17ad6",
.hash = "12203c3dacf3a7aa7aee5fc5763dd7b40399bd1c34d1483330b6bd5a76bffef22d82",
},
.zgui = .{
.url = "git+https://git.musuka.dev/paoda/zgui#7f8d05101e96c64314d7926c80ee157dcb89da4e",
.hash = "1220bd81a1c7734892b1d4233ed047710487787873c85dd5fc76d1764a331ed2ff43",
},
},
}

View File

@@ -1,36 +0,0 @@
$SDL2Version = "2.30.0"
$ArchiveFile = ".\SDL2-devel-mingw.zip"
$Json = @"
{
"x86_64-windows-gnu": {
"include": ".build_config\\SDL2\\include",
"libs": ".build_config\\SDL2\\lib",
"bin": ".build_config\\SDL2\\bin"
}
}
"@
New-Item -Force -ItemType Directory -Path .\.build_config
Set-Location -Path .build_config -PassThru
if (!(Test-Path -PathType Leaf $ArchiveFile)) {
Invoke-WebRequest "https://github.com/libsdl-org/SDL/releases/download/release-$SDL2Version/SDL2-devel-$SDL2Version-mingw.zip" -OutFile $ArchiveFile
}
Expand-Archive $ArchiveFile
if (Test-Path -PathType Container .\SDL2) {
Remove-Item -Recurse .\SDL2
}
New-Item -Force -ItemType Directory -Path .\SDL2
Get-ChildItem -Path ".\SDL2-devel-mingw\SDL2-$SDL2Version\x86_64-w64-mingw32" | Move-Item -Destination .\SDL2
# #include <SDL.h>
Move-Item -Force -Path .\SDL2\include\SDL2\* -Destination .\SDL2\include
Remove-Item -Force .\SDL2\include\SDL2
New-Item -Force .\sdl.json -Value $Json
Remove-Item -Recurse .\SDL2-devel-mingw
Set-Location -Path .. -PassThru

View File

@@ -1,4 +1,4 @@
[host] [Host]
# Using nearest-neighbour scaling, how many times the native resolution # Using nearest-neighbour scaling, how many times the native resolution
# of the game bow should the screen be? # of the game bow should the screen be?
win_scale = 3 win_scale = 3
@@ -7,7 +7,7 @@ vsync = true
# Mute ZBA # Mute ZBA
mute = false mute = false
[guest] [Guest]
# Sync Emulation to Audio # Sync Emulation to Audio
audio_sync = true audio_sync = true
# Sync Emulation to Video # Sync Emulation to Video
@@ -17,7 +17,7 @@ force_rtc = false
# Skip BIOS # Skip BIOS
skip_bios = false skip_bios = false
[debug] [Debug]
# Enable detailed CPU logs # Enable detailed CPU logs
cpu_trace = false cpu_trace = false
# When false and builtin.mode == .Debug, ZBA will panic # When false and builtin.mode == .Debug, ZBA will panic

4163
lib/gl.zig

File diff suppressed because it is too large Load Diff

1
lib/known-folders Submodule

Submodule lib/known-folders added at 24845b0103

View File

@@ -26,13 +26,13 @@ fn BitType(comptime FieldType: type, comptime ValueType: type, comptime shamt: u
} }
pub fn read(self: anytype) ValueType { pub fn read(self: anytype) ValueType {
return @bitCast(@as(u1, @truncate(self.bits.field().* >> shamt))); return @bitCast(ValueType, @truncate(u1, self.bits.field().* >> shamt));
} }
// Since these are mostly used with MMIO, I want to avoid // Since these are mostly used with MMIO, I want to avoid
// reading the memory just to write it again, also races // reading the memory just to write it again, also races
pub fn write(self: anytype, val: ValueType) void { pub fn write(self: anytype, val: ValueType) void {
if (@as(bool, @bitCast(val))) { if (@bitCast(bool, val)) {
self.set(); self.set();
} else { } else {
self.unset(); self.unset();
@@ -67,17 +67,17 @@ pub fn Bitfield(comptime FieldType: type, comptime shamt: usize, comptime num_bi
dummy: FieldType, dummy: FieldType,
fn field(self: anytype) PtrCastPreserveCV(@This(), @TypeOf(self), FieldType) { fn field(self: anytype) PtrCastPreserveCV(@This(), @TypeOf(self), FieldType) {
return @ptrCast(self); return @ptrCast(PtrCastPreserveCV(@This(), @TypeOf(self), FieldType), self);
} }
pub fn write(self: anytype, val: ValueType) void { pub fn write(self: anytype, val: ValueType) void {
self.field().* &= ~self_mask; self.field().* &= ~self_mask;
self.field().* |= @as(FieldType, @intCast(val)) << shamt; self.field().* |= @intCast(FieldType, val) << shamt;
} }
pub fn read(self: anytype) ValueType { pub fn read(self: anytype) ValueType {
const val: FieldType = self.field().*; const val: FieldType = self.field().*;
return @intCast((val & self_mask) >> shamt); return @intCast(ValueType, (val & self_mask) >> shamt);
} }
}; };
} }

1
lib/zig-clap Submodule

Submodule lib/zig-clap added at e5d09c4b2d

1
lib/zig-datetime Submodule

Submodule lib/zig-datetime added at 5ec1c36cf3

1
lib/zig-toml Submodule

Submodule lib/zig-toml added at 5dfa919e03

View File

@@ -1,5 +1,5 @@
const std = @import("std"); const std = @import("std");
const tomlz = @import("tomlz"); const toml = @import("toml");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
@@ -7,7 +7,6 @@ const log = std.log.scoped(.Config);
var state: Config = .{}; var state: Config = .{};
const Config = struct { const Config = struct {
// FIXME: tomlz expects these to be case sensitive
host: Host = .{}, host: Host = .{},
guest: Guest = .{}, guest: Guest = .{},
debug: Debug = .{}, debug: Debug = .{},
@@ -50,14 +49,35 @@ pub fn config() *const Config {
} }
/// Reads a config file and then loads it into the global state /// Reads a config file and then loads it into the global state
pub fn load(allocator: Allocator, file_path: []const u8) !void { pub fn load(allocator: Allocator, config_path: []const u8) !void {
var config_file = try std.fs.cwd().openFile(file_path, .{}); var config_file = try std.fs.cwd().openFile(config_path, .{});
defer config_file.close(); defer config_file.close();
log.info("loaded from {s}", .{file_path}); log.info("loaded from {s}", .{config_path});
const contents = try config_file.readToEndAlloc(allocator, try config_file.getEndPos()); const contents = try config_file.readToEndAlloc(allocator, try config_file.getEndPos());
defer allocator.free(contents); defer allocator.free(contents);
state = try tomlz.parser.decode(Config, allocator, 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

@@ -1,6 +1,7 @@
const std = @import("std"); const std = @import("std");
const Arm7tdmi = @import("arm32").Arm7tdmi; const AudioDeviceId = @import("sdl2").SDL_AudioDeviceID;
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
const Bios = @import("bus/Bios.zig"); const Bios = @import("bus/Bios.zig");
const Ewram = @import("bus/Ewram.zig"); const Ewram = @import("bus/Ewram.zig");
const GamePak = @import("bus/GamePak.zig"); const GamePak = @import("bus/GamePak.zig");
@@ -19,7 +20,7 @@ const log = std.log.scoped(.Bus);
const createDmaTuple = @import("bus/dma.zig").create; const createDmaTuple = @import("bus/dma.zig").create;
const createTimerTuple = @import("bus/timer.zig").create; const createTimerTuple = @import("bus/timer.zig").create;
const rotr = @import("zba-util").rotr; const rotr = @import("../util.zig").rotr;
const timings: [2][0x10]u8 = [_][0x10]u8{ const timings: [2][0x10]u8 = [_][0x10]u8{
// BIOS, Unused, EWRAM, IWRAM, I/0, PALRAM, VRAM, OAM, ROM0, ROM0, ROM1, ROM1, ROM2, ROM2, SRAM, Unused // BIOS, Unused, EWRAM, IWRAM, I/0, PALRAM, VRAM, OAM, ROM0, ROM0, ROM1, ROM1, ROM2, ROM2, SRAM, Unused
@@ -33,11 +34,6 @@ pub const fetch_timings: [2][0x10]u8 = [_][0x10]u8{
[_]u8{ 1, 1, 6, 1, 1, 2, 2, 1, 4, 4, 4, 4, 4, 4, 8, 8 }, // 32-bit [_]u8{ 1, 1, 6, 1, 1, 2, 2, 1, 4, 4, 4, 4, 4, 4, 8, 8 }, // 32-bit
}; };
// Fastmem Related
const page_size = 1 * 0x400; // 1KiB
const address_space_size = 0x1000_0000;
const table_len = address_space_size / page_size;
const Self = @This(); const Self = @This();
pak: GamePak, pak: GamePak,
@@ -53,16 +49,7 @@ io: Io,
cpu: *Arm7tdmi, cpu: *Arm7tdmi,
sched: *Scheduler, sched: *Scheduler,
read_table: *const [table_len]?*const anyopaque,
write_tables: [2]*const [table_len]?*anyopaque,
allocator: Allocator,
pub fn init(self: *Self, allocator: Allocator, sched: *Scheduler, cpu: *Arm7tdmi, paths: FilePaths) !void { pub fn init(self: *Self, allocator: Allocator, sched: *Scheduler, cpu: *Arm7tdmi, paths: FilePaths) !void {
const tables = try allocator.alloc(?*anyopaque, 3 * table_len); // Allocate all tables
const read_table = tables[0..table_len];
const write_tables = .{ tables[table_len .. 2 * table_len], tables[2 * table_len .. 3 * table_len] };
self.* = .{ self.* = .{
.pak = try GamePak.init(allocator, cpu, paths.rom, paths.save), .pak = try GamePak.init(allocator, cpu, paths.rom, paths.save),
.bios = try Bios.init(allocator, paths.bios), .bios = try Bios.init(allocator, paths.bios),
@@ -75,17 +62,7 @@ pub fn init(self: *Self, allocator: Allocator, sched: *Scheduler, cpu: *Arm7tdmi
.io = Io.init(), .io = Io.init(),
.cpu = cpu, .cpu = cpu,
.sched = sched, .sched = sched,
.read_table = read_table,
.write_tables = write_tables,
.allocator = allocator,
}; };
self.fillReadTable(read_table);
// Internal Display Memory behaves differently on 8-bit reads
self.fillWriteTable(u32, write_tables[0]);
self.fillWriteTable(u8, write_tables[1]);
} }
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
@@ -94,170 +71,61 @@ pub fn deinit(self: *Self) void {
self.pak.deinit(); self.pak.deinit();
self.bios.deinit(); self.bios.deinit();
self.ppu.deinit(); self.ppu.deinit();
// This is so I can deallocate the original `allocator.alloc`. I have to re-make the type
// since I'm not keeping it around, This is very jank and bad though
// FIXME: please figure out another way
self.allocator.free(@as([*]const ?*anyopaque, @ptrCast(self.read_table[0..]))[0 .. 3 * table_len]);
self.* = undefined; self.* = undefined;
} }
pub fn reset(self: *Self) void { pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
self.bios.reset(); const page = @truncate(u8, address >> 24);
self.ppu.reset(); const aligned_addr = forceAlign(T, address);
self.apu.reset();
self.iwram.reset();
self.ewram.reset();
// https://github.com/ziglang/zig/issues/14705 return switch (page) {
{
comptime var i: usize = 0;
inline while (i < self.dma.len) : (i += 1) {
self.dma[0].reset();
}
}
// https://github.com/ziglang/zig/issues/14705
{
comptime var i: usize = 0;
inline while (i < self.tim.len) : (i += 1) {
self.tim[0].reset();
}
}
self.io.reset();
}
pub fn replaceGamepak(self: *Self, file_path: []const u8) !void {
// Note: `save_path` isn't owned by `Backup`
const save_path = self.pak.backup.save_path;
self.pak.deinit();
self.pak = try GamePak.init(self.allocator, self.cpu, file_path, save_path);
const read_ptr: *[table_len]?*const anyopaque = @constCast(self.read_table);
const write_ptrs: [2]*[table_len]?*anyopaque = .{ @constCast(self.write_tables[0]), @constCast(self.write_tables[1]) };
self.fillReadTable(read_ptr);
self.fillWriteTable(u32, write_ptrs[0]);
self.fillWriteTable(u8, write_ptrs[1]);
}
fn fillReadTable(self: *Self, table: *[table_len]?*const anyopaque) void {
const vramMirror = @import("ppu/Vram.zig").mirror;
for (table, 0..) |*ptr, i| {
const addr: u32 = @intCast(page_size * i);
ptr.* = switch (addr) {
// General Internal Memory // General Internal Memory
0x0000_0000...0x0000_3FFF => null, // BIOS has it's own checks 0x00 => blk: {
0x0200_0000...0x02FF_FFFF => &self.ewram.buf[addr & 0x3FFFF], if (address < Bios.size)
0x0300_0000...0x03FF_FFFF => &self.iwram.buf[addr & 0x7FFF], break :blk self.bios.dbgRead(T, self.cpu.r[15], aligned_addr);
0x0400_0000...0x0400_03FF => null, // I/O
break :blk self.openBus(T, address);
},
0x02 => self.ewram.read(T, aligned_addr),
0x03 => self.iwram.read(T, aligned_addr),
0x04 => self.readIo(T, address),
// Internal Display Memory // Internal Display Memory
0x0500_0000...0x05FF_FFFF => &self.ppu.palette.buf[addr & 0x3FF], 0x05 => self.ppu.palette.read(T, aligned_addr),
0x0600_0000...0x06FF_FFFF => &self.ppu.vram.buf[vramMirror(addr)], 0x06 => self.ppu.vram.read(T, aligned_addr),
0x0700_0000...0x07FF_FFFF => &self.ppu.oam.buf[addr & 0x3FF], 0x07 => self.ppu.oam.read(T, aligned_addr),
// External Memory (Game Pak) // External Memory (Game Pak)
0x0800_0000...0x0DFF_FFFF => self.fillReadTableExternal(addr), 0x08...0x0D => self.pak.dbgRead(T, aligned_addr),
0x0E00_0000...0x0FFF_FFFF => null, // SRAM 0x0E...0x0F => blk: {
else => null, const value = self.pak.backup.read(address);
const multiplier = switch (T) {
u32 => 0x01010101,
u16 => 0x0101,
u8 => 1,
else => @compileError("Backup: Unsupported read width"),
}; };
}
}
fn fillWriteTable(self: *Self, comptime T: type, table: *[table_len]?*const anyopaque) void { break :blk @as(T, value) * multiplier;
comptime std.debug.assert(T == u32 or T == u16 or T == u8); },
const vramMirror = @import("ppu/Vram.zig").mirror; else => self.openBus(T, address),
for (table, 0..) |*ptr, i| {
const addr: u32 = @intCast(page_size * i);
ptr.* = switch (addr) {
// General Internal Memory
0x0000_0000...0x0000_3FFF => null, // BIOS has it's own checks
0x0200_0000...0x02FF_FFFF => &self.ewram.buf[addr & 0x3FFFF],
0x0300_0000...0x03FF_FFFF => &self.iwram.buf[addr & 0x7FFF],
0x0400_0000...0x0400_03FF => null, // I/O
// Internal Display Memory
0x0500_0000...0x05FF_FFFF => if (T != u8) &self.ppu.palette.buf[addr & 0x3FF] else null,
0x0600_0000...0x06FF_FFFF => if (T != u8) &self.ppu.vram.buf[vramMirror(addr)] else null,
0x0700_0000...0x07FF_FFFF => if (T != u8) &self.ppu.oam.buf[addr & 0x3FF] else null,
// External Memory (Game Pak)
0x0800_0000...0x0DFF_FFFF => null, // ROM
0x0E00_0000...0x0FFF_FFFF => null, // SRAM
else => null,
}; };
}
} }
fn fillReadTableExternal(self: *Self, addr: u32) ?*anyopaque { fn readIo(self: *const Self, comptime T: type, unaligned_address: u32) T {
// see `GamePak.zig` for more information about what conditions need to be true const maybe_value = io.read(self, T, forceAlign(T, unaligned_address));
// so that a simple pointer dereference isn't possible return if (maybe_value) |value| value else self.openBus(T, unaligned_address);
std.debug.assert(addr & @as(u32, page_size - 1) == 0); // addr is guaranteed to be page-aligned
const start_addr = addr;
const end_addr = start_addr + page_size;
{
const data = start_addr <= 0x0800_00C4 and 0x0800_00C4 < end_addr; // GPIO Data
const direction = start_addr <= 0x0800_00C6 and 0x0800_00C6 < end_addr; // GPIO Direction
const control = start_addr <= 0x0800_00C8 and 0x0800_00C8 < end_addr; // GPIO Control
const has_gpio = data or direction or control;
const gpio_kind = self.pak.gpio.device.kind;
// There is a GPIO Device, and the current page contains at least one memory-mapped GPIO register
if (gpio_kind != .None and has_gpio) return null;
}
if (self.pak.backup.kind == .Eeprom) {
if (self.pak.buf.len > 0x100_000) {
// We are using a "large" EEPROM which means that if the below check is true
// this page has an address that's reserved for the EEPROM and therefore must
// be handled in slowmem
if (addr & 0x1FF_FFFF > 0x1FF_FEFF) return null;
} else {
// We are using a "small" EEPROM which means that if the below check is true
// (that is, we're in the 0xD address page) then we must handle at least one
// address in this page in slowmem
if (@as(u4, @truncate(addr >> 24)) == 0xD) return null;
}
}
// Finally, the GamePak has some unique behaviour for reads past the end of the ROM,
// so those will be handled by slowmem as well
const masked_addr = addr & 0x1FF_FFFF;
if (masked_addr >= self.pak.buf.len) return null;
return &self.pak.buf[masked_addr];
}
fn readIo(self: *const Self, comptime T: type, address: u32) T {
return io.read(self, T, address) orelse self.openBus(T, address);
} }
fn openBus(self: *const Self, comptime T: type, address: u32) T { fn openBus(self: *const Self, comptime T: type, address: u32) T {
@setCold(true);
const r15 = self.cpu.r[15]; const r15 = self.cpu.r[15];
const word = blk: { const word = blk: {
// If Arm, get the most recently fetched instruction (PC + 8) // If Arm, get the most recently fetched instruction (PC + 8)
//
// FIXME: This is most likely a faulty assumption.
// I think what *actually* happens is that the Bus has a latch for the most
// recently fetched piece of data, which is then returned during Open Bus (also DMA open bus?)
// I can "get away" with this because it's very statistically likely that the most recently latched value is
// the most recently fetched instruction by the pipeline
if (!self.cpu.cpsr.t.read()) break :blk self.cpu.pipe.stage[1].?; if (!self.cpu.cpsr.t.read()) break :blk self.cpu.pipe.stage[1].?;
const page: u8 = @truncate(r15 >> 24); const page = @truncate(u8, r15 >> 24);
// PC + 2 = stage[0] // PC + 2 = stage[0]
// PC + 4 = stage[1] // PC + 4 = stage[1]
@@ -266,7 +134,7 @@ fn openBus(self: *const Self, comptime T: type, address: u32) T {
switch (page) { switch (page) {
// EWRAM, PALRAM, VRAM, and Game ROM (16-bit) // EWRAM, PALRAM, VRAM, and Game ROM (16-bit)
0x02, 0x05, 0x06, 0x08...0x0D => { 0x02, 0x05, 0x06, 0x08...0x0D => {
const halfword: u32 = @as(u16, @truncate(self.cpu.pipe.stage[1].?)); const halfword: u32 = @truncate(u16, self.cpu.pipe.stage[1].?);
break :blk halfword << 16 | halfword; break :blk halfword << 16 | halfword;
}, },
@@ -277,8 +145,8 @@ fn openBus(self: *const Self, comptime T: type, address: u32) T {
const aligned = address & 3 == 0b00; const aligned = address & 3 == 0b00;
// TODO: What to do on PC + 6? // TODO: What to do on PC + 6?
const high: u32 = if (aligned) self.dbgRead(u16, r15 + 4) else @as(u16, @truncate(self.cpu.pipe.stage[1].?)); const high: u32 = if (aligned) self.dbgRead(u16, r15 + 4) else @truncate(u16, self.cpu.pipe.stage[1].?);
const low: u32 = @as(u16, @truncate(self.cpu.pipe.stage[@intFromBool(aligned)].?)); const low: u32 = @truncate(u16, self.cpu.pipe.stage[@boolToInt(aligned)].?);
break :blk high << 16 | low; break :blk high << 16 | low;
}, },
@@ -289,8 +157,8 @@ fn openBus(self: *const Self, comptime T: type, address: u32) T {
// Unaligned: (PC + 4) | (PC + 2) // Unaligned: (PC + 4) | (PC + 2)
const aligned = address & 3 == 0b00; const aligned = address & 3 == 0b00;
const high: u32 = @as(u16, @truncate(self.cpu.pipe.stage[1 - @intFromBool(aligned)].?)); const high: u32 = @truncate(u16, self.cpu.pipe.stage[1 - @boolToInt(aligned)].?);
const low: u32 = @as(u16, @truncate(self.cpu.pipe.stage[@intFromBool(aligned)].?)); const low: u32 = @truncate(u16, self.cpu.pipe.stage[@boolToInt(aligned)].?);
break :blk high << 16 | low; break :blk high << 16 | low;
}, },
@@ -301,112 +169,36 @@ fn openBus(self: *const Self, comptime T: type, address: u32) T {
} }
}; };
return @truncate(word); return @truncate(T, rotr(u32, word, 8 * (address & 3)));
} }
pub fn read(self: *Self, comptime T: type, unaligned_address: u32) T { pub fn read(self: *Self, comptime T: type, address: u32) T {
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits; const page = @truncate(u8, address >> 24);
const page = unaligned_address >> bits; const aligned_addr = forceAlign(T, address);
const offset = unaligned_address & (page_size - 1);
// whether or not we do this in slowmem or fastmem, we should advance the scheduler self.sched.tick += timings[@boolToInt(T == u32)][@truncate(u4, page)];
self.sched.tick += timings[@intFromBool(T == u32)][@as(u4, @truncate(unaligned_address >> 24))];
// We're doing some serious out-of-bounds open-bus reads
if (page >= table_len) return self.openBus(T, unaligned_address);
if (self.read_table[page]) |some_ptr| {
// We have a pointer to a page, cast the pointer to it's underlying type
const ptr: [*]const T = @ptrCast(@alignCast(some_ptr));
// Note: We don't check array length, since we force align the
// lower bits of the address as the GBA would
return ptr[forceAlign(T, offset) / @sizeOf(T)];
}
return self.slowRead(T, unaligned_address);
}
pub fn dbgRead(self: *const Self, comptime T: type, unaligned_address: u32) T {
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits;
const page = unaligned_address >> bits;
const offset = unaligned_address & (page_size - 1);
// We're doing some serious out-of-bounds open-bus reads
if (page >= table_len) return self.openBus(T, unaligned_address);
if (self.read_table[page]) |some_ptr| {
// We have a pointer to a page, cast the pointer to it's underlying type
const ptr: [*]const T = @ptrCast(@alignCast(some_ptr));
// Note: We don't check array length, since we force align the
// lower bits of the address as the GBA would
return ptr[forceAlign(T, offset) / @sizeOf(T)];
}
return self.dbgSlowRead(T, unaligned_address);
}
fn slowRead(self: *Self, comptime T: type, unaligned_address: u32) T {
@setCold(true);
const page: u8 = @truncate(unaligned_address >> 24);
const address = forceAlign(T, unaligned_address);
return switch (page) { return switch (page) {
// General Internal Memory // General Internal Memory
0x00 => blk: { 0x00 => blk: {
if (address < Bios.size) if (address < Bios.size)
break :blk self.bios.read(T, self.cpu.r[15], unaligned_address); break :blk self.bios.read(T, self.cpu.r[15], aligned_addr);
break :blk self.openBus(T, address); break :blk self.openBus(T, address);
}, },
0x02 => unreachable, // completely handled by fastmeme 0x02 => self.ewram.read(T, aligned_addr),
0x03 => unreachable, // completely handled by fastmeme 0x03 => self.iwram.read(T, aligned_addr),
0x04 => self.readIo(T, address), 0x04 => self.readIo(T, address),
// Internal Display Memory // Internal Display Memory
0x05 => unreachable, // completely handled by fastmeme 0x05 => self.ppu.palette.read(T, aligned_addr),
0x06 => unreachable, // completely handled by fastmeme 0x06 => self.ppu.vram.read(T, aligned_addr),
0x07 => unreachable, // completely handled by fastmeme 0x07 => self.ppu.oam.read(T, aligned_addr),
// External Memory (Game Pak) // External Memory (Game Pak)
0x08...0x0D => self.pak.read(T, address), 0x08...0x0D => self.pak.read(T, aligned_addr),
0x0E...0x0F => self.readBackup(T, unaligned_address), 0x0E...0x0F => blk: {
else => self.openBus(T, address), const value = self.pak.backup.read(address);
};
}
fn dbgSlowRead(self: *const Self, comptime T: type, unaligned_address: u32) T {
const page: u8 = @truncate(unaligned_address >> 24);
const address = forceAlign(T, unaligned_address);
return switch (page) {
// General Internal Memory
0x00 => blk: {
if (address < Bios.size)
break :blk self.bios.dbgRead(T, self.cpu.r[15], unaligned_address);
break :blk self.openBus(T, address);
},
0x02 => unreachable, // handled by fastmem
0x03 => unreachable, // handled by fastmem
0x04 => self.readIo(T, address),
// Internal Display Memory
0x05 => unreachable, // handled by fastmem
0x06 => unreachable, // handled by fastmem
0x07 => unreachable, // handled by fastmem
// External Memory (Game Pak)
0x08...0x0D => self.pak.dbgRead(T, address),
0x0E...0x0F => self.readBackup(T, unaligned_address),
else => self.openBus(T, address),
};
}
fn readBackup(self: *const Self, comptime T: type, unaligned_address: u32) T {
const value = self.pak.backup.read(unaligned_address);
const multiplier = switch (T) { const multiplier = switch (T) {
u32 => 0x01010101, u32 => 0x01010101,
@@ -415,122 +207,50 @@ fn readBackup(self: *const Self, comptime T: type, unaligned_address: u32) T {
else => @compileError("Backup: Unsupported read width"), else => @compileError("Backup: Unsupported read width"),
}; };
return @as(T, value) * multiplier; break :blk @as(T, value) * multiplier;
} },
else => self.openBus(T, address),
pub fn write(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits;
const page = unaligned_address >> bits;
const offset = unaligned_address & (page_size - 1);
// whether or not we do this in slowmem or fastmem, we should advance the scheduler
self.sched.tick += timings[@intFromBool(T == u32)][@as(u4, @truncate(unaligned_address >> 24))];
// We're doing some serious out-of-bounds open-bus writes, they do nothing though
if (page >= table_len) return;
if (self.write_tables[@intFromBool(T == u8)][page]) |some_ptr| {
// We have a pointer to a page, cast the pointer to it's underlying type
const ptr: [*]T = @ptrCast(@alignCast(some_ptr));
// Note: We don't check array length, since we force align the
// lower bits of the address as the GBA would
ptr[forceAlign(T, offset) / @sizeOf(T)] = value;
} else {
// we can return early if this is an 8-bit OAM write
if (T == u8 and @as(u8, @truncate(unaligned_address >> 24)) == 0x07) return;
self.slowWrite(T, unaligned_address, value);
}
}
/// Mostly Identical to `Bus.write`, slowmeme is handled by `Bus.dbgSlowWrite`
pub fn dbgWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits;
const page = unaligned_address >> bits;
const offset = unaligned_address & (page_size - 1);
// We're doing some serious out-of-bounds open-bus writes, they do nothing though
if (page >= table_len) return;
if (self.write_tables[@intFromBool(T == u8)][page]) |some_ptr| {
// We have a pointer to a page, cast the pointer to it's underlying type
const ptr: [*]T = @ptrCast(@alignCast(some_ptr));
// Note: We don't check array length, since we force align the
// lower bits of the address as the GBA would
ptr[forceAlign(T, offset) / @sizeOf(T)] = value;
} else {
// we can return early if this is an 8-bit OAM write
if (T == u8 and @as(u8, @truncate(unaligned_address >> 24)) == 0x07) return;
self.dbgSlowWrite(T, unaligned_address, value);
}
}
fn slowWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
@setCold(true);
const page: u8 = @truncate(unaligned_address >> 24);
const address = forceAlign(T, unaligned_address);
switch (page) {
// General Internal Memory
0x00 => self.bios.write(T, address, value),
0x02 => unreachable, // completely handled by fastmem
0x03 => unreachable, // completely handled by fastmem
0x04 => io.write(self, T, address, value),
// Internal Display Memory
0x05 => self.ppu.palette.write(T, address, value),
0x06 => self.ppu.vram.write(T, self.ppu.dispcnt, address, value),
0x07 => unreachable, // completely handled by fastmem
// External Memory (Game Pak)
0x08...0x0D => self.pak.write(T, self.dma[3].word_count, address, value),
0x0E...0x0F => self.pak.backup.write(unaligned_address, @truncate(rotr(T, value, 8 * rotateBy(T, unaligned_address)))),
else => {},
}
}
fn dbgSlowWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
@setCold(true);
const page: u8 = @truncate(unaligned_address >> 24);
const address = forceAlign(T, unaligned_address);
switch (page) {
// General Internal Memory
0x00 => self.bios.write(T, address, value),
0x02 => unreachable, // completely handled by fastmem
0x03 => unreachable, // completely handled by fastmem
0x04 => return, // FIXME: Let debug writes mess with I/O
// Internal Display Memory
0x05 => self.ppu.palette.write(T, address, value),
0x06 => self.ppu.vram.write(T, self.ppu.dispcnt, address, value),
0x07 => unreachable, // completely handled by fastmem
// External Memory (Game Pak)
0x08...0x0D => return, // FIXME: Debug Write to Backup/GPIO w/out messing with state
0x0E...0x0F => return, // FIXME: Debug Write to Backup w/out messing with state
else => {},
}
}
inline fn rotateBy(comptime T: type, address: u32) u32 {
return switch (T) {
u32 => address & 3,
u16 => address & 1,
u8 => 0,
else => @compileError("Unsupported write width"),
}; };
} }
pub inline fn forceAlign(comptime T: type, address: u32) u32 { pub fn write(self: *Self, comptime T: type, address: u32, value: T) void {
const page = @truncate(u8, address >> 24);
const aligned_addr = forceAlign(T, address);
self.sched.tick += timings[@boolToInt(T == u32)][@truncate(u4, page)];
switch (page) {
// General Internal Memory
0x00 => self.bios.write(T, aligned_addr, value),
0x02 => self.ewram.write(T, aligned_addr, value),
0x03 => self.iwram.write(T, aligned_addr, value),
0x04 => io.write(self, T, aligned_addr, value),
// Internal Display Memory
0x05 => self.ppu.palette.write(T, aligned_addr, value),
0x06 => self.ppu.vram.write(T, self.ppu.dispcnt, aligned_addr, value),
0x07 => self.ppu.oam.write(T, aligned_addr, value),
// External Memory (Game Pak)
0x08...0x0D => self.pak.write(T, self.dma[3].word_count, aligned_addr, value),
0x0E...0x0F => {
const rotate_by = switch (T) {
u32 => address & 3,
u16 => address & 1,
u8 => 0,
else => @compileError("Backup: Unsupported write width"),
};
self.pak.backup.write(address, @truncate(u8, rotr(T, value, 8 * rotate_by)));
},
else => {},
}
}
fn forceAlign(comptime T: type, address: u32) u32 {
return switch (T) { return switch (T) {
u32 => address & ~@as(u32, 3), u32 => address & 0xFFFF_FFFC,
u16 => address & ~@as(u32, 1), u16 => address & 0xFFFF_FFFE,
u8 => address, u8 => address,
else => @compileError("Bus: Invalid read/write type"), else => @compileError("Bus: Invalid read/write type"),
}; };

View File

@@ -3,8 +3,9 @@ const SDL = @import("sdl2");
const io = @import("bus/io.zig"); const io = @import("bus/io.zig");
const util = @import("../util.zig"); const util = @import("../util.zig");
const Arm7tdmi = @import("arm32").Arm7tdmi; const AudioDeviceId = SDL.SDL_AudioDeviceID;
const Bus = @import("Bus.zig");
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
const Scheduler = @import("scheduler.zig").Scheduler; const Scheduler = @import("scheduler.zig").Scheduler;
const ToneSweep = @import("apu/ToneSweep.zig"); const ToneSweep = @import("apu/ToneSweep.zig");
const Tone = @import("apu/Tone.zig"); const Tone = @import("apu/Tone.zig");
@@ -13,216 +14,133 @@ const Noise = @import("apu/Noise.zig");
const SoundFifo = std.fifo.LinearFifo(u8, .{ .Static = 0x20 }); const SoundFifo = std.fifo.LinearFifo(u8, .{ .Static = 0x20 });
const getHalf = util.getHalf; const intToBytes = @import("../util.zig").intToBytes;
const setHalf = util.setHalf; const setHi = @import("../util.zig").setHi;
const setLo = @import("../util.zig").setLo;
const log = std.log.scoped(.APU); const log = std.log.scoped(.APU);
pub const host_rate = @import("../platform.zig").sample_rate; pub const host_sample_rate = 1 << 15;
pub const host_format = @import("../platform.zig").sample_format;
pub fn read(comptime T: type, apu: *const Apu, addr: u32) ?T { pub fn read(comptime T: type, apu: *const Apu, addr: u32) ?T {
const byte_addr: u8 = @truncate(addr); const byte = @truncate(u8, addr);
return switch (T) { return switch (T) {
u32 => switch (byte_addr) { u16 => switch (byte) {
0x60 => @as(T, apu.ch1.sound1CntH()) << 16 | apu.ch1.sound1CntL(),
0x64 => apu.ch1.sound1CntX(),
0x68 => apu.ch2.sound2CntL(),
0x6C => apu.ch2.sound2CntH(),
0x70 => @as(T, apu.ch3.sound3CntH()) << 16 | apu.ch3.sound3CntL(),
0x74 => apu.ch3.sound3CntX(),
0x78 => apu.ch4.sound4CntL(),
0x7C => apu.ch4.sound4CntH(),
0x80 => @as(T, apu.dma_cnt.raw) << 16 | apu.psg_cnt.raw, // SOUNDCNT_H, SOUNDCNT_L
0x84 => apu.soundCntX(),
0x88 => apu.bias.raw, // SOUNDBIAS, high is unused
0x8C => null,
0x90, 0x94, 0x98, 0x9C => apu.ch3.wave_dev.read(T, apu.ch3.select, addr),
0xA0 => null, // FIFO_A
0xA4 => null, // FIFO_B
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
},
u16 => switch (byte_addr) {
0x60 => apu.ch1.sound1CntL(), 0x60 => apu.ch1.sound1CntL(),
0x62 => apu.ch1.sound1CntH(), 0x62 => apu.ch1.sound1CntH(),
0x64 => apu.ch1.sound1CntX(), 0x64 => apu.ch1.sound1CntX(),
0x66 => 0x0000, // suite.gba expects 0x0000, not 0xDEAD
0x68 => apu.ch2.sound2CntL(), 0x68 => apu.ch2.sound2CntL(),
0x6A => 0x0000,
0x6C => apu.ch2.sound2CntH(), 0x6C => apu.ch2.sound2CntH(),
0x6E => 0x0000,
0x70 => apu.ch3.sound3CntL(), 0x70 => apu.ch3.select.raw & 0xE0, // SOUND3CNT_L
0x72 => apu.ch3.sound3CntH(), 0x72 => apu.ch3.sound3CntH(),
0x74 => apu.ch3.sound3CntX(), 0x74 => apu.ch3.freq.raw & 0x4000, // SOUND3CNT_X
0x76 => 0x0000,
0x78 => apu.ch4.sound4CntL(), 0x78 => apu.ch4.sound4CntL(),
0x7A => 0x0000,
0x7C => apu.ch4.sound4CntH(), 0x7C => apu.ch4.sound4CntH(),
0x7E => 0x0000,
0x80 => apu.soundCntL(), 0x80 => apu.psg_cnt.raw & 0xFF77, // SOUNDCNT_L
0x82 => apu.soundCntH(), 0x82 => apu.dma_cnt.raw & 0x770F, // SOUNDCNT_H
0x84 => apu.soundCntX(), 0x84 => apu.soundCntX(),
0x86 => 0x0000,
0x88 => apu.bias.raw, // SOUNDBIAS 0x88 => apu.bias.raw, // SOUNDBIAS
0x8A => 0x0000,
0x8C, 0x8E => null,
0x90, 0x92, 0x94, 0x96, 0x98, 0x9A, 0x9C, 0x9E => apu.ch3.wave_dev.read(T, apu.ch3.select, addr),
0xA0, 0xA2 => null, // FIFO_A
0xA4, 0xA6 => null, // FIFO_B
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
},
u8 => switch (byte_addr) {
0x60, 0x61 => @truncate(@as(u16, apu.ch1.sound1CntL()) >> getHalf(byte_addr)),
0x62, 0x63 => @truncate(apu.ch1.sound1CntH() >> getHalf(byte_addr)),
0x64, 0x65 => @truncate(apu.ch1.sound1CntX() >> getHalf(byte_addr)),
0x66, 0x67 => 0x00, // assuming behaviour is identical to that of 16-bit reads
0x68, 0x69 => @truncate(apu.ch2.sound2CntL() >> getHalf(byte_addr)),
0x6A, 0x6B => 0x00,
0x6C, 0x6D => @truncate(apu.ch2.sound2CntH() >> getHalf(byte_addr)),
0x6E, 0x6F => 0x00,
0x70, 0x71 => @truncate(@as(u16, apu.ch3.sound3CntL()) >> getHalf(byte_addr)), // SOUND3CNT_L
0x72, 0x73 => @truncate(apu.ch3.sound3CntH() >> getHalf(byte_addr)),
0x74, 0x75 => @truncate(apu.ch3.sound3CntX() >> getHalf(byte_addr)), // SOUND3CNT_L
0x76, 0x77 => 0x00,
0x78, 0x79 => @truncate(apu.ch4.sound4CntL() >> getHalf(byte_addr)),
0x7A, 0x7B => 0x00,
0x7C, 0x7D => @truncate(apu.ch4.sound4CntH() >> getHalf(byte_addr)),
0x7E, 0x7F => 0x00,
0x80, 0x81 => @truncate(apu.soundCntL() >> getHalf(byte_addr)), // SOUNDCNT_L
0x82, 0x83 => @truncate(apu.soundCntH() >> getHalf(byte_addr)), // SOUNDCNT_H
0x84, 0x85 => @truncate(@as(u16, apu.soundCntX()) >> getHalf(byte_addr)),
0x86, 0x87 => 0x00,
0x88, 0x89 => @truncate(apu.bias.raw >> getHalf(byte_addr)), // SOUNDBIAS
0x8A, 0x8B => 0x00,
0x8C...0x8F => null,
0x90...0x9F => apu.ch3.wave_dev.read(T, apu.ch3.select, addr), 0x90...0x9F => apu.ch3.wave_dev.read(T, apu.ch3.select, addr),
0xA0, 0xA1, 0xA2, 0xA3 => null, // FIFO_A else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
0xA4, 0xA5, 0xA6, 0xA7 => null, // FIFO_B
else => util.io.read.err(T, log, "unexpected {} read from 0x{X:0>8}", .{ T, addr }),
}, },
u8 => switch (byte) {
0x60 => apu.ch1.sound1CntL(), // NR10
0x62 => apu.ch1.duty.raw, // NR11
0x63 => apu.ch1.envelope.raw, // NR12
0x68 => apu.ch2.duty.raw, // NR21
0x69 => apu.ch2.envelope.raw, // NR22
0x73 => apu.ch3.vol.raw, // NR32
0x79 => apu.ch4.envelope.raw, // NR42
0x7C => apu.ch4.poly.raw, // NR43
0x81 => @truncate(u8, apu.psg_cnt.raw >> 8), // NR51
0x84 => apu.soundCntX(),
0x89 => @truncate(u8, apu.bias.raw >> 8), // SOUNDBIAS_H
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
},
u32 => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
else => @compileError("APU: Unsupported read width"), else => @compileError("APU: Unsupported read width"),
}; };
} }
pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void { pub fn write(comptime T: type, apu: *Apu, addr: u32, value: T) void {
const byte_addr: u8 = @truncate(addr); const byte = @truncate(u8, addr);
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return;
switch (T) { switch (T) {
u32 => { u32 => switch (byte) {
// 0x80 and 0x81 handled in setSoundCnt
if (byte_addr < 0x80 and !apu.cnt.apu_enable.read()) return;
switch (byte_addr) {
0x60 => apu.ch1.setSound1Cnt(value), 0x60 => apu.ch1.setSound1Cnt(value),
0x64 => apu.ch1.setSound1CntX(&apu.fs, @truncate(value)), 0x64 => apu.ch1.setSound1CntX(&apu.fs, @truncate(u16, value)),
0x68 => apu.ch2.setSound2CntL(@truncate(u16, value)),
0x68 => apu.ch2.setSound2CntL(@truncate(value)), 0x6C => apu.ch2.setSound2CntH(&apu.fs, @truncate(u16, value)),
0x6C => apu.ch2.setSound2CntH(&apu.fs, @truncate(value)),
0x70 => apu.ch3.setSound3Cnt(value), 0x70 => apu.ch3.setSound3Cnt(value),
0x74 => apu.ch3.setSound3CntX(&apu.fs, @truncate(value)), 0x74 => apu.ch3.setSound3CntX(&apu.fs, @truncate(u16, value)),
0x78 => apu.ch4.setSound4CntL(@truncate(u16, value)),
0x78 => apu.ch4.setSound4CntL(@truncate(value)), 0x7C => apu.ch4.setSound4CntH(&apu.fs, @truncate(u16, value)),
0x7C => apu.ch4.setSound4CntH(&apu.fs, @truncate(value)),
0x80 => apu.setSoundCnt(value), 0x80 => apu.setSoundCnt(value),
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1), // WAVE_RAM
0x88 => apu.bias.raw = @truncate(value), 0x90...0x9F => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
0x8C => {},
0x90, 0x94, 0x98, 0x9C => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
0xA0 => apu.chA.push(value), // FIFO_A 0xA0 => apu.chA.push(value), // FIFO_A
0xA4 => apu.chB.push(value), // FIFO_B 0xA4 => apu.chB.push(value), // FIFO_B
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }), else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
}
}, },
u16 => { u16 => switch (byte) {
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return; 0x60 => apu.ch1.setSound1CntL(@truncate(u8, value)), // SOUND1CNT_L
switch (byte_addr) {
0x60 => apu.ch1.setSound1CntL(@truncate(value)), // SOUND1CNT_L
0x62 => apu.ch1.setSound1CntH(value), 0x62 => apu.ch1.setSound1CntH(value),
0x64 => apu.ch1.setSound1CntX(&apu.fs, value), 0x64 => apu.ch1.setSound1CntX(&apu.fs, value),
0x66 => {},
0x68 => apu.ch2.setSound2CntL(value), 0x68 => apu.ch2.setSound2CntL(value),
0x6A => {},
0x6C => apu.ch2.setSound2CntH(&apu.fs, value), 0x6C => apu.ch2.setSound2CntH(&apu.fs, value),
0x6E => {},
0x70 => apu.ch3.setSound3CntL(@truncate(value)), 0x70 => apu.ch3.setSound3CntL(@truncate(u8, value)),
0x72 => apu.ch3.setSound3CntH(value), 0x72 => apu.ch3.setSound3CntH(value),
0x74 => apu.ch3.setSound3CntX(&apu.fs, value), 0x74 => apu.ch3.setSound3CntX(&apu.fs, value),
0x76 => {},
0x78 => apu.ch4.setSound4CntL(value), 0x78 => apu.ch4.setSound4CntL(value),
0x7A => {},
0x7C => apu.ch4.setSound4CntH(&apu.fs, value), 0x7C => apu.ch4.setSound4CntH(&apu.fs, value),
0x7E => {},
0x80 => apu.setSoundCntL(value), 0x80 => apu.psg_cnt.raw = value, // SOUNDCNT_L
0x82 => apu.setSoundCntH(value), 0x82 => apu.setSoundCntH(value),
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1), 0x84 => apu.setSoundCntX(value >> 7 & 1 == 1),
0x86 => {},
0x88 => apu.bias.raw = value, // SOUNDBIAS 0x88 => apu.bias.raw = value, // SOUNDBIAS
0x8A, 0x8C, 0x8E => {}, // WAVE_RAM
0x90...0x9F => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
0x90, 0x92, 0x94, 0x96, 0x98, 0x9A, 0x9C, 0x9E => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
0xA0, 0xA2 => log.err("Tried to write 0x{X:0>4}{} to FIFO_A", .{ value, T }),
0xA4, 0xA6 => log.err("Tried to write 0x{X:0>4}{} to FIFO_B", .{ value, T }),
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }), else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
}
}, },
u8 => { u8 => switch (byte) {
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return;
switch (byte_addr) {
0x60 => apu.ch1.setSound1CntL(value), 0x60 => apu.ch1.setSound1CntL(value),
0x61 => {},
0x62 => apu.ch1.setNr11(value), 0x62 => apu.ch1.setNr11(value),
0x63 => apu.ch1.setNr12(value), 0x63 => apu.ch1.setNr12(value),
0x64 => apu.ch1.setNr13(value), 0x64 => apu.ch1.setNr13(value),
0x65 => apu.ch1.setNr14(&apu.fs, value), 0x65 => apu.ch1.setNr14(&apu.fs, value),
0x66, 0x67 => {},
0x68 => apu.ch2.setNr21(value), 0x68 => apu.ch2.setNr21(value),
0x69 => apu.ch2.setNr22(value), 0x69 => apu.ch2.setNr22(value),
0x6A, 0x6B => {},
0x6C => apu.ch2.setNr23(value), 0x6C => apu.ch2.setNr23(value),
0x6D => apu.ch2.setNr24(&apu.fs, value), 0x6D => apu.ch2.setNr24(&apu.fs, value),
0x6E, 0x6F => {},
0x70 => apu.ch3.setSound3CntL(value), // NR30 0x70 => apu.ch3.setSound3CntL(value), // NR30
0x71 => {},
0x72 => apu.ch3.setNr31(value), 0x72 => apu.ch3.setNr31(value),
0x73 => apu.ch3.vol.raw = value, // NR32 0x73 => apu.ch3.vol.raw = value, // NR32
0x74 => apu.ch3.setNr33(value), 0x74 => apu.ch3.setNr33(value),
0x75 => apu.ch3.setNr34(&apu.fs, value), 0x75 => apu.ch3.setNr34(&apu.fs, value),
0x76, 0x77 => {},
0x78 => apu.ch4.setNr41(value), 0x78 => apu.ch4.setNr41(value),
0x79 => apu.ch4.setNr42(value), 0x79 => apu.ch4.setNr42(value),
0x7A, 0x7B => {},
0x7C => apu.ch4.poly.raw = value, // NR 43 0x7C => apu.ch4.poly.raw = value, // NR 43
0x7D => apu.ch4.setNr44(&apu.fs, value), 0x7D => apu.ch4.setNr44(&apu.fs, value),
0x7E, 0x7F => {},
0x80, 0x81 => apu.setSoundCntL(setHalf(u16, apu.psg_cnt.raw, byte_addr, value)),
0x82, 0x83 => apu.setSoundCntH(setHalf(u16, apu.dma_cnt.raw, byte_addr, value)),
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1),
0x85 => {},
0x86, 0x87 => {},
0x88, 0x89 => apu.bias.raw = setHalf(u16, apu.bias.raw, byte_addr, value), // SOUNDBIAS
0x8A...0x8F => {},
0x80 => apu.setNr50(value),
0x81 => apu.setNr51(value),
0x82 => apu.setSoundCntH(setLo(u16, apu.dma_cnt.raw, value)),
0x83 => apu.setSoundCntH(setHi(u16, apu.dma_cnt.raw, value)),
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1), // NR52
0x89 => apu.setSoundBiasH(value),
0x90...0x9F => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value), 0x90...0x9F => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
0xA0...0xA3 => log.err("Tried to write 0x{X:0>2}{} to FIFO_A", .{ value, T }),
0xA4...0xA7 => log.err("Tried to write 0x{X:0>2}{} to FIFO_B", .{ value, T }),
else => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }), else => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
}
}, },
else => @compileError("APU: Unsupported write width"), else => @compileError("APU: Unsupported write width"),
} }
@@ -271,7 +189,7 @@ pub const Apu = struct {
.bias = .{ .raw = 0x0200 }, .bias = .{ .raw = 0x0200 },
.sampling_cycle = 0b00, .sampling_cycle = 0b00,
.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, 1 << 15, host_format, 2, host_rate).?, .stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, 1 << 15, SDL.AUDIO_U16, 2, host_sample_rate).?,
.sched = sched, .sched = sched,
.capacitor = 0, .capacitor = 0,
@@ -279,69 +197,27 @@ pub const Apu = struct {
.is_buffer_full = false, .is_buffer_full = false,
}; };
Self.initEvents(apu.sched, apu.interval()); sched.push(.SampleAudio, apu.interval());
sched.push(.{ .ApuChannel = 0 }, @import("apu/signal/Square.zig").interval);
sched.push(.{ .ApuChannel = 1 }, @import("apu/signal/Square.zig").interval);
sched.push(.{ .ApuChannel = 2 }, @import("apu/signal/Wave.zig").interval);
sched.push(.{ .ApuChannel = 3 }, @import("apu/signal/Lfsr.zig").interval);
sched.push(.FrameSequencer, FrameSequencer.interval);
return apu; return apu;
} }
fn initEvents(scheduler: *Scheduler, apu_interval: u64) void { fn reset(self: *Self) void {
scheduler.push(.SampleAudio, apu_interval);
scheduler.push(.{ .ApuChannel = 0 }, @import("apu/signal/Square.zig").interval);
scheduler.push(.{ .ApuChannel = 1 }, @import("apu/signal/Square.zig").interval);
scheduler.push(.{ .ApuChannel = 2 }, @import("apu/signal/Wave.zig").interval);
scheduler.push(.{ .ApuChannel = 3 }, @import("apu/signal/Lfsr.zig").interval);
scheduler.push(.FrameSequencer, FrameSequencer.interval);
}
/// Used when resetting the emulator
pub fn reset(self: *Self) void {
// FIXME: These reset functions are meant to emulate obscure APU behaviour. Write proper emu reset fns
self.ch1.reset(); self.ch1.reset();
self.ch2.reset(); self.ch2.reset();
self.ch3.reset(); self.ch3.reset();
self.ch4.reset(); self.ch4.reset();
self.chA.reset();
self.chB.reset();
self.psg_cnt = .{ .raw = 0 };
self.dma_cnt = .{ .raw = 0 };
self.cnt = .{ .raw = 0 };
self.bias = .{ .raw = 0x200 };
self.sampling_cycle = 0;
self.fs.reset();
Self.initEvents(self.sched, self.interval());
}
/// Emulates the reset behaviour of the APU
fn _reset(self: *Self) void {
// All PSG Registers between 0x0400_0060..0x0400_0081 are zeroed
// 0x0400_0082 and 0x0400_0088 retain their values
self.ch1.reset();
self.ch2.reset();
self.ch3.reset();
self.ch4.reset();
// GBATEK says 4000060h..4000081h I take this to mean inclusive
self.psg_cnt.raw = 0x0000;
} }
/// SOUNDCNT /// SOUNDCNT
fn setSoundCnt(self: *Self, value: u32) void { fn setSoundCnt(self: *Self, value: u32) void {
if (self.cnt.apu_enable.read()) self.setSoundCntL(@truncate(value)); self.psg_cnt.raw = @truncate(u16, value);
self.setSoundCntH(@truncate(value >> 16)); self.setSoundCntH(@truncate(u16, value >> 16));
}
/// SOUNDCNT_L
pub fn soundCntL(self: *const Self) u16 {
return self.psg_cnt.raw & 0xFF77;
}
/// SOUNDCNT_L
pub fn setSoundCntL(self: *Self, value: u16) void {
self.psg_cnt.raw = value;
} }
/// SOUNDCNT_H /// SOUNDCNT_H
@@ -356,11 +232,6 @@ pub const Apu = struct {
self.dma_cnt = new; self.dma_cnt = new;
} }
/// SOUNDCNT_H
pub fn soundCntH(self: *const Self) u16 {
return self.dma_cnt.raw & 0x770F;
}
/// NR52 /// NR52
pub fn setSoundCntX(self: *Self, value: bool) void { pub fn setSoundCntX(self: *Self, value: bool) void {
self.cnt.apu_enable.write(value); self.cnt.apu_enable.write(value);
@@ -369,31 +240,42 @@ pub const Apu = struct {
self.fs.step = 0; // Reset Frame Sequencer self.fs.step = 0; // Reset Frame Sequencer
// Reset Square Wave Offsets // Reset Square Wave Offsets
self.ch1.square.reset(); self.ch1.square.pos = 0;
self.ch2.square.reset(); self.ch2.square.pos = 0;
// Reset Wave // Reset Wave Device Offsets
self.ch3.wave_dev.reset(); self.ch3.wave_dev.offset = 0;
// Rest Noise
self.ch4.lfsr.reset();
} else { } else {
self._reset(); self.reset();
} }
} }
/// NR52 /// NR52
pub fn soundCntX(self: *const Self) u8 { pub fn soundCntX(self: *const Self) u8 {
const apu_enable: u8 = @intFromBool(self.cnt.apu_enable.read()); const apu_enable: u8 = @boolToInt(self.cnt.apu_enable.read());
const ch1_enable: u8 = @intFromBool(self.ch1.enabled); const ch1_enable: u8 = @boolToInt(self.ch1.enabled);
const ch2_enable: u8 = @intFromBool(self.ch2.enabled); const ch2_enable: u8 = @boolToInt(self.ch2.enabled);
const ch3_enable: u8 = @intFromBool(self.ch3.enabled); const ch3_enable: u8 = @boolToInt(self.ch3.enabled);
const ch4_enable: u8 = @intFromBool(self.ch4.enabled); const ch4_enable: u8 = @boolToInt(self.ch4.enabled);
return apu_enable << 7 | ch4_enable << 3 | ch3_enable << 2 | ch2_enable << 1 | ch1_enable; return apu_enable << 7 | ch4_enable << 3 | ch3_enable << 2 | ch2_enable << 1 | ch1_enable;
} }
/// NR50
pub fn setNr50(self: *Self, byte: u8) void {
self.psg_cnt.raw = (self.psg_cnt.raw & 0xFF00) | byte;
}
/// NR51
pub fn setNr51(self: *Self, byte: u8) void {
self.psg_cnt.raw = @as(u16, byte) << 8 | (self.psg_cnt.raw & 0xFF);
}
pub fn setSoundBiasH(self: *Self, byte: u8) void {
self.bias.raw = (@as(u16, byte) << 8) | (self.bias.raw & 0xFF);
}
pub fn sampleAudio(self: *Self, late: u64) void { pub fn sampleAudio(self: *Self, late: u64) void {
self.sched.push(.SampleAudio, self.interval() -| late); self.sched.push(.SampleAudio, self.interval() -| late);
@@ -445,18 +327,19 @@ pub const Apu = struct {
right += if (self.dma_cnt.chB_right.read()) chB_sample else 0; right += if (self.dma_cnt.chB_right.read()) chB_sample else 0;
// Add SOUNDBIAS // Add SOUNDBIAS
// FIXME: SOUNDBIAS is 10-bit but The waveform is centered around 0 if I treat it as 11-bit // FIXME: Is SOUNDBIAS 9-bit or 10-bit?
const bias = @as(i16, self.bias.level.read()) << 2; const bias = @as(i16, self.bias.level.read()) << 1;
left += bias; left += bias;
right += bias; right += bias;
const clamped_left = std.math.clamp(@as(u16, @bitCast(left)), 0, std.math.maxInt(u11)); const clamped_left = std.math.clamp(@bitCast(u16, left), std.math.minInt(u11), std.math.maxInt(u11));
const clamped_right = std.math.clamp(@as(u16, @bitCast(right)), 0, std.math.maxInt(u11)); const clamped_right = std.math.clamp(@bitCast(u16, right), std.math.minInt(u11), std.math.maxInt(u11));
// Extend to 16-bit signed audio samples // Extend to 16-bit signed audio samples
const ext_left = (clamped_left << 5) | (clamped_left >> 6); const ext_left = (clamped_left << 5) | (clamped_left >> 6);
const ext_right = (clamped_right << 5) | (clamped_right >> 6); const ext_right = (clamped_right << 5) | (clamped_right >> 6);
// FIXME: This rarely happens
if (self.sampling_cycle != self.bias.sampling_cycle.read()) self.replaceSDLResampler(); if (self.sampling_cycle != self.bias.sampling_cycle.read()) self.replaceSDLResampler();
_ = SDL.SDL_AudioStreamPut(self.stream, &[2]u16{ ext_left, ext_right }, 2 * @sizeOf(u16)); _ = SDL.SDL_AudioStreamPut(self.stream, &[2]u16{ ext_left, ext_right }, 2 * @sizeOf(u16));
@@ -473,7 +356,7 @@ pub const Apu = struct {
defer SDL.SDL_FreeAudioStream(old_stream); defer SDL.SDL_FreeAudioStream(old_stream);
self.sampling_cycle = self.bias.sampling_cycle.read(); self.sampling_cycle = self.bias.sampling_cycle.read();
self.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, @intCast(sample_rate), host_format, 2, host_rate).?; self.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, @intCast(c_int, sample_rate), SDL.AUDIO_U16, 2, host_sample_rate).?;
} }
fn interval(self: *const Self) u64 { fn interval(self: *const Self) u64 {
@@ -521,20 +404,14 @@ pub const Apu = struct {
pub fn onDmaAudioSampleRequest(self: *Self, cpu: *Arm7tdmi, tim_id: u3) void { pub fn onDmaAudioSampleRequest(self: *Self, cpu: *Arm7tdmi, tim_id: u3) void {
if (!self.cnt.apu_enable.read()) return; if (!self.cnt.apu_enable.read()) return;
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr)); if (@boolToInt(self.dma_cnt.chA_timer.read()) == tim_id) {
if (@intFromBool(self.dma_cnt.chA_timer.read()) == tim_id) {
if (!self.chA.enabled) return;
self.chA.updateSample(); self.chA.updateSample();
if (self.chA.len() <= 15) bus_ptr.dma[1].requestAudio(0x0400_00A0); if (self.chA.len() <= 15) cpu.bus.dma[1].requestAudio(0x0400_00A0);
} }
if (@intFromBool(self.dma_cnt.chB_timer.read()) == tim_id) { if (@boolToInt(self.dma_cnt.chB_timer.read()) == tim_id) {
if (!self.chB.enabled) return;
self.chB.updateSample(); self.chB.updateSample();
if (self.chB.len() <= 15) bus_ptr.dma[2].requestAudio(0x0400_00A4); if (self.chB.len() <= 15) cpu.bus.dma[2].requestAudio(0x0400_00A4);
} }
} }
}; };
@@ -546,31 +423,17 @@ pub fn DmaSound(comptime kind: DmaSoundKind) type {
fifo: SoundFifo, fifo: SoundFifo,
kind: DmaSoundKind, kind: DmaSoundKind,
sample: i8, sample: i8,
enabled: bool,
fn init() Self { fn init() Self {
return .{ return .{
.fifo = SoundFifo.init(), .fifo = SoundFifo.init(),
.kind = kind, .kind = kind,
.sample = 0, .sample = 0,
.enabled = false,
}; };
} }
/// Used when resetting hte emulator (not emulation code)
fn reset(self: *Self) void {
self.* = Self.init();
}
pub fn push(self: *Self, value: u32) void { pub fn push(self: *Self, value: u32) void {
if (!self.enabled) self.enable(); self.fifo.write(&intToBytes(u32, value)) catch |e| log.err("{} Error: {}", .{ kind, e });
self.fifo.write(std.mem.asBytes(&value)) catch |e| log.err("{} Error: {}", .{ kind, e });
}
fn enable(self: *Self) void {
@setCold(true);
self.enabled = true;
} }
pub fn len(self: *const Self) usize { pub fn len(self: *const Self) usize {
@@ -578,11 +441,11 @@ pub fn DmaSound(comptime kind: DmaSoundKind) type {
} }
pub fn updateSample(self: *Self) void { pub fn updateSample(self: *Self) void {
if (self.fifo.readItem()) |sample| self.sample = @bitCast(sample); if (self.fifo.readItem()) |sample| self.sample = @bitCast(i8, sample);
} }
pub fn amplitude(self: *const Self) i16 { pub fn amplitude(self: *const Self) i16 {
return self.sample; return @as(i16, self.sample);
} }
}; };
} }
@@ -593,17 +456,13 @@ const DmaSoundKind = enum {
}; };
pub const FrameSequencer = struct { pub const FrameSequencer = struct {
const interval = (1 << 24) / 512;
const Self = @This(); const Self = @This();
pub const interval = (1 << 24) / 512;
step: u3 = 0, step: u3,
pub fn init() Self { pub fn init() Self {
return .{}; return .{ .step = 0 };
}
pub fn reset(self: *Self) void {
self.* = .{};
} }
pub fn tick(self: *Self) void { pub fn tick(self: *Self) void {

View File

@@ -49,13 +49,10 @@ pub fn init(sched: *Scheduler) Self {
} }
pub fn reset(self: *Self) void { pub fn reset(self: *Self) void {
self.len = 0; // NR41 self.len = 0;
self.envelope.raw = 0; // NR42 self.envelope.raw = 0;
self.poly.raw = 0; // NR43 self.poly.raw = 0;
self.cnt.raw = 0; // NR44 self.cnt.raw = 0;
self.len_dev.reset();
self.env_dev.reset();
self.sample = 0; self.sample = 0;
self.enabled = false; self.enabled = false;
@@ -76,14 +73,14 @@ pub fn sound4CntL(self: *const Self) u16 {
/// NR41, NR42 /// NR41, NR42
pub fn setSound4CntL(self: *Self, value: u16) void { pub fn setSound4CntL(self: *Self, value: u16) void {
self.setNr41(@truncate(value)); self.setNr41(@truncate(u8, value));
self.setNr42(@truncate(value >> 8)); self.setNr42(@truncate(u8, value >> 8));
} }
/// NR41 /// NR41
pub fn setNr41(self: *Self, len: u8) void { pub fn setNr41(self: *Self, len: u8) void {
self.len = @truncate(len); self.len = @truncate(u6, len);
self.len_dev.timer = @as(u7, 64) - self.len; self.len_dev.timer = @as(u7, 64) - @truncate(u6, len);
} }
/// NR42 /// NR42
@@ -99,8 +96,8 @@ pub fn sound4CntH(self: *const Self) u16 {
/// NR43, NR44 /// NR43, NR44
pub fn setSound4CntH(self: *Self, fs: *const FrameSequencer, value: u16) void { pub fn setSound4CntH(self: *Self, fs: *const FrameSequencer, value: u16) void {
self.poly.raw = @truncate(value); self.poly.raw = @truncate(u8, value);
self.setNr44(fs, @truncate(value >> 8)); self.setNr44(fs, @truncate(u8, value >> 8));
} }
/// NR44 /// NR44

View File

@@ -43,12 +43,9 @@ pub fn init(sched: *Scheduler) Self {
} }
pub fn reset(self: *Self) void { pub fn reset(self: *Self) void {
self.duty.raw = 0; // NR21 self.duty.raw = 0;
self.envelope.raw = 0; // NR22 self.envelope.raw = 0;
self.freq.raw = 0; // NR32, NR24 self.freq.raw = 0;
self.len_dev.reset();
self.env_dev.reset();
self.sample = 0; self.sample = 0;
self.enabled = false; self.enabled = false;
@@ -77,14 +74,14 @@ pub fn sound2CntL(self: *const Self) u16 {
/// NR21, NR22 /// NR21, NR22
pub fn setSound2CntL(self: *Self, value: u16) void { pub fn setSound2CntL(self: *Self, value: u16) void {
self.setNr21(@truncate(value)); self.setNr21(@truncate(u8, value));
self.setNr22(@truncate(value >> 8)); self.setNr22(@truncate(u8, value >> 8));
} }
/// NR21 /// NR21
pub fn setNr21(self: *Self, value: u8) void { pub fn setNr21(self: *Self, value: u8) void {
self.duty.raw = value; self.duty.raw = value;
self.len_dev.timer = @as(u7, 64) - @as(u6, @truncate(value)); self.len_dev.timer = @as(u7, 64) - @truncate(u6, value);
} }
/// NR22 /// NR22
@@ -100,8 +97,8 @@ pub fn sound2CntH(self: *const Self) u16 {
/// NR23, NR24 /// NR23, NR24
pub fn setSound2CntH(self: *Self, fs: *const FrameSequencer, value: u16) void { pub fn setSound2CntH(self: *Self, fs: *const FrameSequencer, value: u16) void {
self.setNr23(@truncate(value)); self.setNr23(@truncate(u8, value));
self.setNr24(fs, @truncate(value >> 8)); self.setNr24(fs, @truncate(u8, value >> 8));
} }
/// NR23 /// NR23

View File

@@ -50,14 +50,12 @@ pub fn init(sched: *Scheduler) Self {
} }
pub fn reset(self: *Self) void { pub fn reset(self: *Self) void {
self.sweep.raw = 0; // NR10 self.sweep.raw = 0;
self.duty.raw = 0; // NR11 self.sweep_dev.calc_performed = false;
self.envelope.raw = 0; // NR12
self.freq.raw = 0; // NR13, NR14
self.len_dev.reset(); self.duty.raw = 0;
self.sweep_dev.reset(); self.envelope.raw = 0;
self.env_dev.reset(); self.freq.raw = 0;
self.sample = 0; self.sample = 0;
self.enabled = false; self.enabled = false;
@@ -81,8 +79,8 @@ pub fn onToneSweepEvent(self: *Self, late: u64) void {
/// NR10, NR11, NR12 /// NR10, NR11, NR12
pub fn setSound1Cnt(self: *Self, value: u32) void { pub fn setSound1Cnt(self: *Self, value: u32) void {
self.setSound1CntL(@truncate(value)); self.setSound1CntL(@truncate(u8, value));
self.setSound1CntH(@truncate(value >> 16)); self.setSound1CntH(@truncate(u16, value >> 16));
} }
/// NR10 /// NR10
@@ -94,9 +92,10 @@ pub fn sound1CntL(self: *const Self) u8 {
pub fn setSound1CntL(self: *Self, value: u8) void { pub fn setSound1CntL(self: *Self, value: u8) void {
const new = io.Sweep{ .raw = value }; const new = io.Sweep{ .raw = value };
if (!new.direction.read()) { if (self.sweep.direction.read() and !new.direction.read()) {
// If at least one (1) sweep calculation has been made with // Sweep Negate bit has been cleared
// the negate bit set (since last trigger), disable the channel // 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; if (self.sweep_dev.calc_performed) self.enabled = false;
} }
@@ -111,14 +110,14 @@ pub fn sound1CntH(self: *const Self) u16 {
/// NR11, NR12 /// NR11, NR12
pub fn setSound1CntH(self: *Self, value: u16) void { pub fn setSound1CntH(self: *Self, value: u16) void {
self.setNr11(@truncate(value)); self.setNr11(@truncate(u8, value));
self.setNr12(@truncate(value >> 8)); self.setNr12(@truncate(u8, value >> 8));
} }
/// NR11 /// NR11
pub fn setNr11(self: *Self, value: u8) void { pub fn setNr11(self: *Self, value: u8) void {
self.duty.raw = value; self.duty.raw = value;
self.len_dev.timer = @as(u7, 64) - @as(u6, @truncate(value)); self.len_dev.timer = @as(u7, 64) - @truncate(u6, value);
} }
/// NR12 /// NR12
@@ -134,8 +133,8 @@ pub fn sound1CntX(self: *const Self) u16 {
/// NR13, NR14 /// NR13, NR14
pub fn setSound1CntX(self: *Self, fs: *const FrameSequencer, value: u16) void { pub fn setSound1CntX(self: *Self, fs: *const FrameSequencer, value: u16) void {
self.setNr13(@truncate(value)); self.setNr13(@truncate(u8, value));
self.setNr14(fs, @truncate(value >> 8)); self.setNr14(fs, @truncate(u8, value >> 8));
} }
/// NR13 /// NR13

View File

@@ -42,13 +42,10 @@ pub fn init(sched: *Scheduler) Self {
} }
pub fn reset(self: *Self) void { pub fn reset(self: *Self) void {
self.select.raw = 0; // NR30 self.select.raw = 0;
self.length = 0; // NR31 self.length = 0;
self.vol.raw = 0; // NR32 self.vol.raw = 0;
self.freq.raw = 0; // NR33, NR34 self.freq.raw = 0;
self.len_dev.reset();
self.wave_dev.reset();
self.sample = 0; self.sample = 0;
self.enabled = false; self.enabled = false;
@@ -64,8 +61,8 @@ pub fn tick(self: *Self, comptime kind: Tick) void {
/// NR30, NR31, NR32 /// NR30, NR31, NR32
pub fn setSound3Cnt(self: *Self, value: u32) void { pub fn setSound3Cnt(self: *Self, value: u32) void {
self.setSound3CntL(@truncate(value)); self.setSound3CntL(@truncate(u8, value));
self.setSound3CntH(@truncate(value >> 16)); self.setSound3CntH(@truncate(u16, value >> 16));
} }
/// NR30 /// NR30
@@ -74,11 +71,6 @@ pub fn setSound3CntL(self: *Self, value: u8) void {
if (!self.select.enabled.read()) self.enabled = false; if (!self.select.enabled.read()) self.enabled = false;
} }
/// NR30
pub fn sound3CntL(self: *const Self) u8 {
return self.select.raw & 0xE0;
}
/// NR31, NR32 /// NR31, NR32
pub fn sound3CntH(self: *const Self) u16 { pub fn sound3CntH(self: *const Self) u16 {
return @as(u16, self.length & 0xE0) << 8; return @as(u16, self.length & 0xE0) << 8;
@@ -86,8 +78,8 @@ pub fn sound3CntH(self: *const Self) u16 {
/// NR31, NR32 /// NR31, NR32
pub fn setSound3CntH(self: *Self, value: u16) void { pub fn setSound3CntH(self: *Self, value: u16) void {
self.setNr31(@truncate(value)); self.setNr31(@truncate(u8, value));
self.vol.raw = @truncate(value >> 8); self.vol.raw = (@truncate(u8, value >> 8));
} }
/// NR31 /// NR31
@@ -98,13 +90,8 @@ pub fn setNr31(self: *Self, len: u8) void {
/// NR33, NR34 /// NR33, NR34
pub fn setSound3CntX(self: *Self, fs: *const FrameSequencer, value: u16) void { pub fn setSound3CntX(self: *Self, fs: *const FrameSequencer, value: u16) void {
self.setNr33(@truncate(value)); self.setNr33(@truncate(u8, value));
self.setNr34(fs, @truncate(value >> 8)); self.setNr34(fs, @truncate(u8, value >> 8));
}
/// NR33, NR34
pub fn sound3CntX(self: *const Self) u16 {
return self.freq.raw & 0x4000;
} }
/// NR33 /// NR33

View File

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

View File

@@ -1,13 +1,9 @@
const Self = @This(); const Self = @This();
timer: u9 = 0, timer: u9,
pub fn create() Self { pub fn create() Self {
return .{}; return .{ .timer = 0 };
}
pub fn reset(self: *Self) void {
self.* = .{};
} }
pub fn tick(self: *Self, enabled: bool, ch_enable: *bool) void { pub fn tick(self: *Self, enabled: bool, ch_enable: *bool) void {

View File

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

View File

@@ -1,7 +1,9 @@
//! Linear Feedback Shift Register
const io = @import("../../bus/io.zig"); const io = @import("../../bus/io.zig");
/// Linear Feedback Shift Register
const Scheduler = @import("../../scheduler.zig").Scheduler; const Scheduler = @import("../../scheduler.zig").Scheduler;
const FrameSequencer = @import("../../apu.zig").FrameSequencer;
const Noise = @import("../Noise.zig");
const Self = @This(); const Self = @This();
pub const interval: u64 = (1 << 24) / (1 << 22); pub const interval: u64 = (1 << 24) / (1 << 22);
@@ -19,11 +21,6 @@ pub fn create(sched: *Scheduler) Self {
}; };
} }
pub fn reset(self: *Self) void {
self.shift = 0;
self.timer = 0;
}
pub fn sample(self: *const Self) i8 { pub fn sample(self: *const Self) i8 {
return if ((~self.shift & 1) == 1) 1 else -1; return if ((~self.shift & 1) == 1) 1 else -1;
} }
@@ -38,7 +35,7 @@ pub fn reload(self: *Self, poly: io.PolyCounter) void {
} }
/// Scheduler Event Handler for LFSR Timer Expire /// Scheduler Event Handler for LFSR Timer Expire
/// FIXME: This gets called a lot, slowing down the scheduler /// FIXME: This gets called a lot, clogging up the Scheduler
pub fn onLfsrTimerExpire(self: *Self, poly: io.PolyCounter, late: u64) void { pub fn onLfsrTimerExpire(self: *Self, poly: io.PolyCounter, late: u64) void {
// Obscure: "Using a noise channel clock shift of 14 or 15 // Obscure: "Using a noise channel clock shift of 14 or 15
// results in the LFSR receiving no clocks." // results in the LFSR receiving no clocks."

View File

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

View File

@@ -2,6 +2,8 @@ const std = @import("std");
const io = @import("../../bus/io.zig"); const io = @import("../../bus/io.zig");
const Scheduler = @import("../../scheduler.zig").Scheduler; const Scheduler = @import("../../scheduler.zig").Scheduler;
const FrameSequencer = @import("../../apu.zig").FrameSequencer;
const Wave = @import("../Wave.zig");
const buf_len = 0x20; const buf_len = 0x20;
pub const interval: u64 = (1 << 24) / (1 << 22); pub const interval: u64 = (1 << 24) / (1 << 22);
@@ -18,7 +20,7 @@ pub fn read(self: *const Self, comptime T: type, nr30: io.WaveSelect, addr: u32)
const base = if (!nr30.bank.read()) @as(u32, 0x10) else 0; // Read from the Opposite Bank in Use const base = if (!nr30.bank.read()) @as(u32, 0x10) else 0; // Read from the Opposite Bank in Use
const i = base + addr - 0x0400_0090; const i = base + addr - 0x0400_0090;
return std.mem.readInt(T, self.buf[i..][0..@sizeOf(T)], .little); 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 { pub fn write(self: *Self, comptime T: type, nr30: io.WaveSelect, addr: u32, value: T) void {
@@ -26,7 +28,7 @@ pub fn write(self: *Self, comptime T: type, nr30: io.WaveSelect, addr: u32, valu
const base = if (!nr30.bank.read()) @as(u32, 0x10) else 0; // Write to the Opposite Bank in Use const base = if (!nr30.bank.read()) @as(u32, 0x10) else 0; // Write to the Opposite Bank in Use
const i = base + addr - 0x0400_0090; const i = base + addr - 0x0400_0090;
std.mem.writeInt(T, self.buf[i..][0..@sizeOf(T)], value, .little); std.mem.writeIntSliceLittle(T, self.buf[i..][0..@sizeOf(T)], value);
} }
pub fn init(sched: *Scheduler) Self { pub fn init(sched: *Scheduler) Self {
@@ -38,13 +40,6 @@ pub fn init(sched: *Scheduler) Self {
}; };
} }
pub fn reset(self: *Self) void {
self.timer = 0;
self.offset = 0;
// sample buffer isn't reset because it's outside of the range of what NR52{7}'s effects
}
/// Reload internal Wave Timer /// Reload internal Wave Timer
pub fn reload(self: *Self, value: u11) void { pub fn reload(self: *Self, value: u11) void {
self.sched.removeScheduledEvent(.{ .ApuChannel = 2 }); self.sched.removeScheduledEvent(.{ .ApuChannel = 2 });
@@ -70,7 +65,7 @@ pub fn sample(self: *const Self, nr30: io.WaveSelect) u4 {
const base = if (nr30.bank.read()) @as(u32, 0x10) else 0; const base = if (nr30.bank.read()) @as(u32, 0x10) else 0;
const value = self.buf[base + self.offset / 2]; const value = self.buf[base + self.offset / 2];
return if (self.offset & 1 == 0) @truncate(value >> 4) else @truncate(value); return if (self.offset & 1 == 0) @truncate(u4, value >> 4) else @truncate(u4, value);
} }
/// TODO: Write comment /// TODO: Write comment

View File

@@ -3,9 +3,6 @@ const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const log = std.log.scoped(.Bios); const log = std.log.scoped(.Bios);
const rotr = @import("zba-util").rotr;
const forceAlign = @import("../Bus.zig").forceAlign;
/// Size of the BIOS in bytes /// Size of the BIOS in bytes
pub const size = 0x4000; pub const size = 0x4000;
const Self = @This(); const Self = @This();
@@ -13,37 +10,21 @@ const Self = @This();
buf: ?[]u8, buf: ?[]u8,
allocator: Allocator, allocator: Allocator,
addr_latch: u32 = 0, addr_latch: u32,
// https://github.com/ITotalJustice/notorious_beeg/issues/106 pub fn read(self: *Self, comptime T: type, r15: u32, addr: u32) T {
pub fn read(self: *Self, comptime T: type, r15: u32, address: u32) T {
if (r15 < Self.size) { if (r15 < Self.size) {
const addr = forceAlign(T, address);
self.addr_latch = addr; self.addr_latch = addr;
return self._read(T, addr); return self._read(T, addr);
} }
log.warn("Open Bus! Read from 0x{X:0>8}, but PC was 0x{X:0>8}", .{ address, r15 }); log.debug("Rejected read since r15=0x{X:0>8}", .{r15});
const value = self._read(u32, self.addr_latch); return @truncate(T, self._read(T, self.addr_latch + 8));
return @truncate(rotr(u32, value, 8 * rotateBy(T, address)));
} }
fn rotateBy(comptime T: type, address: u32) u32 { pub fn dbgRead(self: *const Self, comptime T: type, r15: u32, addr: u32) T {
return switch (T) { if (r15 < Self.size) return self._read(T, addr);
u8 => address & 3, return @truncate(T, self._read(T, self.addr_latch + 8));
u16 => address & 2,
u32 => 0,
else => @compileError("bios: unsupported read width"),
};
}
pub fn dbgRead(self: *const Self, comptime T: type, r15: u32, address: u32) T {
if (r15 < Self.size) return self._read(T, forceAlign(T, address));
const value = self._read(u32, self.addr_latch);
return @truncate(rotr(u32, value, 8 * rotateBy(T, address)));
} }
/// Read without the GBA safety checks /// Read without the GBA safety checks
@@ -51,7 +32,7 @@ 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 }); 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) { return switch (T) {
u32, u16, u8 => std.mem.readInt(T, buf[addr..][0..@sizeOf(T)], .little), u32, u16, u8 => std.mem.readIntSliceLittle(T, buf[addr..][0..@sizeOf(T)]),
else => @compileError("BIOS: Unsupported read width"), else => @compileError("BIOS: Unsupported read width"),
}; };
} }
@@ -62,28 +43,18 @@ pub fn write(_: *Self, comptime T: type, addr: u32, value: T) void {
} }
pub fn init(allocator: Allocator, maybe_path: ?[]const u8) !Self { pub fn init(allocator: Allocator, maybe_path: ?[]const u8) !Self {
if (maybe_path == null) return .{ .buf = null, .allocator = allocator }; const buf: ?[]u8 = if (maybe_path) |path| blk: {
const file_path = maybe_path.?; const file = try std.fs.cwd().openFile(path, .{});
const buf = try allocator.alloc(u8, Self.size);
errdefer allocator.free(buf);
var self: Self = .{ .buf = buf, .allocator = allocator };
try self.load(file_path);
return self;
}
pub fn load(self: *Self, file_path: []const u8) !void {
const file = try std.fs.cwd().openFile(file_path, .{});
defer file.close(); defer file.close();
const len = try file.readAll(self.buf orelse return error.UnallocatedBuffer); break :blk try file.readToEndAlloc(allocator, try file.getEndPos());
if (len != Self.size) log.err("Expected BIOS to be {}B, was {}B", .{ Self.size, len }); } else null;
}
pub fn reset(self: *Self) void { return Self{
self.addr_latch = 0; .buf = buf,
.allocator = allocator,
.addr_latch = 0,
};
} }
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {

View File

@@ -11,7 +11,7 @@ pub fn read(self: *const Self, comptime T: type, address: usize) T {
const addr = address & 0x3FFFF; const addr = address & 0x3FFFF;
return switch (T) { return switch (T) {
u32, u16, u8 => std.mem.readInt(T, self.buf[addr..][0..@sizeOf(T)], .little), u32, u16, u8 => std.mem.readIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)]),
else => @compileError("EWRAM: Unsupported read width"), else => @compileError("EWRAM: Unsupported read width"),
}; };
} }
@@ -20,14 +20,14 @@ pub fn write(self: *const Self, comptime T: type, address: usize, value: T) void
const addr = address & 0x3FFFF; const addr = address & 0x3FFFF;
return switch (T) { return switch (T) {
u32, u16, u8 => std.mem.writeInt(T, self.buf[addr..][0..@sizeOf(T)], value, .little), u32, u16, u8 => std.mem.writeIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)], value),
else => @compileError("EWRAM: Unsupported write width"), else => @compileError("EWRAM: Unsupported write width"),
}; };
} }
pub fn init(allocator: Allocator) !Self { pub fn init(allocator: Allocator) !Self {
const buf = try allocator.alloc(u8, ewram_size); const buf = try allocator.alloc(u8, ewram_size);
@memset(buf, 0); std.mem.set(u8, buf, 0);
return Self{ return Self{
.buf = buf, .buf = buf,
@@ -35,10 +35,6 @@ pub fn init(allocator: Allocator) !Self {
}; };
} }
pub fn reset(self: *Self) void {
@memset(self.buf, 0);
}
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
self.allocator.free(self.buf); self.allocator.free(self.buf);
self.* = undefined; self.* = undefined;

View File

@@ -1,7 +1,11 @@
const std = @import("std"); const std = @import("std");
const config = @import("../../config.zig"); const config = @import("../../config.zig");
const Arm7tdmi = @import("arm32").Arm7tdmi; const Bit = @import("bitfield").Bit;
const Bitfield = @import("bitfield").Bitfield;
const DateTime = @import("datetime").datetime.Datetime;
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
const Backup = @import("backup.zig").Backup; const Backup = @import("backup.zig").Backup;
const Gpio = @import("gpio.zig").Gpio; const Gpio = @import("gpio.zig").Gpio;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
@@ -30,7 +34,7 @@ pub fn read(self: *Self, comptime T: type, address: u32) T {
// Addresses 0x0D00_0000 to 0x0DFF_FFFF are reserved for EEPROM accesses if // Addresses 0x0D00_0000 to 0x0DFF_FFFF are reserved for EEPROM accesses if
// * Backup type is EEPROM // * Backup type is EEPROM
// * Small ROM (less than 16MB) // * Small ROM (less than 16MB)
if (@as(u8, @truncate(address >> 24)) == 0x0D) if (@truncate(u8, address >> 24) == 0x0D)
return self.backup.eeprom.read(); return self.backup.eeprom.read();
} }
} }
@@ -77,7 +81,7 @@ inline fn get(self: *const Self, i: u32) u8 {
if (i < self.buf.len) return self.buf[i]; if (i < self.buf.len) return self.buf[i];
const lhs = i >> 1 & 0xFFFF; const lhs = i >> 1 & 0xFFFF;
return @truncate(lhs >> 8 * @as(u5, @truncate(i & 1))); return @truncate(u8, lhs >> 8 * @truncate(u5, i & 1));
} }
pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T { pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
@@ -94,7 +98,7 @@ pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
// Addresses 0x0D00_0000 to 0x0DFF_FFFF are reserved for EEPROM accesses if // Addresses 0x0D00_0000 to 0x0DFF_FFFF are reserved for EEPROM accesses if
// * Backup type is EEPROM // * Backup type is EEPROM
// * Small ROM (less than 16MB) // * Small ROM (less than 16MB)
if (@as(u8, @truncate(address >> 24)) == 0x0D) if (@truncate(u8, address >> 24) == 0x0D)
return self.backup.eeprom.dbgRead(); return self.backup.eeprom.dbgRead();
} }
} }
@@ -105,13 +109,14 @@ pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
switch (T) { switch (T) {
u32 => switch (address) { u32 => switch (address) {
// FIXME: Do I even need to implement these? // TODO: Do I even need to implement these?
0x0800_00C4 => std.debug.panic("Handle 32-bit GPIO Data/Direction Reads", .{}), 0x0800_00C4 => std.debug.panic("Handle 32-bit GPIO Data/Direction Reads", .{}),
0x0800_00C6 => std.debug.panic("Handle 32-bit GPIO Direction/Control Reads", .{}), 0x0800_00C6 => std.debug.panic("Handle 32-bit GPIO Direction/Control Reads", .{}),
0x0800_00C8 => std.debug.panic("Handle 32-bit GPIO Control Reads", .{}), 0x0800_00C8 => std.debug.panic("Handle 32-bit GPIO Control Reads", .{}),
else => {}, else => {},
}, },
u16 => switch (address) { u16 => switch (address) {
// FIXME: What do 16-bit GPIO Reads look like?
0x0800_00C4 => return self.gpio.read(.Data), 0x0800_00C4 => return self.gpio.read(.Data),
0x0800_00C6 => return self.gpio.read(.Direction), 0x0800_00C6 => return self.gpio.read(.Direction),
0x0800_00C8 => return self.gpio.read(.Control), 0x0800_00C8 => return self.gpio.read(.Control),
@@ -139,7 +144,7 @@ pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value
const addr = address & 0x1FF_FFFF; const addr = address & 0x1FF_FFFF;
if (self.backup.kind == .Eeprom) { if (self.backup.kind == .Eeprom) {
const bit: u1 = @truncate(value); const bit = @truncate(u1, value);
if (self.buf.len > 0x100_0000) { // Large if (self.buf.len > 0x100_0000) { // Large
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if // Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
@@ -151,7 +156,7 @@ pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value
// Addresses 0x0D00_0000 to 0x0DFF_FFFF are reserved for EEPROM accesses if // Addresses 0x0D00_0000 to 0x0DFF_FFFF are reserved for EEPROM accesses if
// * Backup type is EEPROM // * Backup type is EEPROM
// * Small ROM (less than 16MB) // * Small ROM (less than 16MB)
if (@as(u8, @truncate(address >> 24)) == 0x0D) if (@truncate(u8, address >> 24) == 0x0D)
return self.backup.eeprom.write(word_count, &self.backup.buf, bit); return self.backup.eeprom.write(word_count, &self.backup.buf, bit);
} }
} }
@@ -159,19 +164,19 @@ pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value
switch (T) { switch (T) {
u32 => switch (address) { u32 => switch (address) {
0x0800_00C4 => { 0x0800_00C4 => {
self.gpio.write(.Data, @as(u4, @truncate(value))); self.gpio.write(.Data, @truncate(u4, value));
self.gpio.write(.Direction, @as(u4, @truncate(value >> 16))); self.gpio.write(.Direction, @truncate(u4, value >> 16));
}, },
0x0800_00C6 => { 0x0800_00C6 => {
self.gpio.write(.Direction, @as(u4, @truncate(value))); self.gpio.write(.Direction, @truncate(u4, value));
self.gpio.write(.Control, @as(u1, @truncate(value >> 16))); self.gpio.write(.Control, @truncate(u1, value >> 16));
}, },
else => log.err("Wrote {} 0x{X:0>8} to 0x{X:0>8}, Unhandled", .{ T, value, address }), else => log.err("Wrote {} 0x{X:0>8} to 0x{X:0>8}, Unhandled", .{ T, value, address }),
}, },
u16 => switch (address) { u16 => switch (address) {
0x0800_00C4 => self.gpio.write(.Data, @as(u4, @truncate(value))), 0x0800_00C4 => self.gpio.write(.Data, @truncate(u4, value)),
0x0800_00C6 => self.gpio.write(.Direction, @as(u4, @truncate(value))), 0x0800_00C6 => self.gpio.write(.Direction, @truncate(u4, value)),
0x0800_00C8 => self.gpio.write(.Control, @as(u1, @truncate(value))), 0x0800_00C8 => self.gpio.write(.Control, @truncate(u1, value)),
else => log.err("Wrote {} 0x{X:0>4} to 0x{X:0>8}, Unhandled", .{ T, value, address }), else => log.err("Wrote {} 0x{X:0>4} to 0x{X:0>8}, Unhandled", .{ T, value, address }),
}, },
u8 => log.debug("Wrote {} 0x{X:0>2} to 0x{X:0>8}, Ignored.", .{ T, value, address }), u8 => log.debug("Wrote {} 0x{X:0>2} to 0x{X:0>8}, Ignored.", .{ T, value, address }),
@@ -179,30 +184,23 @@ pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value
} }
} }
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, maybe_rom: ?[]const u8, maybe_save: ?[]const u8) !Self { pub fn init(allocator: Allocator, cpu: *Arm7tdmi, rom_path: []const u8, save_path: ?[]const u8) !Self {
const Device = Gpio.Device; const file = try std.fs.cwd().openFile(rom_path, .{});
const items: struct { []u8, [12]u8, Backup.Kind, Device.Kind } = if (maybe_rom) |file_path| blk: {
const file = try std.fs.cwd().openFile(file_path, .{});
defer file.close(); defer file.close();
const buffer = try file.readToEndAlloc(allocator, try file.getEndPos()); const file_buf = try file.readToEndAlloc(allocator, try file.getEndPos());
const title = buffer[0xA0..0xAC]; const title = file_buf[0xA0..0xAC].*;
logHeader(buffer, title); const kind = Backup.guess(file_buf);
const device = if (config.config().guest.force_rtc) .Rtc else guessDevice(file_buf);
const device_kind = if (config.config().guest.force_rtc) .Rtc else guessDevice(buffer); logHeader(file_buf, &title);
break :blk .{ buffer, title.*, Backup.guess(buffer), device_kind };
} else .{ try allocator.alloc(u8, 0), [_]u8{0} ** 12, .None, .None };
const title = items[1];
return .{ return .{
.buf = items[0], .buf = file_buf,
.allocator = allocator, .allocator = allocator,
.title = title, .title = title,
.backup = try Backup.init(allocator, items[2], title, maybe_save), .backup = try Backup.init(allocator, kind, title, save_path),
.gpio = try Gpio.init(allocator, cpu, items[3]), .gpio = try Gpio.init(allocator, cpu, device),
}; };
} }
@@ -220,24 +218,25 @@ fn guessDevice(buf: []const u8) Gpio.Device.Kind {
// Try to Guess if ROM uses RTC // Try to Guess if ROM uses RTC
const needle = "RTC_V"; // I was told SIIRTC_V, though Pokemen Firered (USA) is a false negative const needle = "RTC_V"; // I was told SIIRTC_V, though Pokemen Firered (USA) is a false negative
// TODO: Use new for loop syntax?
var i: usize = 0; var i: usize = 0;
while ((i + needle.len) < buf.len) : (i += 1) { while ((i + needle.len) < buf.len) : (i += 1) {
if (std.mem.eql(u8, needle, buf[i..(i + needle.len)])) return .Rtc; if (std.mem.eql(u8, needle, buf[i..(i + needle.len)])) return .Rtc;
} }
// TODO: Detect other GPIO devices // TODO: Detect other GPIO devices
return .None; return .None;
} }
fn logHeader(buf: []const u8, title: *const [12]u8) void { fn logHeader(buf: []const u8, title: *const [12]u8) void {
const code = buf[0xAC..0xB0];
const maker = buf[0xB0..0xB2];
const version = buf[0xBC]; const version = buf[0xBC];
log.info("Title: {s}", .{title}); log.info("Title: {s}", .{title});
if (version != 0) log.info("Version: {}", .{version}); if (version != 0) log.info("Version: {}", .{version});
log.info("Game Code: {s}", .{code});
log.info("Game Code: {s}", .{buf[0xAC..0xB0]}); log.info("Maker Code: {s}", .{maker});
log.info("Maker Code: {s}", .{buf[0xB0..0xB2]});
} }
test "OOB Access" { test "OOB Access" {

View File

@@ -11,7 +11,7 @@ pub fn read(self: *const Self, comptime T: type, address: usize) T {
const addr = address & 0x7FFF; const addr = address & 0x7FFF;
return switch (T) { return switch (T) {
u32, u16, u8 => std.mem.readInt(T, self.buf[addr..][0..@sizeOf(T)], .little), u32, u16, u8 => std.mem.readIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)]),
else => @compileError("IWRAM: Unsupported read width"), else => @compileError("IWRAM: Unsupported read width"),
}; };
} }
@@ -20,14 +20,14 @@ pub fn write(self: *const Self, comptime T: type, address: usize, value: T) void
const addr = address & 0x7FFF; const addr = address & 0x7FFF;
return switch (T) { return switch (T) {
u32, u16, u8 => std.mem.writeInt(T, self.buf[addr..][0..@sizeOf(T)], value, .little), u32, u16, u8 => std.mem.writeIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)], value),
else => @compileError("IWRAM: Unsupported write width"), else => @compileError("IWRAM: Unsupported write width"),
}; };
} }
pub fn init(allocator: Allocator) !Self { pub fn init(allocator: Allocator) !Self {
const buf = try allocator.alloc(u8, iwram_size); const buf = try allocator.alloc(u8, iwram_size);
@memset(buf, 0); std.mem.set(u8, buf, 0);
return Self{ return Self{
.buf = buf, .buf = buf,
@@ -35,10 +35,6 @@ pub fn init(allocator: Allocator) !Self {
}; };
} }
pub fn reset(self: *Self) void {
@memset(self.buf, 0);
}
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
self.allocator.free(self.buf); self.allocator.free(self.buf);
self.* = undefined; self.* = undefined;

View File

@@ -6,6 +6,7 @@ const Eeprom = @import("backup/eeprom.zig").Eeprom;
const Flash = @import("backup/Flash.zig"); const Flash = @import("backup/Flash.zig");
const escape = @import("../../util.zig").escape; const escape = @import("../../util.zig").escape;
const span = @import("../../util.zig").span;
const Needle = struct { str: []const u8, kind: Backup.Kind }; const Needle = struct { str: []const u8, kind: Backup.Kind };
const backup_kinds = [6]Needle{ const backup_kinds = [6]Needle{
@@ -32,7 +33,7 @@ pub const Backup = struct {
flash: Flash, flash: Flash,
eeprom: Eeprom, eeprom: Eeprom,
pub const Kind = enum { const Kind = enum {
Eeprom, Eeprom,
Sram, Sram,
Flash, Flash,
@@ -77,7 +78,7 @@ pub const Backup = struct {
switch (addr) { switch (addr) {
0x0000 => if (self.kind == .Flash1M and self.flash.set_bank) { 0x0000 => if (self.kind == .Flash1M and self.flash.set_bank) {
self.flash.bank = @truncate(byte); self.flash.bank = @truncate(u1, byte);
}, },
0x5555 => { 0x5555 => {
if (self.flash.state == .Command) { if (self.flash.state == .Command) {
@@ -110,7 +111,7 @@ pub const Backup = struct {
}; };
const buf = try allocator.alloc(u8, buf_size); const buf = try allocator.alloc(u8, buf_size);
@memset(buf, 0xFF); std.mem.set(u8, buf, 0xFF);
var backup = Self{ var backup = Self{
.buf = buf, .buf = buf,
@@ -137,7 +138,6 @@ pub const Backup = struct {
for (backup_kinds) |needle| { for (backup_kinds) |needle| {
const needle_len = needle.str.len; const needle_len = needle.str.len;
// TODO: Use new for loop syntax?
var i: usize = 0; var i: usize = 0;
while ((i + needle_len) < rom.len) : (i += 1) { while ((i + needle_len) < rom.len) : (i += 1) {
if (std.mem.eql(u8, needle.str, rom[i..][0..needle_len])) return needle.kind; if (std.mem.eql(u8, needle.str, rom[i..][0..needle_len])) return needle.kind;
@@ -151,8 +151,8 @@ pub const Backup = struct {
const file_path = try self.savePath(allocator, path); const file_path = try self.savePath(allocator, path);
defer allocator.free(file_path); defer allocator.free(file_path);
const expected = "untitled.sav"; // FIXME: Don't rely on this lol
if (std.mem.eql(u8, file_path[file_path.len - expected.len .. file_path.len], expected)) { if (std.mem.eql(u8, file_path[file_path.len - 12 .. file_path.len], "untitled.sav")) {
return log.err("ROM header lacks title, no save loaded", .{}); return log.err("ROM header lacks title, no save loaded", .{});
} }
@@ -163,7 +163,7 @@ pub const Backup = struct {
switch (self.kind) { switch (self.kind) {
.Sram, .Flash, .Flash1M => { .Sram, .Flash, .Flash1M => {
if (self.buf.len == file_buf.len) { if (self.buf.len == file_buf.len) {
@memcpy(self.buf, file_buf); std.mem.copy(u8, self.buf, file_buf);
return log.info("Loaded Save from {s}", .{file_path}); return log.info("Loaded Save from {s}", .{file_path});
} }
@@ -174,7 +174,7 @@ pub const Backup = struct {
self.eeprom.kind = if (file_buf.len == 0x200) .Small else .Large; self.eeprom.kind = if (file_buf.len == 0x200) .Small else .Large;
self.buf = try allocator.alloc(u8, file_buf.len); self.buf = try allocator.alloc(u8, file_buf.len);
@memcpy(self.buf, file_buf); std.mem.copy(u8, self.buf, file_buf);
return log.info("Loaded Save from {s}", .{file_path}); return log.info("Loaded Save from {s}", .{file_path});
} }
@@ -195,7 +195,7 @@ pub const Backup = struct {
} }
fn saveName(self: *const Self, allocator: Allocator) ![]const u8 { fn saveName(self: *const Self, allocator: Allocator) ![]const u8 {
const title_str = std.mem.sliceTo(&escape(self.title), 0); const title_str = span(&escape(self.title));
const name = if (title_str.len != 0) title_str else "untitled"; const name = if (title_str.len != 0) title_str else "untitled";
return try std.mem.concat(allocator, u8, &[_][]const u8{ name, ".sav" }); return try std.mem.concat(allocator, u8, &[_][]const u8{ name, ".sav" });

View File

@@ -44,7 +44,7 @@ pub fn handleCommand(self: *Self, buf: []u8, byte: u8) void {
0xB0 => self.set_bank = true, 0xB0 => self.set_bank = true,
0x80 => self.prep_erase = true, 0x80 => self.prep_erase = true,
0x10 => { 0x10 => {
@memset(buf, 0xFF); std.mem.set(u8, buf, 0xFF);
self.prep_erase = false; self.prep_erase = false;
}, },
0xA0 => self.prep_write = true, 0xA0 => self.prep_write = true,
@@ -61,7 +61,7 @@ pub fn shouldEraseSector(self: *const Self, addr: usize, byte: u8) bool {
pub fn erase(self: *Self, buf: []u8, sector: usize) void { pub fn erase(self: *Self, buf: []u8, sector: usize) void {
const start = self.address() + (sector & 0xF000); const start = self.address() + (sector & 0xF000);
@memset(buf[start..][0..0x1000], 0xFF); std.mem.set(u8, buf[start..][0..0x1000], 0xFF);
self.prep_erase = false; self.prep_erase = false;
self.state = .Ready; self.state = .Ready;
} }

View File

@@ -58,14 +58,12 @@ pub const Eeprom = struct {
log.err("Failed to resize EEPROM buf to {} bytes", .{len}); log.err("Failed to resize EEPROM buf to {} bytes", .{len});
std.debug.panic("EEPROM entered irrecoverable state {}", .{e}); std.debug.panic("EEPROM entered irrecoverable state {}", .{e});
}; };
std.mem.set(u8, buf.*, 0xFF);
// FIXME: ptr to a slice?
@memset(buf.*, 0xFF);
} }
} }
if (self.state == .RequestEnd) { if (self.state == .RequestEnd) {
// if (bit != 0) log.debug("EEPROM Request did not end in 0u1. TODO: is this ok?", .{}); if (bit != 0) log.debug("EEPROM Request did not end in 0u1. TODO: is this ok?", .{});
self.state = .Ready; self.state = .Ready;
return; return;
} }
@@ -108,7 +106,7 @@ pub const Eeprom = struct {
switch (self.state) { switch (self.state) {
.Ready => { .Ready => {
if (self.writer.len() == 2) { if (self.writer.len() == 2) {
const req: u2 = @intCast(self.writer.finish()); const req = @intCast(u2, self.writer.finish());
switch (req) { switch (req) {
0b11 => self.state = .Read, 0b11 => self.state = .Read,
0b10 => self.state = .Write, 0b10 => self.state = .Write,
@@ -120,8 +118,8 @@ pub const Eeprom = struct {
switch (self.kind) { switch (self.kind) {
.Large => { .Large => {
if (self.writer.len() == 14) { if (self.writer.len() == 14) {
const addr: u10 = @intCast(self.writer.finish()); const addr = @intCast(u10, self.writer.finish());
const value = std.mem.readInt(u64, buf[@as(u13, addr) * 8 ..][0..8], .little); const value = std.mem.readIntSliceLittle(u64, buf[@as(u13, addr) * 8 ..][0..8]);
self.reader.configure(value); self.reader.configure(value);
self.state = .RequestEnd; self.state = .RequestEnd;
@@ -130,8 +128,8 @@ pub const Eeprom = struct {
.Small => { .Small => {
if (self.writer.len() == 6) { if (self.writer.len() == 6) {
// FIXME: Duplicated code from above // FIXME: Duplicated code from above
const addr: u6 = @intCast(self.writer.finish()); const addr = @intCast(u6, self.writer.finish());
const value = std.mem.readInt(u64, buf[@as(u13, addr) * 8 ..][0..8], .little); const value = std.mem.readIntSliceLittle(u64, buf[@as(u13, addr) * 8 ..][0..8]);
self.reader.configure(value); self.reader.configure(value);
self.state = .RequestEnd; self.state = .RequestEnd;
@@ -144,13 +142,13 @@ pub const Eeprom = struct {
switch (self.kind) { switch (self.kind) {
.Large => { .Large => {
if (self.writer.len() == 14) { if (self.writer.len() == 14) {
self.addr = @as(u10, @intCast(self.writer.finish())); self.addr = @intCast(u10, self.writer.finish());
self.state = .WriteTransfer; self.state = .WriteTransfer;
} }
}, },
.Small => { .Small => {
if (self.writer.len() == 6) { if (self.writer.len() == 6) {
self.addr = @as(u6, @intCast(self.writer.finish())); self.addr = @intCast(u6, self.writer.finish());
self.state = .WriteTransfer; self.state = .WriteTransfer;
} }
}, },
@@ -159,7 +157,7 @@ pub const Eeprom = struct {
}, },
.WriteTransfer => { .WriteTransfer => {
if (self.writer.len() == 64) { if (self.writer.len() == 64) {
std.mem.writeInt(u64, buf[self.addr * 8 ..][0..8], self.writer.finish(), .little); std.mem.writeIntSliceLittle(u64, buf[self.addr * 8 ..][0..8], self.writer.finish());
self.state = .RequestEnd; self.state = .RequestEnd;
} }
}, },
@@ -186,9 +184,11 @@ const Reader = struct {
fn read(self: *Self) u1 { fn read(self: *Self) u1 {
if (!self.enabled) return 1; if (!self.enabled) return 1;
const bit: u1 = if (self.i < 4) 0 else blk: { const bit = if (self.i < 4) blk: {
const idx: u6 = @intCast(63 - (self.i - 4)); break :blk 0;
break :blk @truncate(self.data >> idx); } else blk: {
const idx = @intCast(u6, 63 - (self.i - 4));
break :blk @truncate(u1, self.data >> idx);
}; };
self.i = (self.i + 1) % (64 + 4); self.i = (self.i + 1) % (64 + 4);
@@ -200,11 +200,11 @@ const Reader = struct {
fn dbgRead(self: *const Self) u1 { fn dbgRead(self: *const Self) u1 {
if (!self.enabled) return 1; if (!self.enabled) return 1;
const bit: u1 = if (self.i < 4) blk: { const bit = if (self.i < 4) blk: {
break :blk 0; break :blk 0;
} else blk: { } else blk: {
const idx: u6 = @intCast(63 - (self.i - 4)); const idx = @intCast(u6, 63 - (self.i - 4));
break :blk @truncate(self.data >> idx); break :blk @truncate(u1, self.data >> idx);
}; };
return bit; return bit;
@@ -228,7 +228,7 @@ const Writer = struct {
} }
fn requestWrite(self: *Self, bit: u1) void { fn requestWrite(self: *Self, bit: u1) void {
const idx: u1 = @intCast(1 - self.i); const idx = @intCast(u1, 1 - self.i);
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx); self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
self.i += 1; self.i += 1;
} }
@@ -242,13 +242,13 @@ const Writer = struct {
.Unknown => unreachable, .Unknown => unreachable,
}; };
const idx: u4 = @intCast(size - self.i); const idx = @intCast(u4, size - self.i);
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx); self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
self.i += 1; self.i += 1;
} }
fn dataWrite(self: *Self, bit: u1) void { fn dataWrite(self: *Self, bit: u1) void {
const idx: u6 = @intCast(63 - self.i); const idx = @intCast(u6, 63 - self.i);
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx); self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
self.i += 1; self.i += 1;
} }

View File

@@ -3,143 +3,91 @@ const util = @import("../../util.zig");
const DmaControl = @import("io.zig").DmaControl; const DmaControl = @import("io.zig").DmaControl;
const Bus = @import("../Bus.zig"); const Bus = @import("../Bus.zig");
const Arm7tdmi = @import("arm32").Arm7tdmi; const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
pub const DmaTuple = struct { DmaController(0), DmaController(1), DmaController(2), DmaController(3) }; pub const DmaTuple = std.meta.Tuple(&[_]type{ DmaController(0), DmaController(1), DmaController(2), DmaController(3) });
const log = std.log.scoped(.DmaTransfer); const log = std.log.scoped(.DmaTransfer);
const getHalf = util.getHalf; const setHi = util.setHi;
const setHalf = util.setHalf; const setLo = util.setLo;
const setQuart = util.setQuart;
const handleInterrupt = @import("../cpu_util.zig").handleInterrupt;
const rotr = @import("zba-util").rotr;
pub fn create() DmaTuple { pub fn create() DmaTuple {
return .{ DmaController(0).init(), DmaController(1).init(), DmaController(2).init(), DmaController(3).init() }; return .{ DmaController(0).init(), DmaController(1).init(), DmaController(2).init(), DmaController(3).init() };
} }
pub fn read(comptime T: type, dma: *const DmaTuple, addr: u32) ?T { pub fn read(comptime T: type, dma: *const DmaTuple, addr: u32) ?T {
const byte_addr: u8 = @truncate(addr); const byte = @truncate(u8, addr);
return switch (T) { return switch (T) {
u32 => switch (byte_addr) { u32 => switch (byte) {
0xB0, 0xB4 => null, // DMA0SAD, DMA0DAD, 0xB8 => @as(T, dma.*[0].cnt.raw) << 16,
0xB8 => @as(T, dma.*[0].dmacntH()) << 16, // DMA0CNT_L is write-only 0xC4 => @as(T, dma.*[1].cnt.raw) << 16,
0xBC, 0xC0 => null, // DMA1SAD, DMA1DAD 0xD0 => @as(T, dma.*[2].cnt.raw) << 16,
0xC4 => @as(T, dma.*[1].dmacntH()) << 16, // DMA1CNT_L is write-only 0xDC => @as(T, dma.*[3].cnt.raw) << 16,
0xC8, 0xCC => null, // DMA2SAD, DMA2DAD else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
0xD0 => @as(T, dma.*[2].dmacntH()) << 16, // DMA2CNT_L is write-only
0xD4, 0xD8 => null, // DMA3SAD, DMA3DAD
0xDC => @as(T, dma.*[3].dmacntH()) << 16, // DMA3CNT_L is write-only
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
}, },
u16 => switch (byte_addr) { u16 => switch (byte) {
0xB0, 0xB2, 0xB4, 0xB6 => null, // DMA0SAD, DMA0DAD 0xBA => dma.*[0].cnt.raw,
0xB8 => 0x0000, // DMA0CNT_L, suite.gba expects 0x0000 instead of 0xDEAD 0xC6 => dma.*[1].cnt.raw,
0xBA => dma.*[0].dmacntH(), 0xD2 => dma.*[2].cnt.raw,
0xDE => dma.*[3].cnt.raw,
0xBC, 0xBE, 0xC0, 0xC2 => null, // DMA1SAD, DMA1DAD else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
0xC4 => 0x0000, // DMA1CNT_L
0xC6 => dma.*[1].dmacntH(),
0xC8, 0xCA, 0xCC, 0xCE => null, // DMA2SAD, DMA2DAD
0xD0 => 0x0000, // DMA2CNT_L
0xD2 => dma.*[2].dmacntH(),
0xD4, 0xD6, 0xD8, 0xDA => null, // DMA3SAD, DMA3DAD
0xDC => 0x0000, // DMA3CNT_L
0xDE => dma.*[3].dmacntH(),
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
},
u8 => switch (byte_addr) {
0xB0...0xB7 => null, // DMA0SAD, DMA0DAD
0xB8, 0xB9 => 0x00, // DMA0CNT_L
0xBA, 0xBB => @truncate(dma.*[0].dmacntH() >> getHalf(byte_addr)),
0xBC...0xC3 => null, // DMA1SAD, DMA1DAD
0xC4, 0xC5 => 0x00, // DMA1CNT_L
0xC6, 0xC7 => @truncate(dma.*[1].dmacntH() >> getHalf(byte_addr)),
0xC8...0xCF => null, // DMA2SAD, DMA2DAD
0xD0, 0xD1 => 0x00, // DMA2CNT_L
0xD2, 0xD3 => @truncate(dma.*[2].dmacntH() >> getHalf(byte_addr)),
0xD4...0xDB => null, // DMA3SAD, DMA3DAD
0xDC, 0xDD => 0x00, // DMA3CNT_L
0xDE, 0xDF => @truncate(dma.*[3].dmacntH() >> getHalf(byte_addr)),
else => util.io.read.err(T, log, "unexpected {} read from 0x{X:0>8}", .{ T, addr }),
}, },
u8 => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
else => @compileError("DMA: Unsupported read width"), else => @compileError("DMA: Unsupported read width"),
}; };
} }
pub fn write(comptime T: type, dma: *DmaTuple, addr: u32, value: T) void { pub fn write(comptime T: type, dma: *DmaTuple, addr: u32, value: T) void {
const byte_addr: u8 = @truncate(addr); const byte = @truncate(u8, addr);
switch (T) { switch (T) {
u32 => switch (byte_addr) { u32 => switch (byte) {
0xB0 => dma.*[0].setDmasad(value), 0xB0 => dma.*[0].setDmasad(value),
0xB4 => dma.*[0].setDmadad(value), 0xB4 => dma.*[0].setDmadad(value),
0xB8 => dma.*[0].setDmacnt(value), 0xB8 => dma.*[0].setDmacnt(value),
0xBC => dma.*[1].setDmasad(value), 0xBC => dma.*[1].setDmasad(value),
0xC0 => dma.*[1].setDmadad(value), 0xC0 => dma.*[1].setDmadad(value),
0xC4 => dma.*[1].setDmacnt(value), 0xC4 => dma.*[1].setDmacnt(value),
0xC8 => dma.*[2].setDmasad(value), 0xC8 => dma.*[2].setDmasad(value),
0xCC => dma.*[2].setDmadad(value), 0xCC => dma.*[2].setDmadad(value),
0xD0 => dma.*[2].setDmacnt(value), 0xD0 => dma.*[2].setDmacnt(value),
0xD4 => dma.*[3].setDmasad(value), 0xD4 => dma.*[3].setDmasad(value),
0xD8 => dma.*[3].setDmadad(value), 0xD8 => dma.*[3].setDmadad(value),
0xDC => dma.*[3].setDmacnt(value), 0xDC => dma.*[3].setDmacnt(value),
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }), else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
}, },
u16 => switch (byte_addr) { u16 => switch (byte) {
0xB0, 0xB2 => dma.*[0].setDmasad(setHalf(u32, dma.*[0].sad, byte_addr, value)), 0xB0 => dma.*[0].setDmasad(setLo(u32, dma.*[0].sad, value)),
0xB4, 0xB6 => dma.*[0].setDmadad(setHalf(u32, dma.*[0].dad, byte_addr, 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), 0xB8 => dma.*[0].setDmacntL(value),
0xBA => dma.*[0].setDmacntH(value), 0xBA => dma.*[0].setDmacntH(value),
0xBC, 0xBE => dma.*[1].setDmasad(setHalf(u32, dma.*[1].sad, byte_addr, value)), 0xBC => dma.*[1].setDmasad(setLo(u32, dma.*[1].sad, value)),
0xC0, 0xC2 => dma.*[1].setDmadad(setHalf(u32, dma.*[1].dad, byte_addr, 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), 0xC4 => dma.*[1].setDmacntL(value),
0xC6 => dma.*[1].setDmacntH(value), 0xC6 => dma.*[1].setDmacntH(value),
0xC8, 0xCA => dma.*[2].setDmasad(setHalf(u32, dma.*[2].sad, byte_addr, value)), 0xC8 => dma.*[2].setDmasad(setLo(u32, dma.*[2].sad, value)),
0xCC, 0xCE => dma.*[2].setDmadad(setHalf(u32, dma.*[2].dad, byte_addr, 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), 0xD0 => dma.*[2].setDmacntL(value),
0xD2 => dma.*[2].setDmacntH(value), 0xD2 => dma.*[2].setDmacntH(value),
0xD4, 0xD6 => dma.*[3].setDmasad(setHalf(u32, dma.*[3].sad, byte_addr, value)), 0xD4 => dma.*[3].setDmasad(setLo(u32, dma.*[3].sad, value)),
0xD8, 0xDA => dma.*[3].setDmadad(setHalf(u32, dma.*[3].dad, byte_addr, 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), 0xDC => dma.*[3].setDmacntL(value),
0xDE => dma.*[3].setDmacntH(value), 0xDE => dma.*[3].setDmacntH(value),
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }), else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
}, },
u8 => switch (byte_addr) { u8 => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
0xB0, 0xB1, 0xB2, 0xB3 => dma.*[0].setDmasad(setQuart(dma.*[0].sad, byte_addr, value)),
0xB4, 0xB5, 0xB6, 0xB7 => dma.*[0].setDmadad(setQuart(dma.*[0].dad, byte_addr, value)),
0xB8, 0xB9 => dma.*[0].setDmacntL(setHalf(u16, dma.*[0].word_count, byte_addr, value)),
0xBA, 0xBB => dma.*[0].setDmacntH(setHalf(u16, dma.*[0].cnt.raw, byte_addr, value)),
0xBC, 0xBD, 0xBE, 0xBF => dma.*[1].setDmasad(setQuart(dma.*[1].sad, byte_addr, value)),
0xC0, 0xC1, 0xC2, 0xC3 => dma.*[1].setDmadad(setQuart(dma.*[1].dad, byte_addr, value)),
0xC4, 0xC5 => dma.*[1].setDmacntL(setHalf(u16, dma.*[1].word_count, byte_addr, value)),
0xC6, 0xC7 => dma.*[1].setDmacntH(setHalf(u16, dma.*[1].cnt.raw, byte_addr, value)),
0xC8, 0xC9, 0xCA, 0xCB => dma.*[2].setDmasad(setQuart(dma.*[2].sad, byte_addr, value)),
0xCC, 0xCD, 0xCE, 0xCF => dma.*[2].setDmadad(setQuart(dma.*[2].dad, byte_addr, value)),
0xD0, 0xD1 => dma.*[2].setDmacntL(setHalf(u16, dma.*[2].word_count, byte_addr, value)),
0xD2, 0xD3 => dma.*[2].setDmacntH(setHalf(u16, dma.*[2].cnt.raw, byte_addr, value)),
0xD4, 0xD5, 0xD6, 0xD7 => dma.*[3].setDmasad(setQuart(dma.*[3].sad, byte_addr, value)),
0xD8, 0xD9, 0xDA, 0xDB => dma.*[3].setDmadad(setQuart(dma.*[3].dad, byte_addr, value)),
0xDC, 0xDD => dma.*[3].setDmacntL(setHalf(u16, dma.*[3].word_count, byte_addr, value)),
0xDE, 0xDF => dma.*[3].setDmacntH(setHalf(u16, dma.*[3].cnt.raw, byte_addr, value)),
else => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
},
else => @compileError("DMA: Unsupported write width"), else => @compileError("DMA: Unsupported write width"),
} }
} }
@@ -151,7 +99,6 @@ fn DmaController(comptime id: u2) type {
const sad_mask: u32 = if (id == 0) 0x07FF_FFFF else 0x0FFF_FFFF; const sad_mask: u32 = if (id == 0) 0x07FF_FFFF else 0x0FFF_FFFF;
const dad_mask: u32 = if (id != 3) 0x07FF_FFFF else 0x0FFF_FFFF; const dad_mask: u32 = if (id != 3) 0x07FF_FFFF else 0x0FFF_FFFF;
const WordCount = if (id == 3) u16 else u14;
/// Write-only. The first address in a DMA transfer. (DMASAD) /// Write-only. The first address in a DMA transfer. (DMASAD)
/// Note: use writeSrc instead of manipulating src_addr directly /// Note: use writeSrc instead of manipulating src_addr directly
@@ -160,19 +107,17 @@ fn DmaController(comptime id: u2) type {
/// Note: Use writeDst instead of manipulatig dst_addr directly /// Note: Use writeDst instead of manipulatig dst_addr directly
dad: u32, dad: u32,
/// Write-only. The Word Count for the DMA Transfer (DMACNT_L) /// Write-only. The Word Count for the DMA Transfer (DMACNT_L)
word_count: WordCount, word_count: if (id == 3) u16 else u14,
/// Read / Write. DMACNT_H /// Read / Write. DMACNT_H
/// Note: Use writeControl instead of manipulating cnt directly. /// Note: Use writeControl instead of manipulating cnt directly.
cnt: DmaControl, cnt: DmaControl,
/// Internal. The last successfully read value
data_latch: u32,
/// Internal. Currrent Source Address /// Internal. Currrent Source Address
sad_latch: u32, sad_latch: u32,
/// Internal. Current Destination Address /// Internal. Current Destination Address
dad_latch: u32, dad_latch: u32,
/// Internal. Word Count /// Internal. Word Count
_word_count: WordCount, _word_count: if (id == 3) u16 else u14,
/// Some DMA Transfers are enabled during Hblank / VBlank and / or /// Some DMA Transfers are enabled during Hblank / VBlank and / or
/// have delays. Thefore bit 15 of DMACNT isn't actually something /// have delays. Thefore bit 15 of DMACNT isn't actually something
@@ -189,17 +134,11 @@ fn DmaController(comptime id: u2) type {
// Internals // Internals
.sad_latch = 0, .sad_latch = 0,
.dad_latch = 0, .dad_latch = 0,
.data_latch = 0,
._word_count = 0, ._word_count = 0,
.in_progress = false, .in_progress = false,
}; };
} }
pub fn reset(self: *Self) void {
self.* = Self.init();
}
pub fn setDmasad(self: *Self, addr: u32) void { pub fn setDmasad(self: *Self, addr: u32) void {
self.sad = addr & sad_mask; self.sad = addr & sad_mask;
} }
@@ -209,11 +148,7 @@ fn DmaController(comptime id: u2) type {
} }
pub fn setDmacntL(self: *Self, halfword: u16) void { pub fn setDmacntL(self: *Self, halfword: u16) void {
self.word_count = @truncate(halfword); self.word_count = @truncate(@TypeOf(self.word_count), halfword);
}
pub fn dmacntH(self: *const Self) u16 {
return self.cnt.raw & if (id == 3) 0xFFE0 else 0xF7E0;
} }
pub fn setDmacntH(self: *Self, halfword: u16) void { pub fn setDmacntH(self: *Self, halfword: u16) void {
@@ -223,7 +158,7 @@ fn DmaController(comptime id: u2) type {
// Reload Internals on Rising Edge. // Reload Internals on Rising Edge.
self.sad_latch = self.sad; self.sad_latch = self.sad;
self.dad_latch = self.dad; self.dad_latch = self.dad;
self._word_count = if (self.word_count == 0) std.math.maxInt(WordCount) else self.word_count; 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 // Only a Start Timing of 00 has a DMA Transfer immediately begin
self.in_progress = new.start_timing.read() == 0b00; self.in_progress = new.start_timing.read() == 0b00;
@@ -233,46 +168,32 @@ fn DmaController(comptime id: u2) type {
} }
pub fn setDmacnt(self: *Self, word: u32) void { pub fn setDmacnt(self: *Self, word: u32) void {
self.setDmacntL(@truncate(word)); self.setDmacntL(@truncate(u16, word));
self.setDmacntH(@truncate(word >> 16)); self.setDmacntH(@truncate(u16, word >> 16));
} }
pub fn step(self: *Self, cpu: *Arm7tdmi) void { pub fn step(self: *Self, cpu: *Arm7tdmi) void {
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
const is_fifo = (id == 1 or id == 2) and self.cnt.start_timing.read() == 0b11; const is_fifo = (id == 1 or id == 2) and self.cnt.start_timing.read() == 0b11;
const sad_adj: Adjustment = @enumFromInt(self.cnt.sad_adj.read()); const sad_adj = @intToEnum(Adjustment, self.cnt.sad_adj.read());
const dad_adj: Adjustment = if (is_fifo) .Fixed else @enumFromInt(self.cnt.dad_adj.read()); const dad_adj = if (is_fifo) .Fixed else @intToEnum(Adjustment, self.cnt.dad_adj.read());
const transfer_type = is_fifo or self.cnt.transfer_type.read(); const transfer_type = is_fifo or self.cnt.transfer_type.read();
const offset: u32 = if (transfer_type) @sizeOf(u32) else @sizeOf(u16); const offset: u32 = if (transfer_type) @sizeOf(u32) else @sizeOf(u16);
const mask = if (transfer_type) ~@as(u32, 3) else ~@as(u32, 1); const mask = if (transfer_type) ~@as(u32, 3) else ~@as(u32, 1);
const sad_addr = self.sad_latch & mask;
const dad_addr = self.dad_latch & mask;
if (transfer_type) { if (transfer_type) {
if (sad_addr >= 0x0200_0000) self.data_latch = cpu.bus.read(u32, sad_addr); cpu.bus.write(u32, self.dad_latch & mask, cpu.bus.read(u32, self.sad_latch & mask));
cpu.bus.write(u32, dad_addr, self.data_latch);
} else { } else {
if (sad_addr >= 0x0200_0000) { cpu.bus.write(u16, self.dad_latch & mask, cpu.bus.read(u16, self.sad_latch & mask));
const value: u32 = cpu.bus.read(u16, sad_addr);
self.data_latch = value << 16 | value;
} }
cpu.bus.write(u16, dad_addr, @as(u16, @truncate(rotr(u32, self.data_latch, 8 * (dad_addr & 3))))); switch (sad_adj) {
}
switch (@as(u8, @truncate(sad_addr >> 24))) {
// according to fleroviux, DMAs with a source address in ROM misbehave
// the resultant behaviour is that the source address will increment despite what DMAXCNT says
0x08...0x0D => self.sad_latch +%= offset, // obscure behaviour
else => switch (sad_adj) {
.Increment => self.sad_latch +%= offset, .Increment => self.sad_latch +%= offset,
.Decrement => self.sad_latch -%= offset, .Decrement => self.sad_latch -%= offset,
// FIXME: Is just ignoring this ok?
.IncrementReload => log.err("{} is a prohibited adjustment on SAD", .{sad_adj}), .IncrementReload => log.err("{} is a prohibited adjustment on SAD", .{sad_adj}),
.Fixed => {}, .Fixed => {},
},
} }
switch (dad_adj) { switch (dad_adj) {
@@ -286,13 +207,13 @@ fn DmaController(comptime id: u2) type {
if (self._word_count == 0) { if (self._word_count == 0) {
if (self.cnt.irq.read()) { if (self.cnt.irq.read()) {
switch (id) { switch (id) {
0 => bus_ptr.io.irq.dma0.set(), 0 => cpu.bus.io.irq.dma0.set(),
1 => bus_ptr.io.irq.dma1.set(), 1 => cpu.bus.io.irq.dma1.set(),
2 => bus_ptr.io.irq.dma2.set(), 2 => cpu.bus.io.irq.dma2.set(),
3 => bus_ptr.io.irq.dma3.set(), 3 => cpu.bus.io.irq.dma3.set(),
} }
handleInterrupt(cpu); cpu.handleInterrupt();
} }
// If we're not repeating, Fire the IRQs and disable the DMA // If we're not repeating, Fire the IRQs and disable the DMA
@@ -321,7 +242,7 @@ fn DmaController(comptime id: u2) type {
// Reload internal DAD latch if we are in IncrementRelaod // Reload internal DAD latch if we are in IncrementRelaod
if (self.in_progress) { if (self.in_progress) {
self._word_count = if (self.word_count == 0) std.math.maxInt(@TypeOf(self._word_count)) else self.word_count; self._word_count = if (self.word_count == 0) std.math.maxInt(@TypeOf(self._word_count)) else self.word_count;
if (@as(Adjustment, @enumFromInt(self.cnt.dad_adj.read())) == .IncrementReload) self.dad_latch = self.dad; if (@intToEnum(Adjustment, self.cnt.dad_adj.read()) == .IncrementReload) self.dad_latch = self.dad;
} }
} }
@@ -346,7 +267,10 @@ fn DmaController(comptime id: u2) type {
} }
pub fn onBlanking(bus: *Bus, comptime kind: DmaKind) void { pub fn onBlanking(bus: *Bus, comptime kind: DmaKind) void {
inline for (0..4) |i| bus.dma[i].poll(kind); bus.dma[0].poll(kind);
bus.dma[1].poll(kind);
bus.dma[2].poll(kind);
bus.dma[3].poll(kind);
} }
const Adjustment = enum(u2) { const Adjustment = enum(u2) {

View File

@@ -1,14 +1,11 @@
const std = @import("std"); const std = @import("std");
const Bit = @import("bitfield").Bit; const Bit = @import("bitfield").Bit;
const Bitfield = @import("bitfield").Bitfield;
const DateTime = @import("datetime").datetime.Datetime; const DateTime = @import("datetime").datetime.Datetime;
const Arm7tdmi = @import("arm32").Arm7tdmi; const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
const Bus = @import("../Bus.zig");
const Scheduler = @import("../scheduler.zig").Scheduler;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const handleInterrupt = @import("../cpu_util.zig").handleInterrupt;
/// GPIO Register Implementation /// GPIO Register Implementation
pub const Gpio = struct { pub const Gpio = struct {
const Self = @This(); const Self = @This();
@@ -31,8 +28,8 @@ pub const Gpio = struct {
fn step(self: *Device, value: u4) u4 { fn step(self: *Device, value: u4) u4 {
return switch (self.kind) { return switch (self.kind) {
.Rtc => blk: { .Rtc => blk: {
const clock: *Clock = @ptrCast(@alignCast(self.ptr.?)); const clock = @ptrCast(*Clock, @alignCast(@alignOf(*Clock), self.ptr.?));
break :blk clock.step(.{ .raw = value }); break :blk clock.step(Clock.Data{ .raw = value });
}, },
.None => value, .None => value,
}; };
@@ -75,7 +72,7 @@ pub const Gpio = struct {
self.* = .{ self.* = .{
.data = 0b0000, .data = 0b0000,
.direction = 0b1111, // TODO: What is GPIO Direction set to by default? .direction = 0b1111, // TODO: What is GPIO DIrection set to by default?
.cnt = 0b0, .cnt = 0b0,
.device = switch (kind) { .device = switch (kind) {
@@ -94,7 +91,7 @@ pub const Gpio = struct {
pub fn deinit(self: *Self, allocator: Allocator) void { pub fn deinit(self: *Self, allocator: Allocator) void {
switch (self.device.kind) { switch (self.device.kind) {
.Rtc => allocator.destroy(@as(*Clock, @ptrCast(@alignCast(self.device.ptr.?)))), .Rtc => allocator.destroy(@ptrCast(*Clock, @alignCast(@alignOf(*Clock), self.device.ptr.?))),
.None => {}, .None => {},
} }
@@ -146,16 +143,16 @@ pub const Clock = struct {
/// 2. A `count`, which keeps track of which byte is currently being read /// 2. A `count`, which keeps track of which byte is currently being read
/// 3. An index, which keeps track of which bit of the byte determined by `count` is being read /// 3. An index, which keeps track of which bit of the byte determined by `count` is being read
fn read(self: *Reader, clock: *const Clock, register: Register) u1 { fn read(self: *Reader, clock: *const Clock, register: Register) u1 {
const idx: u3 = @intCast(self.i); const idx = @intCast(u3, self.i);
defer self.i += 1; defer self.i += 1;
// FIXME: What do I do about the unused bits? // FIXME: What do I do about the unused bits?
return switch (register) { return switch (register) {
.Control => @truncate(switch (self.count) { .Control => @truncate(u1, switch (self.count) {
0 => clock.cnt.raw >> idx, 0 => clock.cnt.raw >> idx,
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 1 byte)", .{ self.count, register }), else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 1 byte)", .{ self.count, register }),
}), }),
.DateTime => @truncate(switch (self.count) { .DateTime => @truncate(u1, switch (self.count) {
// Date // Date
0 => clock.year >> idx, 0 => clock.year >> idx,
1 => @as(u8, clock.month) >> idx, 1 => @as(u8, clock.month) >> idx,
@@ -168,7 +165,7 @@ pub const Clock = struct {
6 => @as(u8, clock.second) >> idx, 6 => @as(u8, clock.second) >> idx,
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 7 bytes)", .{ self.count, register }), else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 7 bytes)", .{ self.count, register }),
}), }),
.Time => @truncate(switch (self.count) { .Time => @truncate(u1, switch (self.count) {
0 => @as(u8, clock.hour) >> idx, 0 => @as(u8, clock.hour) >> idx,
1 => @as(u8, clock.minute) >> idx, 1 => @as(u8, clock.minute) >> idx,
2 => @as(u8, clock.second) >> idx, 2 => @as(u8, clock.second) >> idx,
@@ -207,7 +204,7 @@ pub const Clock = struct {
/// Append a bit to the internal bit buffer (aka an integer) /// Append a bit to the internal bit buffer (aka an integer)
fn push(self: *Writer, value: u1) void { fn push(self: *Writer, value: u1) void {
const idx: u3 = @intCast(self.i); const idx = @intCast(u3, self.i);
self.buf = (self.buf & ~(@as(u8, 1) << idx)) | @as(u8, value) << idx; self.buf = (self.buf & ~(@as(u8, 1) << idx)) | @as(u8, value) << idx;
self.i += 1; self.i += 1;
} }
@@ -290,22 +287,20 @@ pub const Clock = struct {
.gpio = gpio, // Can't use Arm7tdmi ptr b/c not initialized yet .gpio = gpio, // Can't use Arm7tdmi ptr b/c not initialized yet
}; };
const sched_ptr: *Scheduler = @ptrCast(@alignCast(cpu.sched.ptr)); cpu.sched.push(.RealTimeClock, 1 << 24); // Every Second
sched_ptr.push(.RealTimeClock, 1 << 24); // Every Second
} }
pub fn onClockUpdate(self: *Self, late: u64) void { pub fn onClockUpdate(self: *Self, late: u64) void {
const sched_ptr: *Scheduler = @ptrCast(@alignCast(self.cpu.sched.ptr)); self.cpu.sched.push(.RealTimeClock, (1 << 24) -| late); // Reschedule
sched_ptr.push(.RealTimeClock, (1 << 24) -| late); // Reschedule
const now = DateTime.now(); const now = DateTime.now();
self.year = bcd(@intCast(now.date.year - 2000)); self.year = bcd(u8, @intCast(u8, now.date.year - 2000));
self.month = @truncate(bcd(now.date.month)); self.month = bcd(u5, now.date.month);
self.day = @truncate(bcd(now.date.day)); self.day = bcd(u6, now.date.day);
self.weekday = @truncate(bcd((now.date.weekday() + 1) % 7)); // API is Monday = 0, Sunday = 6. We want Sunday = 0, Saturday = 6 self.weekday = bcd(u3, (now.date.weekday() + 1) % 7); // API is Monday = 0, Sunday = 6. We want Sunday = 0, Saturday = 6
self.hour = @truncate(bcd(now.time.hour)); self.hour = bcd(u6, now.time.hour);
self.minute = @truncate(bcd(now.time.minute)); self.minute = bcd(u7, now.time.minute);
self.second = @truncate(bcd(now.time.second)); self.second = bcd(u7, now.time.second);
} }
fn step(self: *Self, value: Data) u4 { fn step(self: *Self, value: Data) u4 {
@@ -321,14 +316,14 @@ pub const Clock = struct {
} }
} }
break :blk @truncate(value.raw); break :blk @truncate(u4, value.raw);
}, },
.Command => blk: { .Command => blk: {
if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state}); if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state});
// If SCK rises, sample SIO // If SCK rises, sample SIO
if (!cache.sck.read() and value.sck.read()) { if (!cache.sck.read() and value.sck.read()) {
self.writer.push(@intFromBool(value.sio.read())); self.writer.push(@boolToInt(value.sio.read()));
if (self.writer.finished()) { if (self.writer.finished()) {
self.state = self.processCommand(self.writer.buf); self.state = self.processCommand(self.writer.buf);
@@ -338,14 +333,14 @@ pub const Clock = struct {
} }
} }
break :blk @truncate(value.raw); break :blk @truncate(u4, value.raw);
}, },
.Write => |register| blk: { .Write => |register| blk: {
if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state}); if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state});
// If SCK rises, sample SIO // If SCK rises, sample SIO
if (!cache.sck.read() and value.sck.read()) { if (!cache.sck.read() and value.sck.read()) {
self.writer.push(@intFromBool(value.sio.read())); self.writer.push(@boolToInt(value.sio.read()));
const register_width: u32 = switch (register) { const register_width: u32 = switch (register) {
.Control => 1, .Control => 1,
@@ -364,7 +359,7 @@ pub const Clock = struct {
} }
} }
break :blk @truncate(value.raw); break :blk @truncate(u4, value.raw);
}, },
.Read => |register| blk: { .Read => |register| blk: {
if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state}); if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state});
@@ -390,7 +385,7 @@ pub const Clock = struct {
} }
} }
break :blk @truncate(ret.raw); break :blk @truncate(u4, ret.raw);
}, },
}; };
} }
@@ -403,13 +398,11 @@ pub const Clock = struct {
} }
fn irq(self: *Self) void { fn irq(self: *Self) void {
const bus_ptr: *Bus = @ptrCast(@alignCast(self.cpu.bus.ptr));
// TODO: Confirm that this is the right behaviour // TODO: Confirm that this is the right behaviour
log.debug("Force GamePak IRQ", .{}); log.debug("Force GamePak IRQ", .{});
bus_ptr.io.irq.game_pak.set(); self.cpu.bus.io.irq.game_pak.set();
handleInterrupt(self.cpu); self.cpu.handleInterrupt();
} }
fn processCommand(self: *Self, raw_command: u8) State { fn processCommand(self: *Self, raw_command: u8) State {
@@ -429,7 +422,7 @@ pub const Clock = struct {
log.debug("Handling Command 0x{X:0>2} [0b{b:0>8}]", .{ command, command }); log.debug("Handling Command 0x{X:0>2} [0b{b:0>8}]", .{ command, command });
const is_write = command & 1 == 0; const is_write = command & 1 == 0;
const rtc_register: u3 = @truncate(command >> 1 & 0x7); const rtc_register = @truncate(u3, command >> 1 & 0x7);
if (is_write) { if (is_write) {
return switch (rtc_register) { return switch (rtc_register) {
@@ -457,8 +450,16 @@ pub const Clock = struct {
} }
}; };
/// Converts an 8-bit unsigned integer to its BCD representation. fn bcd(comptime T: type, value: u8) T {
/// Note: Algorithm only works for values between 0 and 99 inclusive. var input = value;
fn bcd(value: u8) u8 { var ret: u8 = 0;
return ((value / 10) << 4) + (value % 10); var shift: u3 = 0;
while (input > 0) {
ret |= (input % 10) << (shift << 2);
shift += 1;
input /= 10;
}
return @truncate(T, ret);
} }

View File

@@ -1,16 +1,18 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin");
const timer = @import("timer.zig"); const timer = @import("timer.zig");
const dma = @import("dma.zig"); const dma = @import("dma.zig");
const apu = @import("../apu.zig"); const apu = @import("../apu.zig");
const ppu = @import("../ppu.zig");
const util = @import("../../util.zig"); const util = @import("../../util.zig");
const Bit = @import("bitfield").Bit; const Bit = @import("bitfield").Bit;
const Bitfield = @import("bitfield").Bitfield; const Bitfield = @import("bitfield").Bitfield;
const Bus = @import("../Bus.zig"); const Bus = @import("../Bus.zig");
const DmaController = @import("dma.zig").DmaController;
const Scheduler = @import("../scheduler.zig").Scheduler;
const getHalf = util.getHalf; const setHi = util.setLo;
const setHalf = util.setHalf; const setLo = util.setHi;
const log = std.log.scoped(.@"I/O"); const log = std.log.scoped(.@"I/O");
@@ -22,29 +24,23 @@ pub const Io = struct {
ie: InterruptEnable, ie: InterruptEnable,
irq: InterruptRequest, irq: InterruptRequest,
postflg: PostFlag, postflg: PostFlag,
waitcnt: WaitControl,
haltcnt: HaltControl, haltcnt: HaltControl,
keyinput: AtomicKeyInput, keyinput: KeyInput,
pub fn init() Self { pub fn init() Self {
return .{ return .{
.ime = false, .ime = false,
.ie = .{ .raw = 0x0000 }, .ie = .{ .raw = 0x0000 },
.irq = .{ .raw = 0x0000 }, .irq = .{ .raw = 0x0000 },
.keyinput = AtomicKeyInput.init(.{ .raw = 0x03FF }), .keyinput = .{ .raw = 0x03FF },
.waitcnt = .{ .raw = 0x0000_0000 }, // Bit 15 == 0 for GBA
.postflg = .FirstBoot, .postflg = .FirstBoot,
.haltcnt = .Execute, .haltcnt = .Execute,
}; };
} }
pub fn reset(self: *Self) void {
self.* = Self.init();
}
fn setIrqs(self: *Io, word: u32) void { fn setIrqs(self: *Io, word: u32) void {
self.ie.raw = @truncate(word); self.ie.raw = @truncate(u16, word);
self.irq.raw &= ~@as(u16, @truncate(word >> 16)); self.irq.raw &= ~@truncate(u16, word >> 16);
} }
}; };
@@ -52,10 +48,9 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) ?T {
return switch (T) { return switch (T) {
u32 => switch (address) { u32 => switch (address) {
// Display // Display
0x0400_0000...0x0400_0054 => ppu.read(T, &bus.ppu, address), 0x0400_0000 => bus.ppu.dispcnt.raw,
0x0400_0004 => @as(T, bus.ppu.vcount.raw) << 16 | bus.ppu.dispstat.raw,
// Sound 0x0400_0006 => @as(T, bus.ppu.bg[0].cnt.raw) << 16 | bus.ppu.vcount.raw,
0x0400_0060...0x0400_00A4 => apu.read(T, &bus.apu, address),
// DMA Transfers // DMA Transfers
0x0400_00B0...0x0400_00DC => dma.read(T, &bus.dma, address), 0x0400_00B0...0x0400_00DC => dma.read(T, &bus.dma, address),
@@ -73,18 +68,26 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) ?T {
0x0400_0150 => util.io.read.todo(log, "Read {} from JOY_RECV", .{T}), 0x0400_0150 => util.io.read.todo(log, "Read {} from JOY_RECV", .{T}),
// Interrupts // Interrupts
0x0400_0200 => @as(u32, bus.io.irq.raw) << 16 | bus.io.ie.raw, 0x0400_0200 => @as(T, bus.io.irq.raw) << 16 | bus.io.ie.raw,
0x0400_0204 => bus.io.waitcnt.raw, 0x0400_0208 => @boolToInt(bus.io.ime),
0x0400_0208 => @intFromBool(bus.io.ime),
0x0400_0300 => @intFromEnum(bus.io.postflg),
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }), else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
}, },
u16 => switch (address) { u16 => switch (address) {
// Display // Display
0x0400_0000...0x0400_0054 => ppu.read(T, &bus.ppu, address), 0x0400_0000 => bus.ppu.dispcnt.raw,
0x0400_0004 => bus.ppu.dispstat.raw,
0x0400_0006 => bus.ppu.vcount.raw,
0x0400_0008 => bus.ppu.bg[0].cnt.raw,
0x0400_000A => bus.ppu.bg[1].cnt.raw,
0x0400_000C => bus.ppu.bg[2].cnt.raw,
0x0400_000E => bus.ppu.bg[3].cnt.raw,
0x0400_004C => util.io.read.todo(log, "Read {} from MOSAIC", .{T}),
0x0400_0050 => bus.ppu.bldcnt.raw,
0x0400_0052 => bus.ppu.bldalpha.raw,
0x0400_0054 => bus.ppu.bldy.raw,
// Sound // Sound
0x0400_0060...0x0400_00A6 => apu.read(T, &bus.apu, address), 0x0400_0060...0x0400_009E => apu.read(T, &bus.apu, address),
// DMA Transfers // DMA Transfers
0x0400_00B0...0x0400_00DE => dma.read(T, &bus.dma, address), 0x0400_00B0...0x0400_00DE => dma.read(T, &bus.dma, address),
@@ -96,38 +99,32 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) ?T {
0x0400_0128 => util.io.read.todo(log, "Read {} from SIOCNT", .{T}), 0x0400_0128 => util.io.read.todo(log, "Read {} from SIOCNT", .{T}),
// Keypad Input // Keypad Input
0x0400_0130 => bus.io.keyinput.load(.monotonic), 0x0400_0130 => bus.io.keyinput.raw,
// Serial Communication 2 // Serial Communication 2
0x0400_0134 => util.io.read.todo(log, "Read {} from RCNT", .{T}), 0x0400_0134 => util.io.read.todo(log, "Read {} from RCNT", .{T}),
0x0400_0136 => 0x0000,
0x0400_0142 => 0x0000,
0x0400_015A => 0x0000,
// Interrupts // Interrupts
0x0400_0200 => bus.io.ie.raw, 0x0400_0200 => bus.io.ie.raw,
0x0400_0202 => bus.io.irq.raw, 0x0400_0202 => bus.io.irq.raw,
0x0400_0204 => bus.io.waitcnt.raw, 0x0400_0204 => util.io.read.todo(log, "Read {} from WAITCNT", .{T}),
0x0400_0206 => 0x0000, 0x0400_0208 => @boolToInt(bus.io.ime),
0x0400_0208 => @intFromBool(bus.io.ime),
0x0400_020A => 0x0000,
0x0400_0300 => @intFromEnum(bus.io.postflg),
0x0400_0302 => 0x0000,
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }), else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
}, },
u8 => return switch (address) { u8 => return switch (address) {
// Display // Display
0x0400_0000...0x0400_0055 => ppu.read(T, &bus.ppu, address), 0x0400_0000 => @truncate(T, bus.ppu.dispcnt.raw),
0x0400_0004 => @truncate(T, bus.ppu.dispstat.raw),
0x0400_0005 => @truncate(T, bus.ppu.dispcnt.raw >> 8),
0x0400_0006 => @truncate(T, bus.ppu.vcount.raw),
0x0400_0008 => @truncate(T, bus.ppu.bg[0].cnt.raw),
0x0400_0009 => @truncate(T, bus.ppu.bg[0].cnt.raw >> 8),
0x0400_000A => @truncate(T, bus.ppu.bg[1].cnt.raw),
0x0400_000B => @truncate(T, bus.ppu.bg[1].cnt.raw >> 8),
// Sound // Sound
0x0400_0060...0x0400_00A7 => apu.read(T, &bus.apu, address), 0x0400_0060...0x0400_00A7 => apu.read(T, &bus.apu, address),
// DMA Transfers
0x0400_00B0...0x0400_00DF => dma.read(T, &bus.dma, address),
// Timers
0x0400_0100...0x0400_010F => timer.read(T, &bus.tim, address),
// Serial Communication 1 // Serial Communication 1
0x0400_0128 => util.io.read.todo(log, "Read {} from SIOCNT_L", .{T}), 0x0400_0128 => util.io.read.todo(log, "Read {} from SIOCNT_L", .{T}),
@@ -136,20 +133,10 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) ?T {
// Serial Communication 2 // Serial Communication 2
0x0400_0135 => util.io.read.todo(log, "Read {} from RCNT_H", .{T}), 0x0400_0135 => util.io.read.todo(log, "Read {} from RCNT_H", .{T}),
0x0400_0136, 0x0400_0137 => 0x00,
0x0400_0142, 0x0400_0143 => 0x00,
0x0400_015A, 0x0400_015B => 0x00,
// Interrupts // Interrupts
0x0400_0200, 0x0400_0201 => @truncate(bus.io.ie.raw >> getHalf(@truncate(address))), 0x0400_0200 => @truncate(T, bus.io.ie.raw),
0x0400_0202, 0x0400_0203 => @truncate(bus.io.irq.raw >> getHalf(@truncate(address))), 0x0400_0300 => @enumToInt(bus.io.postflg),
0x0400_0204, 0x0400_0205 => @truncate(bus.io.waitcnt.raw >> getHalf(@truncate(address))),
0x0400_0206, 0x0400_0207 => 0x00,
0x0400_0208, 0x0400_0209 => @truncate(@as(u16, @intFromBool(bus.io.ime)) >> getHalf(@truncate(address))),
0x0400_020A, 0x0400_020B => 0x00,
0x0400_0300 => @intFromEnum(bus.io.postflg),
0x0400_0301 => null,
0x0400_0302, 0x0400_0303 => 0x00,
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }), else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
}, },
else => @compileError("I/O: Unsupported read width"), else => @compileError("I/O: Unsupported read width"),
@@ -160,7 +147,34 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
return switch (T) { return switch (T) {
u32 => switch (address) { u32 => switch (address) {
// Display // Display
0x0400_0000...0x0400_0054 => ppu.write(T, &bus.ppu, address, value), 0x0400_0000 => bus.ppu.dispcnt.raw = @truncate(u16, value),
0x0400_0004 => {
bus.ppu.dispstat.raw = @truncate(u16, value);
bus.ppu.vcount.raw = @truncate(u16, value >> 16);
},
0x0400_0008 => bus.ppu.setAdjCnts(0, value),
0x0400_000C => bus.ppu.setAdjCnts(2, value),
0x0400_0010 => bus.ppu.setBgOffsets(0, value),
0x0400_0014 => bus.ppu.setBgOffsets(1, value),
0x0400_0018 => bus.ppu.setBgOffsets(2, value),
0x0400_001C => bus.ppu.setBgOffsets(3, value),
0x0400_0020 => bus.ppu.aff_bg[0].writePaPb(value),
0x0400_0024 => bus.ppu.aff_bg[0].writePcPd(value),
0x0400_0028 => bus.ppu.aff_bg[0].setX(bus.ppu.dispstat.vblank.read(), value),
0x0400_002C => bus.ppu.aff_bg[0].setY(bus.ppu.dispstat.vblank.read(), value),
0x0400_0030 => bus.ppu.aff_bg[1].writePaPb(value),
0x0400_0034 => bus.ppu.aff_bg[1].writePcPd(value),
0x0400_0038 => bus.ppu.aff_bg[1].setX(bus.ppu.dispstat.vblank.read(), value),
0x0400_003C => bus.ppu.aff_bg[1].setY(bus.ppu.dispstat.vblank.read(), value),
0x0400_0040 => bus.ppu.win.setH(value),
0x0400_0044 => bus.ppu.win.setV(value),
0x0400_0048 => bus.ppu.win.setIo(value),
0x0400_004C => log.debug("Wrote 0x{X:0>8} to MOSAIC", .{value}),
0x0400_0050 => {
bus.ppu.bldcnt.raw = @truncate(u16, value);
bus.ppu.bldalpha.raw = @truncate(u16, value >> 16);
},
0x0400_0054 => bus.ppu.bldy.raw = @truncate(u16, value),
0x0400_0058...0x0400_005C => {}, // Unused 0x0400_0058...0x0400_005C => {}, // Unused
// Sound // Sound
@@ -196,28 +210,65 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
// Interrupts // Interrupts
0x0400_0200 => bus.io.setIrqs(value), 0x0400_0200 => bus.io.setIrqs(value),
0x0400_0204 => bus.io.waitcnt.set(@truncate(value)), 0x0400_0204 => log.debug("Wrote 0x{X:0>8} to WAITCNT", .{value}),
0x0400_0208 => bus.io.ime = value & 1 == 1, 0x0400_0208 => bus.io.ime = value & 1 == 1,
0x0400_0300 => { 0x0400_020C...0x0400_021C => {}, // Unused
bus.io.postflg = @enumFromInt(value & 1);
bus.io.haltcnt = if (value >> 15 & 1 == 0) .Halt else @panic("TODO: Implement STOP");
},
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, address }), else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, address }),
}, },
u16 => switch (address) { u16 => switch (address) {
// Display // Display
0x0400_0000...0x0400_0054 => ppu.write(T, &bus.ppu, address, value), 0x0400_0000 => bus.ppu.dispcnt.raw = value,
0x0400_0056 => {}, // Not used 0x0400_0004 => bus.ppu.dispstat.raw = value,
0x0400_0006 => {}, // vcount is read-only
0x0400_0008 => bus.ppu.bg[0].cnt.raw = value,
0x0400_000A => bus.ppu.bg[1].cnt.raw = value,
0x0400_000C => bus.ppu.bg[2].cnt.raw = value,
0x0400_000E => bus.ppu.bg[3].cnt.raw = value,
0x0400_0010 => bus.ppu.bg[0].hofs.raw = value, // TODO: Don't write out every HOFS / VOFS?
0x0400_0012 => bus.ppu.bg[0].vofs.raw = value,
0x0400_0014 => bus.ppu.bg[1].hofs.raw = value,
0x0400_0016 => bus.ppu.bg[1].vofs.raw = value,
0x0400_0018 => bus.ppu.bg[2].hofs.raw = value,
0x0400_001A => bus.ppu.bg[2].vofs.raw = value,
0x0400_001C => bus.ppu.bg[3].hofs.raw = value,
0x0400_001E => bus.ppu.bg[3].vofs.raw = value,
0x0400_0020 => bus.ppu.aff_bg[0].pa = @bitCast(i16, value),
0x0400_0022 => bus.ppu.aff_bg[0].pb = @bitCast(i16, value),
0x0400_0024 => bus.ppu.aff_bg[0].pc = @bitCast(i16, value),
0x0400_0026 => bus.ppu.aff_bg[0].pd = @bitCast(i16, value),
0x0400_0028 => bus.ppu.aff_bg[0].x = @bitCast(i32, setLo(u32, @bitCast(u32, bus.ppu.aff_bg[0].x), value)),
0x0400_002A => bus.ppu.aff_bg[0].x = @bitCast(i32, setHi(u32, @bitCast(u32, bus.ppu.aff_bg[0].x), value)),
0x0400_002C => bus.ppu.aff_bg[0].y = @bitCast(i32, setLo(u32, @bitCast(u32, bus.ppu.aff_bg[0].y), value)),
0x0400_002E => bus.ppu.aff_bg[0].y = @bitCast(i32, setHi(u32, @bitCast(u32, bus.ppu.aff_bg[0].y), value)),
0x0400_0030 => bus.ppu.aff_bg[1].pa = @bitCast(i16, value),
0x0400_0032 => bus.ppu.aff_bg[1].pb = @bitCast(i16, value),
0x0400_0034 => bus.ppu.aff_bg[1].pc = @bitCast(i16, value),
0x0400_0036 => bus.ppu.aff_bg[1].pd = @bitCast(i16, value),
0x0400_0038 => bus.ppu.aff_bg[1].x = @bitCast(i32, setLo(u32, @bitCast(u32, bus.ppu.aff_bg[1].x), value)),
0x0400_003A => bus.ppu.aff_bg[1].x = @bitCast(i32, setHi(u32, @bitCast(u32, bus.ppu.aff_bg[1].x), value)),
0x0400_003C => bus.ppu.aff_bg[1].y = @bitCast(i32, setLo(u32, @bitCast(u32, bus.ppu.aff_bg[1].y), value)),
0x0400_003E => bus.ppu.aff_bg[1].y = @bitCast(i32, setHi(u32, @bitCast(u32, bus.ppu.aff_bg[1].y), value)),
0x0400_0040 => bus.ppu.win.h[0].raw = value,
0x0400_0042 => bus.ppu.win.h[1].raw = value,
0x0400_0044 => bus.ppu.win.v[0].raw = value,
0x0400_0046 => bus.ppu.win.v[1].raw = value,
0x0400_0048 => bus.ppu.win.in.raw = value,
0x0400_004A => bus.ppu.win.out.raw = value,
0x0400_004C => log.debug("Wrote 0x{X:0>4} to MOSAIC", .{value}),
0x0400_0050 => bus.ppu.bldcnt.raw = value,
0x0400_0052 => bus.ppu.bldalpha.raw = value,
0x0400_0054 => bus.ppu.bldy.raw = value,
0x0400_004E, 0x0400_0056 => {}, // Not used
// Sound // Sound
0x0400_0060...0x0400_00A6 => apu.write(T, &bus.apu, address, value), 0x0400_0060...0x0400_009E => apu.write(T, &bus.apu, address, value),
// Dma Transfers // Dma Transfers
0x0400_00B0...0x0400_00DE => dma.write(T, &bus.dma, address, value), 0x0400_00B0...0x0400_00DE => dma.write(T, &bus.dma, address, value),
// Timers // Timers
0x0400_0100...0x0400_010E => timer.write(T, &bus.tim, address, value), 0x0400_0100...0x0400_010E => timer.write(T, &bus.tim, address, value),
0x0400_0114 => {}, 0x0400_0114 => {}, // TODO: Gyakuten Saiban writes 0x8000 to 0x0400_0114
0x0400_0110 => {}, // Not Used, 0x0400_0110 => {}, // Not Used,
// Serial Communication 1 // Serial Communication 1
@@ -241,29 +292,37 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
// Interrupts // Interrupts
0x0400_0200 => bus.io.ie.raw = value, 0x0400_0200 => bus.io.ie.raw = value,
0x0400_0202 => bus.io.irq.raw &= ~value, 0x0400_0202 => bus.io.irq.raw &= ~value,
0x0400_0204 => bus.io.waitcnt.set(value), 0x0400_0204 => log.debug("Wrote 0x{X:0>4} to WAITCNT", .{value}),
0x0400_0206 => {},
0x0400_0208 => bus.io.ime = value & 1 == 1, 0x0400_0208 => bus.io.ime = value & 1 == 1,
0x0400_020A => {}, 0x0400_0206, 0x0400_020A => {}, // Not Used
0x0400_0300 => {
bus.io.postflg = @enumFromInt(value & 1);
bus.io.haltcnt = if (value >> 15 & 1 == 0) .Halt else @panic("TODO: Implement STOP");
},
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, address }), else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, address }),
}, },
u8 => switch (address) { u8 => switch (address) {
// Display // Display
0x0400_0000...0x0400_0055 => ppu.write(T, &bus.ppu, address, value), 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),
// Sound // Sound
0x0400_0060...0x0400_00A7 => apu.write(T, &bus.apu, address, value), 0x0400_0060...0x0400_00A7 => apu.write(T, &bus.apu, address, value),
// Dma Transfers
0x0400_00B0...0x0400_00DF => dma.write(T, &bus.dma, address, value),
// Timers
0x0400_0100...0x0400_010F => timer.write(T, &bus.tim, address, value),
// Serial Communication 1 // Serial Communication 1
0x0400_0120 => log.debug("Wrote 0x{X:0>2} to SIODATA32_L_L", .{value}), 0x0400_0120 => log.debug("Wrote 0x{X:0>2} to SIODATA32_L_L", .{value}),
0x0400_0128 => log.debug("Wrote 0x{X:0>2} to SIOCNT_L", .{value}), 0x0400_0128 => log.debug("Wrote 0x{X:0>2} to SIOCNT_L", .{value}),
@@ -273,16 +332,9 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
0x0400_0140 => log.debug("Wrote 0x{X:0>2} to JOYCNT_L", .{value}), 0x0400_0140 => log.debug("Wrote 0x{X:0>2} to JOYCNT_L", .{value}),
// Interrupts // Interrupts
0x0400_0200, 0x0400_0201 => bus.io.ie.raw = setHalf(u16, bus.io.ie.raw, @truncate(address), value),
0x0400_0202 => bus.io.irq.raw &= ~@as(u16, value), 0x0400_0202 => bus.io.irq.raw &= ~@as(u16, value),
0x0400_0203 => bus.io.irq.raw &= ~@as(u16, value) << 8, // TODO: Is this good?
0x0400_0204, 0x0400_0205 => bus.io.waitcnt.set(setHalf(u16, bus.io.waitcnt.raw, @truncate(address), value)),
0x0400_0206, 0x0400_0207 => {},
0x0400_0208 => bus.io.ime = value & 1 == 1, 0x0400_0208 => bus.io.ime = value & 1 == 1,
0x0400_0209 => {}, 0x0400_0300 => bus.io.postflg = std.meta.intToEnum(PostFlag, value & 1) catch unreachable,
0x0400_020A, 0x0400_020B => {},
0x0400_0300 => bus.io.postflg = @enumFromInt(value & 1),
0x0400_0301 => bus.io.haltcnt = if (value >> 7 & 1 == 0) .Halt else std.debug.panic("TODO: Implement STOP", .{}), 0x0400_0301 => bus.io.haltcnt = if (value >> 7 & 1 == 0) .Halt else std.debug.panic("TODO: Implement STOP", .{}),
0x0400_0410 => log.debug("Wrote 0x{X:0>2} to the common yet undocumented 0x{X:0>8}", .{ value, address }), 0x0400_0410 => log.debug("Wrote 0x{X:0>2} to the common yet undocumented 0x{X:0>8}", .{ value, address }),
@@ -321,22 +373,14 @@ pub const DisplayControl = extern union {
/// Read / Write /// Read / Write
pub const DisplayStatus = extern union { pub const DisplayStatus = extern union {
/// read-only
vblank: Bit(u16, 0), vblank: Bit(u16, 0),
/// read-only
hblank: Bit(u16, 1), hblank: Bit(u16, 1),
// read-only
coincidence: Bit(u16, 2), coincidence: Bit(u16, 2),
vblank_irq: Bit(u16, 3), vblank_irq: Bit(u16, 3),
hblank_irq: Bit(u16, 4), hblank_irq: Bit(u16, 4),
vcount_irq: Bit(u16, 5), vcount_irq: Bit(u16, 5),
vcount_trigger: Bitfield(u16, 8, 8), vcount_trigger: Bitfield(u16, 8, 8),
raw: u16, raw: u16,
pub fn set(self: *DisplayStatus, value: u16) void {
const mask: u16 = 0x00C7; // set bits are read-only
self.raw = (self.raw & mask) | (value & ~mask);
}
}; };
/// Read Only /// Read Only
@@ -350,10 +394,10 @@ const InterruptEnable = extern union {
vblank: Bit(u16, 0), vblank: Bit(u16, 0),
hblank: Bit(u16, 1), hblank: Bit(u16, 1),
coincidence: Bit(u16, 2), coincidence: Bit(u16, 2),
tim0: Bit(u16, 3), tm0_overflow: Bit(u16, 3),
tim1: Bit(u16, 4), tm1_overflow: Bit(u16, 4),
tim2: Bit(u16, 5), tm2_overflow: Bit(u16, 5),
tim3: Bit(u16, 6), tm3_overflow: Bit(u16, 6),
serial: Bit(u16, 7), serial: Bit(u16, 7),
dma0: Bit(u16, 8), dma0: Bit(u16, 8),
dma1: Bit(u16, 9), dma1: Bit(u16, 9),
@@ -366,7 +410,7 @@ const InterruptEnable = extern union {
/// Read Only /// Read Only
/// 0 = Pressed, 1 = Released /// 0 = Pressed, 1 = Released
pub const KeyInput = extern union { const KeyInput = extern union {
a: Bit(u16, 0), a: Bit(u16, 0),
b: Bit(u16, 1), b: Bit(u16, 1),
select: Bit(u16, 2), select: Bit(u16, 2),
@@ -380,32 +424,6 @@ pub const KeyInput = extern union {
raw: u16, raw: u16,
}; };
const AtomicKeyInput = struct {
const Self = @This();
const AtomicOrder = std.builtin.AtomicOrder;
inner: KeyInput,
pub fn init(value: KeyInput) Self {
return .{ .inner = value };
}
pub inline fn load(self: *const Self, comptime ordering: AtomicOrder) u16 {
return switch (ordering) {
.acq_rel, .release => @compileError("not supported for atomic loads"),
else => @atomicLoad(u16, &self.inner.raw, ordering),
};
}
pub inline fn fetchOr(self: *Self, value: u16, comptime ordering: AtomicOrder) void {
_ = @atomicRmw(u16, &self.inner.raw, .Or, value, ordering);
}
pub inline fn fetchAnd(self: *Self, value: u16, comptime ordering: AtomicOrder) void {
_ = @atomicRmw(u16, &self.inner.raw, .And, value, ordering);
}
};
// Read / Write // Read / Write
pub const BackgroundControl = extern union { pub const BackgroundControl = extern union {
priority: Bitfield(u16, 0, 2), priority: Bitfield(u16, 0, 2),
@@ -458,6 +476,8 @@ const u8WriteKind = enum { Hi, Lo };
/// Write-only /// Write-only
pub const WinH = extern union { pub const WinH = extern union {
const Self = @This();
x2: Bitfield(u16, 0, 8), x2: Bitfield(u16, 0, 8),
x1: Bitfield(u16, 8, 8), x1: Bitfield(u16, 8, 8),
raw: u16, raw: u16,
@@ -470,6 +490,13 @@ pub const WinV = extern union {
y2: Bitfield(u16, 0, 8), y2: Bitfield(u16, 0, 8),
y1: Bitfield(u16, 8, 8), y1: Bitfield(u16, 8, 8),
raw: u16, 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,
};
}
}; };
pub const WinIn = extern union { pub const WinIn = extern union {
@@ -657,24 +684,3 @@ pub const SoundBias = extern union {
sampling_cycle: Bitfield(u16, 14, 2), sampling_cycle: Bitfield(u16, 14, 2),
raw: u16, raw: u16,
}; };
/// Read / Write
pub const WaitControl = extern union {
sram_cnt: Bitfield(u16, 0, 2),
s0_first: Bitfield(u16, 2, 2),
s0_second: Bit(u16, 4),
s1_first: Bitfield(u16, 5, 2),
s1_second: Bit(u16, 7),
s2_first: Bitfield(u16, 8, 2),
s2_second: Bit(u16, 10),
phi_out: Bitfield(u16, 11, 2),
prefetch_enable: Bit(u16, 14),
pak_kind: Bit(u16, 15),
raw: u16,
pub fn set(self: *WaitControl, value: u16) void {
const mask: u16 = 0x8000; // set bits are read-only
self.raw = (self.raw & mask) | (value & ~mask);
}
};

View File

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

727
src/core/cpu.zig Normal file
View File

@@ -0,0 +1,727 @@
const std = @import("std");
const util = @import("../util.zig");
const Bus = @import("Bus.zig");
const Bit = @import("bitfield").Bit;
const Bitfield = @import("bitfield").Bitfield;
const Scheduler = @import("scheduler.zig").Scheduler;
const FilePaths = @import("../util.zig").FilePaths;
const Logger = @import("../util.zig").Logger;
const File = std.fs.File;
// ARM Instructions
pub const arm = struct {
pub const InstrFn = *const fn (*Arm7tdmi, *Bus, u32) void;
const lut: [0x1000]InstrFn = populate();
const processing = @import("cpu/arm/data_processing.zig").dataProcessing;
const psrTransfer = @import("cpu/arm/psr_transfer.zig").psrTransfer;
const transfer = @import("cpu/arm/single_data_transfer.zig").singleDataTransfer;
const halfSignedTransfer = @import("cpu/arm/half_signed_data_transfer.zig").halfAndSignedDataTransfer;
const blockTransfer = @import("cpu/arm/block_data_transfer.zig").blockDataTransfer;
const branch = @import("cpu/arm/branch.zig").branch;
const branchExchange = @import("cpu/arm/branch.zig").branchAndExchange;
const swi = @import("cpu/arm/software_interrupt.zig").armSoftwareInterrupt;
const swap = @import("cpu/arm/single_data_swap.zig").singleDataSwap;
const multiply = @import("cpu/arm/multiply.zig").multiply;
const multiplyLong = @import("cpu/arm/multiply.zig").multiplyLong;
/// Determine index into ARM InstrFn LUT
fn idx(opcode: u32) u12 {
return @truncate(u12, opcode >> 20 & 0xFF) << 4 | @truncate(u12, opcode >> 4 & 0xF);
}
// Undefined ARM Instruction handler
fn und(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
const id = idx(opcode);
cpu.panic("[CPU/Decode] ID: 0x{X:0>3} 0x{X:0>8} is an illegal opcode", .{ id, opcode });
}
fn populate() [0x1000]InstrFn {
return comptime {
@setEvalBranchQuota(0xE000);
var ret = [_]InstrFn{und} ** 0x1000;
var i: usize = 0;
while (i < ret.len) : (i += 1) {
ret[i] = switch (@as(u2, i >> 10)) {
0b00 => if (i == 0x121) blk: {
break :blk branchExchange;
} else if (i & 0xFCF == 0x009) blk: {
const A = i >> 5 & 1 == 1;
const S = i >> 4 & 1 == 1;
break :blk multiply(A, S);
} else if (i & 0xFBF == 0x109) blk: {
const B = i >> 6 & 1 == 1;
break :blk swap(B);
} else if (i & 0xF8F == 0x089) blk: {
const U = i >> 6 & 1 == 1;
const A = i >> 5 & 1 == 1;
const S = i >> 4 & 1 == 1;
break :blk multiplyLong(U, A, S);
} else if (i & 0xE49 == 0x009 or i & 0xE49 == 0x049) blk: {
const P = i >> 8 & 1 == 1;
const U = i >> 7 & 1 == 1;
const I = i >> 6 & 1 == 1;
const W = i >> 5 & 1 == 1;
const L = i >> 4 & 1 == 1;
break :blk halfSignedTransfer(P, U, I, W, L);
} else if (i & 0xD90 == 0x100) blk: {
const I = i >> 9 & 1 == 1;
const R = i >> 6 & 1 == 1;
const kind = i >> 4 & 0x3;
break :blk psrTransfer(I, R, kind);
} else blk: {
const I = i >> 9 & 1 == 1;
const S = i >> 4 & 1 == 1;
const instrKind = i >> 5 & 0xF;
break :blk processing(I, S, instrKind);
},
0b01 => if (i >> 9 & 1 == 1 and i & 1 == 1) und else blk: {
const I = i >> 9 & 1 == 1;
const P = i >> 8 & 1 == 1;
const U = i >> 7 & 1 == 1;
const B = i >> 6 & 1 == 1;
const W = i >> 5 & 1 == 1;
const L = i >> 4 & 1 == 1;
break :blk transfer(I, P, U, B, W, L);
},
else => switch (@as(u2, i >> 9 & 0x3)) {
// MSB is guaranteed to be 1
0b00 => blk: {
const P = i >> 8 & 1 == 1;
const U = i >> 7 & 1 == 1;
const S = i >> 6 & 1 == 1;
const W = i >> 5 & 1 == 1;
const L = i >> 4 & 1 == 1;
break :blk blockTransfer(P, U, S, W, L);
},
0b01 => blk: {
const L = i >> 8 & 1 == 1;
break :blk branch(L);
},
0b10 => und, // COP Data Transfer
0b11 => if (i >> 8 & 1 == 1) swi() else und, // COP Data Operation + Register Transfer
},
};
}
return ret;
};
}
};
// THUMB Instructions
pub const thumb = struct {
pub const InstrFn = *const fn (*Arm7tdmi, *Bus, u16) void;
const lut: [0x400]InstrFn = populate();
const processing = @import("cpu/thumb/data_processing.zig");
const alu = @import("cpu/thumb/alu.zig").fmt4;
const transfer = @import("cpu/thumb/data_transfer.zig");
const block_transfer = @import("cpu/thumb/block_data_transfer.zig");
const swi = @import("cpu/thumb/software_interrupt.zig").fmt17;
const branch = @import("cpu/thumb/branch.zig");
/// Determine index into THUMB InstrFn LUT
fn idx(opcode: u16) u10 {
return @truncate(u10, opcode >> 6);
}
/// Undefined THUMB Instruction Handler
fn und(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
const id = idx(opcode);
cpu.panic("[CPU/Decode] ID: 0b{b:0>10} 0x{X:0>2} is an illegal opcode", .{ id, opcode });
}
fn populate() [0x400]InstrFn {
return comptime {
@setEvalBranchQuota(5025); // This is exact
var ret = [_]InstrFn{und} ** 0x400;
var i: usize = 0;
while (i < ret.len) : (i += 1) {
ret[i] = switch (@as(u3, i >> 7 & 0x7)) {
0b000 => if (i >> 5 & 0x3 == 0b11) blk: {
const I = i >> 4 & 1 == 1;
const is_sub = i >> 3 & 1 == 1;
const rn = i & 0x7;
break :blk processing.fmt2(I, is_sub, rn);
} else blk: {
const op = i >> 5 & 0x3;
const offset = i & 0x1F;
break :blk processing.fmt1(op, offset);
},
0b001 => blk: {
const op = i >> 5 & 0x3;
const rd = i >> 2 & 0x7;
break :blk processing.fmt3(op, rd);
},
0b010 => switch (@as(u2, i >> 5 & 0x3)) {
0b00 => if (i >> 4 & 1 == 1) blk: {
const op = i >> 2 & 0x3;
const h1 = i >> 1 & 1;
const h2 = i & 1;
break :blk processing.fmt5(op, h1, h2);
} else blk: {
const op = i & 0xF;
break :blk alu(op);
},
0b01 => blk: {
const rd = i >> 2 & 0x7;
break :blk transfer.fmt6(rd);
},
else => blk: {
const op = i >> 4 & 0x3;
const T = i >> 3 & 1 == 1;
break :blk transfer.fmt78(op, T);
},
},
0b011 => blk: {
const B = i >> 6 & 1 == 1;
const L = i >> 5 & 1 == 1;
const offset = i & 0x1F;
break :blk transfer.fmt9(B, L, offset);
},
else => switch (@as(u3, i >> 6 & 0x7)) {
// MSB is guaranteed to be 1
0b000 => blk: {
const L = i >> 5 & 1 == 1;
const offset = i & 0x1F;
break :blk transfer.fmt10(L, offset);
},
0b001 => blk: {
const L = i >> 5 & 1 == 1;
const rd = i >> 2 & 0x7;
break :blk transfer.fmt11(L, rd);
},
0b010 => blk: {
const isSP = i >> 5 & 1 == 1;
const rd = i >> 2 & 0x7;
break :blk processing.fmt12(isSP, rd);
},
0b011 => if (i >> 4 & 1 == 1) blk: {
const L = i >> 5 & 1 == 1;
const R = i >> 2 & 1 == 1;
break :blk block_transfer.fmt14(L, R);
} else blk: {
const S = i >> 1 & 1 == 1;
break :blk processing.fmt13(S);
},
0b100 => blk: {
const L = i >> 5 & 1 == 1;
const rb = i >> 2 & 0x7;
break :blk block_transfer.fmt15(L, rb);
},
0b101 => if (i >> 2 & 0xF == 0b1111) blk: {
break :blk thumb.swi();
} else blk: {
const cond = i >> 2 & 0xF;
break :blk branch.fmt16(cond);
},
0b110 => branch.fmt18(),
0b111 => blk: {
const is_low = i >> 5 & 1 == 1;
break :blk branch.fmt19(is_low);
},
},
};
}
return ret;
};
}
};
const log = std.log.scoped(.Arm7Tdmi);
pub const Arm7tdmi = struct {
const Self = @This();
r: [16]u32,
pipe: Pipeline,
sched: *Scheduler,
bus: *Bus,
cpsr: PSR,
spsr: PSR,
/// Storage for R8_fiq -> R12_fiq and their normal counterparts
/// e.g [r[0 + 8], fiq_r[0 + 8], r[1 + 8], fiq_r[1 + 8]...]
banked_fiq: [2 * 5]u32,
/// Storage for r13_<mode>, r14_<mode>
/// e.g. [r13, r14, r13_svc, r14_svc]
banked_r: [2 * 6]u32,
banked_spsr: [5]PSR,
logger: ?Logger,
pub fn init(sched: *Scheduler, bus: *Bus, log_file: ?std.fs.File) Self {
return Self{
.r = [_]u32{0x00} ** 16,
.pipe = Pipeline.init(),
.sched = sched,
.bus = bus,
.cpsr = .{ .raw = 0x0000_001F },
.spsr = .{ .raw = 0x0000_0000 },
.banked_fiq = [_]u32{0x00} ** 10,
.banked_r = [_]u32{0x00} ** 12,
.banked_spsr = [_]PSR{.{ .raw = 0x0000_0000 }} ** 5,
.logger = if (log_file) |file| Logger.init(file) else null,
};
}
inline fn bankedIdx(mode: Mode, kind: BankedKind) usize {
const idx: usize = switch (mode) {
.User, .System => 0,
.Supervisor => 1,
.Abort => 2,
.Undefined => 3,
.Irq => 4,
.Fiq => 5,
};
return (idx * 2) + if (kind == .R14) @as(usize, 1) else 0;
}
inline fn bankedSpsrIndex(mode: Mode) usize {
return switch (mode) {
.Supervisor => 0,
.Abort => 1,
.Undefined => 2,
.Irq => 3,
.Fiq => 4,
else => std.debug.panic("[CPU/Mode] {} does not have a SPSR Register", .{mode}),
};
}
inline fn bankedFiqIdx(i: usize, mode: Mode) usize {
return (i * 2) + if (mode == .Fiq) @as(usize, 1) else 0;
}
pub inline fn hasSPSR(self: *const Self) bool {
const mode = getModeChecked(self, self.cpsr.mode.read());
return switch (mode) {
.System, .User => false,
else => true,
};
}
pub inline fn isPrivileged(self: *const Self) bool {
const mode = getModeChecked(self, self.cpsr.mode.read());
return switch (mode) {
.User => false,
else => true,
};
}
pub inline fn isHalted(self: *const Self) bool {
return self.bus.io.haltcnt == .Halt;
}
pub fn setCpsr(self: *Self, value: u32) void {
if (value & 0x1F != self.cpsr.raw & 0x1F) self.changeModeFromIdx(@truncate(u5, value & 0x1F));
self.cpsr.raw = value;
}
fn changeModeFromIdx(self: *Self, next: u5) void {
self.changeMode(getModeChecked(self, next));
}
pub fn setUserModeRegister(self: *Self, idx: usize, value: u32) void {
const current = getModeChecked(self, self.cpsr.mode.read());
switch (idx) {
8...12 => {
if (current == .Fiq) {
self.banked_fiq[bankedFiqIdx(idx - 8, .User)] = value;
} else self.r[idx] = value;
},
13, 14 => switch (current) {
.User, .System => self.r[idx] = value,
else => {
const kind = std.meta.intToEnum(BankedKind, idx - 13) catch unreachable;
self.banked_r[bankedIdx(.User, kind)] = value;
},
},
else => self.r[idx] = value, // R0 -> R7 and R15
}
}
pub fn getUserModeRegister(self: *Self, idx: usize) u32 {
const current = getModeChecked(self, self.cpsr.mode.read());
return switch (idx) {
8...12 => if (current == .Fiq) self.banked_fiq[bankedFiqIdx(idx - 8, .User)] else self.r[idx],
13, 14 => switch (current) {
.User, .System => self.r[idx],
else => blk: {
const kind = std.meta.intToEnum(BankedKind, idx - 13) catch unreachable;
break :blk self.banked_r[bankedIdx(.User, kind)];
},
},
else => self.r[idx], // R0 -> R7 and R15
};
}
pub fn changeMode(self: *Self, next: Mode) void {
const now = getModeChecked(self, self.cpsr.mode.read());
// Bank R8 -> r12
var i: usize = 0;
while (i < 5) : (i += 1) {
self.banked_fiq[bankedFiqIdx(i, now)] = self.r[8 + i];
}
// Bank r13, r14, SPSR
switch (now) {
.User, .System => {
self.banked_r[bankedIdx(now, .R13)] = self.r[13];
self.banked_r[bankedIdx(now, .R14)] = self.r[14];
},
else => {
self.banked_r[bankedIdx(now, .R13)] = self.r[13];
self.banked_r[bankedIdx(now, .R14)] = self.r[14];
self.banked_spsr[bankedSpsrIndex(now)] = self.spsr;
},
}
// Grab R8 -> R12
i = 0;
while (i < 5) : (i += 1) {
self.r[8 + i] = self.banked_fiq[bankedFiqIdx(i, next)];
}
// Grab r13, r14, SPSR
switch (next) {
.User, .System => {
self.r[13] = self.banked_r[bankedIdx(next, .R13)];
self.r[14] = self.banked_r[bankedIdx(next, .R14)];
},
else => {
self.r[13] = self.banked_r[bankedIdx(next, .R13)];
self.r[14] = self.banked_r[bankedIdx(next, .R14)];
self.spsr = self.banked_spsr[bankedSpsrIndex(next)];
},
}
self.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[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;
}
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);
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);
if (checkCond(self.cpsr, @truncate(u4, opcode >> 28))) {
arm.lut[arm.idx(opcode)](self, self.bus, opcode);
}
}
}
pub fn stepDmaTransfer(self: *Self) bool {
const dma0 = &self.bus.dma[0];
const dma1 = &self.bus.dma[1];
const dma2 = &self.bus.dma[2];
const dma3 = &self.bus.dma[3];
if (dma0.in_progress) {
dma0.step(self);
return true;
}
if (dma1.in_progress) {
dma1.step(self);
return true;
}
if (dma2.in_progress) {
dma2.step(self);
return true;
}
if (dma3.in_progress) {
dma3.step(self);
return true;
}
return false;
}
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!", .{});
self.bus.io.haltcnt = .Execute;
// FIXME: This seems weird, but retAddr.gba suggests I need to make these changes
const ret_addr = self.r[15] - if (self.cpsr.t.read()) 0 else @as(u32, 4);
const new_spsr = self.cpsr.raw;
self.changeMode(.Irq);
self.cpsr.t.write(false);
self.cpsr.i.write(true);
self.r[14] = ret_addr;
self.spsr.raw = new_spsr;
self.r[15] = 0x0000_0018;
self.pipe.reload(self);
}
inline fn fetch(self: *Self, comptime T: type, address: u32) T {
comptime std.debug.assert(T == u32 or T == u16); // Opcode may be 32-bit (ARM) or 16-bit (THUMB)
// Bus.read will advance the scheduler. There are different timings for CPU fetches,
// so we want to undo what Bus.read will apply. We can do this by caching the current tick
// This is very dumb.
//
// FIXME: Please rework this
const tick_cache = self.sched.tick;
defer self.sched.tick = tick_cache + Bus.fetch_timings[@boolToInt(T == u32)][@truncate(u4, address >> 24)];
return self.bus.read(T, address);
}
pub fn panic(self: *const Self, comptime format: []const u8, args: anytype) noreturn {
var i: usize = 0;
while (i < 16) : (i += 4) {
const i_1 = i + 1;
const i_2 = i + 2;
const i_3 = i + 3;
std.debug.print("R{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\n", .{ i, self.r[i], i_1, self.r[i_1], i_2, self.r[i_2], i_3, self.r[i_3] });
}
std.debug.print("cpsr: 0x{X:0>8} ", .{self.cpsr.raw});
prettyPrintPsr(&self.cpsr);
std.debug.print("spsr: 0x{X:0>8} ", .{self.spsr.raw});
prettyPrintPsr(&self.spsr);
std.debug.print("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);
std.debug.print("opcode: ID: 0x{b:0>10} 0x{X:0>4}\n", .{ id, opcode });
} else {
const opcode = self.bus.dbgRead(u32, self.r[15] - 4);
const id = arm.idx(opcode);
std.debug.print("opcode: ID: 0x{X:0>3} 0x{X:0>8}\n", .{ id, opcode });
}
std.debug.print("tick: {}\n\n", .{self.sched.tick});
std.debug.panic(format, args);
}
fn prettyPrintPsr(psr: *const PSR) void {
std.debug.print("[", .{});
if (psr.n.read()) std.debug.print("N", .{}) else std.debug.print("-", .{});
if (psr.z.read()) std.debug.print("Z", .{}) else std.debug.print("-", .{});
if (psr.c.read()) std.debug.print("C", .{}) else std.debug.print("-", .{});
if (psr.v.read()) std.debug.print("V", .{}) else std.debug.print("-", .{});
if (psr.i.read()) std.debug.print("I", .{}) else std.debug.print("-", .{});
if (psr.f.read()) std.debug.print("F", .{}) else std.debug.print("-", .{});
if (psr.t.read()) std.debug.print("T", .{}) else std.debug.print("-", .{});
std.debug.print("|", .{});
if (getMode(psr.mode.read())) |mode| std.debug.print("{s}", .{modeString(mode)}) else std.debug.print("---", .{});
std.debug.print("]\n", .{});
}
fn modeString(mode: Mode) []const u8 {
return switch (mode) {
.User => "usr",
.Fiq => "fiq",
.Irq => "irq",
.Supervisor => "svc",
.Abort => "abt",
.Undefined => "und",
.System => "sys",
};
}
fn mgbaLog(self: *const Self, file: *const File, opcode: u32) !void {
const thumb_fmt = "{X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} cpsr: {X:0>8} | {X:0>4}:\n";
const arm_fmt = "{X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} cpsr: {X:0>8} | {X:0>8}:\n";
var buf: [0x100]u8 = [_]u8{0x00} ** 0x100; // this is larger than it needs to be
const r0 = self.r[0];
const r1 = self.r[1];
const r2 = self.r[2];
const r3 = self.r[3];
const r4 = self.r[4];
const r5 = self.r[5];
const r6 = self.r[6];
const r7 = self.r[7];
const r8 = self.r[8];
const r9 = self.r[9];
const r10 = self.r[10];
const r11 = self.r[11];
const r12 = self.r[12];
const r13 = self.r[13];
const r14 = self.r[14];
const r15 = self.r[15] -| if (self.cpsr.t.read()) 2 else @as(u32, 4);
const c_psr = self.cpsr.raw;
var log_str: []u8 = undefined;
if (self.cpsr.t.read()) {
if (opcode >> 11 == 0x1E) {
// Instruction 1 of a BL Opcode, print in ARM mode
const other_half = self.bus.debugRead(u16, self.r[15] - 2);
const bl_opcode = @as(u32, opcode) << 16 | other_half;
log_str = try std.fmt.bufPrint(&buf, arm_fmt, .{ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, c_psr, bl_opcode });
} else {
log_str = try std.fmt.bufPrint(&buf, thumb_fmt, .{ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, c_psr, opcode });
}
} else {
log_str = try std.fmt.bufPrint(&buf, arm_fmt, .{ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, c_psr, opcode });
}
_ = try file.writeAll(log_str);
}
};
pub fn checkCond(cpsr: PSR, cond: u4) bool {
return switch (cond) {
0x0 => cpsr.z.read(), // EQ - Equal
0x1 => !cpsr.z.read(), // NE - Not equal
0x2 => cpsr.c.read(), // CS - Unsigned higher or same
0x3 => !cpsr.c.read(), // CC - Unsigned lower
0x4 => cpsr.n.read(), // MI - Negative
0x5 => !cpsr.n.read(), // PL - Positive or zero
0x6 => cpsr.v.read(), // VS - Overflow
0x7 => !cpsr.v.read(), // VC - No overflow
0x8 => cpsr.c.read() and !cpsr.z.read(), // HI - unsigned higher
0x9 => !cpsr.c.read() or cpsr.z.read(), // LS - unsigned lower or same
0xA => cpsr.n.read() == cpsr.v.read(), // GE - Greater or equal
0xB => cpsr.n.read() != cpsr.v.read(), // LT - Less than
0xC => !cpsr.z.read() and (cpsr.n.read() == cpsr.v.read()), // GT - Greater than
0xD => cpsr.z.read() or (cpsr.n.read() != cpsr.v.read()), // LE - Less than or equal
0xE => true, // AL - Always
0xF => false, // NV - Never (reserved in ARMv3 and up, but seems to have not changed?)
};
}
const 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),
f: Bit(u32, 6),
i: Bit(u32, 7),
v: Bit(u32, 28),
c: Bit(u32, 29),
z: Bit(u32, 30),
n: Bit(u32, 31),
raw: u32,
};
const Mode = enum(u5) {
User = 0b10000,
Fiq = 0b10001,
Irq = 0b10010,
Supervisor = 0b10011,
Abort = 0b10111,
Undefined = 0b11011,
System = 0b11111,
};
const BankedKind = enum(u1) {
R13 = 0,
R14,
};
fn getMode(bits: u5) ?Mode {
return std.meta.intToEnum(Mode, bits) catch null;
}
fn getModeChecked(cpu: *const Arm7tdmi, bits: u5) Mode {
return getMode(bits) orelse cpu.panic("[CPU/CPSR] 0b{b:0>5} is an invalid CPU mode", .{bits});
}

View File

@@ -0,0 +1,112 @@
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
pub fn blockDataTransfer(comptime P: bool, comptime U: bool, comptime S: bool, comptime W: bool, comptime L: bool) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u32) void {
const rn = @truncate(u4, opcode >> 16 & 0xF);
const rlist = opcode & 0xFFFF;
const r15 = rlist >> 15 & 1 == 1;
var count: u32 = 0;
var i: u5 = 0;
var first: u4 = 0;
var write_to_base = true;
while (i < 16) : (i += 1) {
const r = @truncate(u4, 15 - i);
if (rlist >> r & 1 == 1) {
first = r;
count += 1;
}
}
var start = cpu.r[rn];
if (U) {
start += if (P) 4 else 0;
} else {
start = start - (4 * count) + if (!P) 4 else 0;
}
var end = cpu.r[rn];
if (U) {
end = end + (4 * count) - if (!P) 4 else 0;
} else {
end -= if (P) 4 else 0;
}
var new_base = cpu.r[rn];
if (U) {
new_base += 4 * count;
} else {
new_base -= 4 * count;
}
var address = start;
if (rlist == 0) {
var und_addr = cpu.r[rn];
if (U) {
und_addr += if (P) 4 else 0;
} else {
und_addr -= 0x40 - if (!P) 4 else 0;
}
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);
}
cpu.r[rn] = if (U) cpu.r[rn] + 0x40 else cpu.r[rn] - 0x40;
return;
}
i = first;
while (i < 16) : (i += 1) {
if (rlist >> i & 1 == 1) {
transfer(cpu, bus, r15, i, address);
address += 4;
if (W and !L and write_to_base) {
cpu.r[rn] = new_base;
write_to_base = false;
}
}
}
if (W and L and rlist >> rn & 1 == 0) cpu.r[rn] = new_base;
}
fn transfer(cpu: *Arm7tdmi, bus: *Bus, r15_present: bool, i: u5, address: u32) void {
if (L) {
if (S and !r15_present) {
// Always Transfer User mode Registers
cpu.setUserModeRegister(i, bus.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);
}
}
} 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
} else {
bus.write(u32, address, cpu.r[i] + if (i == 0xF) 4 else @as(u32, 0));
}
}
}
}.inner;
}

View File

@@ -0,0 +1,28 @@
const std = @import("std");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
const sext = @import("../../../util.zig").sext;
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);
}
}.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);
}

View File

@@ -0,0 +1,183 @@
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;
pub fn dataProcessing(comptime I: bool, comptime S: bool, comptime kind: u4) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
const rd = @truncate(u4, opcode >> 12 & 0xF);
const rn = opcode >> 16 & 0xF;
const old_carry = @boolToInt(cpu.cpsr.c.read());
// If certain conditions are met, PC is 12 ahead instead of 8
// TODO: Why these conditions?
if (!I and opcode >> 4 & 1 == 1) cpu.r[15] += 4;
const op1 = cpu.r[rn];
const amount = @truncate(u8, (opcode >> 8 & 0xF) << 1);
const op2 = if (I) ror(S, &cpu.cpsr, opcode & 0xFF, amount) else exec(S, cpu, opcode);
// Undo special condition from above
if (!I and opcode >> 4 & 1 == 1) cpu.r[15] -= 4;
var result: u32 = undefined;
var overflow: bool = undefined;
// Perform Data Processing Logic
switch (kind) {
0x0 => result = op1 & op2, // AND
0x1 => result = op1 ^ op2, // EOR
0x2 => result = op1 -% op2, // SUB
0x3 => result = op2 -% op1, // RSB
0x4 => result = add(&overflow, op1, op2), // ADD
0x5 => result = adc(&overflow, op1, op2, old_carry), // ADC
0x6 => result = sbc(op1, op2, old_carry), // SBC
0x7 => result = sbc(op2, op1, old_carry), // RSC
0x8 => {
// TST
if (rd == 0xF)
return undefinedTestBehaviour(cpu);
result = op1 & op2;
},
0x9 => {
// TEQ
if (rd == 0xF)
return undefinedTestBehaviour(cpu);
result = op1 ^ op2;
},
0xA => {
// CMP
if (rd == 0xF)
return undefinedTestBehaviour(cpu);
result = 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);
}
},
}
// 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
},
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);
}
},
0x4, 0x5 => if (S and rd != 0xF) {
// ADD, ADC Flags
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
cpu.cpsr.c.write(overflow);
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
},
0x6, 0x7 => if (S and rd != 0xF) {
// SBC, RSC Flags
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
if (kind == 0x6) {
// SBC specific
const subtrahend = @as(u64, op2) -% old_carry +% 1;
cpu.cpsr.c.write(subtrahend <= op1);
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
} else {
// RSC Specific
const subtrahend = @as(u64, op1) -% old_carry +% 1;
cpu.cpsr.c.write(subtrahend <= op2);
cpu.cpsr.v.write(((op2 ^ result) & (~op1 ^ result)) >> 31 & 1 == 1);
}
},
0x8, 0x9, 0xA, 0xB => {
// Test Operation Flags
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
if (kind == 0xA) {
// CMP specific
cpu.cpsr.c.write(op2 <= op1);
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
} else if (kind == 0xB) {
// CMN specific
cpu.cpsr.c.write(overflow);
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
} else {
// TST, TEQ specific
// Barrel Shifter should always calc CPSR C in TST
if (!S) _ = exec(true, cpu, opcode);
}
},
}
}
}.inner;
}
pub fn sbc(left: u32, right: u32, old_carry: u1) u32 {
// TODO: Make your own version (thanks peach.bot)
const subtrahend = @as(u64, right) -% old_carry +% 1;
const ret = @truncate(u32, left -% subtrahend);
return ret;
}
pub fn add(overflow: *bool, left: u32, right: u32) u32 {
var ret: u32 = undefined;
overflow.* = @addWithOverflow(u32, left, right, &ret);
return ret;
}
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);
overflow.* = first or second;
return ret;
}
fn undefinedTestBehaviour(cpu: *Arm7tdmi) void {
@setCold(true);
cpu.setCpsr(cpu.spsr.raw);
}

View File

@@ -0,0 +1,58 @@
const std = @import("std");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
const sext = @import("../../../util.zig").sext;
const rotr = @import("../../../util.zig").rotr;
pub fn halfAndSignedDataTransfer(comptime P: bool, comptime U: bool, comptime I: bool, comptime W: bool, comptime L: bool) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u32) void {
const rn = opcode >> 16 & 0xF;
const rd = opcode >> 12 & 0xF;
const rm = opcode & 0xF;
const imm_offset_high = opcode >> 8 & 0xF;
const base = cpu.r[rn] + if (!L and rn == 0xF) 4 else @as(u32, 0);
const offset = if (I) imm_offset_high << 4 | rm else cpu.r[rm];
const modified_base = if (U) base +% offset else base -% offset;
var address = if (P) modified_base else base;
var result: u32 = undefined;
if (L) {
switch (@truncate(u2, opcode >> 5)) {
0b01 => {
// LDRH
const value = bus.read(u16, address);
result = rotr(u32, value, 8 * (address & 1));
},
0b10 => {
// LDRSB
result = sext(u32, u8, bus.read(u8, address));
},
0b11 => {
// LDRSH
result = if (address & 1 == 1) blk: {
break :blk sext(u32, u8, bus.read(u8, address));
} else blk: {
break :blk sext(u32, u16, bus.read(u16, address));
};
},
0b00 => unreachable, // SWP
}
} else {
if (opcode >> 5 & 0x01 == 0x01) {
// STRH
bus.write(u16, address, @truncate(u16, cpu.r[rd]));
} else unreachable; // SWP
}
address = modified_base;
if (W and P or !P) cpu.r[rn] = address;
if (L) cpu.r[rd] = result; // // This emulates the LDR rd == rn behaviour
}
}.inner;
}

View File

@@ -0,0 +1,57 @@
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
pub fn multiply(comptime A: bool, comptime S: bool) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
const rd = opcode >> 16 & 0xF;
const rn = opcode >> 12 & 0xF;
const rs = opcode >> 8 & 0xF;
const rm = opcode & 0xF;
const temp: u64 = @as(u64, cpu.r[rm]) * @as(u64, cpu.r[rs]) + if (A) cpu.r[rn] else 0;
const result = @truncate(u32, temp);
cpu.r[rd] = result;
if (S) {
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
// V is unaffected, C is *actually* undefined in ARMv4
}
}
}.inner;
}
pub fn multiplyLong(comptime U: bool, comptime A: bool, comptime S: bool) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
const rd_hi = opcode >> 16 & 0xF;
const rd_lo = opcode >> 12 & 0xF;
const rs = opcode >> 8 & 0xF;
const rm = opcode & 0xF;
if (U) {
// Signed (WHY IS IT U THEN?)
var result: i64 = @as(i64, @bitCast(i32, cpu.r[rm])) * @as(i64, @bitCast(i32, cpu.r[rs]));
if (A) result +%= @bitCast(i64, @as(u64, cpu.r[rd_hi]) << 32 | @as(u64, cpu.r[rd_lo]));
cpu.r[rd_hi] = @bitCast(u32, @truncate(i32, result >> 32));
cpu.r[rd_lo] = @bitCast(u32, @truncate(i32, result));
} else {
// Unsigned
var result: u64 = @as(u64, cpu.r[rm]) * @as(u64, cpu.r[rs]);
if (A) result +%= @as(u64, cpu.r[rd_hi]) << 32 | @as(u64, cpu.r[rd_lo]);
cpu.r[rd_hi] = @truncate(u32, result >> 32);
cpu.r[rd_lo] = @truncate(u32, result);
}
if (S) {
cpu.cpsr.z.write(cpu.r[rd_hi] == 0 and cpu.r[rd_lo] == 0);
cpu.cpsr.n.write(cpu.r[rd_hi] >> 31 & 1 == 1);
// C and V are set to meaningless values
}
}
}.inner;
}

View File

@@ -0,0 +1,59 @@
const std = @import("std");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
const PSR = @import("../../cpu.zig").PSR;
const log = std.log.scoped(.PsrTransfer);
const rotr = @import("../../../util.zig").rotr;
pub fn psrTransfer(comptime I: bool, comptime R: bool, comptime kind: u2) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
switch (kind) {
0b00 => {
// MRS
const rd = opcode >> 12 & 0xF;
if (R and !cpu.hasSPSR()) log.err("Tried to read SPSR from User/System Mode", .{});
cpu.r[rd] = if (R) cpu.spsr.raw else cpu.cpsr.raw;
},
0b10 => {
// MSR
const field_mask = @truncate(u4, opcode >> 16 & 0xF);
const rm_idx = opcode & 0xF;
const right = if (I) rotr(u32, opcode & 0xFF, (opcode >> 8 & 0xF) * 2) else cpu.r[rm_idx];
if (R and !cpu.hasSPSR()) log.err("Tried to write to SPSR in User/System Mode", .{});
if (R) {
// arm.gba seems to expect the SPSR to do somethign in SYS mode,
// so we just assume that despite writing to the SPSR in USR or SYS mode
// being UNPREDICTABLE, it just magically has a working SPSR somehow
cpu.spsr.raw = fieldMask(&cpu.spsr, field_mask, right);
} else {
if (cpu.isPrivileged()) cpu.setCpsr(fieldMask(&cpu.cpsr, field_mask, right));
}
},
else => cpu.panic("[CPU/PSR Transfer] Bits 21:220 of {X:0>8} are undefined", .{opcode}),
}
}
}.inner;
}
fn fieldMask(psr: *const PSR, field_mask: u4, right: u32) u32 {
// This bitwise ORs bits 3 and 0 of the field mask into a u2
// We do this because we only care about bits 7:0 and 31:28 of the CPSR
const bits = @truncate(u2, (field_mask >> 2 & 0x2) | (field_mask & 1));
const mask: u32 = switch (bits) {
0b00 => 0x0000_0000,
0b01 => 0x0000_00FF,
0b10 => 0xF000_0000,
0b11 => 0xF000_00FF,
};
return (psr.raw & ~mask) | (right & mask);
}

View File

@@ -0,0 +1,31 @@
const std = @import("std");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
const rotr = @import("../../../util.zig").rotr;
pub fn singleDataSwap(comptime B: bool) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u32) void {
const rn = opcode >> 16 & 0xF;
const rd = opcode >> 12 & 0xF;
const rm = opcode & 0xF;
const address = cpu.r[rn];
if (B) {
// SWPB
const value = bus.read(u8, address);
bus.write(u8, address, @truncate(u8, cpu.r[rm]));
cpu.r[rd] = value;
} else {
// SWP
const value = rotr(u32, bus.read(u32, address), 8 * (address & 0x3));
bus.write(u32, address, cpu.r[rm]);
cpu.r[rd] = value;
}
}
}.inner;
}

View File

@@ -0,0 +1,60 @@
const std = @import("std");
const util = @import("../../../util.zig");
const shifter = @import("../barrel_shifter.zig");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
const rotr = @import("../../../util.zig").rotr;
pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool, comptime B: bool, comptime W: bool, comptime L: bool) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u32) void {
const rn = opcode >> 16 & 0xF;
const rd = opcode >> 12 & 0xF;
// rn is r15 and L is not set, the PC is 12 ahead
const base = cpu.r[rn] + if (!L and rn == 0xF) 4 else @as(u32, 0);
const offset = if (I) shifter.immediate(false, cpu, opcode) else opcode & 0xFFF;
const modified_base = if (U) base +% offset else base -% offset;
var address = if (P) modified_base else base;
var result: u32 = undefined;
if (L) {
if (B) {
// LDRB
result = bus.read(u8, address);
} else {
// LDR
const value = bus.read(u32, address);
result = rotr(u32, value, 8 * (address & 0x3));
}
} else {
if (B) {
// STRB
const value = cpu.r[rd] + if (rd == 0xF) 4 else @as(u32, 0); // PC is 12 ahead
bus.write(u8, address, @truncate(u8, value));
} else {
// STR
const value = cpu.r[rd] + if (rd == 0xF) 4 else @as(u32, 0);
bus.write(u32, address, value);
}
}
address = modified_base;
if (W and P or !P) {
cpu.r[rn] = address;
if (rn == 0xF) cpu.pipe.reload(cpu);
}
if (L) {
// This emulates the LDR rd == rn behaviour
cpu.r[rd] = result;
if (rd == 0xF) cpu.pipe.reload(cpu);
}
}
}.inner;
}

View File

@@ -0,0 +1,23 @@
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
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 cpsr = cpu.cpsr.raw;
// Switch Mode
cpu.changeMode(.Supervisor);
cpu.cpsr.t.write(false); // Force ARM Mode
cpu.cpsr.i.write(true); // Disable normal interrupts
cpu.r[14] = ret_addr; // Resume Execution
cpu.spsr.raw = cpsr; // Previous mode CPSR
cpu.r[15] = 0x0000_0008;
cpu.pipe.reload(cpu);
}
}.inner;
}

View File

@@ -0,0 +1,149 @@
const std = @import("std");
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
const CPSR = @import("../cpu.zig").PSR;
const rotr = @import("../../util.zig").rotr;
pub fn exec(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
var result: u32 = undefined;
if (opcode >> 4 & 1 == 1) {
result = register(S, cpu, opcode);
} else {
result = immediate(S, cpu, opcode);
}
return result;
}
fn register(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
const rs_idx = opcode >> 8 & 0xF;
const rm = cpu.r[opcode & 0xF];
const rs = @truncate(u8, cpu.r[rs_idx]);
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),
};
}
pub fn immediate(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
const amount = @truncate(u8, opcode >> 7 & 0x1F);
const rm = cpu.r[opcode & 0xF];
var result: u32 = undefined;
if (amount == 0) {
switch (@truncate(u2, opcode >> 5)) {
0b00 => {
// LSL #0
result = rm;
},
0b01 => {
// LSR #0 aka LSR #32
if (S) cpu.cpsr.c.write(rm >> 31 & 1 == 1);
result = 0x0000_0000;
},
0b10 => {
// ASR #0 aka ASR #32
result = @bitCast(u32, @bitCast(i32, rm) >> 31);
if (S) cpu.cpsr.c.write(result >> 31 & 1 == 1);
},
0b11 => {
// ROR #0 aka RRX
const carry: u32 = @boolToInt(cpu.cpsr.c.read());
if (S) cpu.cpsr.c.write(rm & 1 == 1);
result = (carry << 31) | (rm >> 1);
},
}
} else {
switch (@truncate(u2, opcode >> 5)) {
0b00 => result = lsl(S, &cpu.cpsr, rm, amount),
0b01 => result = lsr(S, &cpu.cpsr, rm, amount),
0b10 => result = asr(S, &cpu.cpsr, rm, amount),
0b11 => result = ror(S, &cpu.cpsr, rm, amount),
}
}
return result;
}
pub fn lsl(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
const amount = @truncate(u5, total_amount);
const bit_count: u8 = @typeInfo(u32).Int.bits;
var result: u32 = 0x0000_0000;
if (total_amount < bit_count) {
// We can perform a well-defined shift here
result = rm << amount;
if (S and total_amount != 0) {
const carry_bit = @truncate(u5, bit_count - amount);
cpsr.c.write(rm >> carry_bit & 1 == 1);
}
} else {
if (S) {
if (total_amount == bit_count) {
// Shifted all bits out, carry bit is bit 0 of rm
cpsr.c.write(rm & 1 == 1);
} else {
cpsr.c.write(false);
}
}
}
return result;
}
pub fn lsr(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u32) u32 {
const amount = @truncate(u5, total_amount);
const bit_count: u8 = @typeInfo(u32).Int.bits;
var result: u32 = 0x0000_0000;
if (total_amount < bit_count) {
// We can perform a well-defined shift
result = rm >> amount;
if (S and total_amount != 0) cpsr.c.write(rm >> (amount - 1) & 1 == 1);
} else {
if (S) {
if (total_amount == bit_count) {
// LSR #32
cpsr.c.write(rm >> 31 & 1 == 1);
} else {
// All bits have been shifted out, including carry bit
cpsr.c.write(false);
}
}
}
return result;
}
pub fn asr(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
const amount = @truncate(u5, total_amount);
const bit_count: u8 = @typeInfo(u32).Int.bits;
var result: u32 = 0x0000_0000;
if (total_amount < bit_count) {
result = @bitCast(u32, @bitCast(i32, rm) >> amount);
if (S and total_amount != 0) cpsr.c.write(rm >> (amount - 1) & 1 == 1);
} else {
// ASR #32 and ASR #>32 have the same result
result = @bitCast(u32, @bitCast(i32, rm) >> 31);
if (S) cpsr.c.write(result >> 31 & 1 == 1);
}
return result;
}
pub fn ror(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
const result = rotr(u32, rm, total_amount);
if (S and total_amount != 0) {
cpsr.c.write(result >> 31 & 1 == 1);
}
return result;
}

102
src/core/cpu/thumb/alu.zig Normal file
View File

@@ -0,0 +1,102 @@
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
const adc = @import("../arm/data_processing.zig").adc;
const sbc = @import("../arm/data_processing.zig").sbc;
const lsl = @import("../barrel_shifter.zig").lsl;
const lsr = @import("../barrel_shifter.zig").lsr;
const asr = @import("../barrel_shifter.zig").asr;
const ror = @import("../barrel_shifter.zig").ror;
pub fn fmt4(comptime op: u4) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
const rs = opcode >> 3 & 0x7;
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
},
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);
}
},
0x5, 0xB => {
// ADC, CMN
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
cpu.cpsr.c.write(overflow);
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
},
0x6 => {
// SBC
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
const subtrahend = @as(u64, op2) -% carry +% 1;
cpu.cpsr.c.write(subtrahend <= op1);
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
},
0x9 => {
// NEG
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
cpu.cpsr.c.write(op2 <= 0);
cpu.cpsr.v.write(((0 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
},
0xD => {
// Multiplication
cpu.cpsr.n.write(result >> 31 & 1 == 1);
cpu.cpsr.z.write(result == 0);
// V is unaffected, assuming similar behaviour to ARMv4 MUL C is undefined
},
}
}
}.inner;
}

View File

@@ -0,0 +1,101 @@
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
pub fn fmt14(comptime L: bool, comptime R: bool) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
const count = @boolToInt(R) + countRlist(opcode);
const start = cpu.r[13] - if (!L) count * 4 else 0;
var end = cpu.r[13];
if (L) {
end += count * 4;
} else {
end -= 4;
}
var address = start;
var i: u4 = 0;
while (i < 8) : (i += 1) {
if (opcode >> i & 1 == 1) {
if (L) {
cpu.r[i] = bus.read(u32, address);
} else {
bus.write(u32, address, cpu.r[i]);
}
address += 4;
}
}
if (R) {
if (L) {
const value = bus.read(u32, address);
cpu.r[15] = value & ~@as(u32, 1);
cpu.pipe.reload(cpu);
} else {
bus.write(u32, address, cpu.r[14]);
}
address += 4;
}
cpu.r[13] = if (L) end else start;
}
}.inner;
}
pub fn fmt15(comptime L: bool, comptime rb: u3) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
var address = cpu.r[rb];
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);
}
cpu.r[rb] += 0x40;
return;
}
var i: u4 = 0;
var first_write = true;
while (i < 8) : (i += 1) {
if (opcode >> i & 1 == 1) {
if (L) {
cpu.r[i] = bus.read(u32, address);
} else {
bus.write(u32, address, cpu.r[i]);
}
if (!L and first_write) {
cpu.r[rb] = end_address;
first_write = false;
}
address += 4;
}
}
if (L and opcode >> rb & 1 != 1) cpu.r[rb] = address;
}
}.inner;
}
inline fn countRlist(opcode: u16) u32 {
var count: u32 = 0;
comptime var i: u4 = 0;
inline while (i < 8) : (i += 1) {
if (opcode >> (7 - i) & 1 == 1) count += 1;
}
return count;
}

View File

@@ -0,0 +1,54 @@
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
const checkCond = @import("../../cpu.zig").checkCond;
const sext = @import("../../../util.zig").sext;
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});
if (!checkCond(cpu.cpsr, cond)) return;
cpu.r[15] +%= sext(u32, u8, opcode & 0xFF) << 1;
cpu.pipe.reload(cpu);
}
}.inner;
}
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);
}
}.inner;
}
pub fn fmt19(comptime is_low: bool) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
// BL
const offset = opcode & 0x7FF;
if (is_low) {
// Instruction 2
const next_opcode = cpu.r[15] - 2;
cpu.r[15] = cpu.r[14] +% (offset << 1);
cpu.r[14] = next_opcode | 1;
cpu.pipe.reload(cpu);
} else {
// Instruction 1
const lr_offset = sext(u32, u11, offset) << 12;
cpu.r[14] = (cpu.r[15] +% lr_offset) & ~@as(u32, 1);
}
}
}.inner;
}

View File

@@ -0,0 +1,201 @@
const std = @import("std");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
const add = @import("../arm/data_processing.zig").add;
const lsl = @import("../barrel_shifter.zig").lsl;
const lsr = @import("../barrel_shifter.zig").lsr;
const asr = @import("../barrel_shifter.zig").asr;
pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
const rs = opcode >> 3 & 0x7;
const rd = opcode & 0x7;
const result = switch (op) {
0b00 => blk: {
// LSL
if (offset == 0) {
break :blk cpu.r[rs];
} else {
break :blk lsl(true, &cpu.cpsr, cpu.r[rs], offset);
}
},
0b01 => blk: {
// LSR
if (offset == 0) {
cpu.cpsr.c.write(cpu.r[rs] >> 31 & 1 == 1);
break :blk @as(u32, 0);
} else {
break :blk lsr(true, &cpu.cpsr, cpu.r[rs], offset);
}
},
0b10 => blk: {
// ASR
if (offset == 0) {
cpu.cpsr.c.write(cpu.r[rs] >> 31 & 1 == 1);
break :blk @bitCast(u32, @bitCast(i32, cpu.r[rs]) >> 31);
} else {
break :blk asr(true, &cpu.cpsr, cpu.r[rs], offset);
}
},
else => cpu.panic("[CPU/THUMB.1] 0b{b:0>2} is not a valid op", .{op}),
};
// 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);
}
}.inner;
}
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 op1 = cpu.r[rd];
const op2 = cpu.r[rs];
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
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);
},
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;
}
pub fn fmt2(comptime I: bool, is_sub: bool, rn: u3) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
const rs = opcode >> 3 & 0x7;
const rd = @truncate(u3, opcode);
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);
} 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);
}
}
}.inner;
}
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);
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);
},
}
}
}.inner;
}
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 right = (opcode & 0xFF) << 2;
cpu.r[rd] = left + right;
}
}.inner;
}
pub fn fmt13(comptime S: bool) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
// ADD
const offset = (opcode & 0x7F) << 2;
cpu.r[13] = if (S) cpu.r[13] - offset else cpu.r[13] + offset;
}
}.inner;
}

View File

@@ -0,0 +1,150 @@
const std = @import("std");
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
const rotr = @import("../../../util.zig").rotr;
const sext = @import("../../../util.zig").sext;
pub fn fmt6(comptime rd: u3) InstrFn {
return struct {
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);
}
}.inner;
}
pub fn fmt78(comptime op: u2, comptime T: bool) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
const ro = opcode >> 6 & 0x7;
const rb = opcode >> 3 & 0x7;
const rd = opcode & 0x7;
const address = cpu.r[rb] +% cpu.r[ro];
if (T) {
// Format 8
switch (op) {
0b00 => {
// STRH
bus.write(u16, address, @truncate(u16, cpu.r[rd]));
},
0b01 => {
// LDSB
cpu.r[rd] = sext(u32, u8, bus.read(u8, address));
},
0b10 => {
// LDRH
const value = bus.read(u16, address);
cpu.r[rd] = rotr(u32, value, 8 * (address & 1));
},
0b11 => {
// LDRSH
cpu.r[rd] = if (address & 1 == 1) blk: {
break :blk sext(u32, u8, bus.read(u8, address));
} else blk: {
break :blk sext(u32, u16, bus.read(u16, address));
};
},
}
} else {
// Format 7
switch (op) {
0b00 => {
// STR
bus.write(u32, address, cpu.r[rd]);
},
0b01 => {
// STRB
bus.write(u8, address, @truncate(u8, cpu.r[rd]));
},
0b10 => {
// LDR
const value = bus.read(u32, address);
cpu.r[rd] = rotr(u32, value, 8 * (address & 0x3));
},
0b11 => {
// LDRB
cpu.r[rd] = bus.read(u8, address);
},
}
}
}
}.inner;
}
pub fn fmt9(comptime B: bool, comptime L: bool, comptime offset: u5) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
const rb = opcode >> 3 & 0x7;
const rd = opcode & 0x7;
if (L) {
if (B) {
// LDRB
const address = cpu.r[rb] + offset;
cpu.r[rd] = bus.read(u8, address);
} else {
// LDR
const address = cpu.r[rb] + (@as(u32, offset) << 2);
const value = bus.read(u32, address);
cpu.r[rd] = rotr(u32, value, 8 * (address & 0x3));
}
} else {
if (B) {
// STRB
const address = cpu.r[rb] + offset;
bus.write(u8, address, @truncate(u8, cpu.r[rd]));
} else {
// STR
const address = cpu.r[rb] + (@as(u32, offset) << 2);
bus.write(u32, address, cpu.r[rd]);
}
}
}
}.inner;
}
pub fn fmt10(comptime L: bool, comptime offset: u5) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
const rb = opcode >> 3 & 0x7;
const rd = opcode & 0x7;
const address = cpu.r[rb] + (@as(u6, offset) << 1);
if (L) {
// LDRH
const value = bus.read(u16, address);
cpu.r[rd] = rotr(u32, value, 8 * (address & 1));
} else {
// STRH
bus.write(u16, address, @truncate(u16, cpu.r[rd]));
}
}
}.inner;
}
pub fn fmt11(comptime L: bool, comptime rd: u3) InstrFn {
return struct {
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
const offset = (opcode & 0xFF) << 2;
const address = cpu.r[13] + offset;
if (L) {
// LDR
const value = bus.read(u32, address);
cpu.r[rd] = rotr(u32, value, 8 * (address & 0x3));
} else {
// STR
bus.write(u32, address, cpu.r[rd]);
}
}
}.inner;
}

View File

@@ -0,0 +1,23 @@
const Bus = @import("../../Bus.zig");
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
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 cpsr = cpu.cpsr.raw;
// Switch Mode
cpu.changeMode(.Supervisor);
cpu.cpsr.t.write(false); // Force ARM Mode
cpu.cpsr.i.write(true); // Disable normal interrupts
cpu.r[14] = ret_addr; // Resume Execution
cpu.spsr.raw = cpsr; // Previous mode CPSR
cpu.r[15] = 0x0000_0008;
cpu.pipe.reload(cpu);
}
}.inner;
}

View File

@@ -1,75 +0,0 @@
const std = @import("std");
const Arm7tdmi = @import("arm32").Arm7tdmi;
const Bank = @import("arm32").Arm7tdmi.Bank;
const Bus = @import("Bus.zig");
pub inline fn isHalted(cpu: *const Arm7tdmi) bool {
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
return bus_ptr.io.haltcnt == .Halt;
}
pub fn stepDmaTransfer(cpu: *Arm7tdmi) bool {
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
inline for (0..4) |i| {
if (bus_ptr.dma[i].in_progress) {
bus_ptr.dma[i].step(cpu);
return true;
}
}
return false;
}
pub fn handleInterrupt(cpu: *Arm7tdmi) void {
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
const should_handle = bus_ptr.io.ie.raw & bus_ptr.io.irq.raw;
// Return if IME is disabled, CPSR I is set or there is nothing to handle
if (!bus_ptr.io.ime or cpu.cpsr.i.read() or should_handle == 0) return;
// If Pipeline isn't full, we have a bug
std.debug.assert(cpu.pipe.isFull());
// log.debug("Handling Interrupt!", .{});
bus_ptr.io.haltcnt = .Execute;
// FIXME: This seems weird, but retAddr.gba suggests I need to make these changes
const ret_addr = cpu.r[15] - if (cpu.cpsr.t.read()) 0 else @as(u32, 4);
const new_spsr = cpu.cpsr.raw;
cpu.changeMode(.Irq);
cpu.cpsr.t.write(false);
cpu.cpsr.i.write(true);
cpu.r[14] = ret_addr;
cpu.spsr.raw = new_spsr;
cpu.r[15] = 0x0000_0018;
cpu.pipe.reload(cpu);
}
/// Advances state so that the BIOS is skipped
///
/// Note: This accesses the CPU's bus ptr so it only may be called
/// once the Bus has been properly initialized
///
/// TODO: Make above notice impossible to do in code
pub fn fastBoot(cpu: *Arm7tdmi) void {
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
cpu.r = std.mem.zeroes([16]u32);
// cpu.r[0] = 0x08000000;
// cpu.r[1] = 0x000000EA;
cpu.r[13] = 0x0300_7F00;
cpu.r[15] = 0x0800_0000;
cpu.bank.r[Bank.regIdx(.Irq, .R13)] = 0x0300_7FA0;
cpu.bank.r[Bank.regIdx(.Supervisor, .R13)] = 0x0300_7FE0;
// cpu.cpsr.raw = 0x6000001F;
cpu.cpsr.raw = 0x0000_001F;
bus_ptr.bios.addr_latch = 0x0000_00DC + 8;
}

View File

@@ -2,59 +2,29 @@ const std = @import("std");
const SDL = @import("sdl2"); const SDL = @import("sdl2");
const config = @import("../config.zig"); const config = @import("../config.zig");
const Scheduler = @import("scheduler.zig").Scheduler;
const Arm7tdmi = @import("arm32").Arm7tdmi;
const Bus = @import("Bus.zig"); const Bus = @import("Bus.zig");
const Tracker = @import("../util.zig").FpsTracker; const Scheduler = @import("scheduler.zig").Scheduler;
const Channel = @import("../util.zig").Queue; const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
const FpsTracker = @import("../util.zig").FpsTracker;
const stepDmaTransfer = @import("cpu_util.zig").stepDmaTransfer; const FilePaths = @import("../util.zig").FilePaths;
const isHalted = @import("cpu_util.zig").isHalted;
const Timer = std.time.Timer; const Timer = std.time.Timer;
const Thread = std.Thread;
const Atomic = std.atomic.Atomic;
const Allocator = std.mem.Allocator;
pub const Synchro = struct { // 228 Lines which consist of 308 dots (which are 4 cycles long)
const AtomicBool = std.atomic.Value(bool); const cycles_per_frame: u64 = 228 * (308 * 4); //280896
const clock_rate: u64 = 1 << 24; // 16.78MHz
// FIXME: This Enum ends up being really LARGE!!! // TODO: Don't truncate this, be more accurate w/ timing
pub const Message = union(enum) { // 59.6046447754ns (truncated to just 59ns)
rom_path: [std.fs.MAX_PATH_BYTES]u8, const clock_period: u64 = std.time.ns_per_s / clock_rate;
bios_path: [std.fs.MAX_PATH_BYTES]u8, const frame_period = (clock_period * cycles_per_frame);
restart: void,
};
paused: AtomicBool = AtomicBool.init(true), // FIXME: can ui_busy and paused be the same? // 59.7275005696Hz
should_quit: AtomicBool = AtomicBool.init(false), pub const frame_rate = @intToFloat(f64, std.time.ns_per_s) /
((@intToFloat(f64, std.time.ns_per_s) / @intToFloat(f64, clock_rate)) * @intToFloat(f64, cycles_per_frame));
ch: Channel(Message),
pub fn init(allocator: std.mem.Allocator) !@This() {
const msg_buf = try allocator.alloc(Message, 1);
return .{ .ch = Channel(Message).init(msg_buf) };
}
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
allocator.free(self.ch.inner.buf);
self.* = undefined;
}
};
/// 4 Cycles in 1 dot
const cycles_per_dot = 4;
/// The GBA draws 228 Horizontal which each consist 308 dots
/// (note: not all lines are visible)
const cycles_per_frame = 228 * (308 * cycles_per_dot); //280896
/// The GBA ARM7TDMI runs at 2^24 Hz
const clock_rate = 1 << 24; // 16.78MHz
/// The # of nanoseconds a frame should take
const frame_period = (std.time.ns_per_s * cycles_per_frame) / clock_rate;
/// Exact Value: 59.7275005696Hz
/// The inverse of the frame period
pub const frame_rate: f64 = @as(f64, @floatFromInt(clock_rate)) / cycles_per_frame;
const log = std.log.scoped(.Emulation); const log = std.log.scoped(.Emulation);
@@ -65,37 +35,30 @@ const RunKind = enum {
LimitedFPS, LimitedFPS,
}; };
pub fn run(cpu: *Arm7tdmi, scheduler: *Scheduler, tracker: *Tracker, sync: *Synchro) void { pub fn run(quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: *FpsTracker) void {
const audio_sync = config.config().guest.audio_sync and !config.config().host.mute; const audio_sync = config.config().guest.audio_sync;
if (audio_sync) log.info("Audio sync enabled", .{}); if (audio_sync) log.info("Audio sync enabled", .{});
if (config.config().guest.video_sync) { if (config.config().guest.video_sync) {
inner(.LimitedFPS, audio_sync, cpu, scheduler, tracker, sync); inner(.LimitedFPS, audio_sync, quit, scheduler, cpu, tracker);
} else { } else {
inner(.UnlimitedFPS, audio_sync, cpu, scheduler, tracker, sync); inner(.UnlimitedFPS, audio_sync, quit, scheduler, cpu, tracker);
} }
} }
fn inner(comptime kind: RunKind, audio_sync: bool, cpu: *Arm7tdmi, scheduler: *Scheduler, tracker: ?*Tracker, sync: *Synchro) void { fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: ?*FpsTracker) void {
if (kind == .UnlimitedFPS or kind == .LimitedFPS) { if (kind == .UnlimitedFPS or kind == .LimitedFPS) {
std.debug.assert(tracker != null); std.debug.assert(tracker != null);
log.info("FPS tracking enabled", .{}); log.info("FPS tracking enabled", .{});
} }
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
// FIXME: audioSync accesses emulator state without any guarantees
switch (kind) { switch (kind) {
.Unlimited, .UnlimitedFPS => { .Unlimited, .UnlimitedFPS => {
log.info("Emulation w/out video sync", .{}); log.info("Emulation w/out video sync", .{});
while (!sync.should_quit.load(.monotonic)) { while (!quit.load(.SeqCst)) {
handleChannel(cpu, &sync.ch);
if (sync.paused.load(.monotonic)) continue;
runFrame(scheduler, cpu); runFrame(scheduler, cpu);
audioSync(audio_sync, bus_ptr.apu.stream, &bus_ptr.apu.is_buffer_full); audioSync(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
if (kind == .UnlimitedFPS) tracker.?.tick(); if (kind == .UnlimitedFPS) tracker.?.tick();
} }
@@ -105,10 +68,7 @@ fn inner(comptime kind: RunKind, audio_sync: bool, cpu: *Arm7tdmi, scheduler: *S
var timer = Timer.start() catch @panic("failed to initalize std.timer.Timer"); var timer = Timer.start() catch @panic("failed to initalize std.timer.Timer");
var wake_time: u64 = frame_period; var wake_time: u64 = frame_period;
while (!sync.should_quit.load(.monotonic)) { while (!quit.load(.SeqCst)) {
handleChannel(cpu, &sync.ch);
if (sync.paused.load(.monotonic)) continue;
runFrame(scheduler, cpu); runFrame(scheduler, cpu);
const new_wake_time = videoSync(&timer, wake_time); const new_wake_time = videoSync(&timer, wake_time);
@@ -117,7 +77,7 @@ fn inner(comptime kind: RunKind, audio_sync: bool, cpu: *Arm7tdmi, scheduler: *S
// the amount of time needed for audio to catch up rather than // the amount of time needed for audio to catch up rather than
// our expected wake-up time // our expected wake-up time
audioSync(audio_sync, bus_ptr.apu.stream, &bus_ptr.apu.is_buffer_full); audioSync(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
if (!audio_sync) spinLoop(&timer, wake_time); if (!audio_sync) spinLoop(&timer, wake_time);
wake_time = new_wake_time; wake_time = new_wake_time;
@@ -127,30 +87,14 @@ fn inner(comptime kind: RunKind, audio_sync: bool, cpu: *Arm7tdmi, scheduler: *S
} }
} }
inline fn handleChannel(cpu: *Arm7tdmi, channel: *Channel(Synchro.Message)) void {
const message = channel.pop() orelse return;
switch (message) {
.rom_path => |path_buf| {
const path = std.mem.sliceTo(&path_buf, 0);
replaceGamepak(cpu, path) catch |e| log.err("failed to replace GamePak: {}", .{e});
},
.bios_path => |path_buf| {
const path = std.mem.sliceTo(&path_buf, 0);
replaceBios(cpu, path) catch |e| log.err("failed to replace BIOS: {}", .{e});
},
.restart => reset(cpu),
}
}
pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void { pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
const frame_end = sched.tick + cycles_per_frame; const frame_end = sched.tick + cycles_per_frame;
while (sched.tick < frame_end) { while (sched.tick < frame_end) {
if (!stepDmaTransfer(cpu)) { if (!cpu.stepDmaTransfer()) {
if (isHalted(cpu)) { if (cpu.isHalted()) {
// Fast-forward to next Event // Fast-forward to next Event
sched.tick = sched.nextTimestamp(); sched.tick = sched.queue.peek().?.tick;
} else { } else {
cpu.step(); cpu.step();
} }
@@ -161,7 +105,6 @@ pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
} }
fn audioSync(audio_sync: bool, stream: *SDL.SDL_AudioStream, is_buffer_full: *bool) void { fn audioSync(audio_sync: bool, stream: *SDL.SDL_AudioStream, is_buffer_full: *bool) void {
comptime std.debug.assert(@import("../platform.zig").sample_format == SDL.AUDIO_U16);
const sample_size = 2 * @sizeOf(u16); const sample_size = 2 * @sizeOf(u16);
const max_buf_size: c_int = 0x400; const max_buf_size: c_int = 0x400;
@@ -172,10 +115,6 @@ fn audioSync(audio_sync: bool, stream: *SDL.SDL_AudioStream, is_buffer_full: *bo
// If Busy is false, there's no need to sync here // If Busy is false, there's no need to sync here
if (!still_full) return; if (!still_full) return;
// TODO: Refactor!!!!
// while (SDL.SDL_AudioStreamAvailable(stream) > sample_size * max_buf_size >> 1)
// std.atomic.spinLoopHint();
while (true) { while (true) {
still_full = SDL.SDL_AudioStreamAvailable(stream) > sample_size * max_buf_size >> 1; still_full = SDL.SDL_AudioStreamAvailable(stream) > sample_size * max_buf_size >> 1;
if (!audio_sync or !still_full) break; if (!audio_sync or !still_full) break;
@@ -193,10 +132,11 @@ fn videoSync(timer: *Timer, wake_time: u64) u64 {
// TODO: Better sleep impl? // TODO: Better sleep impl?
fn sleep(timer: *Timer, wake_time: u64) ?u64 { fn sleep(timer: *Timer, wake_time: u64) ?u64 {
// const step = std.time.ns_per_ms * 10; // 10ms
const timestamp = timer.read(); const timestamp = timer.read();
// ns_late is non zero if we are late. // ns_late is non zero if we are late.
var ns_late = timestamp -| wake_time; const ns_late = timestamp -| wake_time;
// If we're more than a frame late, skip the rest of this loop // If we're more than a frame late, skip the rest of this loop
// Recalculate what our new wake time should be so that we can // Recalculate what our new wake time should be so that we can
@@ -204,145 +144,19 @@ fn sleep(timer: *Timer, wake_time: u64) ?u64 {
if (ns_late > frame_period) return timestamp + frame_period; if (ns_late > frame_period) return timestamp + frame_period;
const sleep_for = frame_period - ns_late; const sleep_for = frame_period - ns_late;
const step = 2 * std.time.ns_per_ms; // Granularity of 2ms // // Employ several sleep calls in periods of 10ms
const times = sleep_for / step; // // By doing this the behaviour should average out to be
// // more consistent
// const loop_count = sleep_for / step; // How many groups of 10ms
for (0..times) |_| { // var i: usize = 0;
std.time.sleep(step); // while (i < loop_count) : (i += 1) std.time.sleep(step);
// Upon wakeup, check to see if this particular sleep was longer than expected std.time.sleep(sleep_for);
// if so we should exit early, but probably not skip a whole frame period
ns_late = timer.read() -| wake_time;
if (ns_late > frame_period) return null;
}
return null; return null;
} }
fn spinLoop(timer: *Timer, wake_time: u64) void { fn spinLoop(timer: *Timer, wake_time: u64) void {
while (timer.read() < wake_time) while (true) if (timer.read() > wake_time) break;
std.atomic.spinLoopHint();
}
pub const EmuThing = struct {
const Self = @This();
const Interface = @import("gdbstub").Emulator;
const Allocator = std.mem.Allocator;
pub const target =
\\<target version="1.0">
\\ <architecture>armv4t</architecture>
\\ <feature name="org.gnu.gdb.arm.core">
\\ <reg name="r0" bitsize="32" type="uint32"/>
\\ <reg name="r1" bitsize="32" type="uint32"/>
\\ <reg name="r2" bitsize="32" type="uint32"/>
\\ <reg name="r3" bitsize="32" type="uint32"/>
\\ <reg name="r4" bitsize="32" type="uint32"/>
\\ <reg name="r5" bitsize="32" type="uint32"/>
\\ <reg name="r6" bitsize="32" type="uint32"/>
\\ <reg name="r7" bitsize="32" type="uint32"/>
\\ <reg name="r8" bitsize="32" type="uint32"/>
\\ <reg name="r9" bitsize="32" type="uint32"/>
\\ <reg name="r10" bitsize="32" type="uint32"/>
\\ <reg name="r11" bitsize="32" type="uint32"/>
\\ <reg name="r12" bitsize="32" type="uint32"/>
\\ <reg name="sp" bitsize="32" type="data_ptr"/>
\\ <reg name="lr" bitsize="32"/>
\\ <reg name="pc" bitsize="32" type="code_ptr"/>
\\
\\ <reg name="cpsr" bitsize="32" regnum="25"/>
\\ </feature>
\\</target>
;
// Game Pak SRAM isn't included
// TODO: Can i be more specific here?
pub const map =
\\ <memory-map version="1.0">
\\ <memory type="rom" start="0x00000000" length="0x00004000"/>
\\ <memory type="ram" start="0x02000000" length="0x00040000"/>
\\ <memory type="ram" start="0x03000000" length="0x00008000"/>
\\ <memory type="ram" start="0x04000000" length="0x00000400"/>
\\ <memory type="ram" start="0x05000000" length="0x00000400"/>
\\ <memory type="ram" start="0x06000000" length="0x00018000"/>
\\ <memory type="ram" start="0x07000000" length="0x00000400"/>
\\ <memory type="rom" start="0x08000000" length="0x02000000"/>
\\ <memory type="rom" start="0x0A000000" length="0x02000000"/>
\\ <memory type="rom" start="0x0C000000" length="0x02000000"/>
\\ </memory-map>
;
cpu: *Arm7tdmi,
scheduler: *Scheduler,
pub fn init(cpu: *Arm7tdmi, scheduler: *Scheduler) Self {
return .{ .cpu = cpu, .scheduler = scheduler };
}
pub fn interface(self: *Self, allocator: Allocator) Interface {
return Interface.init(allocator, self);
}
pub fn read(self: *const Self, addr: u32) u8 {
return self.cpu.bus.dbgRead(u8, addr);
}
pub fn write(self: *Self, addr: u32, value: u8) void {
self.cpu.bus.dbgWrite(u8, addr, value);
}
pub fn registers(self: *const Self) *[16]u32 {
return &self.cpu.r;
}
pub fn cpsr(self: *const Self) u32 {
return self.cpu.cpsr.raw;
}
pub fn step(self: *Self) void {
const cpu = self.cpu;
const sched = self.scheduler;
// Is true when we have executed one (1) instruction
var did_step: bool = false;
// TODO: How can I make it easier to keep this in lock-step with runFrame?
while (!did_step) {
if (!stepDmaTransfer(cpu)) {
if (isHalted(cpu)) {
// Fast-forward to next Event
sched.tick = sched.queue.peek().?.tick;
} else {
cpu.step();
did_step = true;
}
}
if (sched.tick >= sched.nextTimestamp()) sched.handleEvent(cpu);
}
}
};
fn reset(cpu: *Arm7tdmi) void {
// @breakpoint();
cpu.sched.reset(); // Yes this is order sensitive, see the PPU reset for why
cpu.bus.reset();
cpu.reset();
}
fn replaceGamepak(cpu: *Arm7tdmi, file_path: []const u8) !void {
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
try bus_ptr.replaceGamepak(file_path);
reset(cpu);
}
fn replaceBios(cpu: *Arm7tdmi, file_path: []const u8) !void {
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
const allocator = bus_ptr.bios.allocator;
const bios_len = 0x4000;
bus_ptr.bios.buf = try allocator.alloc(u8, bios_len);
try bus_ptr.bios.load(file_path);
} }

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@ pub fn read(self: *const Self, comptime T: type, address: usize) T {
const addr = address & 0x3FF; const addr = address & 0x3FF;
return switch (T) { return switch (T) {
u32, u16, u8 => std.mem.readInt(T, self.buf[addr..][0..@sizeOf(T)], .little), u32, u16, u8 => std.mem.readIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)]),
else => @compileError("OAM: Unsupported read width"), else => @compileError("OAM: Unsupported read width"),
}; };
} }
@@ -21,7 +21,7 @@ pub fn write(self: *Self, comptime T: type, address: usize, value: T) void {
const addr = address & 0x3FF; const addr = address & 0x3FF;
switch (T) { switch (T) {
u32, u16 => std.mem.writeInt(T, self.buf[addr..][0..@sizeOf(T)], value, .little), u32, u16 => std.mem.writeIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)], value),
u8 => return, // 8-bit writes are explicitly ignored u8 => return, // 8-bit writes are explicitly ignored
else => @compileError("OAM: Unsupported write width"), else => @compileError("OAM: Unsupported write width"),
} }
@@ -29,15 +29,11 @@ pub fn write(self: *Self, comptime T: type, address: usize, value: T) void {
pub fn init(allocator: Allocator) !Self { pub fn init(allocator: Allocator) !Self {
const buf = try allocator.alloc(u8, buf_len); const buf = try allocator.alloc(u8, buf_len);
@memset(buf, 0); std.mem.set(u8, buf, 0);
return Self{ .buf = buf, .allocator = allocator }; return Self{ .buf = buf, .allocator = allocator };
} }
pub fn reset(self: *Self) void {
@memset(self.buf, 0);
}
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
self.allocator.free(self.buf); self.allocator.free(self.buf);
self.* = undefined; self.* = undefined;

View File

@@ -12,7 +12,7 @@ pub fn read(self: *const Self, comptime T: type, address: usize) T {
const addr = address & 0x3FF; const addr = address & 0x3FF;
return switch (T) { return switch (T) {
u32, u16, u8 => std.mem.readInt(T, self.buf[addr..][0..@sizeOf(T)], .little), u32, u16, u8 => std.mem.readIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)]),
else => @compileError("PALRAM: Unsupported read width"), else => @compileError("PALRAM: Unsupported read width"),
}; };
} }
@@ -21,10 +21,10 @@ pub fn write(self: *Self, comptime T: type, address: usize, value: T) void {
const addr = address & 0x3FF; const addr = address & 0x3FF;
switch (T) { switch (T) {
u32, u16 => std.mem.writeInt(T, self.buf[addr..][0..@sizeOf(T)], value, .little), u32, u16 => std.mem.writeIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)], value),
u8 => { u8 => {
const align_addr = addr & ~@as(u32, 1); // Aligned to Halfword boundary const align_addr = addr & ~@as(u32, 1); // Aligned to Halfword boundary
std.mem.writeInt(u16, self.buf[align_addr..][0..@sizeOf(u16)], @as(u16, value) * 0x101, .little); std.mem.writeIntSliceLittle(u16, self.buf[align_addr..][0..@sizeOf(u16)], @as(u16, value) * 0x101);
}, },
else => @compileError("PALRAM: Unsupported write width"), else => @compileError("PALRAM: Unsupported write width"),
} }
@@ -32,20 +32,16 @@ pub fn write(self: *Self, comptime T: type, address: usize, value: T) void {
pub fn init(allocator: Allocator) !Self { pub fn init(allocator: Allocator) !Self {
const buf = try allocator.alloc(u8, buf_len); const buf = try allocator.alloc(u8, buf_len);
@memset(buf, 0); std.mem.set(u8, buf, 0);
return Self{ .buf = buf, .allocator = allocator }; return Self{ .buf = buf, .allocator = allocator };
} }
pub fn reset(self: *Self) void {
@memset(self.buf, 0);
}
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
self.allocator.free(self.buf); self.allocator.free(self.buf);
self.* = undefined; self.* = undefined;
} }
pub inline fn backdrop(self: *const Self) u16 { pub fn backdrop(self: *const Self) u16 {
return std.mem.readInt(u16, self.buf[0..2], .little); return self.read(u16, 0);
} }

View File

@@ -13,7 +13,7 @@ pub fn read(self: *const Self, comptime T: type, address: usize) T {
const addr = Self.mirror(address); const addr = Self.mirror(address);
return switch (T) { return switch (T) {
u32, u16, u8 => std.mem.readInt(T, self.buf[addr..][0..@sizeOf(T)], .little), u32, u16, u8 => std.mem.readIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)]),
else => @compileError("VRAM: Unsupported read width"), else => @compileError("VRAM: Unsupported read width"),
}; };
} }
@@ -23,7 +23,7 @@ pub fn write(self: *Self, comptime T: type, dispcnt: io.DisplayControl, address:
const idx = Self.mirror(address); const idx = Self.mirror(address);
switch (T) { switch (T) {
u32, u16 => std.mem.writeInt(T, self.buf[idx..][0..@sizeOf(T)], value, .little), u32, u16 => std.mem.writeIntSliceLittle(T, self.buf[idx..][0..@sizeOf(T)], value),
u8 => { u8 => {
// Ignore write if it falls within the boundaries of OBJ VRAM // Ignore write if it falls within the boundaries of OBJ VRAM
switch (mode) { switch (mode) {
@@ -32,7 +32,7 @@ pub fn write(self: *Self, comptime T: type, dispcnt: io.DisplayControl, address:
} }
const align_idx = idx & ~@as(u32, 1); // Aligned to a halfword boundary const align_idx = idx & ~@as(u32, 1); // Aligned to a halfword boundary
std.mem.writeInt(u16, self.buf[align_idx..][0..@sizeOf(u16)], @as(u16, value) * 0x101, .little); std.mem.writeIntSliceLittle(u16, self.buf[align_idx..][0..@sizeOf(u16)], @as(u16, value) * 0x101);
}, },
else => @compileError("VRAM: Unsupported write width"), else => @compileError("VRAM: Unsupported write width"),
} }
@@ -40,21 +40,17 @@ pub fn write(self: *Self, comptime T: type, dispcnt: io.DisplayControl, address:
pub fn init(allocator: Allocator) !Self { pub fn init(allocator: Allocator) !Self {
const buf = try allocator.alloc(u8, buf_len); const buf = try allocator.alloc(u8, buf_len);
@memset(buf, 0); std.mem.set(u8, buf, 0);
return Self{ .buf = buf, .allocator = allocator }; return Self{ .buf = buf, .allocator = allocator };
} }
pub fn reset(self: *Self) void {
@memset(self.buf, 0);
}
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
self.allocator.free(self.buf); self.allocator.free(self.buf);
self.* = undefined; self.* = undefined;
} }
pub fn mirror(address: usize) usize { fn mirror(address: usize) usize {
// Mirrored in steps of 128K (64K + 32K + 32K) (abcc) // Mirrored in steps of 128K (64K + 32K + 32K) (abcc)
const addr = address & 0x1FFFF; const addr = address & 0x1FFFF;

View File

@@ -1,7 +1,7 @@
const std = @import("std"); const std = @import("std");
const Arm7tdmi = @import("arm32").Arm7tdmi;
const Bus = @import("Bus.zig"); const Bus = @import("Bus.zig");
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
const Clock = @import("bus/gpio.zig").Clock; const Clock = @import("bus/gpio.zig").Clock;
const Order = std.math.Order; const Order = std.math.Order;
@@ -12,11 +12,11 @@ const log = std.log.scoped(.Scheduler);
pub const Scheduler = struct { pub const Scheduler = struct {
const Self = @This(); const Self = @This();
tick: u64 = 0, tick: u64,
queue: PriorityQueue(Event, void, lessThan), queue: PriorityQueue(Event, void, lessThan),
pub fn init(allocator: Allocator) Self { pub fn init(allocator: Allocator) Self {
var sched = Self{ .queue = PriorityQueue(Event, void, lessThan).init(allocator, {}) }; var sched = Self{ .tick = 0, .queue = PriorityQueue(Event, void, lessThan).init(allocator, {}) };
sched.queue.add(.{ .kind = .HeatDeath, .tick = std.math.maxInt(u64) }) catch unreachable; sched.queue.add(.{ .kind = .HeatDeath, .tick = std.math.maxInt(u64) }) catch unreachable;
return sched; return sched;
@@ -27,27 +27,14 @@ pub const Scheduler = struct {
self.* = undefined; self.* = undefined;
} }
pub fn reset(self: *Self) void {
// `std.PriorityQueue` provides no reset function, so we will just create a new one
const allocator = self.queue.allocator;
self.queue.deinit();
var new_queue = PriorityQueue(Event, void, lessThan).init(allocator, {});
new_queue.add(.{ .kind = .HeatDeath, .tick = std.math.maxInt(u64) }) catch unreachable;
self.* = .{ .queue = new_queue };
}
pub inline fn now(self: *const Self) u64 { pub inline fn now(self: *const Self) u64 {
return self.tick; return self.tick;
} }
pub fn handleEvent(self: *Self, cpu: *Arm7tdmi) void { pub fn handleEvent(self: *Self, cpu: *Arm7tdmi) void {
const event = self.queue.remove(); if (self.queue.removeOrNull()) |event| {
const late = self.tick - event.tick; const late = self.tick - event.tick;
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
switch (event.kind) { switch (event.kind) {
.HeatDeath => { .HeatDeath => {
log.err("u64 overflow. This *actually* should never happen.", .{}); log.err("u64 overflow. This *actually* should never happen.", .{});
@@ -55,45 +42,54 @@ pub const Scheduler = struct {
}, },
.Draw => { .Draw => {
// The end of a VDraw // The end of a VDraw
bus_ptr.ppu.drawScanline(); cpu.bus.ppu.drawScanline();
bus_ptr.ppu.onHdrawEnd(cpu, late); cpu.bus.ppu.onHdrawEnd(cpu, late);
}, },
.TimerOverflow => |id| { .TimerOverflow => |id| {
switch (id) { switch (id) {
inline 0...3 => |idx| bus_ptr.tim[idx].onTimerExpire(cpu, late), 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),
} }
}, },
.ApuChannel => |id| { .ApuChannel => |id| {
switch (id) { switch (id) {
0 => bus_ptr.apu.ch1.onToneSweepEvent(late), 0 => cpu.bus.apu.ch1.onToneSweepEvent(late),
1 => bus_ptr.apu.ch2.onToneEvent(late), 1 => cpu.bus.apu.ch2.onToneEvent(late),
2 => bus_ptr.apu.ch3.onWaveEvent(late), 2 => cpu.bus.apu.ch3.onWaveEvent(late),
3 => bus_ptr.apu.ch4.onNoiseEvent(late), 3 => cpu.bus.apu.ch4.onNoiseEvent(late),
} }
}, },
.RealTimeClock => { .RealTimeClock => {
const device = &bus_ptr.pak.gpio.device; const device = &cpu.bus.pak.gpio.device;
if (device.kind != .Rtc or device.ptr == null) return; if (device.kind != .Rtc or device.ptr == null) return;
const clock: *Clock = @ptrCast(@alignCast(device.ptr.?)); const clock = @ptrCast(*Clock, @alignCast(@alignOf(*Clock), device.ptr.?));
clock.onClockUpdate(late); clock.onClockUpdate(late);
}, },
.FrameSequencer => bus_ptr.apu.onSequencerTick(late), .FrameSequencer => cpu.bus.apu.onSequencerTick(late),
.SampleAudio => bus_ptr.apu.sampleAudio(late), .SampleAudio => cpu.bus.apu.sampleAudio(late),
.HBlank => bus_ptr.ppu.onHblankEnd(cpu, late), // The end of a HBlank .HBlank => cpu.bus.ppu.onHblankEnd(cpu, late), // The end of a HBlank
.VBlank => bus_ptr.ppu.onHdrawEnd(cpu, late), // The end of a VBlank .VBlank => cpu.bus.ppu.onHdrawEnd(cpu, late), // The end of a VBlank
}
} }
} }
/// Removes the **first** scheduled event of type `needle` /// Removes the **first** scheduled event of type `needle`
pub fn removeScheduledEvent(self: *Self, needle: EventKind) void { pub fn removeScheduledEvent(self: *Self, needle: EventKind) void {
for (self.queue.items, 0..) |event, i| { var it = self.queue.iterator();
var i: usize = 0;
while (it.next()) |event| : (i += 1) {
if (std.meta.eql(event.kind, needle)) { if (std.meta.eql(event.kind, needle)) {
// invalidates the slice we're iterating over // This invalidates the iterator
_ = self.queue.removeIndex(i); _ = self.queue.removeIndex(i);
// log.debug("Removed {?}@{}", .{ event.kind, event.tick }); // Since removing something from the PQ invalidates the iterator,
// this implementation can safely only remove the first instance of
// a Scheduled Event. Exit Early
break; break;
} }
} }

View File

@@ -1,511 +0,0 @@
//! Namespace for dealing with ZBA's immediate-mode GUI
//! Currently, ZBA uses zgui from https://github.com/michal-z/zig-gamedev
//! which provides Zig bindings for https://github.com/ocornut/imgui under the hood
const std = @import("std");
const zgui = @import("zgui");
const gl = @import("gl");
const nfd = @import("nfd");
const config = @import("config.zig");
const emu = @import("core/emu.zig");
const Gui = @import("platform.zig").Gui;
const Arm7tdmi = @import("arm32").Arm7tdmi;
const Scheduler = @import("core/scheduler.zig").Scheduler;
const Bus = @import("core/Bus.zig");
const Synchro = @import("core/emu.zig").Synchro;
const RingBuffer = @import("zba-util").RingBuffer;
const Dimensions = @import("platform.zig").Dimensions;
const Allocator = std.mem.Allocator;
const GLuint = gl.GLuint;
const gba_width = @import("core/ppu.zig").width;
const gba_height = @import("core/ppu.zig").height;
const log = std.log.scoped(.Imgui);
// two seconds worth of fps values into the past
const histogram_len = 0x80;
/// Immediate-Mode GUI State
pub const State = struct {
title: [12:0]u8,
fps_hist: RingBuffer(u32),
should_quit: bool = false,
emulation: Emulation,
win_stat: WindowStatus = .{},
const WindowStatus = struct {
show_deps: bool = false,
show_regs: bool = false,
show_schedule: bool = false,
show_perf: bool = false,
show_palette: bool = false,
};
const Emulation = union(enum) {
Active,
Inactive,
Transition: enum { Active, Inactive },
};
/// if zba is initialized with a ROM already provided, this initializer should be called
/// with `title_opt` being non-null
pub fn init(allocator: Allocator, title_opt: ?*const [12]u8) !@This() {
const history = try allocator.alloc(u32, histogram_len);
return .{
.title = handleTitle(title_opt),
.emulation = if (title_opt == null) .Inactive else .{ .Transition = .Active },
.fps_hist = RingBuffer(u32).init(history),
};
}
pub fn deinit(self: *@This(), allocator: Allocator) void {
allocator.free(self.fps_hist.buf);
self.* = undefined;
}
};
pub fn draw(state: *State, sync: *Synchro, dim: Dimensions, cpu: *const Arm7tdmi, tex_id: GLuint) bool {
const scn_scale = config.config().host.win_scale;
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
zgui.backend.newFrame(@floatFromInt(dim.width), @floatFromInt(dim.height));
state.title = handleTitle(&bus_ptr.pak.title);
{
_ = zgui.beginMainMenuBar();
defer zgui.endMainMenuBar();
if (zgui.beginMenu("File", true)) {
defer zgui.endMenu();
if (zgui.menuItem("Quit", .{}))
state.should_quit = true;
if (zgui.menuItem("Insert ROM", .{})) blk: {
const file_path = tmp: {
const path_opt = nfd.openFileDialog("gba", null) catch |e| {
log.err("file dialog failed to open: {}", .{e});
break :blk;
};
break :tmp path_opt orelse {
log.warn("did not receive a file path", .{});
break :blk;
};
};
defer nfd.freePath(file_path);
log.info("user chose: \"{s}\"", .{file_path});
const message = tmp: {
var msg: Synchro.Message = .{ .rom_path = undefined };
@memcpy(msg.rom_path[0..file_path.len], file_path);
break :tmp msg;
};
sync.ch.push(message) catch |e| {
log.err("failed to send file path to emu thread: {}", .{e});
break :blk;
};
state.emulation = .{ .Transition = .Active };
}
if (zgui.menuItem("Load BIOS", .{})) blk: {
const file_path = tmp: {
const path_opt = nfd.openFileDialog("bin", null) catch |e| {
log.err("file dialog failed to open: {}", .{e});
break :blk;
};
break :tmp path_opt orelse {
log.warn("did not receive a file path", .{});
break :blk;
};
};
defer nfd.freePath(file_path);
log.info("user chose: \"{s}\"", .{file_path});
const message = tmp: {
var msg: Synchro.Message = .{ .bios_path = undefined };
@memcpy(msg.bios_path[0..file_path.len], file_path);
break :tmp msg;
};
sync.ch.push(message) catch |e| {
log.err("failed to send file path to emu thread: {}", .{e});
break :blk;
};
}
}
if (zgui.beginMenu("Emulation", true)) {
defer zgui.endMenu();
if (zgui.menuItem("Registers", .{ .selected = state.win_stat.show_regs }))
state.win_stat.show_regs = true;
if (zgui.menuItem("Palette", .{ .selected = state.win_stat.show_palette }))
state.win_stat.show_palette = true;
if (zgui.menuItem("Schedule", .{ .selected = state.win_stat.show_schedule }))
state.win_stat.show_schedule = true;
if (zgui.menuItem("Paused", .{ .selected = state.emulation == .Inactive })) {
state.emulation = switch (state.emulation) {
.Active => .{ .Transition = .Inactive },
.Inactive => .{ .Transition = .Active },
else => state.emulation,
};
}
if (zgui.menuItem("Restart", .{}))
sync.ch.push(.restart) catch |e| log.err("failed to send restart req to emu thread: {}", .{e});
}
if (zgui.beginMenu("Stats", true)) {
defer zgui.endMenu();
if (zgui.menuItem("Performance", .{ .selected = state.win_stat.show_perf }))
state.win_stat.show_perf = true;
}
if (zgui.beginMenu("Help", true)) {
defer zgui.endMenu();
if (zgui.menuItem("Dependencies", .{ .selected = state.win_stat.show_deps }))
state.win_stat.show_deps = true;
}
}
{
const w: f32 = @floatFromInt(gba_width * scn_scale);
const h: f32 = @floatFromInt(gba_height * scn_scale);
const window_title = std.mem.sliceTo(&state.title, 0);
_ = zgui.begin(window_title, .{ .flags = .{ .no_resize = true, .always_auto_resize = true } });
defer zgui.end();
zgui.image(@ptrFromInt(tex_id), .{ .w = w, .h = h });
}
// TODO: Any other steps to respect the copyright of the libraries I use?
if (state.win_stat.show_deps) {
_ = zgui.begin("Dependencies", .{ .popen = &state.win_stat.show_deps });
defer zgui.end();
zgui.bulletText("known-folders by ziglibs", .{});
zgui.bulletText("nfd-zig by Fabio Arnold", .{});
{
zgui.indent(.{});
defer zgui.unindent(.{});
zgui.bulletText("nativefiledialog by Michael Labbe", .{});
}
zgui.bulletText("SDL.zig by Felix Queißner", .{});
{
zgui.indent(.{});
defer zgui.unindent(.{});
zgui.bulletText("SDL by Sam Lantinga", .{});
}
zgui.bulletText("tomlz by Matthew Hall", .{});
zgui.bulletText("zba-gdbstub by Rekai Musuka", .{});
zgui.bulletText("zba-util by Rekai Musuka", .{});
zgui.bulletText("zgui by Michal Ziulek", .{});
{
zgui.indent(.{});
defer zgui.unindent(.{});
zgui.bulletText("DearImGui by Omar Cornut", .{});
}
zgui.bulletText("zig-clap by Jimmi Holst Christensen", .{});
zgui.bulletText("zig-datetime by Jairus Martin", .{});
zgui.newLine();
zgui.bulletText("bitfield.zig by Hannes Bredberg and FlorenceOS contributors", .{});
zgui.bulletText("zig-opengl by Felix Queißner", .{});
{
zgui.indent(.{});
defer zgui.unindent(.{});
zgui.bulletText("OpenGL-Registry by The Khronos Group", .{});
}
}
if (state.win_stat.show_regs) {
_ = zgui.begin("Guest Registers", .{ .popen = &state.win_stat.show_regs });
defer zgui.end();
for (0..8) |i| {
zgui.text("R{}: 0x{X:0>8}", .{ i, cpu.r[i] });
zgui.sameLine(.{});
const padding = if (8 + i < 10) " " else "";
zgui.text("{s}R{}: 0x{X:0>8}", .{ padding, 8 + i, cpu.r[8 + i] });
}
zgui.separator();
widgets.psr("CPSR", cpu.cpsr);
widgets.psr("SPSR", cpu.spsr);
zgui.separator();
widgets.interrupts(" IE", bus_ptr.io.ie);
widgets.interrupts("IRQ", bus_ptr.io.irq);
}
if (state.win_stat.show_perf) {
_ = zgui.begin("Performance", .{ .popen = &state.win_stat.show_perf });
defer zgui.end();
const tmp = blk: {
var buf: [histogram_len]u32 = undefined;
const len = state.fps_hist.copy(&buf);
break :blk .{ buf, len };
};
const values = tmp[0];
const len = tmp[1];
if (len == values.len) _ = state.fps_hist.pop();
const sorted = blk: {
var buf: @TypeOf(values) = undefined;
@memcpy(buf[0..len], values[0..len]);
std.mem.sort(u32, buf[0..len], {}, std.sort.asc(u32));
break :blk buf;
};
const y_max: f64 = 2 * if (len != 0) @as(f64, @floatFromInt(sorted[len - 1])) else emu.frame_rate;
const x_max: f64 = @floatFromInt(values.len);
const y_args = .{ .flags = .{ .no_grid_lines = true } };
const x_args = .{ .flags = .{ .no_grid_lines = true, .no_tick_labels = true, .no_tick_marks = true } };
if (zgui.plot.beginPlot("Emulation FPS", .{ .w = 0.0, .flags = .{ .no_title = true, .no_frame = true } })) {
defer zgui.plot.endPlot();
zgui.plot.setupLegend(.{ .north = true, .east = true }, .{});
zgui.plot.setupAxis(.x1, x_args);
zgui.plot.setupAxis(.y1, y_args);
zgui.plot.setupAxisLimits(.y1, .{ .min = 0.0, .max = y_max, .cond = .always });
zgui.plot.setupAxisLimits(.x1, .{ .min = 0.0, .max = x_max, .cond = .always });
zgui.plot.setupFinish();
zgui.plot.plotLineValues("FPS", u32, .{ .v = values[0..len] });
}
const stats: struct { u32, u32, u32 } = blk: {
if (len == 0) break :blk .{ 0, 0, 0 };
const average: u32 = average: {
var sum: u32 = 0;
for (sorted[0..len]) |value| sum += value;
break :average @intCast(sum / len);
};
const median = sorted[len / 2];
const low = sorted[len / 100]; // 1% Low
break :blk .{ average, median, low };
};
zgui.text("Average: {:0>3} fps", .{stats[0]});
zgui.text(" Median: {:0>3} fps", .{stats[1]});
zgui.text(" 1% Low: {:0>3} fps", .{stats[2]});
}
if (state.win_stat.show_schedule) {
_ = zgui.begin("Schedule", .{ .popen = &state.win_stat.show_schedule });
defer zgui.end();
const scheduler = cpu.sched;
zgui.text("tick: {X:0>16}", .{scheduler.now()});
zgui.separator();
const sched_ptr: *Scheduler = @ptrCast(@alignCast(cpu.sched.ptr));
const Event = std.meta.Child(@TypeOf(sched_ptr.queue.items));
var items: [20]Event = undefined;
const len = @min(sched_ptr.queue.items.len, items.len);
@memcpy(items[0..len], sched_ptr.queue.items[0..len]);
std.mem.sort(Event, items[0..len], {}, widgets.eventDesc(Event));
for (items[0..len]) |event| {
zgui.text("{X:0>16} | {?}", .{ event.tick, event.kind });
}
}
if (state.win_stat.show_palette) {
_ = zgui.begin("Palette", .{ .popen = &state.win_stat.show_palette });
defer zgui.end();
widgets.paletteGrid(.Background, cpu);
zgui.sameLine(.{ .spacing = 20.0 });
widgets.paletteGrid(.Object, cpu);
}
// {
// zgui.showDemoWindow(null);
// }
return true; // request redraw
}
const widgets = struct {
const PaletteKind = enum { Background, Object };
fn paletteGrid(comptime kind: PaletteKind, cpu: *const Arm7tdmi) void {
_ = zgui.beginGroup();
defer zgui.endGroup();
const address: u32 = switch (kind) {
.Background => 0x0500_0000,
.Object => 0x0500_0200,
};
for (0..0x100) |i| {
const offset: u32 = @truncate(i);
const bgr555 = cpu.bus.dbgRead(u16, address + offset * @sizeOf(u16));
widgets.colourSquare(bgr555);
if ((i + 1) % 0x10 != 0) zgui.sameLine(.{});
}
zgui.text(@tagName(kind), .{});
}
fn colourSquare(bgr555: u16) void {
// FIXME: working with the packed struct enum is currently broken :pensive:
const ImguiColorEditFlags_NoInputs: u32 = 1 << 5;
const ImguiColorEditFlags_NoPicker: u32 = 1 << 2;
const flags: zgui.ColorEditFlags = @bitCast(ImguiColorEditFlags_NoInputs | ImguiColorEditFlags_NoPicker);
const b: f32 = @floatFromInt(bgr555 >> 10 & 0x1f);
const g: f32 = @floatFromInt(bgr555 >> 5 & 0x1F);
const r: f32 = @floatFromInt(bgr555 & 0x1F);
var col = [_]f32{ r / 31.0, g / 31.0, b / 31.0 };
_ = zgui.colorEdit3("", .{ .col = &col, .flags = flags });
}
fn interrupts(comptime label: []const u8, int: anytype) void {
const h = 15.0;
const w = 9.0 * 2 + 3.5;
const ww = 9.0 * 3;
{
zgui.text(label ++ ":", .{});
zgui.sameLine(.{});
_ = zgui.selectable("VBL", .{ .w = w, .h = h, .selected = int.vblank.read() });
zgui.sameLine(.{});
_ = zgui.selectable("HBL", .{ .w = w, .h = h, .selected = int.hblank.read() });
zgui.sameLine(.{});
_ = zgui.selectable("VCT", .{ .w = w, .h = h, .selected = int.coincidence.read() });
{
zgui.sameLine(.{});
_ = zgui.selectable("TIM0", .{ .w = ww, .h = h, .selected = int.tim0.read() });
zgui.sameLine(.{});
_ = zgui.selectable("TIM1", .{ .w = ww, .h = h, .selected = int.tim1.read() });
zgui.sameLine(.{});
_ = zgui.selectable("TIM2", .{ .w = ww, .h = h, .selected = int.tim2.read() });
zgui.sameLine(.{});
_ = zgui.selectable("TIM3", .{ .w = ww, .h = h, .selected = int.tim3.read() });
}
zgui.sameLine(.{});
_ = zgui.selectable("SRL", .{ .w = w, .h = h, .selected = int.serial.read() });
{
zgui.sameLine(.{});
_ = zgui.selectable("DMA0", .{ .w = ww, .h = h, .selected = int.dma0.read() });
zgui.sameLine(.{});
_ = zgui.selectable("DMA1", .{ .w = ww, .h = h, .selected = int.dma1.read() });
zgui.sameLine(.{});
_ = zgui.selectable("DMA2", .{ .w = ww, .h = h, .selected = int.dma2.read() });
zgui.sameLine(.{});
_ = zgui.selectable("DMA3", .{ .w = ww, .h = h, .selected = int.dma3.read() });
}
zgui.sameLine(.{});
_ = zgui.selectable("KPD", .{ .w = w, .h = h, .selected = int.keypad.read() });
zgui.sameLine(.{});
_ = zgui.selectable("GPK", .{ .w = w, .h = h, .selected = int.game_pak.read() });
}
}
fn psr(comptime label: []const u8, register: anytype) void {
const Mode = @import("arm32").arm.Mode;
const maybe_mode = std.meta.intToEnum(Mode, register.mode.read()) catch null;
const mode = if (maybe_mode) |mode| mode.toString() else "???";
const w = 9.0;
const h = 15.0;
zgui.text(label ++ ": 0x{X:0>8}", .{register.raw});
zgui.sameLine(.{});
_ = zgui.selectable("N", .{ .w = w, .h = h, .selected = register.n.read() });
zgui.sameLine(.{});
_ = zgui.selectable("Z", .{ .w = w, .h = h, .selected = register.z.read() });
zgui.sameLine(.{});
_ = zgui.selectable("C", .{ .w = w, .h = h, .selected = register.c.read() });
zgui.sameLine(.{});
_ = zgui.selectable("V", .{ .w = w, .h = h, .selected = register.v.read() });
zgui.sameLine(.{});
zgui.text("{s}", .{mode});
}
fn eventDesc(comptime T: type) fn (void, T, T) bool {
return struct {
fn inner(_: void, left: T, right: T) bool {
return left.tick > right.tick;
}
}.inner;
}
};
fn handleTitle(title_opt: ?*const [12]u8) [12:0]u8 {
if (title_opt == null) return "[N/A Title]\x00".*; // No ROM present
const title = title_opt.?;
// ROM Title is an empty string (ImGui hates these)
if (title[0] == '\x00') return "[No Title]\x00\x00".*;
return title.* ++ [_:0]u8{};
}

View File

@@ -4,21 +4,17 @@ const known_folders = @import("known_folders");
const clap = @import("clap"); const clap = @import("clap");
const config = @import("config.zig"); const config = @import("config.zig");
const emu = @import("core/emu.zig");
const Synchro = @import("core/emu.zig").Synchro;
const Gui = @import("platform.zig").Gui; const Gui = @import("platform.zig").Gui;
const Bus = @import("core/Bus.zig"); const Bus = @import("core/Bus.zig");
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
const Scheduler = @import("core/scheduler.zig").Scheduler; const Scheduler = @import("core/scheduler.zig").Scheduler;
const FilePaths = @import("util.zig").FilePaths; const FilePaths = @import("util.zig").FilePaths;
const FpsTracker = @import("util.zig").FpsTracker;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Arm7tdmi = @import("arm32").Arm7tdmi;
const IBus = @import("arm32").Bus;
const IScheduler = @import("arm32").Scheduler;
const log = std.log.scoped(.Cli); const log = std.log.scoped(.Cli);
const width = @import("core/ppu.zig").width;
const height = @import("core/ppu.zig").height;
pub const log_level = if (builtin.mode != .Debug) .info else std.log.default_level; pub const log_level = if (builtin.mode != .Debug) .info else std.log.default_level;
// CLI Arguments + Help Text // CLI Arguments + Help Text
@@ -26,74 +22,44 @@ const params = clap.parseParamsComptime(
\\-h, --help Display this help and exit. \\-h, --help Display this help and exit.
\\-s, --skip Skip BIOS. \\-s, --skip Skip BIOS.
\\-b, --bios <str> Optional path to a GBA BIOS ROM. \\-b, --bios <str> Optional path to a GBA BIOS ROM.
\\ --gdb Run ZBA from the context of a GDB Server
\\<str> Path to the GBA GamePak ROM. \\<str> Path to the GBA GamePak ROM.
\\ \\
); );
pub fn main() void { pub fn main() anyerror!void {
// Main Allocator for ZBA // Main Allocator for ZBA
var gpa = std.heap.GeneralPurposeAllocator(.{}){}; var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer std.debug.assert(gpa.deinit() == .ok); defer std.debug.assert(!gpa.deinit());
const allocator = gpa.allocator(); const allocator = gpa.allocator();
// Determine the Data Directory (stores saves) // Determine the Data Directory (stores saves, config file, etc.)
const data_path = blk: { const data_path = blk: {
const result = known_folders.getPath(allocator, .data); const result = known_folders.getPath(allocator, .data);
const option = result catch |e| exitln("interrupted while determining the data folder: {}", .{e}); const option = result catch |e| exitln("interrupted while attempting to find a data directory: {}", .{e});
const path = option orelse exitln("no valid data folder found", .{}); const path = option orelse exitln("no valid data directory could be found", .{});
ensureDataDirsExist(path) catch |e| exitln("failed to create folders under \"{s}\": {}", .{ path, e }); ensureDirectoriesExist(path) catch |e| exitln("failed to create directories under \"{s}\": {}", .{ path, e });
break :blk path; break :blk path;
}; };
defer allocator.free(data_path); defer allocator.free(data_path);
// Determine the Config Directory
const config_path = blk: {
const result = known_folders.getPath(allocator, .roaming_configuration);
const option = result catch |e| exitln("interreupted while determining the config folder: {}", .{e});
const path = option orelse exitln("no valid config folder found", .{});
ensureConfigDirExists(path) catch |e| exitln("failed to create required folder \"{s}\": {}", .{ path, e });
break :blk path;
};
defer allocator.free(config_path);
// Parse CLI // Parse CLI
const result = clap.parse(clap.Help, &params, clap.parsers.default, .{}) catch |e| exitln("failed to parse cli: {}", .{e});
const result = clap.parse(clap.Help, &params, clap.parsers.default, .{ .allocator = allocator }) catch |e| exitln("failed to parse cli: {}", .{e});
defer result.deinit(); defer result.deinit();
// TODO: Move config file to XDG Config directory? // TODO: Move config file to XDG Config directory?
const cfg_file_path = configFilePath(allocator, config_path) catch |e| exitln("failed to ready config file for access: {}", .{e}); const config_path = configFilePath(allocator, data_path) catch |e| exitln("failed to determine the config file path for ZBA: {}", .{e});
defer allocator.free(cfg_file_path); defer allocator.free(config_path);
config.load(allocator, cfg_file_path) catch |e| exitln("failed to load config file: {}", .{e}); config.load(allocator, config_path) catch |e| exitln("failed to read config file: {}", .{e});
var paths = handleArguments(allocator, data_path, &result) catch |e| exitln("failed to handle cli arguments: {}", .{e}); const paths = handleArguments(allocator, data_path, &result) catch |e| exitln("failed to handle cli arguments: {}", .{e});
defer paths.deinit(allocator); defer if (paths.save) |path| allocator.free(path);
// if paths.bios is null, then we want to see if it's in the data directory const log_file = if (config.config().debug.cpu_trace) blk: {
if (paths.bios == null) blk: { break :blk std.fs.cwd().createFile("zba.log", .{}) catch |e| exitln("failed to create trace log file: {}", .{e});
const bios_path = std.mem.join(allocator, "/", &.{ data_path, "zba", "gba_bios.bin" }) catch |e| exitln("failed to allocate backup bios dir path: {}", .{e}); } else null;
defer allocator.free(bios_path);
_ = std.fs.cwd().statFile(bios_path) catch |e| switch (e) {
error.FileNotFound => { // ZBA will crash on attempt to read BIOS but that's fine
log.err("file located at {s} was not found", .{bios_path});
break :blk;
},
else => exitln("error when checking \"{s}\": {}", .{ bios_path, e }),
};
paths.bios = allocator.dupe(u8, bios_path) catch |e| exitln("failed to duplicate path to bios: {}", .{e});
}
const log_file = switch (config.config().debug.cpu_trace) {
true => std.fs.cwd().createFile("zba.log", .{}) catch |e| exitln("failed to create trace log file: {}", .{e}),
false => null,
};
defer if (log_file) |file| file.close(); defer if (log_file) |file| file.close();
// TODO: Take Emulator Init Code out of main.zig // TODO: Take Emulator Init Code out of main.zig
@@ -101,80 +67,29 @@ pub fn main() void {
defer scheduler.deinit(); defer scheduler.deinit();
var bus: Bus = undefined; var bus: Bus = undefined;
var cpu = Arm7tdmi.init(&scheduler, &bus, log_file);
const ischeduler = IScheduler.init(&scheduler);
const ibus = IBus.init(&bus);
var cpu = Arm7tdmi.init(ischeduler, ibus);
bus.init(allocator, &scheduler, &cpu, paths) catch |e| exitln("failed to init zba bus: {}", .{e}); bus.init(allocator, &scheduler, &cpu, paths) catch |e| exitln("failed to init zba bus: {}", .{e});
defer bus.deinit(); defer bus.deinit();
if (config.config().guest.skip_bios or result.args.skip != 0 or paths.bios == null) { if (config.config().guest.skip_bios or result.args.skip or paths.bios == null) {
@import("core/cpu_util.zig").fastBoot(&cpu); cpu.fastBoot();
} }
const title_ptr = if (paths.rom != null) &bus.pak.title else null; var gui = Gui.init(&bus.pak.title, &bus.apu, width, height);
// TODO: Just copy the title instead of grabbing a pointer to it
var gui = Gui.init(allocator, &bus.apu, title_ptr) catch |e| exitln("failed to init gui: {}", .{e});
defer gui.deinit(); defer gui.deinit();
var sync = Synchro.init(allocator) catch |e| exitln("failed to allocate sync types: {}", .{e}); gui.run(&cpu, &scheduler) catch |e| exitln("failed to run gui thread: {}", .{e});
defer sync.deinit(allocator);
if (result.args.gdb != 0) {
const Server = @import("gdbstub").Server;
const EmuThing = @import("core/emu.zig").EmuThing;
var wrapper = EmuThing.init(&cpu, &scheduler);
var emulator = wrapper.interface(allocator);
defer emulator.deinit();
log.info("Ready to connect", .{});
var server = Server.init(
emulator,
.{ .memory_map = EmuThing.map, .target = EmuThing.target },
) catch |e| exitln("failed to init gdb server: {}", .{e});
defer server.deinit(allocator);
log.info("Starting GDB Server Thread", .{});
const thread = std.Thread.spawn(.{}, Server.run, .{ &server, allocator, &sync.should_quit }) catch |e| exitln("gdb server thread crashed: {}", .{e});
defer thread.join();
gui.run(.{
.cpu = &cpu,
.scheduler = &scheduler,
.sync = &sync,
}) catch |e| exitln("main thread panicked: {}", .{e});
} else {
var tracker = FpsTracker.init();
const thread = std.Thread.spawn(.{}, emu.run, .{ &cpu, &scheduler, &tracker, &sync }) catch |e| exitln("emu thread panicked: {}", .{e});
defer thread.join();
gui.run(.{
.cpu = &cpu,
.scheduler = &scheduler,
.tracker = &tracker,
.sync = &sync,
}) catch |e| exitln("main thread panicked: {}", .{e});
}
} }
fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const clap.Result(clap.Help, &params, clap.parsers.default)) !FilePaths { pub fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const clap.Result(clap.Help, &params, clap.parsers.default)) !FilePaths {
const rom_path = try romPath(allocator, result); const rom_path = romPath(result);
errdefer if (rom_path) |path| allocator.free(path); log.info("ROM path: {s}", .{rom_path});
const bios_path: ?[]const u8 = if (result.args.bios) |path| try allocator.dupe(u8, path) else null; const bios_path = result.args.bios;
errdefer if (bios_path) |path| allocator.free(path); if (bios_path) |path| log.info("BIOS path: {s}", .{path}) else log.warn("No BIOS provided", .{});
const save_path = try std.fs.path.join(allocator, &[_][]const u8{ data_path, "zba", "save" }); const save_path = try std.fs.path.join(allocator, &[_][]const u8{ data_path, "zba", "save" });
log.info("ROM path: {?s}", .{rom_path});
log.info("BIOS path: {?s}", .{bios_path});
log.info("Save path: {s}", .{save_path}); log.info("Save path: {s}", .{save_path});
return .{ return .{
@@ -184,8 +99,8 @@ fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const c
}; };
} }
fn configFilePath(allocator: Allocator, config_path: []const u8) ![]const u8 { fn configFilePath(allocator: Allocator, data_path: []const u8) ![]const u8 {
const path = try std.fs.path.join(allocator, &[_][]const u8{ config_path, "zba", "config.toml" }); const path = try std.fs.path.join(allocator, &[_][]const u8{ data_path, "zba", "config.toml" });
errdefer allocator.free(path); errdefer allocator.free(path);
// We try to create the file exclusively, meaning that we err out if the file already exists. // We try to create the file exclusively, meaning that we err out if the file already exists.
@@ -194,34 +109,30 @@ fn configFilePath(allocator: Allocator, config_path: []const u8) ![]const u8 {
std.fs.accessAbsolute(path, .{}) catch |e| { std.fs.accessAbsolute(path, .{}) catch |e| {
if (e != error.FileNotFound) return e; if (e != error.FileNotFound) return e;
const config_file = std.fs.createFileAbsolute(path, .{}) catch |err| exitln("failed to create \"{s}\": {}", .{ path, err }); const config_file = try std.fs.createFileAbsolute(path, .{});
defer config_file.close(); defer config_file.close();
try config_file.writeAll(@embedFile("example.toml")); try config_file.writeAll(@embedFile("../example.toml"));
}; };
return path; return path;
} }
fn ensureDataDirsExist(data_path: []const u8) !void { fn ensureDirectoriesExist(data_path: []const u8) !void {
var dir = try std.fs.openDirAbsolute(data_path, .{}); var dir = try std.fs.openDirAbsolute(data_path, .{});
defer dir.close(); defer dir.close();
// We want to make sure: %APPDATA%/zba and %APPDATA%/zba/save exist
// (~/.local/share/zba/save for linux, ??? for macOS)
// Will recursively create directories // Will recursively create directories
try dir.makePath("zba" ++ std.fs.path.sep_str ++ "save"); try dir.makePath("zba" ++ [_]u8{std.fs.path.sep} ++ "save");
} }
fn ensureConfigDirExists(config_path: []const u8) !void { fn romPath(result: *const clap.Result(clap.Help, &params, clap.parsers.default)) []const u8 {
var dir = try std.fs.openDirAbsolute(config_path, .{});
defer dir.close();
try dir.makePath("zba");
}
fn romPath(allocator: Allocator, result: *const clap.Result(clap.Help, &params, clap.parsers.default)) !?[]const u8 {
return switch (result.positionals.len) { return switch (result.positionals.len) {
0 => null, 1 => result.positionals[0],
1 => try allocator.dupe(u8, result.positionals[0]), 0 => exitln("ZBA requires a path to a GamePak ROM", .{}),
else => exitln("ZBA received too many positional arguments.", .{}), else => exitln("ZBA received too many positional arguments.", .{}),
}; };
} }
@@ -230,5 +141,5 @@ fn exitln(comptime format: []const u8, args: anytype) noreturn {
const stderr = std.io.getStdErr().writer(); const stderr = std.io.getStdErr().writer();
stderr.print(format, args) catch {}; // Just exit already... stderr.print(format, args) catch {}; // Just exit already...
stderr.writeByte('\n') catch {}; stderr.writeByte('\n') catch {};
std.process.exit(1); std.os.exit(1);
} }

View File

@@ -1,251 +1,249 @@
const std = @import("std"); const std = @import("std");
const SDL = @import("sdl2"); const SDL = @import("sdl2");
const gl = @import("gl"); const gl = @import("gl");
const zgui = @import("zgui");
const emu = @import("core/emu.zig"); const emu = @import("core/emu.zig");
const config = @import("config.zig"); const config = @import("config.zig");
const imgui = @import("imgui.zig");
const Apu = @import("core/apu.zig").Apu; const Apu = @import("core/apu.zig").Apu;
const Arm7tdmi = @import("arm32").Arm7tdmi; const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
const Bus = @import("core/Bus.zig");
const Scheduler = @import("core/scheduler.zig").Scheduler; const Scheduler = @import("core/scheduler.zig").Scheduler;
const FpsTracker = @import("util.zig").FpsTracker; const FpsTracker = @import("util.zig").FpsTracker;
const Synchro = @import("core/emu.zig").Synchro;
const KeyInput = @import("core/bus/io.zig").KeyInput;
const span = @import("util.zig").span;
const pitch = @import("core/ppu.zig").framebuf_pitch;
const gba_width = @import("core/ppu.zig").width; const gba_width = @import("core/ppu.zig").width;
const gba_height = @import("core/ppu.zig").height; const gba_height = @import("core/ppu.zig").height;
const GLuint = gl.GLuint; const default_title: []const u8 = "ZBA";
const GLsizei = gl.GLsizei;
const SDL_GLContext = *anyopaque;
const Allocator = std.mem.Allocator;
pub const Dimensions = struct { width: u32, height: u32 };
const default_dim: Dimensions = .{ .width = 1280, .height = 720 };
pub const sample_rate = 1 << 15;
pub const sample_format = SDL.AUDIO_U16;
const window_title = "ZBA";
pub const Gui = struct { pub const Gui = struct {
const Self = @This(); const Self = @This();
const SDL_GLContext = *anyopaque; // SDL.SDL_GLContext is a ?*anyopaque
const log = std.log.scoped(.Gui); const log = std.log.scoped(.Gui);
// zig fmt: off
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, window: *SDL.SDL_Window,
ctx: SDL_GLContext, ctx: SDL_GLContext,
title: []const u8,
audio: Audio, audio: Audio,
state: imgui.State, program_id: gl.GLuint,
allocator: Allocator,
pub fn init(allocator: Allocator, apu: *Apu, title_opt: ?*const [12]u8) !Self { 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_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO) < 0) panic();
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_PROFILE_MASK, SDL.SDL_GL_CONTEXT_PROFILE_CORE) < 0) panic(); if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_PROFILE_MASK, SDL.SDL_GL_CONTEXT_PROFILE_CORE) < 0) panic();
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic(); if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic();
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic(); if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic();
const win_scale = @intCast(c_int, config.config().host.win_scale);
const window = SDL.SDL_CreateWindow( const window = SDL.SDL_CreateWindow(
window_title, default_title.ptr,
SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED,
SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED,
default_dim.width, @as(c_int, width * win_scale),
default_dim.height, @as(c_int, height * win_scale),
SDL.SDL_WINDOW_OPENGL | SDL.SDL_WINDOW_SHOWN | SDL.SDL_WINDOW_RESIZABLE, SDL.SDL_WINDOW_OPENGL | SDL.SDL_WINDOW_SHOWN,
) orelse panic(); ) orelse panic();
const ctx = SDL.SDL_GL_CreateContext(window) orelse panic(); const ctx = SDL.SDL_GL_CreateContext(window) orelse panic();
if (SDL.SDL_GL_MakeCurrent(window, ctx) < 0) panic(); if (SDL.SDL_GL_MakeCurrent(window, ctx) < 0) panic();
gl.load(ctx, Self.glGetProcAddress) catch {}; gl.load(ctx, Self.glGetProcAddress) catch @panic("gl.load failed");
if (SDL.SDL_GL_SetSwapInterval(@intFromBool(config.config().host.vsync)) < 0) panic(); if (SDL.SDL_GL_SetSwapInterval(@boolToInt(config.config().host.vsync)) < 0) panic();
zgui.init(allocator); const program_id = compileShaders();
zgui.plot.init();
zgui.backend.init(window, ctx, "#version 330 core");
// zgui.io.setIniFilename(null);
return Self{ return Self{
.window = window, .window = window,
.title = span(title),
.ctx = ctx, .ctx = ctx,
.program_id = program_id,
.audio = Audio.init(apu), .audio = Audio.init(apu),
.allocator = allocator,
.state = try imgui.State.init(allocator, title_opt),
}; };
} }
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();
const thread = try std.Thread.spawn(.{}, emu.run, .{ &quit, scheduler, cpu, &tracker });
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) {
switch (event.type) {
SDL.SDL_QUIT => break :emu_loop,
SDL.SDL_KEYDOWN => {
const io = &cpu.bus.io;
const key_code = event.key.keysym.sym;
switch (key_code) {
SDL.SDLK_UP => io.keyinput.up.unset(),
SDL.SDLK_DOWN => io.keyinput.down.unset(),
SDL.SDLK_LEFT => io.keyinput.left.unset(),
SDL.SDLK_RIGHT => io.keyinput.right.unset(),
SDL.SDLK_x => io.keyinput.a.unset(),
SDL.SDLK_z => io.keyinput.b.unset(),
SDL.SDLK_a => io.keyinput.shoulder_l.unset(),
SDL.SDLK_s => io.keyinput.shoulder_r.unset(),
SDL.SDLK_RETURN => io.keyinput.start.unset(),
SDL.SDLK_RSHIFT => io.keyinput.select.unset(),
else => {},
}
},
SDL.SDL_KEYUP => {
const io = &cpu.bus.io;
const key_code = event.key.keysym.sym;
switch (key_code) {
SDL.SDLK_UP => io.keyinput.up.set(),
SDL.SDLK_DOWN => io.keyinput.down.set(),
SDL.SDLK_LEFT => io.keyinput.left.set(),
SDL.SDLK_RIGHT => io.keyinput.right.set(),
SDL.SDLK_x => io.keyinput.a.set(),
SDL.SDLK_z => io.keyinput.b.set(),
SDL.SDLK_a => io.keyinput.shoulder_l.set(),
SDL.SDLK_s => io.keyinput.shoulder_r.set(),
SDL.SDLK_RETURN => io.keyinput.start.set(),
SDL.SDLK_RSHIFT => io.keyinput.select.set(),
SDL.SDLK_i => log.err("Sample Count: {}", .{@intCast(u32, SDL.SDL_AudioStreamAvailable(cpu.bus.apu.stream)) / (2 * @sizeOf(u16))}),
SDL.SDLK_j => log.err("Scheduler Capacity: {} | Scheduler Event Count: {}", .{ scheduler.queue.capacity(), scheduler.queue.count() }),
SDL.SDLK_k => {
// Dump IWRAM to file
log.info("PC: 0x{X:0>8}", .{cpu.r[15]});
log.info("LR: 0x{X:0>8}", .{cpu.r[14]});
// const iwram_file = try std.fs.cwd().createFile("iwram.bin", .{});
// defer iwram_file.close();
// try iwram_file.writeAll(cpu.bus.iwram.buf);
},
else => {},
}
},
else => {},
}
}
// Emulator has an internal Double Buffer
const framebuf = cpu.bus.ppu.framebuf.get(.Renderer);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gba_width, gba_height, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, framebuf.ptr);
gl.useProgram(self.program_id);
gl.bindVertexArray(vao_id);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, null);
SDL.SDL_GL_SwapWindow(self.window);
const dyn_title = std.fmt.bufPrint(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, tracker.value() }) catch unreachable;
SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr);
}
quit.store(true, .SeqCst); // Terminate Emulator Thread
}
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
self.audio.deinit(); self.audio.deinit();
self.state.deinit(self.allocator); // TODO: Buffer deletions
gl.deleteProgram(self.program_id);
zgui.backend.deinit();
zgui.plot.deinit();
zgui.deinit();
SDL.SDL_GL_DeleteContext(self.ctx); SDL.SDL_GL_DeleteContext(self.ctx);
SDL.SDL_DestroyWindow(self.window); SDL.SDL_DestroyWindow(self.window);
SDL.SDL_Quit(); SDL.SDL_Quit();
self.* = undefined; self.* = undefined;
} }
const RunOptions = struct {
sync: *Synchro,
tracker: ?*FpsTracker = null,
cpu: *Arm7tdmi,
scheduler: *Scheduler,
};
pub fn run(self: *Self, opt: RunOptions) !void {
const cpu = opt.cpu;
const tracker = opt.tracker;
const sync = opt.sync;
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
const vao_id = opengl_impl.vao();
defer gl.deleteVertexArrays(1, &[_]GLuint{vao_id});
const emu_tex = opengl_impl.screenTex(bus_ptr.ppu.framebuf.get(.Renderer));
const out_tex = opengl_impl.outTex();
defer gl.deleteTextures(2, &[_]GLuint{ emu_tex, out_tex });
const fbo_id = try opengl_impl.frameBuffer(out_tex);
defer gl.deleteFramebuffers(1, &fbo_id);
const prog_id = try opengl_impl.program(); // Dynamic Shaders?
defer gl.deleteProgram(prog_id);
var win_dim: Dimensions = default_dim;
emu_loop: while (true) {
// Outside of `SDL.SDL_QUIT` below, the DearImgui UI might signal that the program
// should exit, in which case we should also handle this
if (self.state.should_quit or sync.should_quit.load(.monotonic)) break :emu_loop;
var event: SDL.SDL_Event = undefined;
while (SDL.SDL_PollEvent(&event) != 0) {
_ = zgui.backend.processEvent(&event);
switch (event.type) {
SDL.SDL_QUIT => break :emu_loop,
SDL.SDL_KEYDOWN => {
// TODO: Make use of compare_and_xor?
const key_code = event.key.keysym.sym;
var keyinput: KeyInput = .{ .raw = 0x0000 };
switch (key_code) {
SDL.SDLK_UP => keyinput.up.set(),
SDL.SDLK_DOWN => keyinput.down.set(),
SDL.SDLK_LEFT => keyinput.left.set(),
SDL.SDLK_RIGHT => keyinput.right.set(),
SDL.SDLK_x => keyinput.a.set(),
SDL.SDLK_z => keyinput.b.set(),
SDL.SDLK_a => keyinput.shoulder_l.set(),
SDL.SDLK_s => keyinput.shoulder_r.set(),
SDL.SDLK_RETURN => keyinput.start.set(),
SDL.SDLK_RSHIFT => keyinput.select.set(),
else => {},
}
bus_ptr.io.keyinput.fetchAnd(~keyinput.raw, .monotonic);
},
SDL.SDL_KEYUP => {
// TODO: Make use of compare_and_xor?
const key_code = event.key.keysym.sym;
var keyinput: KeyInput = .{ .raw = 0x0000 };
switch (key_code) {
SDL.SDLK_UP => keyinput.up.set(),
SDL.SDLK_DOWN => keyinput.down.set(),
SDL.SDLK_LEFT => keyinput.left.set(),
SDL.SDLK_RIGHT => keyinput.right.set(),
SDL.SDLK_x => keyinput.a.set(),
SDL.SDLK_z => keyinput.b.set(),
SDL.SDLK_a => keyinput.shoulder_l.set(),
SDL.SDLK_s => keyinput.shoulder_r.set(),
SDL.SDLK_RETURN => keyinput.start.set(),
SDL.SDLK_RSHIFT => keyinput.select.set(),
else => {},
}
bus_ptr.io.keyinput.fetchOr(keyinput.raw, .monotonic);
},
SDL.SDL_WINDOWEVENT => {
if (event.window.event == SDL.SDL_WINDOWEVENT_RESIZED) {
log.debug("window resized to: {}x{}", .{ event.window.data1, event.window.data2 });
win_dim.width = @intCast(event.window.data1);
win_dim.height = @intCast(event.window.data2);
}
},
else => {},
}
}
var zgui_redraw: bool = false;
switch (self.state.emulation) {
.Transition => |inner| switch (inner) {
.Active => {
sync.paused.store(false, .monotonic);
if (!config.config().host.mute) SDL.SDL_PauseAudioDevice(self.audio.device, 0);
self.state.emulation = .Active;
},
.Inactive => {
// Assert that double pausing is impossible
SDL.SDL_PauseAudioDevice(self.audio.device, 1);
sync.paused.store(true, .monotonic);
self.state.emulation = .Inactive;
},
},
.Active => {
// Add FPS count to the histogram
if (tracker) |t| self.state.fps_hist.push(t.value()) catch {};
// Draw GBA Screen to Texture
{
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo_id);
defer gl.bindFramebuffer(gl.FRAMEBUFFER, 0);
gl.viewport(0, 0, gba_width, gba_height);
opengl_impl.drawScreen(emu_tex, prog_id, vao_id, bus_ptr.ppu.framebuf.get(.Renderer));
}
// FIXME: We only really care about locking the audio device (and therefore writing silence)
// since if nfd-zig is used the emu may be paused for way too long. Perhaps we should try and limit
// spurious calls to SDL_LockAudioDevice?
SDL.SDL_LockAudioDevice(self.audio.device);
defer SDL.SDL_UnlockAudioDevice(self.audio.device);
zgui_redraw = imgui.draw(&self.state, sync, win_dim, cpu, out_tex);
},
.Inactive => zgui_redraw = imgui.draw(&self.state, sync, win_dim, cpu, out_tex),
}
if (zgui_redraw) {
// Background Colour
const size = zgui.io.getDisplaySize();
gl.viewport(0, 0, @intFromFloat(size[0]), @intFromFloat(size[1]));
gl.clearColor(0, 0, 0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
zgui.backend.draw();
}
SDL.SDL_GL_SwapWindow(self.window);
}
sync.should_quit.store(true, .monotonic);
}
fn glGetProcAddress(ctx: SDL.SDL_GLContext, proc: [:0]const u8) ?*anyopaque { fn glGetProcAddress(ctx: SDL.SDL_GLContext, proc: [:0]const u8) ?*anyopaque {
_ = ctx; _ = ctx;
return SDL.SDL_GL_GetProcAddress(proc.ptr); return SDL.SDL_GL_GetProcAddress(proc.ptr);
@@ -255,6 +253,7 @@ pub const Gui = struct {
const Audio = struct { const Audio = struct {
const Self = @This(); const Self = @This();
const log = std.log.scoped(.PlatformAudio); const log = std.log.scoped(.PlatformAudio);
const sample_rate = @import("core/apu.zig").host_sample_rate;
device: SDL.SDL_AudioDeviceID, device: SDL.SDL_AudioDeviceID,
@@ -262,18 +261,17 @@ const Audio = struct {
var have: SDL.SDL_AudioSpec = undefined; var have: SDL.SDL_AudioSpec = undefined;
var want: SDL.SDL_AudioSpec = std.mem.zeroes(SDL.SDL_AudioSpec); var want: SDL.SDL_AudioSpec = std.mem.zeroes(SDL.SDL_AudioSpec);
want.freq = sample_rate; want.freq = sample_rate;
want.format = sample_format; want.format = SDL.AUDIO_U16;
want.channels = 2; want.channels = 2;
want.samples = 0x100; want.samples = 0x100;
want.callback = Self.callback; want.callback = Self.callback;
want.userdata = apu; want.userdata = apu;
std.debug.assert(sample_format == SDL.AUDIO_U16);
log.info("Host Sample Rate: {}Hz, Host Format: SDL.AUDIO_U16", .{sample_rate});
const device = SDL.SDL_OpenAudioDevice(null, 0, &want, &have, 0); const device = SDL.SDL_OpenAudioDevice(null, 0, &want, &have, 0);
if (device == 0) panic(); if (device == 0) panic();
SDL.SDL_PauseAudioDevice(device, 0); // Unpause Audio
return .{ .device = device }; return .{ .device = device };
} }
@@ -283,9 +281,18 @@ const Audio = struct {
} }
export fn callback(userdata: ?*anyopaque, stream: [*c]u8, len: c_int) void { export fn callback(userdata: ?*anyopaque, stream: [*c]u8, len: c_int) void {
const apu: *Apu = @ptrCast(@alignCast(userdata)); 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); _ = 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
// if (written == 0) std.mem.set(u8, stream[0..@intCast(usize, len)], 0x40);
} }
}; };
@@ -293,124 +300,3 @@ fn panic() noreturn {
const str = @as(?[*:0]const u8, SDL.SDL_GetError()) orelse "unknown error"; const str = @as(?[*:0]const u8, SDL.SDL_GetError()) orelse "unknown error";
@panic(std.mem.sliceTo(str, 0)); @panic(std.mem.sliceTo(str, 0));
} }
const opengl_impl = struct {
fn drawScreen(tex_id: GLuint, prog_id: GLuint, vao_id: GLuint, buf: []const u8) void {
gl.bindTexture(gl.TEXTURE_2D, tex_id);
defer gl.bindTexture(gl.TEXTURE_2D, 0);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gba_width, gba_height, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, buf.ptr);
// Bind VAO
gl.bindVertexArray(vao_id);
defer gl.bindVertexArray(0);
// Use compiled frag + vertex shader
gl.useProgram(prog_id);
defer gl.useProgram(0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 3);
}
fn program() !GLuint {
const vert_shader = @embedFile("shader/pixelbuf.vert");
const frag_shader = @embedFile("shader/pixelbuf.frag");
const vs = gl.createShader(gl.VERTEX_SHADER);
defer gl.deleteShader(vs);
gl.shaderSource(vs, 1, &[_][*c]const u8{vert_shader}, 0);
gl.compileShader(vs);
if (!shader.didCompile(vs)) return error.VertexCompileError;
const fs = gl.createShader(gl.FRAGMENT_SHADER);
defer gl.deleteShader(fs);
gl.shaderSource(fs, 1, &[_][*c]const u8{frag_shader}, 0);
gl.compileShader(fs);
if (!shader.didCompile(fs)) return error.FragmentCompileError;
const prog = gl.createProgram();
gl.attachShader(prog, vs);
gl.attachShader(prog, fs);
gl.linkProgram(prog);
return prog;
}
fn vao() GLuint {
var vao_id: GLuint = undefined;
gl.genVertexArrays(1, &vao_id);
return vao_id;
}
fn screenTex(buf: []const u8) GLuint {
var tex_id: GLuint = undefined;
gl.genTextures(1, &tex_id);
gl.bindTexture(gl.TEXTURE_2D, tex_id);
defer gl.bindTexture(gl.TEXTURE_2D, 0);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gba_width, gba_height, 0, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, buf.ptr);
return tex_id;
}
fn outTex() GLuint {
var tex_id: GLuint = undefined;
gl.genTextures(1, &tex_id);
gl.bindTexture(gl.TEXTURE_2D, tex_id);
defer gl.bindTexture(gl.TEXTURE_2D, 0);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gba_width, gba_height, 0, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, null);
return tex_id;
}
fn frameBuffer(tex_id: GLuint) !GLuint {
var fbo_id: GLuint = undefined;
gl.genFramebuffers(1, &fbo_id);
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo_id);
defer gl.bindFramebuffer(gl.FRAMEBUFFER, 0);
gl.framebufferTexture(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, tex_id, 0);
gl.drawBuffers(1, &@as(GLuint, gl.COLOR_ATTACHMENT0));
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE)
return error.FrameBufferObejctInitFailed;
return fbo_id;
}
const shader = struct {
const log = std.log.scoped(.shader);
fn didCompile(id: gl.GLuint) bool {
var success: gl.GLint = undefined;
gl.getShaderiv(id, gl.COMPILE_STATUS, &success);
if (success == 0) err(id);
return success == 1;
}
fn err(id: gl.GLuint) void {
const buf_len = 512;
var error_msg: [buf_len]u8 = undefined;
gl.getShaderInfoLog(id, buf_len, 0, &error_msg);
log.err("{s}", .{std.mem.sliceTo(&error_msg, 0)});
}
};
};

View File

@@ -1,6 +1,7 @@
#version 330 core #version 330 core
out vec4 frag_color; out vec4 frag_color;
in vec3 color;
in vec2 uv; in vec2 uv;
uniform sampler2D screen; uniform sampler2D screen;

View File

@@ -1,10 +1,13 @@
#version 330 core #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; out vec2 uv;
const vec2 pos[3] = vec2[3](vec2(-1.0f, -1.0f), vec2(-1.0f, 3.0f), vec2(3.0f, -1.0f));
const vec2 uvs[3] = vec2[3](vec2( 0.0f, 0.0f), vec2( 0.0f, 2.0f), vec2(2.0f, 0.0f));
void main() { void main() {
uv = uvs[gl_VertexID]; color = in_color;
gl_Position = vec4(pos[gl_VertexID], 0.0, 1.0); uv = in_uv;
gl_Position = vec4(pos, 1.0);
} }

View File

@@ -3,32 +3,53 @@ const builtin = @import("builtin");
const config = @import("config.zig"); const config = @import("config.zig");
const Log2Int = std.math.Log2Int; const Log2Int = std.math.Log2Int;
const Arm7tdmi = @import("arm32").Arm7tdmi; const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
// Sign-Extend value of type `T` to type `U`
pub fn sext(comptime T: type, comptime U: type, value: T) T {
// U must have less bits than T
comptime std.debug.assert(@typeInfo(U).Int.bits <= @typeInfo(T).Int.bits);
const iT = std.meta.Int(.signed, @typeInfo(T).Int.bits);
const ExtU = if (@typeInfo(U).Int.signedness == .unsigned) T else iT;
const shift = @intCast(Log2Int(T), @typeInfo(T).Int.bits - @typeInfo(U).Int.bits);
return @bitCast(T, @bitCast(iT, @as(ExtU, @truncate(U, value)) << shift) >> shift);
}
/// See https://godbolt.org/z/W3en9Eche
pub inline fn rotr(comptime T: type, x: T, r: anytype) T {
if (@typeInfo(T).Int.signedness == .signed)
@compileError("cannot rotate signed integer");
const ar = @intCast(Log2Int(T), @mod(r, @typeInfo(T).Int.bits));
return x >> ar | x << (1 +% ~ar);
}
pub const FpsTracker = struct { pub const FpsTracker = struct {
const Self = @This(); const Self = @This();
fps: u32, fps: u32,
count: std.atomic.Value(u32), count: std.atomic.Atomic(u32),
timer: std.time.Timer, timer: std.time.Timer,
pub fn init() Self { pub fn init() Self {
return .{ return .{
.fps = 0, .fps = 0,
.count = std.atomic.Value(u32).init(0), .count = std.atomic.Atomic(u32).init(0),
.timer = std.time.Timer.start() catch unreachable, .timer = std.time.Timer.start() catch unreachable,
}; };
} }
pub fn tick(self: *Self) void { pub fn tick(self: *Self) void {
_ = self.count.fetchAdd(1, .monotonic); _ = self.count.fetchAdd(1, .Monotonic);
} }
pub fn value(self: *Self) u32 { pub fn value(self: *Self) u32 {
if (self.timer.read() >= std.time.ns_per_s) { if (self.timer.read() >= std.time.ns_per_s) {
self.fps = self.count.swap(0, .monotonic); self.fps = self.count.swap(0, .SeqCst);
self.timer.reset(); self.timer.reset();
} }
@@ -36,6 +57,68 @@ pub const FpsTracker = struct {
} }
}; };
pub fn intToBytes(comptime T: type, value: anytype) [@sizeOf(T)]u8 {
comptime std.debug.assert(@typeInfo(T) == .Int);
var result: [@sizeOf(T)]u8 = undefined;
var i: Log2Int(T) = 0;
while (i < result.len) : (i += 1) result[i] = @truncate(u8, value >> i * @bitSizeOf(u8));
return result;
}
/// The Title from the GBA Cartridge is an Uppercase ASCII string which is
/// null-padded to 12 bytes
///
/// This function returns a slice of the ASCII string without the null terminator(s)
/// (essentially, a proper Zig/Rust/Any modern language String)
pub fn span(title: *const [12]u8) []const u8 {
const end = std.mem.indexOfScalar(u8, title, '\x00');
return title[0 .. end orelse title.len];
}
test "span" {
var example: *const [12]u8 = "POKEMON_EMER";
try std.testing.expectEqualSlices(u8, "POKEMON_EMER", span(example));
example = "POKEMON_EME\x00";
try std.testing.expectEqualSlices(u8, "POKEMON_EME", span(example));
example = "POKEMON_EM\x00\x00";
try std.testing.expectEqualSlices(u8, "POKEMON_EM", span(example));
example = "POKEMON_E\x00\x00\x00";
try std.testing.expectEqualSlices(u8, "POKEMON_E", span(example));
example = "POKEMON_\x00\x00\x00\x00";
try std.testing.expectEqualSlices(u8, "POKEMON_", span(example));
example = "POKEMON\x00\x00\x00\x00\x00";
try std.testing.expectEqualSlices(u8, "POKEMON", span(example));
example = "POKEMO\x00\x00\x00\x00\x00\x00";
try std.testing.expectEqualSlices(u8, "POKEMO", span(example));
example = "POKEM\x00\x00\x00\x00\x00\x00\x00";
try std.testing.expectEqualSlices(u8, "POKEM", span(example));
example = "POKE\x00\x00\x00\x00\x00\x00\x00\x00";
try std.testing.expectEqualSlices(u8, "POKE", span(example));
example = "POK\x00\x00\x00\x00\x00\x00\x00\x00\x00";
try std.testing.expectEqualSlices(u8, "POK", span(example));
example = "PO\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
try std.testing.expectEqualSlices(u8, "PO", span(example));
example = "P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
try std.testing.expectEqualSlices(u8, "P", span(example));
example = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
try std.testing.expectEqualSlices(u8, "", span(example));
}
/// Creates a copy of a title with all Filesystem-invalid characters replaced /// Creates a copy of a title with all Filesystem-invalid characters replaced
/// ///
/// e.g. POKEPIN R/S to POKEPIN R_S /// e.g. POKEPIN R/S to POKEPIN R_S
@@ -50,15 +133,9 @@ pub fn escape(title: [12]u8) [12]u8 {
} }
pub const FilePaths = struct { pub const FilePaths = struct {
rom: ?[]const u8, rom: []const u8,
bios: ?[]const u8, bios: ?[]const u8,
save: []const u8, save: ?[]const u8,
pub fn deinit(self: @This(), allocator: Allocator) void {
if (self.rom) |path| allocator.free(path);
if (self.bios) |path| allocator.free(path);
allocator.free(self.save);
}
}; };
pub const io = struct { pub const io = struct {
@@ -68,9 +145,7 @@ pub const io = struct {
return 0; return 0;
} }
pub fn undef(comptime T: type, comptime log: anytype, comptime format: []const u8, args: anytype) ?T { pub fn undef(comptime T: type, log: anytype, comptime format: []const u8, args: anytype) ?T {
@setCold(true);
const unhandled_io = config.config().debug.unhandled_io; const unhandled_io = config.config().debug.unhandled_io;
log.warn(format, args); log.warn(format, args);
@@ -78,13 +153,6 @@ pub const io = struct {
return null; return null;
} }
pub fn err(comptime T: type, comptime log: anytype, comptime format: []const u8, args: anytype) ?T {
@setCold(true);
log.err(format, args);
return null;
}
}; };
pub const write = struct { pub const write = struct {
@@ -122,7 +190,7 @@ pub const Logger = struct {
if (cpu.cpsr.t.read()) { if (cpu.cpsr.t.read()) {
if (opcode >> 11 == 0x1E) { if (opcode >> 11 == 0x1E) {
// Instruction 1 of a BL Opcode, print in ARM mode // Instruction 1 of a BL Opcode, print in ARM mode
const low = cpu.bus.dbgRead(u16, cpu.r[15] - 2); const low = cpu.bus.dbgRead(u16, cpu.r[15]);
const bl_opcode = @as(u32, opcode) << 16 | low; const bl_opcode = @as(u32, opcode) << 16 | low;
self.print(arm_fmt, Self.fmtArgs(cpu, bl_opcode)) catch @panic("failed to write to log file"); self.print(arm_fmt, Self.fmtArgs(cpu, bl_opcode)) catch @panic("failed to write to log file");
@@ -207,37 +275,22 @@ pub const audio = struct {
}; };
}; };
/// Sets a quarter (8) of the bits of the u32 `left` to the value of u8 `right` /// Sets the high bits of an integer to a value
pub inline fn setQuart(left: u32, addr: u8, right: u8) u32 { pub inline fn setHi(comptime T: type, left: T, right: HalfInt(T)) T {
const offset: u2 = @truncate(addr); return switch (T) {
u32 => (left & 0xFFFF_0000) | right,
return switch (offset) { u16 => (left & 0xFF00) | right,
0b00 => (left & 0xFFFF_FF00) | right, u8 => (left & 0xF0) | right,
0b01 => (left & 0xFFFF_00FF) | @as(u32, right) << 8, else => @compileError("unsupported type"),
0b10 => (left & 0xFF00_FFFF) | @as(u32, right) << 16,
0b11 => (left & 0x00FF_FFFF) | @as(u32, right) << 24,
}; };
} }
/// Calculates the correct shift offset for an aligned/unaligned u8 read /// sets the low bits of an integer to a value
/// pub inline fn setLo(comptime T: type, left: T, right: HalfInt(T)) T {
/// TODO: Support u16 reads of u32 values?
pub inline fn getHalf(byte: u8) u4 {
return @as(u4, @truncate(byte & 1)) << 3;
}
pub inline fn setHalf(comptime T: type, left: T, addr: u8, right: HalfInt(T)) T {
const offset: u1 = @truncate(addr >> if (T == u32) 1 else 0);
return switch (T) { return switch (T) {
u32 => switch (offset) { u32 => (left & 0x0000_FFFF) | @as(u32, right) << 16,
0b0 => (left & 0xFFFF_0000) | right, u16 => (left & 0x00FF) | @as(u16, right) << 8,
0b1 => (left & 0x0000_FFFF) | @as(u32, right) << 16, u8 => (left & 0x0F) | @as(u8, right) << 4,
},
u16 => switch (offset) {
0b0 => (left & 0xFF00) | right,
0b1 => (left & 0x00FF) | @as(u16, right) << 8,
},
else => @compileError("unsupported type"), else => @compileError("unsupported type"),
}; };
} }
@@ -257,7 +310,7 @@ pub const FrameBuffer = struct {
layers: [2][]u8, layers: [2][]u8,
buf: []u8, buf: []u8,
current: u1 = 0, current: u1,
allocator: Allocator, allocator: Allocator,
@@ -266,22 +319,18 @@ pub const FrameBuffer = struct {
pub fn init(allocator: Allocator, comptime len: comptime_int) !Self { pub fn init(allocator: Allocator, comptime len: comptime_int) !Self {
const buf = try allocator.alloc(u8, len * 2); const buf = try allocator.alloc(u8, len * 2);
@memset(buf, 0); std.mem.set(u8, buf, 0);
return .{ return .{
// Front and Back Framebuffers // Front and Back Framebuffers
.layers = [_][]u8{ buf[0..][0..len], buf[len..][0..len] }, .layers = [_][]u8{ buf[0..][0..len], buf[len..][0..len] },
.buf = buf, .buf = buf,
.current = 0,
.allocator = allocator, .allocator = allocator,
}; };
} }
pub fn reset(self: *Self) void {
@memset(self.buf, 0);
self.current = 0;
}
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
self.allocator.free(self.buf); self.allocator.free(self.buf);
self.* = undefined; self.* = undefined;
@@ -295,31 +344,3 @@ pub const FrameBuffer = struct {
return self.layers[if (dev == .Emulator) self.current else ~self.current]; return self.layers[if (dev == .Emulator) self.current else ~self.current];
} }
}; };
const RingBuffer = @import("zba-util").RingBuffer;
// TODO: Lock Free Queue?
pub fn Queue(comptime T: type) type {
return struct {
inner: RingBuffer(T),
mtx: std.Thread.Mutex = .{},
pub fn init(buf: []T) @This() {
return .{ .inner = RingBuffer(T).init(buf) };
}
pub fn push(self: *@This(), value: T) !void {
self.mtx.lock();
defer self.mtx.unlock();
try self.inner.push(value);
}
pub fn pop(self: *@This()) ?T {
self.mtx.lock();
defer self.mtx.unlock();
return self.inner.pop();
}
};
}