Compare commits
92 Commits
2dbbfab206
...
apu-things
Author | SHA1 | Date | |
---|---|---|---|
2f2c03b96d | |||
9a2b7a48c0 | |||
fe908a6ea9 | |||
bf95eee3f1 | |||
240fbcb1df | |||
26db340077 | |||
20f611b7b5 | |||
f9aefedf60 | |||
d7e3d34726 | |||
2294dc8832 | |||
4af86e1cb3 | |||
9fcbbe7d57 | |||
c3f67e38a1 | |||
46e29245b7 | |||
002e33b48b | |||
5bb25fe214 | |||
66db2e6049 | |||
c5cf471912 | |||
4ed4f8e143 | |||
f31699d921 | |||
96a9ae2ca5 | |||
ee1c0bb313 | |||
558c03b12b | |||
7d8fbbb086 | |||
9fd405a896 | |||
5d7cf3a8a2 | |||
1230aa1e91 | |||
accecb3350 | |||
1e0ade8f55 | |||
429676ad43 | |||
ef39d9a7b8 | |||
986bc9448e | |||
d34893ba72 | |||
b8a5fb95c1 | |||
102b2c946b | |||
505b1b9608 | |||
2851c140ea | |||
637d81ce44 | |||
bc52461f0f | |||
c395c04a6e | |||
9eb4f8f191 | |||
f774256c42 | |||
5c15d039e1 | |||
28e9342c25 | |||
af8ec4db5b | |||
5d47e5d167 | |||
5101fbd809 | |||
472457b9f3 | |||
2ef4bb7dcc | |||
9a732ea6f8 | |||
f80799a593 | |||
ca67ca3183 | |||
47fc49deb6 | |||
472215b4c2 | |||
c9a423d094 | |||
1d163fa56f | |||
13710a3236 | |||
6154585e77 | |||
7debdc490d | |||
58375795bf | |||
f0dca29836 | |||
c75682dbd4 | |||
36832ba1fb | |||
647bd83224 | |||
c831f67d1a | |||
268961262d | |||
3e62feacba | |||
d859cee365 | |||
371cf4cc12 | |||
10aec67ee0 | |||
4eb715a138 | |||
14b24787ab | |||
eb7ffa29f4 | |||
4b8ed3cebb | |||
928ce674d9 | |||
945dbec013 | |||
dd98066a34 | |||
a2868dfe9e | |||
22979d9450 | |||
712c58391d | |||
407774d798 | |||
16f8f4c953 | |||
143ffd95f7 | |||
250ff25ed7 | |||
eff52ac1bb | |||
e60b556f72 | |||
3a3e6acc6a | |||
4b4bc7f894 | |||
325208d460 | |||
f44a1a49fd | |||
1575f517a9 | |||
26dba16789 |
58
.github/workflows/main.yml
vendored
Normal file
58
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
name: Nightly
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "**.zig"
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
runs-on: ${{matrix.os}}
|
||||||
|
steps:
|
||||||
|
- uses: goto-bus-stop/setup-zig@v2
|
||||||
|
with:
|
||||||
|
version: master
|
||||||
|
- name: prepare-linux
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install libsdl2-dev
|
||||||
|
- name: prepare-windows
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
run: |
|
||||||
|
vcpkg integrate install
|
||||||
|
vcpkg install sdl2:x64-windows
|
||||||
|
git config --global core.autocrlf false
|
||||||
|
- name: prepare-macos
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
run: |
|
||||||
|
brew install sdl2
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
- name: build
|
||||||
|
run: zig build -Drelease-safe
|
||||||
|
- name: upload
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: zba-${{matrix.os}}
|
||||||
|
path: zig-out/bin
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
- uses: goto-bus-stop/setup-zig@v2
|
||||||
|
with:
|
||||||
|
version: master
|
||||||
|
- run: zig fmt src/**/*.zig
|
||||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,7 +1,7 @@
|
|||||||
/.vscode
|
/.vscode
|
||||||
/bin
|
/bin
|
||||||
/zig-cache
|
**/zig-cache
|
||||||
/zig-out
|
**/zig-out
|
||||||
/docs
|
/docs
|
||||||
**/*.log
|
**/*.log
|
||||||
**/*.bin
|
**/*.bin
|
||||||
|
133
README.md
133
README.md
@@ -1,81 +1,110 @@
|
|||||||
# ZBA (working title)
|
# ZBA (working title)
|
||||||
|
|
||||||
A Game Boy Advance Emulator written in Zig ⚡!
|
A Game Boy Advance Emulator written in Zig ⚡!
|
||||||
|
|
||||||
## 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).
|
|
||||||
|
|
||||||
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
|
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).
|
||||||
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
|
- [ ] 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))
|
||||||
- [ ] Shaders (see [this branch](https://git.musuka.dev/paoda/zba/src/branch/opengl))
|
|
||||||
- [ ] Audio Resampler (Having issues with SDL2's)
|
- [ ] Audio Resampler (Having issues with SDL2's)
|
||||||
- [ ] Immediate Mode GUI
|
- [ ] Immediate Mode GUI
|
||||||
- [ ] Refactoring for easy-ish perf boosts
|
- [ ] Refactoring for easy-ish perf boosts
|
||||||
|
|
||||||
## Tests
|
## Usage
|
||||||
- [x] [jsmolka's GBA Test Collection](https://github.com/jsmolka/gba-tests)
|
|
||||||
- [x] `arm.gba` and `thumb.gba`
|
As it currently exists, ZBA is run from the terminal. In your console of choice, type `./zba --help` to see what you can do.
|
||||||
- [x] `flash64.gba`, `flash128.gba`, `none.gba`, and `sram.gba`
|
|
||||||
- [x] `hello.gba`, `shades.gba`, and `stripes.gba`
|
I typically find myself typing `./zba -b ./bin/bios.bin ./bin/test/suite.gba` to see how badly my "cool new feature" broke everything else.
|
||||||
- [x] `memory.gba`
|
|
||||||
- [x] `bios.gba`
|
Need a BIOS? Why not try using the open-source [Cult-Of-GBA BIOS](https://github.com/Cult-of-GBA/BIOS) written by [fleroviux](https://github.com/fleroviux) and [DenSinH](https://github.com/DenSinH)?
|
||||||
- [x] `nes.gba`
|
|
||||||
- [ ] [DenSinH's GBA ROMs](https://github.com/DenSinH/GBARoms)
|
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.
|
||||||
- [x] `eeprom-test` and `flash-test`
|
|
||||||
- [x] `midikey2freq`
|
## Tests
|
||||||
- [ ] `swi-tests-random`
|
|
||||||
- [ ] [destoer's GBA Tests](https://github.com/destoer/gba_tests)
|
GBA Tests | [jsmolka](https://github.com/jsmolka/)
|
||||||
- [x] `cond_invalid.gba`
|
--- | ---
|
||||||
- [x] `dma_priority.gba`
|
`arm.gba`, `thumb.gba` | PASS
|
||||||
- [x] `hello_world.gba`
|
`memory.gba`, `bios.gba` | PASS
|
||||||
- [x] `if_ack.gba`
|
`flash64.gba`, `flash128.gba` | PASS
|
||||||
- [ ] `line_timing.gba`
|
`sram.gba` | PASS
|
||||||
- [ ] `lyc_midline.gba`
|
`none.gba` | PASS
|
||||||
- [ ] `window_midframe.gba`
|
`hello.gba`, `shades.gba`, `stripes.gba` | PASS
|
||||||
- [x] [ladystarbreeze's GBA Test Collection](https://github.com/ladystarbreeze/GBA-Test-Collection)
|
`nes.gba` | PASS
|
||||||
- [x] `retAddr.gba`
|
|
||||||
- [x] `helloWorld.gba`
|
GBARoms | [DenSinH](https://github.com/DenSinH/)
|
||||||
- [x] `helloAudio.gba`
|
--- | ---
|
||||||
- [x] [`armwrestler-gba-fixed.gba`](https://github.com/destoer/armwrestler-gba-fixed)
|
`eeprom-test`, `flash-test` | PASS
|
||||||
- [x] [FuzzARM](https://github.com/DenSinH/FuzzARM)
|
`midikey2freq` | PASS
|
||||||
|
`swi-tests-random` | FAIL
|
||||||
|
|
||||||
|
gba_tests | [destoer](https://github.com/destoer/)
|
||||||
|
--- | ---
|
||||||
|
`cond_invalid.gba` | PASS
|
||||||
|
`dma_priority.gba` | PASS
|
||||||
|
`hello_world.gba` | PASS
|
||||||
|
`if_ack.gba` | PASS
|
||||||
|
`line_timing.gba` | FAIL
|
||||||
|
`lyc_midline.gba` | FAIL
|
||||||
|
`window_midframe.gba` | FAIL
|
||||||
|
|
||||||
|
GBA Test Collection | [ladystarbreeze](https://github.com/ladystarbreeze)
|
||||||
|
--- | ---
|
||||||
|
`retAddr.gba` | PASS
|
||||||
|
`helloWorld.gba` | PASS
|
||||||
|
`helloAudio.gba` | PASS
|
||||||
|
|
||||||
|
FuzzARM | [DenSinH](https://github.com/DenSinH/)
|
||||||
|
--- | ---
|
||||||
|
`main.gba` | PASS
|
||||||
|
|
||||||
|
arm7wrestler GBA Fixed | [destoer](https://github.com/destoer)
|
||||||
|
--- | ---
|
||||||
|
`armwrestler-gba-fixed.gba` | PASS
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
* [GBATEK](https://problemkaputt.de/gbatek.htm)
|
|
||||||
* [TONC](https://coranac.com/tonc/text/toc.htm)
|
- [GBATEK](https://problemkaputt.de/gbatek.htm)
|
||||||
* [ARM Architecture Reference Manual](https://www.intel.com/content/dam/www/programmable/us/en/pdfs/literature/third-party/ddi0100e_arm_arm.pdf)
|
- [TONC](https://coranac.com/tonc/text/toc.htm)
|
||||||
* [ARM7TDMI Data Sheet](https://www.dca.fee.unicamp.br/cursos/EA871/references/ARM/ARM7TDMIDataSheet.pdf)
|
- [ARM Architecture Reference Manual](https://www.intel.com/content/dam/www/programmable/us/en/pdfs/literature/third-party/ddi0100e_arm_arm.pdf)
|
||||||
|
- [ARM7TDMI Data Sheet](https://www.dca.fee.unicamp.br/cursos/EA871/references/ARM/ARM7TDMIDataSheet.pdf)
|
||||||
|
|
||||||
## Compiling
|
## Compiling
|
||||||
Most recently built on Zig [0.10.0-dev.4324+c23b3e6fd](https://github.com/ziglang/zig/tree/c23b3e6fd)
|
|
||||||
|
Most recently built on Zig [0.11.0-dev.368+1829b6eab](https://github.com/ziglang/zig/tree/1829b6eab)
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
* [SDL.zig](https://github.com/MasterQ32/SDL.zig)
|
|
||||||
* [SDL2](https://www.libsdl.org/download-2.0.php)
|
|
||||||
* [zig-clap](https://github.com/Hejsil/zig-clap)
|
|
||||||
* [known-folders](https://github.com/ziglibs/known-folders)
|
|
||||||
* [zig-toml](https://github.com/aeronavery/zig-toml)
|
|
||||||
* [zig-datetime](https://github.com/frmdstryr/zig-datetime)
|
|
||||||
* [`bitfields.zig`](https://github.com/FlorenceOS/Florence/blob/aaa5a9e568/lib/util/bitfields.zig)
|
|
||||||
|
|
||||||
`bitfields.zig` from [FlorenceOS](https://github.com/FlorenceOS) is included under `lib/util/bitfield.zig`.
|
Dependency | Source
|
||||||
|
--- | ---
|
||||||
|
SDL.zig | <https://github.com/MasterQ32/SDL.zig>
|
||||||
|
zig-clap | <https://github.com/Hejsil/zig-clap>
|
||||||
|
known-folders | <https://github.com/ziglibs/known-folders>
|
||||||
|
zig-toml | <https://github.com/aeronavery/zig-toml>
|
||||||
|
zig-datetime | <https://github.com/frmdstryr/zig-datetime>
|
||||||
|
`bitfields.zig` | [https://github.com/FlorenceOS/Florence](https://github.com/FlorenceOS/Florence/blob/aaa5a9e568/lib/util/bitfields.zig)
|
||||||
|
`gl.zig` | <https://github.com/MasterQ32/zig-opengl>
|
||||||
|
|
||||||
Use `git submodule update --init` from the project root to pull the git submodules `SDL.zig`, `zig-clap`, `known-folders`, `zig-toml` and `zig-datetime`
|
Use `git submodule update --init` from the project root to pull the git submodules `SDL.zig`, `zig-clap`, `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
|
|
||||||
* MacOS: ¯\\\_(ツ)_/¯
|
|
||||||
* Windows: [`vcpkg`](https://github.com/Microsoft/vcpkg) (install `sdl2:x64-windows`)
|
|
||||||
|
|
||||||
`SDL.zig` will provide a helpful compile error if the zig compiler is unable to find SDL2.
|
- Linux: Your distro's package manager
|
||||||
|
- MacOS: ¯\\\_(ツ)_/¯
|
||||||
|
- Windows: [`vcpkg`](https://github.com/Microsoft/vcpkg) (install `sdl2:x64-windows`)
|
||||||
|
|
||||||
Once you've got all the dependencies, execute `zig build -Drelease-fast`. The executable is located at `zig-out/bin/`.
|
`SDL.zig` will provide a helpful compile error if the zig compiler is unable to find SDL2.
|
||||||
|
|
||||||
|
Once you've got all the dependencies, execute `zig build -Drelease-fast`. The executable is located at `zig-out/bin/`.
|
||||||
|
|
||||||
## Controls
|
## Controls
|
||||||
|
|
||||||
Key | Button
|
Key | Button
|
||||||
--- | ---
|
--- | ---
|
||||||
<kbd>X</kbd> | A
|
<kbd>X</kbd> | A
|
||||||
|
12
build.zig
12
build.zig
@@ -1,7 +1,15 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
const Sdk = @import("lib/SDL.zig/Sdk.zig");
|
const Sdk = @import("lib/SDL.zig/Sdk.zig");
|
||||||
|
|
||||||
pub fn build(b: *std.build.Builder) void {
|
pub fn build(b: *std.build.Builder) void {
|
||||||
|
// Minimum Zig Version
|
||||||
|
const min_ver = std.SemanticVersion.parse("0.11.0-dev.323+30eb2a175") catch return; // https://github.com/ziglang/zig/commit/30eb2a175
|
||||||
|
if (builtin.zig_version.order(min_ver).compare(.lt)) {
|
||||||
|
std.log.err("{s}", .{b.fmt("Zig v{} does not meet the minimum version requirement. (Zig v{})", .{ builtin.zig_version, min_ver })});
|
||||||
|
std.os.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
// Standard target options allows the person running `zig build` to choose
|
// Standard target options allows the person running `zig build` to choose
|
||||||
// what target to build for. Here we do not override the defaults, which
|
// what target to build for. Here we do not override the defaults, which
|
||||||
// means any target is allowed, and the default is native. Other options
|
// means any target is allowed, and the default is native. Other options
|
||||||
@@ -13,6 +21,7 @@ pub fn build(b: *std.build.Builder) void {
|
|||||||
const mode = b.standardReleaseOptions();
|
const mode = b.standardReleaseOptions();
|
||||||
|
|
||||||
const exe = b.addExecutable("zba", "src/main.zig");
|
const exe = b.addExecutable("zba", "src/main.zig");
|
||||||
|
exe.setMainPkgPath("."); // Necessary so that src/main.zig can embed example.toml
|
||||||
exe.setTarget(target);
|
exe.setTarget(target);
|
||||||
|
|
||||||
// Known Folders (%APPDATA%, XDG, etc.)
|
// Known Folders (%APPDATA%, XDG, etc.)
|
||||||
@@ -31,6 +40,9 @@ pub fn build(b: *std.build.Builder) void {
|
|||||||
// TOML Library
|
// TOML Library
|
||||||
exe.addPackagePath("toml", "lib/zig-toml/src/toml.zig");
|
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
|
// Zig SDL Bindings: https://github.com/MasterQ32/SDL.zig
|
||||||
const sdk = Sdk.init(b);
|
const sdk = Sdk.init(b);
|
||||||
sdk.link(exe, .dynamic);
|
sdk.link(exe, .dynamic);
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
[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 = 4
|
win_scale = 3
|
||||||
# Enable VSYNC on the UI thread
|
# Enable VSYNC on the UI thread
|
||||||
vsync = true
|
vsync = true
|
||||||
# Mute ZBA
|
# Mute ZBA
|
||||||
@@ -9,9 +9,9 @@ mute = false
|
|||||||
|
|
||||||
[Guest]
|
[Guest]
|
||||||
# Sync Emulation to Audio
|
# Sync Emulation to Audio
|
||||||
audio_sync = false
|
audio_sync = true
|
||||||
# Sync Emulation to Video
|
# Sync Emulation to Video
|
||||||
video_sync = false
|
video_sync = true
|
||||||
# Force RTC support
|
# Force RTC support
|
||||||
force_rtc = false
|
force_rtc = false
|
||||||
# Skip BIOS
|
# Skip BIOS
|
||||||
|
Submodule lib/SDL.zig updated: 6a9e37687a...00b4356885
5028
lib/gl.zig
Normal file
5028
lib/gl.zig
Normal file
File diff suppressed because it is too large
Load Diff
Submodule lib/zig-clap updated: e5d09c4b2d...a1b01ffeab
Submodule lib/zig-datetime updated: 5ec1c36cf3...932d284521
Submodule lib/zig-toml updated: 5dfa919e03...016b8bcf98
@@ -49,16 +49,19 @@ pub fn config() *const Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Reads a config file and then loads it into the global state
|
/// Reads a config file and then loads it into the global state
|
||||||
pub fn load(allocator: Allocator, config_path: []const u8) !void {
|
pub fn load(allocator: Allocator, file_path: []const u8) !void {
|
||||||
var config_file = try std.fs.cwd().openFile(config_path, .{});
|
var config_file = try std.fs.cwd().openFile(file_path, .{});
|
||||||
defer config_file.close();
|
defer config_file.close();
|
||||||
|
|
||||||
log.info("loaded from {s}", .{config_path});
|
log.info("loaded from {s}", .{file_path});
|
||||||
|
|
||||||
const contents = try config_file.readToEndAlloc(allocator, try config_file.getEndPos());
|
const contents = try config_file.readToEndAlloc(allocator, try config_file.getEndPos());
|
||||||
defer allocator.free(contents);
|
defer allocator.free(contents);
|
||||||
|
|
||||||
const table = try toml.parseContents(allocator, contents, null);
|
var parser = try toml.parseFile(allocator, file_path);
|
||||||
|
defer parser.deinit();
|
||||||
|
|
||||||
|
const table = try parser.parse();
|
||||||
defer table.deinit();
|
defer table.deinit();
|
||||||
|
|
||||||
// TODO: Report unknown config options
|
// TODO: Report unknown config options
|
||||||
|
288
src/core/Bus.zig
288
src/core/Bus.zig
@@ -1,6 +1,5 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
const AudioDeviceId = @import("sdl2").SDL_AudioDeviceID;
|
|
||||||
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
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");
|
||||||
@@ -34,6 +33,11 @@ pub const fetch_timings: [2][0x10]u8 = [_][0x10]u8{
|
|||||||
[_]u8{ 1, 1, 6, 1, 1, 2, 2, 1, 4, 4, 4, 4, 4, 4, 8, 8 }, // 32-bit
|
[_]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,
|
||||||
@@ -49,7 +53,17 @@ 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: *[table_len]?*const anyopaque = tables[0..table_len];
|
||||||
|
const left_write: *[table_len]?*anyopaque = tables[table_len .. 2 * table_len];
|
||||||
|
const right_write: *[table_len]?*anyopaque = tables[2 * table_len .. 3 * table_len];
|
||||||
|
|
||||||
self.* = .{
|
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),
|
||||||
@@ -62,7 +76,20 @@ 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 = .{ left_write, right_write },
|
||||||
|
.allocator = allocator,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// read_table, write_tables, and *Self are not restricted to the lifetime
|
||||||
|
// of this init function so we can initialize our tables here
|
||||||
|
fillReadTable(self, read_table);
|
||||||
|
|
||||||
|
// Internal Display Memory behavious unusually on 8-bit reads
|
||||||
|
// so we have two different tables depending on whether there's an 8-bit read or not
|
||||||
|
fillWriteTable(u32, self, left_write);
|
||||||
|
fillWriteTable(u8, self, right_write);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
pub fn deinit(self: *Self) void {
|
||||||
@@ -71,34 +98,131 @@ pub fn deinit(self: *Self) void {
|
|||||||
self.pak.deinit();
|
self.pak.deinit();
|
||||||
self.bios.deinit();
|
self.bios.deinit();
|
||||||
self.ppu.deinit();
|
self.ppu.deinit();
|
||||||
|
|
||||||
|
// This is so I can deallocate the original `allocator.alloc`. I have to re-make the type
|
||||||
|
// since I'm not keeping it around, This is very jank and bad though
|
||||||
|
// FIXME: please figure out another way
|
||||||
|
self.allocator.free(@ptrCast([*]const ?*anyopaque, self.read_table[0..])[0 .. 3 * table_len]);
|
||||||
self.* = undefined;
|
self.* = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
|
fn fillReadTable(bus: *Self, table: *[table_len]?*const anyopaque) void {
|
||||||
const page = @truncate(u8, address >> 24);
|
const vramMirror = @import("ppu.zig").Vram.mirror;
|
||||||
const aligned_addr = forceAlign(T, address);
|
|
||||||
|
for (table) |*ptr, i| {
|
||||||
|
const addr = page_size * i;
|
||||||
|
|
||||||
|
ptr.* = switch (addr) {
|
||||||
|
// General Internal Memory
|
||||||
|
0x0000_0000...0x0000_3FFF => null, // BIOS has it's own checks
|
||||||
|
0x0200_0000...0x02FF_FFFF => &bus.ewram.buf[addr & 0x3FFFF],
|
||||||
|
0x0300_0000...0x03FF_FFFF => &bus.iwram.buf[addr & 0x7FFF],
|
||||||
|
0x0400_0000...0x0400_03FF => null, // I/O
|
||||||
|
|
||||||
|
// Internal Display Memory
|
||||||
|
0x0500_0000...0x05FF_FFFF => &bus.ppu.palette.buf[addr & 0x3FF],
|
||||||
|
0x0600_0000...0x06FF_FFFF => &bus.ppu.vram.buf[vramMirror(addr)],
|
||||||
|
0x0700_0000...0x07FF_FFFF => &bus.ppu.oam.buf[addr & 0x3FF],
|
||||||
|
|
||||||
|
// External Memory (Game Pak)
|
||||||
|
0x0800_0000...0x0DFF_FFFF => fillTableExternalMemory(bus, addr),
|
||||||
|
0x0E00_0000...0x0FFF_FFFF => null, // SRAM
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fillWriteTable(comptime T: type, bus: *Self, table: *[table_len]?*const anyopaque) void {
|
||||||
|
comptime std.debug.assert(T == u32 or T == u16 or T == u8);
|
||||||
|
const vramMirror = @import("ppu.zig").Vram.mirror;
|
||||||
|
|
||||||
|
for (table) |*ptr, i| {
|
||||||
|
const addr = page_size * i;
|
||||||
|
|
||||||
|
ptr.* = switch (addr) {
|
||||||
|
// General Internal Memory
|
||||||
|
0x0000_0000...0x0000_3FFF => null, // BIOS has it's own checks
|
||||||
|
0x0200_0000...0x02FF_FFFF => &bus.ewram.buf[addr & 0x3FFFF],
|
||||||
|
0x0300_0000...0x03FF_FFFF => &bus.iwram.buf[addr & 0x7FFF],
|
||||||
|
0x0400_0000...0x0400_03FF => null, // I/O
|
||||||
|
|
||||||
|
// Internal Display Memory
|
||||||
|
0x0500_0000...0x05FF_FFFF => if (T != u8) &bus.ppu.palette.buf[addr & 0x3FF] else null,
|
||||||
|
0x0600_0000...0x06FF_FFFF => if (T != u8) &bus.ppu.vram.buf[vramMirror(addr)] else null,
|
||||||
|
0x0700_0000...0x07FF_FFFF => if (T != u8) &bus.ppu.oam.buf[addr & 0x3FF] else null,
|
||||||
|
|
||||||
|
// External Memory (Game Pak)
|
||||||
|
0x0800_0000...0x0DFF_FFFF => null, // ROM
|
||||||
|
0x0E00_0000...0x0FFF_FFFF => null, // SRAM
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fillTableExternalMemory(bus: *Self, addr: usize) ?*anyopaque {
|
||||||
|
// see `GamePak.zig` for more information about what conditions need to be true
|
||||||
|
// so that a simple pointer dereference isn't possible
|
||||||
|
|
||||||
|
const start_addr = addr;
|
||||||
|
const end_addr = addr + page_size;
|
||||||
|
|
||||||
|
const gpio_data = start_addr <= 0x0800_00C4 and 0x0800_00C4 < end_addr;
|
||||||
|
const gpio_direction = start_addr <= 0x0800_00C6 and 0x0800_00C6 < end_addr;
|
||||||
|
const gpio_control = start_addr <= 0x0800_00C8 and 0x0800_00C8 < end_addr;
|
||||||
|
|
||||||
|
if (bus.pak.gpio.device.kind != .None and (gpio_data or gpio_direction or gpio_control)) {
|
||||||
|
// We found a GPIO device, and this page a GPIO register. We want to handle this in slowmem
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bus.pak.backup.kind == .Eeprom) {
|
||||||
|
if (bus.pak.buf.len > 0x100_000) {
|
||||||
|
// We are using a "large" EEPROM which means that if the below check is true
|
||||||
|
// this page has an address that's reserved for the EEPROM and therefore must
|
||||||
|
// be handled in slowmem
|
||||||
|
if (addr & 0x1FF_FFFF > 0x1FF_FEFF) return null;
|
||||||
|
} else {
|
||||||
|
// We are using a "small" EEPROM which means that if the below check is true
|
||||||
|
// (that is, we're in the 0xD address page) then we must handle at least one
|
||||||
|
// address in this page in slowmem
|
||||||
|
if (@truncate(u4, addr >> 24) == 0xD) return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, the GamePak has some unique behaviour for reads past the end of the ROM,
|
||||||
|
// so those will be handled by slowmem as well
|
||||||
|
const masked_addr = addr & 0x1FF_FFFF;
|
||||||
|
if (masked_addr >= bus.pak.buf.len) return null;
|
||||||
|
|
||||||
|
return &bus.pak.buf[masked_addr];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Take advantage of fastmem here too?
|
||||||
|
pub fn dbgRead(self: *const Self, comptime T: type, unaligned_address: u32) T {
|
||||||
|
const page = @truncate(u8, unaligned_address >> 24);
|
||||||
|
const address = forceAlign(T, unaligned_address);
|
||||||
|
|
||||||
return switch (page) {
|
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.dbgRead(T, self.cpu.r[15], aligned_addr);
|
break :blk self.bios.dbgRead(T, self.cpu.r[15], address);
|
||||||
|
|
||||||
break :blk self.openBus(T, address);
|
break :blk self.openBus(T, address);
|
||||||
},
|
},
|
||||||
0x02 => self.ewram.read(T, aligned_addr),
|
0x02 => self.ewram.read(T, address),
|
||||||
0x03 => self.iwram.read(T, aligned_addr),
|
0x03 => self.iwram.read(T, address),
|
||||||
0x04 => self.readIo(T, address),
|
0x04 => self.readIo(T, address),
|
||||||
|
|
||||||
// Internal Display Memory
|
// Internal Display Memory
|
||||||
0x05 => self.ppu.palette.read(T, aligned_addr),
|
0x05 => self.ppu.palette.read(T, address),
|
||||||
0x06 => self.ppu.vram.read(T, aligned_addr),
|
0x06 => self.ppu.vram.read(T, address),
|
||||||
0x07 => self.ppu.oam.read(T, aligned_addr),
|
0x07 => self.ppu.oam.read(T, address),
|
||||||
|
|
||||||
// External Memory (Game Pak)
|
// External Memory (Game Pak)
|
||||||
0x08...0x0D => self.pak.dbgRead(T, aligned_addr),
|
0x08...0x0D => self.pak.dbgRead(T, address),
|
||||||
0x0E...0x0F => blk: {
|
0x0E...0x0F => blk: {
|
||||||
const value = self.pak.backup.read(address);
|
const value = self.pak.backup.read(unaligned_address);
|
||||||
|
|
||||||
const multiplier = switch (T) {
|
const multiplier = switch (T) {
|
||||||
u32 => 0x01010101,
|
u32 => 0x01010101,
|
||||||
@@ -113,16 +237,22 @@ pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn readIo(self: *const Self, comptime T: type, unaligned_address: u32) T {
|
fn readIo(self: *const Self, comptime T: type, address: u32) T {
|
||||||
const maybe_value = io.read(self, T, forceAlign(T, unaligned_address));
|
return io.read(self, T, address) orelse self.openBus(T, address);
|
||||||
return if (maybe_value) |value| value else self.openBus(T, unaligned_address);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn openBus(self: *const Self, comptime T: type, address: u32) T {
|
fn openBus(self: *const Self, comptime T: type, address: u32) T {
|
||||||
|
@setCold(true);
|
||||||
const r15 = self.cpu.r[15];
|
const r15 = self.cpu.r[15];
|
||||||
|
|
||||||
const word = blk: {
|
const word = blk: {
|
||||||
// If Arm, get the most recently fetched instruction (PC + 8)
|
// If Arm, get the most recently fetched instruction (PC + 8)
|
||||||
|
//
|
||||||
|
// FIXME: This is most likely a faulty assumption.
|
||||||
|
// I think what *actually* happens is that the Bus has a latch for the most
|
||||||
|
// recently fetched piece of data, which is then returned during Open Bus (also DMA open bus?)
|
||||||
|
// I can "get away" with this because it's very statistically likely that the most recently latched value is
|
||||||
|
// the most recently fetched instruction by the pipeline
|
||||||
if (!self.cpu.cpsr.t.read()) break :blk self.cpu.pipe.stage[1].?;
|
if (!self.cpu.cpsr.t.read()) break :blk self.cpu.pipe.stage[1].?;
|
||||||
|
|
||||||
const page = @truncate(u8, r15 >> 24);
|
const page = @truncate(u8, r15 >> 24);
|
||||||
@@ -169,36 +299,61 @@ fn openBus(self: *const Self, comptime T: type, address: u32) T {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return @truncate(T, rotr(u32, word, 8 * (address & 3)));
|
return @truncate(T, word);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read(self: *Self, comptime T: type, address: u32) T {
|
pub fn read(self: *Self, comptime T: type, unaligned_address: u32) T {
|
||||||
const page = @truncate(u8, address >> 24);
|
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits;
|
||||||
const aligned_addr = forceAlign(T, address);
|
const page = unaligned_address >> bits;
|
||||||
|
const offset = unaligned_address & (page_size - 1);
|
||||||
|
|
||||||
self.sched.tick += timings[@boolToInt(T == u32)][@truncate(u4, page)];
|
// whether or not we do this in slowmem or fastmem, we should advance the scheduler
|
||||||
|
self.sched.tick += timings[@boolToInt(T == u32)][@truncate(u4, unaligned_address >> 24)];
|
||||||
|
|
||||||
|
// We're doing some serious out-of-bounds open-bus reads
|
||||||
|
if (page >= table_len) return self.openBus(T, unaligned_address);
|
||||||
|
|
||||||
|
if (self.read_table[page]) |some_ptr| {
|
||||||
|
// We have a pointer to a page, cast the pointer to it's underlying type
|
||||||
|
const Ptr = [*]const T;
|
||||||
|
const alignment = @alignOf(std.meta.Child(Ptr));
|
||||||
|
const ptr = @ptrCast(Ptr, @alignCast(alignment, some_ptr));
|
||||||
|
|
||||||
|
// Note: We don't check array length, since we force align the
|
||||||
|
// lower bits of the address as the GBA would
|
||||||
|
return ptr[forceAlign(T, offset) / @sizeOf(T)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.slowRead(T, unaligned_address);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn slowRead(self: *Self, comptime T: type, unaligned_address: u32) T {
|
||||||
|
@setCold(true);
|
||||||
|
|
||||||
|
const page = @truncate(u8, unaligned_address >> 24);
|
||||||
|
const address = forceAlign(T, unaligned_address);
|
||||||
|
|
||||||
return switch (page) {
|
return switch (page) {
|
||||||
// General Internal Memory
|
// General Internal Memory
|
||||||
0x00 => blk: {
|
0x00 => blk: {
|
||||||
if (address < Bios.size)
|
if (address < Bios.size)
|
||||||
break :blk self.bios.read(T, self.cpu.r[15], aligned_addr);
|
break :blk self.bios.read(T, self.cpu.r[15], address);
|
||||||
|
|
||||||
break :blk self.openBus(T, address);
|
break :blk self.openBus(T, address);
|
||||||
},
|
},
|
||||||
0x02 => self.ewram.read(T, aligned_addr),
|
0x02 => unreachable, // completely handled by fastmeme
|
||||||
0x03 => self.iwram.read(T, aligned_addr),
|
0x03 => unreachable, // completely handled by fastmeme
|
||||||
0x04 => self.readIo(T, address),
|
0x04 => self.readIo(T, address),
|
||||||
|
|
||||||
// Internal Display Memory
|
// Internal Display Memory
|
||||||
0x05 => self.ppu.palette.read(T, aligned_addr),
|
0x05 => unreachable, // completely handled by fastmeme
|
||||||
0x06 => self.ppu.vram.read(T, aligned_addr),
|
0x06 => unreachable, // completely handled by fastmeme
|
||||||
0x07 => self.ppu.oam.read(T, aligned_addr),
|
0x07 => unreachable, // completely handled by fastmeme
|
||||||
|
|
||||||
// External Memory (Game Pak)
|
// External Memory (Game Pak)
|
||||||
0x08...0x0D => self.pak.read(T, aligned_addr),
|
0x08...0x0D => self.pak.read(T, address),
|
||||||
0x0E...0x0F => blk: {
|
0x0E...0x0F => blk: {
|
||||||
const value = self.pak.backup.read(address);
|
const value = self.pak.backup.read(unaligned_address);
|
||||||
|
|
||||||
const multiplier = switch (T) {
|
const multiplier = switch (T) {
|
||||||
u32 => 0x01010101,
|
u32 => 0x01010101,
|
||||||
@@ -213,44 +368,71 @@ pub fn read(self: *Self, comptime T: type, address: u32) T {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write(self: *Self, comptime T: type, address: u32, value: T) void {
|
pub fn write(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
|
||||||
const page = @truncate(u8, address >> 24);
|
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits;
|
||||||
const aligned_addr = forceAlign(T, address);
|
const page = unaligned_address >> bits;
|
||||||
|
const offset = unaligned_address & (page_size - 1);
|
||||||
|
|
||||||
self.sched.tick += timings[@boolToInt(T == u32)][@truncate(u4, page)];
|
// whether or not we do this in slowmem or fastmem, we should advance the scheduler
|
||||||
|
self.sched.tick += timings[@boolToInt(T == u32)][@truncate(u4, unaligned_address >> 24)];
|
||||||
|
|
||||||
|
// We're doing some serious out-of-bounds open-bus writes, they do nothing though
|
||||||
|
if (page >= table_len) return;
|
||||||
|
|
||||||
|
if (self.write_tables[@boolToInt(T == u8)][page]) |some_ptr| {
|
||||||
|
// We have a pointer to a page, cast the pointer to it's underlying type
|
||||||
|
const Ptr = [*]T;
|
||||||
|
const alignment = @alignOf(std.meta.Child(Ptr));
|
||||||
|
const ptr = @ptrCast(Ptr, @alignCast(alignment, some_ptr));
|
||||||
|
|
||||||
|
// Note: We don't check array length, since we force align the
|
||||||
|
// lower bits of the address as the GBA would
|
||||||
|
ptr[forceAlign(T, offset) / @sizeOf(T)] = value;
|
||||||
|
} else {
|
||||||
|
// we can return early if this is an 8-bit OAM write
|
||||||
|
if (T == u8 and @truncate(u8, unaligned_address >> 24) == 0x07) return;
|
||||||
|
|
||||||
|
self.slowWrite(T, unaligned_address, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn slowWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
|
||||||
|
// @setCold(true);
|
||||||
|
const page = @truncate(u8, unaligned_address >> 24);
|
||||||
|
const address = forceAlign(T, unaligned_address);
|
||||||
|
|
||||||
switch (page) {
|
switch (page) {
|
||||||
// General Internal Memory
|
// General Internal Memory
|
||||||
0x00 => self.bios.write(T, aligned_addr, value),
|
0x00 => self.bios.write(T, address, value),
|
||||||
0x02 => self.ewram.write(T, aligned_addr, value),
|
0x02 => unreachable, // completely handled by fastmem
|
||||||
0x03 => self.iwram.write(T, aligned_addr, value),
|
0x03 => unreachable, // completely handled by fastmem
|
||||||
0x04 => io.write(self, T, aligned_addr, value),
|
0x04 => io.write(self, T, address, value),
|
||||||
|
|
||||||
// Internal Display Memory
|
// Internal Display Memory
|
||||||
0x05 => self.ppu.palette.write(T, aligned_addr, value),
|
0x05 => self.ppu.palette.write(T, address, value),
|
||||||
0x06 => self.ppu.vram.write(T, self.ppu.dispcnt, aligned_addr, value),
|
0x06 => self.ppu.vram.write(T, self.ppu.dispcnt, address, value),
|
||||||
0x07 => self.ppu.oam.write(T, aligned_addr, value),
|
0x07 => unreachable, // completely handled by fastmem
|
||||||
|
|
||||||
// External Memory (Game Pak)
|
// External Memory (Game Pak)
|
||||||
0x08...0x0D => self.pak.write(T, self.dma[3].word_count, aligned_addr, value),
|
0x08...0x0D => self.pak.write(T, self.dma[3].word_count, address, value),
|
||||||
0x0E...0x0F => {
|
0x0E...0x0F => self.pak.backup.write(unaligned_address, @truncate(u8, rotr(T, value, 8 * rotateBy(T, unaligned_address)))),
|
||||||
const rotate_by = switch (T) {
|
|
||||||
u32 => address & 3,
|
|
||||||
u16 => address & 1,
|
|
||||||
u8 => 0,
|
|
||||||
else => @compileError("Backup: Unsupported write width"),
|
|
||||||
};
|
|
||||||
|
|
||||||
self.pak.backup.write(address, @truncate(u8, rotr(T, value, 8 * rotate_by)));
|
|
||||||
},
|
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn forceAlign(comptime T: type, address: u32) u32 {
|
inline fn rotateBy(comptime T: type, address: u32) u32 {
|
||||||
return switch (T) {
|
return switch (T) {
|
||||||
u32 => address & 0xFFFF_FFFC,
|
u32 => address & 3,
|
||||||
u16 => address & 0xFFFF_FFFE,
|
u16 => address & 1,
|
||||||
|
u8 => 0,
|
||||||
|
else => @compileError("Backup: Unsupported write width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn forceAlign(comptime T: type, address: u32) u32 {
|
||||||
|
return switch (T) {
|
||||||
|
u32 => address & ~@as(u32, 3),
|
||||||
|
u16 => address & ~@as(u32, 1),
|
||||||
u8 => address,
|
u8 => address,
|
||||||
else => @compileError("Bus: Invalid read/write type"),
|
else => @compileError("Bus: Invalid read/write type"),
|
||||||
};
|
};
|
||||||
|
374
src/core/apu.zig
374
src/core/apu.zig
@@ -3,8 +3,6 @@ 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 AudioDeviceId = SDL.SDL_AudioDeviceID;
|
|
||||||
|
|
||||||
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
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");
|
||||||
@@ -14,133 +12,215 @@ const Noise = @import("apu/Noise.zig");
|
|||||||
|
|
||||||
const SoundFifo = std.fifo.LinearFifo(u8, .{ .Static = 0x20 });
|
const SoundFifo = std.fifo.LinearFifo(u8, .{ .Static = 0x20 });
|
||||||
|
|
||||||
const intToBytes = @import("../util.zig").intToBytes;
|
const getHalf = util.getHalf;
|
||||||
const setHi = @import("../util.zig").setHi;
|
const setHalf = util.setHalf;
|
||||||
const setLo = @import("../util.zig").setLo;
|
const intToBytes = util.intToBytes;
|
||||||
|
const RingBuffer = util.RingBuffer;
|
||||||
|
|
||||||
const log = std.log.scoped(.APU);
|
const log = std.log.scoped(.APU);
|
||||||
|
|
||||||
pub const host_sample_rate = 1 << 15;
|
|
||||||
|
|
||||||
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 = @truncate(u8, addr);
|
const byte_addr = @truncate(u8, addr);
|
||||||
|
|
||||||
return switch (T) {
|
return switch (T) {
|
||||||
u16 => switch (byte) {
|
u32 => switch (byte_addr) {
|
||||||
0x60 => apu.ch1.sound1CntL(),
|
0x60 => @as(T, apu.ch1.sound1CntH()) << 16 | apu.ch1.sound1CntL(),
|
||||||
0x62 => apu.ch1.sound1CntH(),
|
|
||||||
0x64 => apu.ch1.sound1CntX(),
|
0x64 => apu.ch1.sound1CntX(),
|
||||||
0x68 => apu.ch2.sound2CntL(),
|
0x68 => apu.ch2.sound2CntL(),
|
||||||
0x6C => apu.ch2.sound2CntH(),
|
0x6C => apu.ch2.sound2CntH(),
|
||||||
|
0x70 => @as(T, apu.ch3.sound3CntH()) << 16 | apu.ch3.sound3CntL(),
|
||||||
0x70 => apu.ch3.select.raw & 0xE0, // SOUND3CNT_L
|
0x74 => apu.ch3.sound3CntX(),
|
||||||
0x72 => apu.ch3.sound3CntH(),
|
|
||||||
0x74 => apu.ch3.freq.raw & 0x4000, // SOUND3CNT_X
|
|
||||||
|
|
||||||
0x78 => apu.ch4.sound4CntL(),
|
0x78 => apu.ch4.sound4CntL(),
|
||||||
0x7C => apu.ch4.sound4CntH(),
|
0x7C => apu.ch4.sound4CntH(),
|
||||||
|
0x80 => @as(T, apu.dma_cnt.raw) << 16 | apu.psg_cnt.raw, // SOUNDCNT_H, SOUNDCNT_L
|
||||||
0x80 => apu.psg_cnt.raw & 0xFF77, // SOUNDCNT_L
|
|
||||||
0x82 => apu.dma_cnt.raw & 0x770F, // SOUNDCNT_H
|
|
||||||
0x84 => apu.soundCntX(),
|
0x84 => apu.soundCntX(),
|
||||||
|
0x88 => apu.bias.raw, // SOUNDBIAS, high is unused
|
||||||
|
0x8C => null,
|
||||||
|
0x90, 0x94, 0x98, 0x9C => apu.ch3.wave_dev.read(T, apu.ch3.select, addr),
|
||||||
|
0xA0 => null, // FIFO_A
|
||||||
|
0xA4 => null, // FIFO_B
|
||||||
|
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
||||||
|
},
|
||||||
|
u16 => switch (byte_addr) {
|
||||||
|
0x60 => apu.ch1.sound1CntL(),
|
||||||
|
0x62 => apu.ch1.sound1CntH(),
|
||||||
|
0x64 => apu.ch1.sound1CntX(),
|
||||||
|
0x66 => 0x0000, // suite.gba expects 0x0000, not 0xDEAD
|
||||||
|
0x68 => apu.ch2.sound2CntL(),
|
||||||
|
0x6A => 0x0000,
|
||||||
|
0x6C => apu.ch2.sound2CntH(),
|
||||||
|
0x6E => 0x0000,
|
||||||
|
0x70 => apu.ch3.sound3CntL(),
|
||||||
|
0x72 => apu.ch3.sound3CntH(),
|
||||||
|
0x74 => apu.ch3.sound3CntX(),
|
||||||
|
0x76 => 0x0000,
|
||||||
|
0x78 => apu.ch4.sound4CntL(),
|
||||||
|
0x7A => 0x0000,
|
||||||
|
0x7C => apu.ch4.sound4CntH(),
|
||||||
|
0x7E => 0x0000,
|
||||||
|
0x80 => apu.soundCntL(),
|
||||||
|
0x82 => apu.soundCntH(),
|
||||||
|
0x84 => apu.soundCntX(),
|
||||||
|
0x86 => 0x0000,
|
||||||
0x88 => apu.bias.raw, // SOUNDBIAS
|
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(T, @as(u16, apu.ch1.sound1CntL()) >> getHalf(byte_addr)),
|
||||||
|
0x62, 0x63 => @truncate(T, apu.ch1.sound1CntH() >> getHalf(byte_addr)),
|
||||||
|
0x64, 0x65 => @truncate(T, apu.ch1.sound1CntX() >> getHalf(byte_addr)),
|
||||||
|
0x66, 0x67 => 0x00, // assuming behaviour is identical to that of 16-bit reads
|
||||||
|
0x68, 0x69 => @truncate(T, apu.ch2.sound2CntL() >> getHalf(byte_addr)),
|
||||||
|
0x6A, 0x6B => 0x00,
|
||||||
|
0x6C, 0x6D => @truncate(T, apu.ch2.sound2CntH() >> getHalf(byte_addr)),
|
||||||
|
0x6E, 0x6F => 0x00,
|
||||||
|
0x70, 0x71 => @truncate(T, @as(u16, apu.ch3.sound3CntL()) >> getHalf(byte_addr)), // SOUND3CNT_L
|
||||||
|
0x72, 0x73 => @truncate(T, apu.ch3.sound3CntH() >> getHalf(byte_addr)),
|
||||||
|
0x74, 0x75 => @truncate(T, apu.ch3.sound3CntX() >> getHalf(byte_addr)), // SOUND3CNT_L
|
||||||
|
0x76, 0x77 => 0x00,
|
||||||
|
0x78, 0x79 => @truncate(T, apu.ch4.sound4CntL() >> getHalf(byte_addr)),
|
||||||
|
0x7A, 0x7B => 0x00,
|
||||||
|
0x7C, 0x7D => @truncate(T, apu.ch4.sound4CntH() >> getHalf(byte_addr)),
|
||||||
|
0x7E, 0x7F => 0x00,
|
||||||
|
0x80, 0x81 => @truncate(T, apu.soundCntL() >> getHalf(byte_addr)), // SOUNDCNT_L
|
||||||
|
0x82, 0x83 => @truncate(T, apu.soundCntH() >> getHalf(byte_addr)), // SOUNDCNT_H
|
||||||
|
0x84, 0x85 => @truncate(T, @as(u16, apu.soundCntX()) >> getHalf(byte_addr)),
|
||||||
|
0x86, 0x87 => 0x00,
|
||||||
|
0x88, 0x89 => @truncate(T, 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),
|
||||||
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
0xA0, 0xA1, 0xA2, 0xA3 => null, // FIFO_A
|
||||||
|
0xA4, 0xA5, 0xA6, 0xA7 => null, // FIFO_B
|
||||||
|
else => util.io.read.err(T, log, "unexpected {} read from 0x{X:0>8}", .{ T, addr }),
|
||||||
},
|
},
|
||||||
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 = @truncate(u8, addr);
|
const byte_addr = @truncate(u8, addr);
|
||||||
|
|
||||||
|
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return;
|
||||||
|
|
||||||
switch (T) {
|
switch (T) {
|
||||||
u32 => switch (byte) {
|
u32 => {
|
||||||
0x60 => apu.ch1.setSound1Cnt(value),
|
// 0x80 and 0x81 handled in setSoundCnt
|
||||||
0x64 => apu.ch1.setSound1CntX(&apu.fs, @truncate(u16, value)),
|
if (byte_addr < 0x80 and !apu.cnt.apu_enable.read()) return;
|
||||||
0x68 => apu.ch2.setSound2CntL(@truncate(u16, value)),
|
|
||||||
0x6C => apu.ch2.setSound2CntH(&apu.fs, @truncate(u16, value)),
|
|
||||||
0x70 => apu.ch3.setSound3Cnt(value),
|
|
||||||
0x74 => apu.ch3.setSound3CntX(&apu.fs, @truncate(u16, value)),
|
|
||||||
0x78 => apu.ch4.setSound4CntL(@truncate(u16, value)),
|
|
||||||
0x7C => apu.ch4.setSound4CntH(&apu.fs, @truncate(u16, value)),
|
|
||||||
|
|
||||||
0x80 => apu.setSoundCnt(value),
|
switch (byte_addr) {
|
||||||
// WAVE_RAM
|
0x60 => apu.ch1.setSound1Cnt(value),
|
||||||
0x90...0x9F => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
|
0x64 => apu.ch1.setSound1CntX(&apu.fs, @truncate(u16, value)),
|
||||||
0xA0 => apu.chA.push(value), // FIFO_A
|
|
||||||
0xA4 => apu.chB.push(value), // FIFO_B
|
0x68 => apu.ch2.setSound2CntL(@truncate(u16, value)),
|
||||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
|
0x6C => apu.ch2.setSound2CntH(&apu.fs, @truncate(u16, value)),
|
||||||
|
|
||||||
|
0x70 => apu.ch3.setSound3Cnt(value),
|
||||||
|
0x74 => apu.ch3.setSound3CntX(&apu.fs, @truncate(u16, value)),
|
||||||
|
|
||||||
|
0x78 => apu.ch4.setSound4CntL(@truncate(u16, value)),
|
||||||
|
0x7C => apu.ch4.setSound4CntH(&apu.fs, @truncate(u16, value)),
|
||||||
|
|
||||||
|
0x80 => apu.setSoundCnt(value),
|
||||||
|
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1),
|
||||||
|
0x88 => apu.bias.raw = @truncate(u16, value),
|
||||||
|
0x8C => {},
|
||||||
|
|
||||||
|
0x90, 0x94, 0x98, 0x9C => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
|
||||||
|
0xA0 => apu.chA.push(value), // FIFO_A
|
||||||
|
0xA4 => apu.chB.push(value), // FIFO_B
|
||||||
|
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||||
|
}
|
||||||
},
|
},
|
||||||
u16 => switch (byte) {
|
u16 => {
|
||||||
0x60 => apu.ch1.setSound1CntL(@truncate(u8, value)), // SOUND1CNT_L
|
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return;
|
||||||
0x62 => apu.ch1.setSound1CntH(value),
|
|
||||||
0x64 => apu.ch1.setSound1CntX(&apu.fs, value),
|
|
||||||
|
|
||||||
0x68 => apu.ch2.setSound2CntL(value),
|
switch (byte_addr) {
|
||||||
0x6C => apu.ch2.setSound2CntH(&apu.fs, value),
|
0x60 => apu.ch1.setSound1CntL(@truncate(u8, value)), // SOUND1CNT_L
|
||||||
|
0x62 => apu.ch1.setSound1CntH(value),
|
||||||
|
0x64 => apu.ch1.setSound1CntX(&apu.fs, value),
|
||||||
|
0x66 => {},
|
||||||
|
|
||||||
0x70 => apu.ch3.setSound3CntL(@truncate(u8, value)),
|
0x68 => apu.ch2.setSound2CntL(value),
|
||||||
0x72 => apu.ch3.setSound3CntH(value),
|
0x6A => {},
|
||||||
0x74 => apu.ch3.setSound3CntX(&apu.fs, value),
|
0x6C => apu.ch2.setSound2CntH(&apu.fs, value),
|
||||||
|
0x6E => {},
|
||||||
|
|
||||||
0x78 => apu.ch4.setSound4CntL(value),
|
0x70 => apu.ch3.setSound3CntL(@truncate(u8, value)),
|
||||||
0x7C => apu.ch4.setSound4CntH(&apu.fs, value),
|
0x72 => apu.ch3.setSound3CntH(value),
|
||||||
|
0x74 => apu.ch3.setSound3CntX(&apu.fs, value),
|
||||||
|
0x76 => {},
|
||||||
|
|
||||||
0x80 => apu.psg_cnt.raw = value, // SOUNDCNT_L
|
0x78 => apu.ch4.setSound4CntL(value),
|
||||||
0x82 => apu.setSoundCntH(value),
|
0x7A => {},
|
||||||
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1),
|
0x7C => apu.ch4.setSound4CntH(&apu.fs, value),
|
||||||
0x88 => apu.bias.raw = value, // SOUNDBIAS
|
0x7E => {},
|
||||||
// WAVE_RAM
|
|
||||||
0x90...0x9F => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
|
0x80 => apu.setSoundCntL(value),
|
||||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
|
0x82 => apu.setSoundCntH(value),
|
||||||
|
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1),
|
||||||
|
0x86 => {},
|
||||||
|
0x88 => apu.bias.raw = value, // SOUNDBIAS
|
||||||
|
0x8A, 0x8C, 0x8E => {},
|
||||||
|
|
||||||
|
0x90, 0x92, 0x94, 0x96, 0x98, 0x9A, 0x9C, 0x9E => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
|
||||||
|
0xA0, 0xA2 => log.err("Tried to write 0x{X:0>4}{} to FIFO_A", .{ value, T }),
|
||||||
|
0xA4, 0xA6 => log.err("Tried to write 0x{X:0>4}{} to FIFO_B", .{ value, T }),
|
||||||
|
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||||
|
}
|
||||||
},
|
},
|
||||||
u8 => switch (byte) {
|
u8 => {
|
||||||
0x60 => apu.ch1.setSound1CntL(value),
|
if (byte_addr <= 0x81 and !apu.cnt.apu_enable.read()) return;
|
||||||
0x62 => apu.ch1.setNr11(value),
|
|
||||||
0x63 => apu.ch1.setNr12(value),
|
|
||||||
0x64 => apu.ch1.setNr13(value),
|
|
||||||
0x65 => apu.ch1.setNr14(&apu.fs, value),
|
|
||||||
|
|
||||||
0x68 => apu.ch2.setNr21(value),
|
switch (byte_addr) {
|
||||||
0x69 => apu.ch2.setNr22(value),
|
0x60 => apu.ch1.setSound1CntL(value),
|
||||||
0x6C => apu.ch2.setNr23(value),
|
0x61 => {},
|
||||||
0x6D => apu.ch2.setNr24(&apu.fs, value),
|
0x62 => apu.ch1.setNr11(value),
|
||||||
|
0x63 => apu.ch1.setNr12(value),
|
||||||
|
0x64 => apu.ch1.setNr13(value),
|
||||||
|
0x65 => apu.ch1.setNr14(&apu.fs, value),
|
||||||
|
0x66, 0x67 => {},
|
||||||
|
|
||||||
0x70 => apu.ch3.setSound3CntL(value), // NR30
|
0x68 => apu.ch2.setNr21(value),
|
||||||
0x72 => apu.ch3.setNr31(value),
|
0x69 => apu.ch2.setNr22(value),
|
||||||
0x73 => apu.ch3.vol.raw = value, // NR32
|
0x6A, 0x6B => {},
|
||||||
0x74 => apu.ch3.setNr33(value),
|
0x6C => apu.ch2.setNr23(value),
|
||||||
0x75 => apu.ch3.setNr34(&apu.fs, value),
|
0x6D => apu.ch2.setNr24(&apu.fs, value),
|
||||||
|
0x6E, 0x6F => {},
|
||||||
|
|
||||||
0x78 => apu.ch4.setNr41(value),
|
0x70 => apu.ch3.setSound3CntL(value), // NR30
|
||||||
0x79 => apu.ch4.setNr42(value),
|
0x71 => {},
|
||||||
0x7C => apu.ch4.poly.raw = value, // NR 43
|
0x72 => apu.ch3.setNr31(value),
|
||||||
0x7D => apu.ch4.setNr44(&apu.fs, value),
|
0x73 => apu.ch3.vol.raw = value, // NR32
|
||||||
|
0x74 => apu.ch3.setNr33(value),
|
||||||
|
0x75 => apu.ch3.setNr34(&apu.fs, value),
|
||||||
|
0x76, 0x77 => {},
|
||||||
|
|
||||||
0x80 => apu.setNr50(value),
|
0x78 => apu.ch4.setNr41(value),
|
||||||
0x81 => apu.setNr51(value),
|
0x79 => apu.ch4.setNr42(value),
|
||||||
0x82 => apu.setSoundCntH(setLo(u16, apu.dma_cnt.raw, value)),
|
0x7A, 0x7B => {},
|
||||||
0x83 => apu.setSoundCntH(setHi(u16, apu.dma_cnt.raw, value)),
|
0x7C => apu.ch4.poly.raw = value, // NR 43
|
||||||
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1), // NR52
|
0x7D => apu.ch4.setNr44(&apu.fs, value),
|
||||||
0x89 => apu.setSoundBiasH(value),
|
0x7E, 0x7F => {},
|
||||||
0x90...0x9F => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
|
|
||||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
|
0x80, 0x81 => apu.setSoundCntL(setHalf(u16, apu.psg_cnt.raw, byte_addr, value)),
|
||||||
|
0x82, 0x83 => apu.setSoundCntH(setHalf(u16, apu.dma_cnt.raw, byte_addr, value)),
|
||||||
|
0x84 => apu.setSoundCntX(value >> 7 & 1 == 1),
|
||||||
|
0x85 => {},
|
||||||
|
0x86, 0x87 => {},
|
||||||
|
0x88, 0x89 => apu.bias.raw = setHalf(u16, apu.bias.raw, byte_addr, value), // SOUNDBIAS
|
||||||
|
0x8A...0x8F => {},
|
||||||
|
|
||||||
|
0x90...0x9F => apu.ch3.wave_dev.write(T, apu.ch3.select, addr, value),
|
||||||
|
0xA0...0xA3 => log.err("Tried to write 0x{X:0>2}{} to FIFO_A", .{ value, T }),
|
||||||
|
0xA4...0xA7 => log.err("Tried to write 0x{X:0>2}{} to FIFO_B", .{ value, T }),
|
||||||
|
else => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||||
|
}
|
||||||
},
|
},
|
||||||
else => @compileError("APU: Unsupported write width"),
|
else => @compileError("APU: Unsupported write width"),
|
||||||
}
|
}
|
||||||
@@ -164,17 +244,20 @@ pub const Apu = struct {
|
|||||||
|
|
||||||
sampling_cycle: u2,
|
sampling_cycle: u2,
|
||||||
|
|
||||||
stream: *SDL.SDL_AudioStream,
|
sample_queue: RingBuffer(u16),
|
||||||
sched: *Scheduler,
|
sched: *Scheduler,
|
||||||
|
|
||||||
fs: FrameSequencer,
|
fs: FrameSequencer,
|
||||||
capacitor: f32,
|
capacitor: f32,
|
||||||
|
|
||||||
is_buffer_full: bool,
|
|
||||||
|
|
||||||
pub const Tick = enum { Length, Envelope, Sweep };
|
pub const Tick = enum { Length, Envelope, Sweep };
|
||||||
|
|
||||||
pub fn init(sched: *Scheduler) Self {
|
pub fn init(sched: *Scheduler) Self {
|
||||||
|
const NUM_CHANNELS: usize = 2;
|
||||||
|
|
||||||
|
const allocator = std.heap.c_allocator;
|
||||||
|
const sample_buf = allocator.alloc(u16, 0x800 * NUM_CHANNELS) catch @panic("failed to allocate sample buffer");
|
||||||
|
|
||||||
const apu: Self = .{
|
const apu: Self = .{
|
||||||
.ch1 = ToneSweep.init(sched),
|
.ch1 = ToneSweep.init(sched),
|
||||||
.ch2 = Tone.init(sched),
|
.ch2 = Tone.init(sched),
|
||||||
@@ -189,12 +272,11 @@ 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, SDL.AUDIO_U16, 2, host_sample_rate).?,
|
.sample_queue = RingBuffer(u16).init(sample_buf),
|
||||||
.sched = sched,
|
.sched = sched,
|
||||||
|
|
||||||
.capacitor = 0,
|
.capacitor = 0,
|
||||||
.fs = FrameSequencer.init(),
|
.fs = FrameSequencer.init(),
|
||||||
.is_buffer_full = false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sched.push(.SampleAudio, apu.interval());
|
sched.push(.SampleAudio, apu.interval());
|
||||||
@@ -208,18 +290,33 @@ pub const Apu = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn reset(self: *Self) void {
|
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.ch1.reset();
|
||||||
self.ch2.reset();
|
self.ch2.reset();
|
||||||
self.ch3.reset();
|
self.ch3.reset();
|
||||||
self.ch4.reset();
|
self.ch4.reset();
|
||||||
|
|
||||||
|
// GBATEK says 4000060h..4000081h I take this to mean inclusive
|
||||||
|
self.psg_cnt.raw = 0x0000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// SOUNDCNT
|
/// SOUNDCNT
|
||||||
fn setSoundCnt(self: *Self, value: u32) void {
|
fn setSoundCnt(self: *Self, value: u32) void {
|
||||||
self.psg_cnt.raw = @truncate(u16, value);
|
if (self.cnt.apu_enable.read()) self.setSoundCntL(@truncate(u16, value));
|
||||||
self.setSoundCntH(@truncate(u16, value >> 16));
|
self.setSoundCntH(@truncate(u16, value >> 16));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
pub fn setSoundCntH(self: *Self, value: u16) void {
|
pub fn setSoundCntH(self: *Self, value: u16) void {
|
||||||
const new: io.DmaSoundControl = .{ .raw = value };
|
const new: io.DmaSoundControl = .{ .raw = value };
|
||||||
@@ -232,6 +329,11 @@ 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);
|
||||||
@@ -240,11 +342,14 @@ pub const Apu = struct {
|
|||||||
self.fs.step = 0; // Reset Frame Sequencer
|
self.fs.step = 0; // Reset Frame Sequencer
|
||||||
|
|
||||||
// Reset Square Wave Offsets
|
// Reset Square Wave Offsets
|
||||||
self.ch1.square.pos = 0;
|
self.ch1.square.reset();
|
||||||
self.ch2.square.pos = 0;
|
self.ch2.square.reset();
|
||||||
|
|
||||||
// Reset Wave Device Offsets
|
// Reset Wave
|
||||||
self.ch3.wave_dev.offset = 0;
|
self.ch3.wave_dev.reset();
|
||||||
|
|
||||||
|
// Rest Noise
|
||||||
|
self.ch4.lfsr.reset();
|
||||||
} else {
|
} else {
|
||||||
self.reset();
|
self.reset();
|
||||||
}
|
}
|
||||||
@@ -262,28 +367,9 @@ pub const Apu = struct {
|
|||||||
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);
|
||||||
|
|
||||||
// Whether the APU is busy or not is determined by the main loop in emu.zig
|
|
||||||
// This should only ever be true (because this side of the emu is single threaded)
|
|
||||||
// When audio sync is disaabled
|
|
||||||
if (self.is_buffer_full) return;
|
|
||||||
|
|
||||||
var left: i16 = 0;
|
var left: i16 = 0;
|
||||||
var right: i16 = 0;
|
var right: i16 = 0;
|
||||||
|
|
||||||
@@ -327,8 +413,8 @@ pub const Apu = struct {
|
|||||||
right += if (self.dma_cnt.chB_right.read()) chB_sample else 0;
|
right += if (self.dma_cnt.chB_right.read()) chB_sample else 0;
|
||||||
|
|
||||||
// Add SOUNDBIAS
|
// Add SOUNDBIAS
|
||||||
// FIXME: Is SOUNDBIAS 9-bit or 10-bit?
|
// FIXME: SOUNDBIAS is 10-bit but The waveform is centered around 0 if I treat it as 11-bit
|
||||||
const bias = @as(i16, self.bias.level.read()) << 1;
|
const bias = @as(i16, self.bias.level.read()) << 2;
|
||||||
left += bias;
|
left += bias;
|
||||||
right += bias;
|
right += bias;
|
||||||
|
|
||||||
@@ -339,24 +425,7 @@ pub const Apu = struct {
|
|||||||
const ext_left = (clamped_left << 5) | (clamped_left >> 6);
|
const ext_left = (clamped_left << 5) | (clamped_left >> 6);
|
||||||
const ext_right = (clamped_right << 5) | (clamped_right >> 6);
|
const ext_right = (clamped_right << 5) | (clamped_right >> 6);
|
||||||
|
|
||||||
// FIXME: This rarely happens
|
self.sample_queue.push(ext_left, ext_right) catch {};
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn replaceSDLResampler(self: *Self) void {
|
|
||||||
@setCold(true);
|
|
||||||
const sample_rate = Self.sampleRate(self.bias.sampling_cycle.read());
|
|
||||||
log.info("Sample Rate changed from {}Hz to {}Hz", .{ Self.sampleRate(self.sampling_cycle), sample_rate });
|
|
||||||
|
|
||||||
// Sampling Cycle (Sample Rate) changed, Craete a new SDL Audio Resampler
|
|
||||||
// FIXME: Replace SDL's Audio Resampler with either a custom or more reliable one
|
|
||||||
const old_stream = self.stream;
|
|
||||||
defer SDL.SDL_FreeAudioStream(old_stream);
|
|
||||||
|
|
||||||
self.sampling_cycle = self.bias.sampling_cycle.read();
|
|
||||||
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 {
|
||||||
@@ -405,11 +474,15 @@ pub const Apu = struct {
|
|||||||
if (!self.cnt.apu_enable.read()) return;
|
if (!self.cnt.apu_enable.read()) return;
|
||||||
|
|
||||||
if (@boolToInt(self.dma_cnt.chA_timer.read()) == tim_id) {
|
if (@boolToInt(self.dma_cnt.chA_timer.read()) == tim_id) {
|
||||||
|
if (!self.chA.enabled) return;
|
||||||
|
|
||||||
self.chA.updateSample();
|
self.chA.updateSample();
|
||||||
if (self.chA.len() <= 15) cpu.bus.dma[1].requestAudio(0x0400_00A0);
|
if (self.chA.len() <= 15) cpu.bus.dma[1].requestAudio(0x0400_00A0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (@boolToInt(self.dma_cnt.chB_timer.read()) == tim_id) {
|
if (@boolToInt(self.dma_cnt.chB_timer.read()) == tim_id) {
|
||||||
|
if (!self.chB.enabled) return;
|
||||||
|
|
||||||
self.chB.updateSample();
|
self.chB.updateSample();
|
||||||
if (self.chB.len() <= 15) cpu.bus.dma[2].requestAudio(0x0400_00A4);
|
if (self.chB.len() <= 15) cpu.bus.dma[2].requestAudio(0x0400_00A4);
|
||||||
}
|
}
|
||||||
@@ -423,19 +496,28 @@ 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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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(&intToBytes(u32, 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 {
|
||||||
return self.fifo.readableLength();
|
return self.fifo.readableLength();
|
||||||
}
|
}
|
||||||
@@ -456,8 +538,8 @@ 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,
|
step: u3,
|
||||||
|
|
||||||
|
@@ -49,10 +49,13 @@ pub fn init(sched: *Scheduler) Self {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(self: *Self) void {
|
pub fn reset(self: *Self) void {
|
||||||
self.len = 0;
|
self.len = 0; // NR41
|
||||||
self.envelope.raw = 0;
|
self.envelope.raw = 0; // NR42
|
||||||
self.poly.raw = 0;
|
self.poly.raw = 0; // NR43
|
||||||
self.cnt.raw = 0;
|
self.cnt.raw = 0; // NR44
|
||||||
|
|
||||||
|
self.len_dev.reset();
|
||||||
|
self.env_dev.reset();
|
||||||
|
|
||||||
self.sample = 0;
|
self.sample = 0;
|
||||||
self.enabled = false;
|
self.enabled = false;
|
||||||
|
@@ -43,9 +43,12 @@ pub fn init(sched: *Scheduler) Self {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(self: *Self) void {
|
pub fn reset(self: *Self) void {
|
||||||
self.duty.raw = 0;
|
self.duty.raw = 0; // NR21
|
||||||
self.envelope.raw = 0;
|
self.envelope.raw = 0; // NR22
|
||||||
self.freq.raw = 0;
|
self.freq.raw = 0; // NR32, NR24
|
||||||
|
|
||||||
|
self.len_dev.reset();
|
||||||
|
self.env_dev.reset();
|
||||||
|
|
||||||
self.sample = 0;
|
self.sample = 0;
|
||||||
self.enabled = false;
|
self.enabled = false;
|
||||||
|
@@ -50,12 +50,14 @@ pub fn init(sched: *Scheduler) Self {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(self: *Self) void {
|
pub fn reset(self: *Self) void {
|
||||||
self.sweep.raw = 0;
|
self.sweep.raw = 0; // NR10
|
||||||
self.sweep_dev.calc_performed = false;
|
self.duty.raw = 0; // NR11
|
||||||
|
self.envelope.raw = 0; // NR12
|
||||||
|
self.freq.raw = 0; // NR13, NR14
|
||||||
|
|
||||||
self.duty.raw = 0;
|
self.len_dev.reset();
|
||||||
self.envelope.raw = 0;
|
self.sweep_dev.reset();
|
||||||
self.freq.raw = 0;
|
self.env_dev.reset();
|
||||||
|
|
||||||
self.sample = 0;
|
self.sample = 0;
|
||||||
self.enabled = false;
|
self.enabled = false;
|
||||||
@@ -92,10 +94,9 @@ pub fn sound1CntL(self: *const Self) u8 {
|
|||||||
pub fn setSound1CntL(self: *Self, value: u8) void {
|
pub fn setSound1CntL(self: *Self, value: u8) void {
|
||||||
const new = io.Sweep{ .raw = value };
|
const new = io.Sweep{ .raw = value };
|
||||||
|
|
||||||
if (self.sweep.direction.read() and !new.direction.read()) {
|
if (!new.direction.read()) {
|
||||||
// Sweep Negate bit has been cleared
|
// If at least one (1) sweep calculation has been made with
|
||||||
// If At least 1 Sweep Calculation has been made since
|
// the negate bit set (since last trigger), disable the channel
|
||||||
// the last trigger, the channel is immediately disabled
|
|
||||||
|
|
||||||
if (self.sweep_dev.calc_performed) self.enabled = false;
|
if (self.sweep_dev.calc_performed) self.enabled = false;
|
||||||
}
|
}
|
||||||
|
@@ -42,10 +42,13 @@ pub fn init(sched: *Scheduler) Self {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(self: *Self) void {
|
pub fn reset(self: *Self) void {
|
||||||
self.select.raw = 0;
|
self.select.raw = 0; // NR30
|
||||||
self.length = 0;
|
self.length = 0; // NR31
|
||||||
self.vol.raw = 0;
|
self.vol.raw = 0; // NR32
|
||||||
self.freq.raw = 0;
|
self.freq.raw = 0; // NR33, NR34
|
||||||
|
|
||||||
|
self.len_dev.reset();
|
||||||
|
self.wave_dev.reset();
|
||||||
|
|
||||||
self.sample = 0;
|
self.sample = 0;
|
||||||
self.enabled = false;
|
self.enabled = false;
|
||||||
@@ -71,6 +74,11 @@ 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;
|
||||||
@@ -94,6 +102,11 @@ pub fn setSound3CntX(self: *Self, fs: *const FrameSequencer, value: u16) void {
|
|||||||
self.setNr34(fs, @truncate(u8, 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
|
||||||
pub fn setNr33(self: *Self, byte: u8) void {
|
pub fn setNr33(self: *Self, byte: u8) void {
|
||||||
self.freq.raw = (self.freq.raw & 0xFF00) | byte;
|
self.freq.raw = (self.freq.raw & 0xFF00) | byte;
|
||||||
|
@@ -11,6 +11,11 @@ pub fn create() Self {
|
|||||||
return .{ .timer = 0, .vol = 0 };
|
return .{ .timer = 0, .vol = 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
self.timer = 0;
|
||||||
|
self.vol = 0;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn tick(self: *Self, nrx2: io.Envelope) void {
|
pub fn tick(self: *Self, nrx2: io.Envelope) void {
|
||||||
if (nrx2.period.read() != 0) {
|
if (nrx2.period.read() != 0) {
|
||||||
if (self.timer != 0) self.timer -= 1;
|
if (self.timer != 0) self.timer -= 1;
|
||||||
|
@@ -6,6 +6,10 @@ pub fn create() Self {
|
|||||||
return .{ .timer = 0 };
|
return .{ .timer = 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
self.timer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn tick(self: *Self, enabled: bool, ch_enable: *bool) void {
|
pub fn tick(self: *Self, enabled: bool, ch_enable: *bool) void {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
if (self.timer == 0) return;
|
if (self.timer == 0) return;
|
||||||
|
@@ -18,13 +18,19 @@ pub fn create() Self {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
self.timer = 0;
|
||||||
|
self.enabled = false;
|
||||||
|
self.shadow = 0;
|
||||||
|
self.calc_performed = false;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn tick(self: *Self, ch1: *ToneSweep) void {
|
pub fn tick(self: *Self, ch1: *ToneSweep) void {
|
||||||
if (self.timer != 0) self.timer -= 1;
|
if (self.timer != 0) self.timer -= 1;
|
||||||
|
|
||||||
if (self.timer == 0) {
|
if (self.timer == 0) {
|
||||||
const period = ch1.sweep.period.read();
|
const period = ch1.sweep.period.read();
|
||||||
self.timer = if (period == 0) 8 else period;
|
self.timer = if (period == 0) 8 else period;
|
||||||
if (!self.calc_performed) self.calc_performed = true;
|
|
||||||
|
|
||||||
if (self.enabled and period != 0) {
|
if (self.enabled and period != 0) {
|
||||||
const new_freq = self.calculate(ch1.sweep, &ch1.enabled);
|
const new_freq = self.calculate(ch1.sweep, &ch1.enabled);
|
||||||
@@ -45,7 +51,10 @@ pub fn calculate(self: *Self, sweep: io.Sweep, ch_enable: *bool) u12 {
|
|||||||
const shadow_shifted = shadow >> sweep.shift.read();
|
const shadow_shifted = shadow >> sweep.shift.read();
|
||||||
const decrease = sweep.direction.read();
|
const decrease = sweep.direction.read();
|
||||||
|
|
||||||
const freq = if (decrease) shadow - shadow_shifted else shadow + shadow_shifted;
|
const freq = if (decrease) blk: {
|
||||||
|
self.calc_performed = true;
|
||||||
|
break :blk shadow - shadow_shifted;
|
||||||
|
} else shadow + shadow_shifted;
|
||||||
if (freq > 0x7FF) ch_enable.* = false;
|
if (freq > 0x7FF) ch_enable.* = false;
|
||||||
|
|
||||||
return freq;
|
return freq;
|
||||||
|
@@ -1,9 +1,7 @@
|
|||||||
|
//! 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);
|
||||||
@@ -21,6 +19,11 @@ pub fn create(sched: *Scheduler) Self {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
self.shift = 0;
|
||||||
|
self.timer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn sample(self: *const Self) i8 {
|
pub fn sample(self: *const Self) i8 {
|
||||||
return if ((~self.shift & 1) == 1) 1 else -1;
|
return if ((~self.shift & 1) == 1) 1 else -1;
|
||||||
}
|
}
|
||||||
@@ -35,7 +38,7 @@ pub fn reload(self: *Self, poly: io.PolyCounter) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Scheduler Event Handler for LFSR Timer Expire
|
/// Scheduler Event Handler for LFSR Timer Expire
|
||||||
/// FIXME: This gets called a lot, clogging up the Scheduler
|
/// FIXME: This gets called a lot, slowing down the scheduler
|
||||||
pub fn onLfsrTimerExpire(self: *Self, poly: io.PolyCounter, late: u64) void {
|
pub fn onLfsrTimerExpire(self: *Self, poly: io.PolyCounter, late: u64) void {
|
||||||
// Obscure: "Using a noise channel clock shift of 14 or 15
|
// Obscure: "Using a noise channel clock shift of 14 or 15
|
||||||
// results in the LFSR receiving no clocks."
|
// results in the LFSR receiving no clocks."
|
||||||
|
@@ -2,7 +2,6 @@ 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");
|
||||||
|
|
||||||
@@ -21,6 +20,11 @@ pub fn init(sched: *Scheduler) Self {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
self.timer = 0;
|
||||||
|
self.pos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
/// Scheduler Event Handler for Square Synth Timer Expire
|
/// Scheduler Event Handler for Square Synth Timer Expire
|
||||||
pub fn onSquareTimerExpire(self: *Self, comptime T: type, nrx34: io.Frequency, late: u64) void {
|
pub fn onSquareTimerExpire(self: *Self, comptime T: type, nrx34: io.Frequency, late: u64) void {
|
||||||
comptime std.debug.assert(T == ToneSweep or T == Tone);
|
comptime std.debug.assert(T == ToneSweep or T == Tone);
|
||||||
|
@@ -2,8 +2,6 @@ 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);
|
||||||
@@ -40,6 +38,13 @@ pub fn init(sched: *Scheduler) Self {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
self.timer = 0;
|
||||||
|
self.offset = 0;
|
||||||
|
|
||||||
|
// sample buffer isn't reset because it's outside of the range of what NR52{7}'s effects
|
||||||
|
}
|
||||||
|
|
||||||
/// Reload internal Wave Timer
|
/// Reload internal Wave Timer
|
||||||
pub fn reload(self: *Self, value: u11) void {
|
pub fn reload(self: *Self, value: u11) void {
|
||||||
self.sched.removeScheduledEvent(.{ .ApuChannel = 2 });
|
self.sched.removeScheduledEvent(.{ .ApuChannel = 2 });
|
||||||
|
@@ -19,7 +19,7 @@ pub fn read(self: *Self, comptime T: type, r15: u32, addr: u32) T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.debug("Rejected read since r15=0x{X:0>8}", .{r15});
|
log.debug("Rejected read since r15=0x{X:0>8}", .{r15});
|
||||||
return @truncate(T, self._read(T, self.addr_latch + 8));
|
return @truncate(T, self._read(T, self.addr_latch));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dbgRead(self: *const Self, comptime T: type, r15: u32, addr: u32) T {
|
pub fn dbgRead(self: *const Self, comptime T: type, r15: u32, addr: u32) T {
|
||||||
|
@@ -1,10 +1,6 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const config = @import("../../config.zig");
|
const config = @import("../../config.zig");
|
||||||
|
|
||||||
const Bit = @import("bitfield").Bit;
|
|
||||||
const Bitfield = @import("bitfield").Bitfield;
|
|
||||||
const DateTime = @import("datetime").datetime.Datetime;
|
|
||||||
|
|
||||||
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
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;
|
||||||
@@ -109,14 +105,13 @@ pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
|
|||||||
|
|
||||||
switch (T) {
|
switch (T) {
|
||||||
u32 => switch (address) {
|
u32 => switch (address) {
|
||||||
// TODO: Do I even need to implement these?
|
// FIXME: Do I even need to implement these?
|
||||||
0x0800_00C4 => std.debug.panic("Handle 32-bit GPIO Data/Direction Reads", .{}),
|
0x0800_00C4 => std.debug.panic("Handle 32-bit GPIO Data/Direction Reads", .{}),
|
||||||
0x0800_00C6 => std.debug.panic("Handle 32-bit GPIO Direction/Control Reads", .{}),
|
0x0800_00C6 => std.debug.panic("Handle 32-bit GPIO Direction/Control Reads", .{}),
|
||||||
0x0800_00C8 => std.debug.panic("Handle 32-bit GPIO Control Reads", .{}),
|
0x0800_00C8 => std.debug.panic("Handle 32-bit GPIO Control Reads", .{}),
|
||||||
else => {},
|
else => {},
|
||||||
},
|
},
|
||||||
u16 => switch (address) {
|
u16 => switch (address) {
|
||||||
// FIXME: What do 16-bit GPIO Reads look like?
|
|
||||||
0x0800_00C4 => return self.gpio.read(.Data),
|
0x0800_00C4 => return self.gpio.read(.Data),
|
||||||
0x0800_00C6 => return self.gpio.read(.Direction),
|
0x0800_00C6 => return self.gpio.read(.Direction),
|
||||||
0x0800_00C8 => return self.gpio.read(.Control),
|
0x0800_00C8 => return self.gpio.read(.Control),
|
||||||
|
@@ -6,7 +6,6 @@ const Eeprom = @import("backup/eeprom.zig").Eeprom;
|
|||||||
const Flash = @import("backup/Flash.zig");
|
const Flash = @import("backup/Flash.zig");
|
||||||
|
|
||||||
const escape = @import("../../util.zig").escape;
|
const escape = @import("../../util.zig").escape;
|
||||||
const span = @import("../../util.zig").span;
|
|
||||||
|
|
||||||
const Needle = struct { str: []const u8, kind: Backup.Kind };
|
const Needle = struct { str: []const u8, kind: Backup.Kind };
|
||||||
const backup_kinds = [6]Needle{
|
const backup_kinds = [6]Needle{
|
||||||
@@ -151,8 +150,8 @@ pub const Backup = struct {
|
|||||||
const file_path = try self.savePath(allocator, path);
|
const file_path = try self.savePath(allocator, path);
|
||||||
defer allocator.free(file_path);
|
defer allocator.free(file_path);
|
||||||
|
|
||||||
// FIXME: Don't rely on this lol
|
const expected = "untitled.sav";
|
||||||
if (std.mem.eql(u8, file_path[file_path.len - 12 .. file_path.len], "untitled.sav")) {
|
if (std.mem.eql(u8, file_path[file_path.len - expected.len .. file_path.len], expected)) {
|
||||||
return log.err("ROM header lacks title, no save loaded", .{});
|
return log.err("ROM header lacks title, no save loaded", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +194,7 @@ pub const Backup = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn saveName(self: *const Self, allocator: Allocator) ![]const u8 {
|
fn saveName(self: *const Self, allocator: Allocator) ![]const u8 {
|
||||||
const title_str = span(&escape(self.title));
|
const title_str = std.mem.sliceTo(&escape(self.title), 0);
|
||||||
const name = if (title_str.len != 0) title_str else "untitled";
|
const name = if (title_str.len != 0) title_str else "untitled";
|
||||||
|
|
||||||
return try std.mem.concat(allocator, u8, &[_][]const u8{ name, ".sav" });
|
return try std.mem.concat(allocator, u8, &[_][]const u8{ name, ".sav" });
|
||||||
|
@@ -63,7 +63,7 @@ pub const Eeprom = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (self.state == .RequestEnd) {
|
if (self.state == .RequestEnd) {
|
||||||
if (bit != 0) log.debug("EEPROM Request did not end in 0u1. TODO: is this ok?", .{});
|
// if (bit != 0) log.debug("EEPROM Request did not end in 0u1. TODO: is this ok?", .{});
|
||||||
self.state = .Ready;
|
self.state = .Ready;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@@ -5,89 +5,140 @@ const DmaControl = @import("io.zig").DmaControl;
|
|||||||
const Bus = @import("../Bus.zig");
|
const Bus = @import("../Bus.zig");
|
||||||
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||||
|
|
||||||
pub const DmaTuple = std.meta.Tuple(&[_]type{ DmaController(0), DmaController(1), DmaController(2), DmaController(3) });
|
pub const DmaTuple = struct { DmaController(0), DmaController(1), DmaController(2), DmaController(3) };
|
||||||
const log = std.log.scoped(.DmaTransfer);
|
const log = std.log.scoped(.DmaTransfer);
|
||||||
|
|
||||||
const setHi = util.setHi;
|
const getHalf = util.getHalf;
|
||||||
const setLo = util.setLo;
|
const setHalf = util.setHalf;
|
||||||
|
const setQuart = util.setQuart;
|
||||||
|
|
||||||
|
const rotr = @import("../../util.zig").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 = @truncate(u8, addr);
|
const byte_addr = @truncate(u8, addr);
|
||||||
|
|
||||||
return switch (T) {
|
return switch (T) {
|
||||||
u32 => switch (byte) {
|
u32 => switch (byte_addr) {
|
||||||
0xB8 => @as(T, dma.*[0].cnt.raw) << 16,
|
0xB0, 0xB4 => null, // DMA0SAD, DMA0DAD,
|
||||||
0xC4 => @as(T, dma.*[1].cnt.raw) << 16,
|
0xB8 => @as(T, dma.*[0].dmacntH()) << 16, // DMA0CNT_L is write-only
|
||||||
0xD0 => @as(T, dma.*[2].cnt.raw) << 16,
|
0xBC, 0xC0 => null, // DMA1SAD, DMA1DAD
|
||||||
0xDC => @as(T, dma.*[3].cnt.raw) << 16,
|
0xC4 => @as(T, dma.*[1].dmacntH()) << 16, // DMA1CNT_L is write-only
|
||||||
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
0xC8, 0xCC => null, // DMA2SAD, DMA2DAD
|
||||||
|
0xD0 => @as(T, dma.*[2].dmacntH()) << 16, // DMA2CNT_L is write-only
|
||||||
|
0xD4, 0xD8 => null, // DMA3SAD, DMA3DAD
|
||||||
|
0xDC => @as(T, dma.*[3].dmacntH()) << 16, // DMA3CNT_L is write-only
|
||||||
|
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
||||||
},
|
},
|
||||||
u16 => switch (byte) {
|
u16 => switch (byte_addr) {
|
||||||
0xBA => dma.*[0].cnt.raw,
|
0xB0, 0xB2, 0xB4, 0xB6 => null, // DMA0SAD, DMA0DAD
|
||||||
0xC6 => dma.*[1].cnt.raw,
|
0xB8 => 0x0000, // DMA0CNT_L, suite.gba expects 0x0000 instead of 0xDEAD
|
||||||
0xD2 => dma.*[2].cnt.raw,
|
0xBA => dma.*[0].dmacntH(),
|
||||||
0xDE => dma.*[3].cnt.raw,
|
|
||||||
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
0xBC, 0xBE, 0xC0, 0xC2 => null, // DMA1SAD, DMA1DAD
|
||||||
|
0xC4 => 0x0000, // DMA1CNT_L
|
||||||
|
0xC6 => dma.*[1].dmacntH(),
|
||||||
|
|
||||||
|
0xC8, 0xCA, 0xCC, 0xCE => null, // DMA2SAD, DMA2DAD
|
||||||
|
0xD0 => 0x0000, // DMA2CNT_L
|
||||||
|
0xD2 => dma.*[2].dmacntH(),
|
||||||
|
|
||||||
|
0xD4, 0xD6, 0xD8, 0xDA => null, // DMA3SAD, DMA3DAD
|
||||||
|
0xDC => 0x0000, // DMA3CNT_L
|
||||||
|
0xDE => dma.*[3].dmacntH(),
|
||||||
|
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
||||||
|
},
|
||||||
|
u8 => switch (byte_addr) {
|
||||||
|
0xB0...0xB7 => null, // DMA0SAD, DMA0DAD
|
||||||
|
0xB8, 0xB9 => 0x00, // DMA0CNT_L
|
||||||
|
0xBA, 0xBB => @truncate(T, dma.*[0].dmacntH() >> getHalf(byte_addr)),
|
||||||
|
|
||||||
|
0xBC...0xC3 => null, // DMA1SAD, DMA1DAD
|
||||||
|
0xC4, 0xC5 => 0x00, // DMA1CNT_L
|
||||||
|
0xC6, 0xC7 => @truncate(T, dma.*[1].dmacntH() >> getHalf(byte_addr)),
|
||||||
|
|
||||||
|
0xC8...0xCF => null, // DMA2SAD, DMA2DAD
|
||||||
|
0xD0, 0xD1 => 0x00, // DMA2CNT_L
|
||||||
|
0xD2, 0xD3 => @truncate(T, dma.*[2].dmacntH() >> getHalf(byte_addr)),
|
||||||
|
|
||||||
|
0xD4...0xDB => null, // DMA3SAD, DMA3DAD
|
||||||
|
0xDC, 0xDD => 0x00, // DMA3CNT_L
|
||||||
|
0xDE, 0xDF => @truncate(T, dma.*[3].dmacntH() >> getHalf(byte_addr)),
|
||||||
|
else => util.io.read.err(T, log, "unexpected {} read from 0x{X:0>8}", .{ T, addr }),
|
||||||
},
|
},
|
||||||
u8 => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
|
||||||
else => @compileError("DMA: Unsupported read width"),
|
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 = @truncate(u8, addr);
|
const byte_addr = @truncate(u8, addr);
|
||||||
|
|
||||||
switch (T) {
|
switch (T) {
|
||||||
u32 => switch (byte) {
|
u32 => switch (byte_addr) {
|
||||||
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) {
|
u16 => switch (byte_addr) {
|
||||||
0xB0 => dma.*[0].setDmasad(setLo(u32, dma.*[0].sad, value)),
|
0xB0, 0xB2 => dma.*[0].setDmasad(setHalf(u32, dma.*[0].sad, byte_addr, value)),
|
||||||
0xB2 => dma.*[0].setDmasad(setHi(u32, dma.*[0].sad, value)),
|
0xB4, 0xB6 => dma.*[0].setDmadad(setHalf(u32, dma.*[0].dad, byte_addr, 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 => dma.*[1].setDmasad(setLo(u32, dma.*[1].sad, value)),
|
0xBC, 0xBE => dma.*[1].setDmasad(setHalf(u32, dma.*[1].sad, byte_addr, value)),
|
||||||
0xBE => dma.*[1].setDmasad(setHi(u32, dma.*[1].sad, value)),
|
0xC0, 0xC2 => dma.*[1].setDmadad(setHalf(u32, dma.*[1].dad, byte_addr, 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 => dma.*[2].setDmasad(setLo(u32, dma.*[2].sad, value)),
|
0xC8, 0xCA => dma.*[2].setDmasad(setHalf(u32, dma.*[2].sad, byte_addr, value)),
|
||||||
0xCA => dma.*[2].setDmasad(setHi(u32, dma.*[2].sad, value)),
|
0xCC, 0xCE => dma.*[2].setDmadad(setHalf(u32, dma.*[2].dad, byte_addr, 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 => dma.*[3].setDmasad(setLo(u32, dma.*[3].sad, value)),
|
0xD4, 0xD6 => dma.*[3].setDmasad(setHalf(u32, dma.*[3].sad, byte_addr, value)),
|
||||||
0xD6 => dma.*[3].setDmasad(setHi(u32, dma.*[3].sad, value)),
|
0xD8, 0xDA => dma.*[3].setDmadad(setHalf(u32, dma.*[3].dad, byte_addr, 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 => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
|
u8 => switch (byte_addr) {
|
||||||
|
0xB0, 0xB1, 0xB2, 0xB3 => dma.*[0].setDmasad(setQuart(dma.*[0].sad, byte_addr, value)),
|
||||||
|
0xB4, 0xB5, 0xB6, 0xB7 => dma.*[0].setDmadad(setQuart(dma.*[0].dad, byte_addr, value)),
|
||||||
|
0xB8, 0xB9 => dma.*[0].setDmacntL(setHalf(u16, dma.*[0].word_count, byte_addr, value)),
|
||||||
|
0xBA, 0xBB => dma.*[0].setDmacntH(setHalf(u16, dma.*[0].cnt.raw, byte_addr, value)),
|
||||||
|
|
||||||
|
0xBC, 0xBD, 0xBE, 0xBF => dma.*[1].setDmasad(setQuart(dma.*[1].sad, byte_addr, value)),
|
||||||
|
0xC0, 0xC1, 0xC2, 0xC3 => dma.*[1].setDmadad(setQuart(dma.*[1].dad, byte_addr, value)),
|
||||||
|
0xC4, 0xC5 => dma.*[1].setDmacntL(setHalf(u16, dma.*[1].word_count, byte_addr, value)),
|
||||||
|
0xC6, 0xC7 => dma.*[1].setDmacntH(setHalf(u16, dma.*[1].cnt.raw, byte_addr, value)),
|
||||||
|
|
||||||
|
0xC8, 0xC9, 0xCA, 0xCB => dma.*[2].setDmasad(setQuart(dma.*[2].sad, byte_addr, value)),
|
||||||
|
0xCC, 0xCD, 0xCE, 0xCF => dma.*[2].setDmadad(setQuart(dma.*[2].dad, byte_addr, value)),
|
||||||
|
0xD0, 0xD1 => dma.*[2].setDmacntL(setHalf(u16, dma.*[2].word_count, byte_addr, value)),
|
||||||
|
0xD2, 0xD3 => dma.*[2].setDmacntH(setHalf(u16, dma.*[2].cnt.raw, byte_addr, value)),
|
||||||
|
|
||||||
|
0xD4, 0xD5, 0xD6, 0xD7 => dma.*[3].setDmasad(setQuart(dma.*[3].sad, byte_addr, value)),
|
||||||
|
0xD8, 0xD9, 0xDA, 0xDB => dma.*[3].setDmadad(setQuart(dma.*[3].dad, byte_addr, value)),
|
||||||
|
0xDC, 0xDD => dma.*[3].setDmacntL(setHalf(u16, dma.*[3].word_count, byte_addr, value)),
|
||||||
|
0xDE, 0xDF => dma.*[3].setDmacntH(setHalf(u16, dma.*[3].cnt.raw, byte_addr, value)),
|
||||||
|
else => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||||
|
},
|
||||||
else => @compileError("DMA: Unsupported write width"),
|
else => @compileError("DMA: Unsupported write width"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,6 +150,7 @@ 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
|
||||||
@@ -107,17 +159,19 @@ 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: if (id == 3) u16 else u14,
|
word_count: WordCount,
|
||||||
/// 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: if (id == 3) u16 else u14,
|
_word_count: WordCount,
|
||||||
|
|
||||||
/// 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
|
||||||
@@ -134,6 +188,8 @@ 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,
|
||||||
};
|
};
|
||||||
@@ -151,6 +207,10 @@ fn DmaController(comptime id: u2) type {
|
|||||||
self.word_count = @truncate(@TypeOf(self.word_count), 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 {
|
||||||
const new = DmaControl{ .raw = halfword };
|
const new = DmaControl{ .raw = halfword };
|
||||||
|
|
||||||
@@ -158,7 +218,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(@TypeOf(self._word_count)) else self.word_count;
|
self._word_count = if (self.word_count == 0) std.math.maxInt(WordCount) else self.word_count;
|
||||||
|
|
||||||
// Only a Start Timing of 00 has a DMA Transfer immediately begin
|
// 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;
|
||||||
@@ -181,19 +241,31 @@ fn DmaController(comptime id: u2) type {
|
|||||||
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) {
|
||||||
cpu.bus.write(u32, self.dad_latch & mask, cpu.bus.read(u32, self.sad_latch & mask));
|
if (sad_addr >= 0x0200_0000) self.data_latch = cpu.bus.read(u32, sad_addr);
|
||||||
|
cpu.bus.write(u32, dad_addr, self.data_latch);
|
||||||
} else {
|
} else {
|
||||||
cpu.bus.write(u16, self.dad_latch & mask, cpu.bus.read(u16, self.sad_latch & mask));
|
if (sad_addr >= 0x0200_0000) {
|
||||||
|
const value: u32 = cpu.bus.read(u16, sad_addr);
|
||||||
|
self.data_latch = value << 16 | value;
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu.bus.write(u16, dad_addr, @truncate(u16, rotr(u32, self.data_latch, 8 * (dad_addr & 3))));
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (sad_adj) {
|
switch (@truncate(u8, sad_addr >> 24)) {
|
||||||
.Increment => self.sad_latch +%= offset,
|
// according to fleroviux, DMAs with a source address in ROM misbehave
|
||||||
.Decrement => self.sad_latch -%= offset,
|
// the resultant behaviour is that the source address will increment despite what DMAXCNT says
|
||||||
// FIXME: Is just ignoring this ok?
|
0x08...0x0D => self.sad_latch +%= offset, // obscure behaviour
|
||||||
.IncrementReload => log.err("{} is a prohibited adjustment on SAD", .{sad_adj}),
|
else => switch (sad_adj) {
|
||||||
.Fixed => {},
|
.Increment => self.sad_latch +%= offset,
|
||||||
|
.Decrement => self.sad_latch -%= offset,
|
||||||
|
.IncrementReload => log.err("{} is a prohibited adjustment on SAD", .{sad_adj}),
|
||||||
|
.Fixed => {},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (dad_adj) {
|
switch (dad_adj) {
|
||||||
@@ -267,10 +339,10 @@ fn DmaController(comptime id: u2) type {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn pollDmaOnBlank(bus: *Bus, comptime kind: DmaKind) void {
|
pub fn pollDmaOnBlank(bus: *Bus, comptime kind: DmaKind) void {
|
||||||
bus.dma[0].poll(kind);
|
comptime var i: usize = 0;
|
||||||
bus.dma[1].poll(kind);
|
inline while (i < 4) : (i += 1) {
|
||||||
bus.dma[2].poll(kind);
|
bus.dma[i].poll(kind);
|
||||||
bus.dma[3].poll(kind);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Adjustment = enum(u2) {
|
const Adjustment = enum(u2) {
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
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("../cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||||
@@ -68,9 +67,11 @@ pub const Gpio = struct {
|
|||||||
log.info("Device: {}", .{kind});
|
log.info("Device: {}", .{kind});
|
||||||
|
|
||||||
const self = try allocator.create(Self);
|
const self = try allocator.create(Self);
|
||||||
|
errdefer allocator.destroy(self);
|
||||||
|
|
||||||
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) {
|
||||||
|
@@ -1,18 +1,16 @@
|
|||||||
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 setHi = util.setLo;
|
const getHalf = util.getHalf;
|
||||||
const setLo = util.setHi;
|
const setHalf = util.setHalf;
|
||||||
|
|
||||||
const log = std.log.scoped(.@"I/O");
|
const log = std.log.scoped(.@"I/O");
|
||||||
|
|
||||||
@@ -24,15 +22,17 @@ pub const Io = struct {
|
|||||||
ie: InterruptEnable,
|
ie: InterruptEnable,
|
||||||
irq: InterruptRequest,
|
irq: InterruptRequest,
|
||||||
postflg: PostFlag,
|
postflg: PostFlag,
|
||||||
|
waitcnt: WaitControl,
|
||||||
haltcnt: HaltControl,
|
haltcnt: HaltControl,
|
||||||
keyinput: KeyInput,
|
keyinput: AtomicKeyInput,
|
||||||
|
|
||||||
pub fn init() Self {
|
pub fn init() Self {
|
||||||
return .{
|
return .{
|
||||||
.ime = false,
|
.ime = false,
|
||||||
.ie = .{ .raw = 0x0000 },
|
.ie = .{ .raw = 0x0000 },
|
||||||
.irq = .{ .raw = 0x0000 },
|
.irq = .{ .raw = 0x0000 },
|
||||||
.keyinput = .{ .raw = 0x03FF },
|
.keyinput = AtomicKeyInput.init(.{ .raw = 0x03FF }),
|
||||||
|
.waitcnt = .{ .raw = 0x0000_0000 }, // Bit 15 == 0 for GBA
|
||||||
.postflg = .FirstBoot,
|
.postflg = .FirstBoot,
|
||||||
.haltcnt = .Execute,
|
.haltcnt = .Execute,
|
||||||
};
|
};
|
||||||
@@ -48,9 +48,10 @@ 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 => bus.ppu.dispcnt.raw,
|
0x0400_0000...0x0400_0054 => ppu.read(T, &bus.ppu, address),
|
||||||
0x0400_0004 => @as(T, bus.ppu.vcount.raw) << 16 | bus.ppu.dispstat.raw,
|
|
||||||
0x0400_0006 => @as(T, bus.ppu.bg[0].cnt.raw) << 16 | bus.ppu.vcount.raw,
|
// Sound
|
||||||
|
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),
|
||||||
@@ -68,26 +69,18 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) ?T {
|
|||||||
0x0400_0150 => util.io.read.todo(log, "Read {} from JOY_RECV", .{T}),
|
0x0400_0150 => util.io.read.todo(log, "Read {} from JOY_RECV", .{T}),
|
||||||
|
|
||||||
// Interrupts
|
// Interrupts
|
||||||
0x0400_0200 => @as(T, bus.io.irq.raw) << 16 | bus.io.ie.raw,
|
0x0400_0200 => @as(u32, bus.io.irq.raw) << 16 | bus.io.ie.raw,
|
||||||
|
0x0400_0204 => bus.io.waitcnt.raw,
|
||||||
0x0400_0208 => @boolToInt(bus.io.ime),
|
0x0400_0208 => @boolToInt(bus.io.ime),
|
||||||
|
0x0400_0300 => @enumToInt(bus.io.postflg),
|
||||||
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
|
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
|
||||||
},
|
},
|
||||||
u16 => switch (address) {
|
u16 => switch (address) {
|
||||||
// Display
|
// Display
|
||||||
0x0400_0000 => bus.ppu.dispcnt.raw,
|
0x0400_0000...0x0400_0054 => ppu.read(T, &bus.ppu, address),
|
||||||
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_009E => apu.read(T, &bus.apu, address),
|
0x0400_0060...0x0400_00A6 => 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),
|
||||||
@@ -99,32 +92,38 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) ?T {
|
|||||||
0x0400_0128 => util.io.read.todo(log, "Read {} from SIOCNT", .{T}),
|
0x0400_0128 => util.io.read.todo(log, "Read {} from SIOCNT", .{T}),
|
||||||
|
|
||||||
// Keypad Input
|
// Keypad Input
|
||||||
0x0400_0130 => bus.io.keyinput.raw,
|
0x0400_0130 => bus.io.keyinput.load(.Monotonic).raw,
|
||||||
|
|
||||||
// Serial Communication 2
|
// Serial Communication 2
|
||||||
0x0400_0134 => util.io.read.todo(log, "Read {} from RCNT", .{T}),
|
0x0400_0134 => util.io.read.todo(log, "Read {} from RCNT", .{T}),
|
||||||
|
0x0400_0136 => 0x0000,
|
||||||
|
0x0400_0142 => 0x0000,
|
||||||
|
0x0400_015A => 0x0000,
|
||||||
|
|
||||||
// Interrupts
|
// Interrupts
|
||||||
0x0400_0200 => bus.io.ie.raw,
|
0x0400_0200 => bus.io.ie.raw,
|
||||||
0x0400_0202 => bus.io.irq.raw,
|
0x0400_0202 => bus.io.irq.raw,
|
||||||
0x0400_0204 => util.io.read.todo(log, "Read {} from WAITCNT", .{T}),
|
0x0400_0204 => bus.io.waitcnt.raw,
|
||||||
|
0x0400_0206 => 0x0000,
|
||||||
0x0400_0208 => @boolToInt(bus.io.ime),
|
0x0400_0208 => @boolToInt(bus.io.ime),
|
||||||
|
0x0400_020A => 0x0000,
|
||||||
|
0x0400_0300 => @enumToInt(bus.io.postflg),
|
||||||
|
0x0400_0302 => 0x0000,
|
||||||
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
|
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
|
||||||
},
|
},
|
||||||
u8 => return switch (address) {
|
u8 => return switch (address) {
|
||||||
// Display
|
// Display
|
||||||
0x0400_0000 => @truncate(T, bus.ppu.dispcnt.raw),
|
0x0400_0000...0x0400_0055 => ppu.read(T, &bus.ppu, address),
|
||||||
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}),
|
||||||
|
|
||||||
@@ -133,10 +132,20 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) ?T {
|
|||||||
|
|
||||||
// Serial Communication 2
|
// Serial Communication 2
|
||||||
0x0400_0135 => util.io.read.todo(log, "Read {} from RCNT_H", .{T}),
|
0x0400_0135 => util.io.read.todo(log, "Read {} from RCNT_H", .{T}),
|
||||||
|
0x0400_0136, 0x0400_0137 => 0x00,
|
||||||
|
0x0400_0142, 0x0400_0143 => 0x00,
|
||||||
|
0x0400_015A, 0x0400_015B => 0x00,
|
||||||
|
|
||||||
// Interrupts
|
// Interrupts
|
||||||
0x0400_0200 => @truncate(T, bus.io.ie.raw),
|
0x0400_0200, 0x0400_0201 => @truncate(T, bus.io.ie.raw >> getHalf(@truncate(u8, address))),
|
||||||
|
0x0400_0202, 0x0400_0203 => @truncate(T, bus.io.irq.raw >> getHalf(@truncate(u8, address))),
|
||||||
|
0x0400_0204, 0x0400_0205 => @truncate(T, bus.io.waitcnt.raw >> getHalf(@truncate(u8, address))),
|
||||||
|
0x0400_0206, 0x0400_0207 => 0x00,
|
||||||
|
0x0400_0208, 0x0400_0209 => @truncate(T, @as(u16, @boolToInt(bus.io.ime)) >> getHalf(@truncate(u8, address))),
|
||||||
|
0x0400_020A, 0x0400_020B => 0x00,
|
||||||
0x0400_0300 => @enumToInt(bus.io.postflg),
|
0x0400_0300 => @enumToInt(bus.io.postflg),
|
||||||
|
0x0400_0301 => null,
|
||||||
|
0x0400_0302, 0x0400_0303 => 0x00,
|
||||||
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
|
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
|
||||||
},
|
},
|
||||||
else => @compileError("I/O: Unsupported read width"),
|
else => @compileError("I/O: Unsupported read width"),
|
||||||
@@ -147,34 +156,7 @@ 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 => bus.ppu.dispcnt.raw = @truncate(u16, value),
|
0x0400_0000...0x0400_0054 => ppu.write(T, &bus.ppu, address, 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
|
||||||
@@ -210,65 +192,28 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
|
|||||||
|
|
||||||
// Interrupts
|
// Interrupts
|
||||||
0x0400_0200 => bus.io.setIrqs(value),
|
0x0400_0200 => bus.io.setIrqs(value),
|
||||||
0x0400_0204 => log.debug("Wrote 0x{X:0>8} to WAITCNT", .{value}),
|
0x0400_0204 => bus.io.waitcnt.set(@truncate(u16, value)),
|
||||||
0x0400_0208 => bus.io.ime = value & 1 == 1,
|
0x0400_0208 => bus.io.ime = value & 1 == 1,
|
||||||
0x0400_020C...0x0400_021C => {}, // Unused
|
0x0400_0300 => {
|
||||||
|
bus.io.postflg = @intToEnum(PostFlag, value & 1);
|
||||||
|
bus.io.haltcnt = if (value >> 15 & 1 == 0) .Halt else @panic("TODO: Implement STOP");
|
||||||
|
},
|
||||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, address }),
|
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, address }),
|
||||||
},
|
},
|
||||||
u16 => switch (address) {
|
u16 => switch (address) {
|
||||||
// Display
|
// Display
|
||||||
0x0400_0000 => bus.ppu.dispcnt.raw = value,
|
0x0400_0000...0x0400_0054 => ppu.write(T, &bus.ppu, address, value),
|
||||||
0x0400_0004 => bus.ppu.dispstat.raw = value,
|
0x0400_0056 => {}, // Not used
|
||||||
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_009E => apu.write(T, &bus.apu, address, value),
|
0x0400_0060...0x0400_00A6 => 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 => {}, // TODO: Gyakuten Saiban writes 0x8000 to 0x0400_0114
|
0x0400_0114 => {},
|
||||||
0x0400_0110 => {}, // Not Used,
|
0x0400_0110 => {}, // Not Used,
|
||||||
|
|
||||||
// Serial Communication 1
|
// Serial Communication 1
|
||||||
@@ -292,27 +237,29 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
|
|||||||
// Interrupts
|
// Interrupts
|
||||||
0x0400_0200 => bus.io.ie.raw = value,
|
0x0400_0200 => bus.io.ie.raw = value,
|
||||||
0x0400_0202 => bus.io.irq.raw &= ~value,
|
0x0400_0202 => bus.io.irq.raw &= ~value,
|
||||||
0x0400_0204 => log.debug("Wrote 0x{X:0>4} to WAITCNT", .{value}),
|
0x0400_0204 => bus.io.waitcnt.set(value),
|
||||||
|
0x0400_0206 => {},
|
||||||
0x0400_0208 => bus.io.ime = value & 1 == 1,
|
0x0400_0208 => bus.io.ime = value & 1 == 1,
|
||||||
0x0400_0206, 0x0400_020A => {}, // Not Used
|
0x0400_020A => {},
|
||||||
|
0x0400_0300 => {
|
||||||
|
bus.io.postflg = @intToEnum(PostFlag, value & 1);
|
||||||
|
bus.io.haltcnt = if (value >> 15 & 1 == 0) .Halt else @panic("TODO: Implement STOP");
|
||||||
|
},
|
||||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, address }),
|
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, address }),
|
||||||
},
|
},
|
||||||
u8 => switch (address) {
|
u8 => switch (address) {
|
||||||
// Display
|
// Display
|
||||||
0x0400_0004 => bus.ppu.dispstat.raw = setLo(u16, bus.ppu.dispstat.raw, value),
|
0x0400_0000...0x0400_0055 => ppu.write(T, &bus.ppu, address, 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_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}),
|
||||||
@@ -322,9 +269,16 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
|
|||||||
0x0400_0140 => log.debug("Wrote 0x{X:0>2} to JOYCNT_L", .{value}),
|
0x0400_0140 => log.debug("Wrote 0x{X:0>2} to JOYCNT_L", .{value}),
|
||||||
|
|
||||||
// Interrupts
|
// Interrupts
|
||||||
|
0x0400_0200, 0x0400_0201 => bus.io.ie.raw = setHalf(u16, bus.io.ie.raw, @truncate(u8, address), value),
|
||||||
0x0400_0202 => bus.io.irq.raw &= ~@as(u16, value),
|
0x0400_0202 => bus.io.irq.raw &= ~@as(u16, value),
|
||||||
|
0x0400_0203 => bus.io.irq.raw &= ~@as(u16, value) << 8, // TODO: Is this good?
|
||||||
|
0x0400_0204, 0x0400_0205 => bus.io.waitcnt.set(setHalf(u16, @truncate(u16, bus.io.waitcnt.raw), @truncate(u8, address), value)),
|
||||||
|
0x0400_0206, 0x0400_0207 => {},
|
||||||
0x0400_0208 => bus.io.ime = value & 1 == 1,
|
0x0400_0208 => bus.io.ime = value & 1 == 1,
|
||||||
0x0400_0300 => bus.io.postflg = std.meta.intToEnum(PostFlag, value & 1) catch unreachable,
|
0x0400_0209 => {},
|
||||||
|
0x0400_020A, 0x0400_020B => {},
|
||||||
|
|
||||||
|
0x0400_0300 => bus.io.postflg = @intToEnum(PostFlag, value & 1),
|
||||||
0x0400_0301 => bus.io.haltcnt = if (value >> 7 & 1 == 0) .Halt else std.debug.panic("TODO: Implement STOP", .{}),
|
0x0400_0301 => bus.io.haltcnt = if (value >> 7 & 1 == 0) .Halt else std.debug.panic("TODO: Implement STOP", .{}),
|
||||||
|
|
||||||
0x0400_0410 => log.debug("Wrote 0x{X:0>2} to the common yet undocumented 0x{X:0>8}", .{ value, address }),
|
0x0400_0410 => log.debug("Wrote 0x{X:0>2} to the common yet undocumented 0x{X:0>8}", .{ value, address }),
|
||||||
@@ -363,14 +317,22 @@ pub const DisplayControl = extern union {
|
|||||||
|
|
||||||
/// Read / Write
|
/// Read / Write
|
||||||
pub const DisplayStatus = extern union {
|
pub const DisplayStatus = extern union {
|
||||||
|
/// read-only
|
||||||
vblank: Bit(u16, 0),
|
vblank: Bit(u16, 0),
|
||||||
|
/// read-only
|
||||||
hblank: Bit(u16, 1),
|
hblank: Bit(u16, 1),
|
||||||
|
// read-only
|
||||||
coincidence: Bit(u16, 2),
|
coincidence: Bit(u16, 2),
|
||||||
vblank_irq: Bit(u16, 3),
|
vblank_irq: Bit(u16, 3),
|
||||||
hblank_irq: Bit(u16, 4),
|
hblank_irq: Bit(u16, 4),
|
||||||
vcount_irq: Bit(u16, 5),
|
vcount_irq: Bit(u16, 5),
|
||||||
vcount_trigger: Bitfield(u16, 8, 8),
|
vcount_trigger: Bitfield(u16, 8, 8),
|
||||||
raw: u16,
|
raw: u16,
|
||||||
|
|
||||||
|
pub fn set(self: *DisplayStatus, value: u16) void {
|
||||||
|
const mask: u16 = 0x00C7; // set bits are read-only
|
||||||
|
self.raw = (self.raw & mask) | (value & ~mask);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Read Only
|
/// Read Only
|
||||||
@@ -414,6 +376,31 @@ const KeyInput = extern union {
|
|||||||
raw: u16,
|
raw: u16,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AtomicKeyInput = struct {
|
||||||
|
const Self = @This();
|
||||||
|
const Ordering = std.atomic.Ordering;
|
||||||
|
|
||||||
|
inner: KeyInput,
|
||||||
|
|
||||||
|
pub fn init(value: KeyInput) Self {
|
||||||
|
return .{ .inner = value };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn load(self: *const Self, comptime ordering: Ordering) KeyInput {
|
||||||
|
return .{ .raw = switch (ordering) {
|
||||||
|
.AcqRel, .Release => @compileError("not supported for atomic loads"),
|
||||||
|
else => @atomicLoad(u16, &self.inner.raw, ordering),
|
||||||
|
} };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn store(self: *Self, value: u16, comptime ordering: Ordering) void {
|
||||||
|
switch (ordering) {
|
||||||
|
.AcqRel, .Acquire => @compileError("not supported for atomic stores"),
|
||||||
|
else => @atomicStore(u16, &self.inner.raw, value, ordering),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Read / Write
|
// Read / Write
|
||||||
pub const BackgroundControl = extern union {
|
pub const BackgroundControl = extern union {
|
||||||
priority: Bitfield(u16, 0, 2),
|
priority: Bitfield(u16, 0, 2),
|
||||||
@@ -661,3 +648,24 @@ pub const SoundBias = extern union {
|
|||||||
sampling_cycle: Bitfield(u16, 14, 2),
|
sampling_cycle: Bitfield(u16, 14, 2),
|
||||||
raw: u16,
|
raw: u16,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Read / Write
|
||||||
|
pub const WaitControl = extern union {
|
||||||
|
sram_cnt: Bitfield(u16, 0, 2),
|
||||||
|
s0_first: Bitfield(u16, 2, 2),
|
||||||
|
s0_second: Bit(u16, 4),
|
||||||
|
s1_first: Bitfield(u16, 5, 2),
|
||||||
|
s1_second: Bit(u16, 7),
|
||||||
|
s2_first: Bitfield(u16, 8, 2),
|
||||||
|
s2_second: Bit(u16, 10),
|
||||||
|
phi_out: Bitfield(u16, 11, 2),
|
||||||
|
|
||||||
|
prefetch_enable: Bit(u16, 14),
|
||||||
|
pak_kind: Bit(u16, 15),
|
||||||
|
raw: u16,
|
||||||
|
|
||||||
|
pub fn set(self: *WaitControl, value: u16) void {
|
||||||
|
const mask: u16 = 0x8000; // set bits are read-only
|
||||||
|
self.raw = (self.raw & mask) | (value & ~mask);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@@ -2,68 +2,99 @@ 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 Event = @import("../scheduler.zig").Event;
|
|
||||||
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||||
|
|
||||||
pub const TimerTuple = std.meta.Tuple(&[_]type{ Timer(0), Timer(1), Timer(2), Timer(3) });
|
pub const TimerTuple = struct { Timer(0), Timer(1), Timer(2), Timer(3) };
|
||||||
const log = std.log.scoped(.Timer);
|
const log = std.log.scoped(.Timer);
|
||||||
|
|
||||||
|
const getHalf = util.getHalf;
|
||||||
|
const 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 = @truncate(u4, addr);
|
const nybble_addr = @truncate(u4, addr);
|
||||||
|
|
||||||
return switch (T) {
|
return switch (T) {
|
||||||
u32 => switch (nybble) {
|
u32 => switch (nybble_addr) {
|
||||||
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.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
||||||
},
|
},
|
||||||
u16 => switch (nybble) {
|
u16 => switch (nybble_addr) {
|
||||||
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.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
||||||
|
},
|
||||||
|
u8 => switch (nybble_addr) {
|
||||||
|
0x0, 0x1 => @truncate(T, tim.*[0].timcntL() >> getHalf(nybble_addr)),
|
||||||
|
0x2, 0x3 => @truncate(T, tim.*[0].cnt.raw >> getHalf(nybble_addr)),
|
||||||
|
|
||||||
|
0x4, 0x5 => @truncate(T, tim.*[1].timcntL() >> getHalf(nybble_addr)),
|
||||||
|
0x6, 0x7 => @truncate(T, tim.*[1].cnt.raw >> getHalf(nybble_addr)),
|
||||||
|
|
||||||
|
0x8, 0x9 => @truncate(T, tim.*[2].timcntL() >> getHalf(nybble_addr)),
|
||||||
|
0xA, 0xB => @truncate(T, tim.*[2].cnt.raw >> getHalf(nybble_addr)),
|
||||||
|
|
||||||
|
0xC, 0xD => @truncate(T, tim.*[3].timcntL() >> getHalf(nybble_addr)),
|
||||||
|
0xE, 0xF => @truncate(T, tim.*[3].cnt.raw >> getHalf(nybble_addr)),
|
||||||
},
|
},
|
||||||
u8 => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
|
||||||
else => @compileError("TIM: Unsupported read width"),
|
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 = @truncate(u4, addr);
|
const nybble_addr = @truncate(u4, addr);
|
||||||
|
|
||||||
return switch (T) {
|
return switch (T) {
|
||||||
u32 => switch (nybble) {
|
u32 => switch (nybble_addr) {
|
||||||
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) {
|
u16 => switch (nybble_addr) {
|
||||||
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 => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
|
u8 => switch (nybble_addr) {
|
||||||
|
0x0, 0x1 => tim.*[0].setTimcntL(setHalf(u16, tim.*[0]._reload, nybble_addr, value)),
|
||||||
|
0x2, 0x3 => tim.*[0].setTimcntH(setHalf(u16, tim.*[0].cnt.raw, nybble_addr, value)),
|
||||||
|
|
||||||
|
0x4, 0x5 => tim.*[1].setTimcntL(setHalf(u16, tim.*[1]._reload, nybble_addr, value)),
|
||||||
|
0x6, 0x7 => tim.*[1].setTimcntH(setHalf(u16, tim.*[1].cnt.raw, nybble_addr, value)),
|
||||||
|
|
||||||
|
0x8, 0x9 => tim.*[2].setTimcntL(setHalf(u16, tim.*[2]._reload, nybble_addr, value)),
|
||||||
|
0xA, 0xB => tim.*[2].setTimcntH(setHalf(u16, tim.*[2].cnt.raw, nybble_addr, value)),
|
||||||
|
|
||||||
|
0xC, 0xD => tim.*[3].setTimcntL(setHalf(u16, tim.*[3]._reload, nybble_addr, value)),
|
||||||
|
0xE, 0xF => tim.*[3].setTimcntH(setHalf(u16, tim.*[3].cnt.raw, nybble_addr, value)),
|
||||||
|
},
|
||||||
else => @compileError("TIM: Unsupported write width"),
|
else => @compileError("TIM: Unsupported write width"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -119,21 +150,36 @@ fn Timer(comptime id: u2) type {
|
|||||||
pub fn setTimcntH(self: *Self, halfword: u16) void {
|
pub fn setTimcntH(self: *Self, halfword: u16) void {
|
||||||
const new = TimerControl{ .raw = halfword };
|
const new = TimerControl{ .raw = halfword };
|
||||||
|
|
||||||
// If Timer happens to be enabled, It will either be resheduled or disabled
|
if (self.cnt.enabled.read()) {
|
||||||
self.sched.removeScheduledEvent(.{ .TimerOverflow = id });
|
// timer was already enabled
|
||||||
|
|
||||||
if (self.cnt.enabled.read() and (new.cascade.read() or !new.enabled.read())) {
|
// If enabled falling edge or cascade falling edge, timer is paused
|
||||||
// Either through the cascade bit or the enable bit, the timer has effectively been disabled
|
if (!new.enabled.read() or (!self.cnt.cascade.read() and new.cascade.read())) {
|
||||||
// The Counter should hold whatever value it should have been at when it was disabled
|
self.sched.removeScheduledEvent(.{ .TimerOverflow = id });
|
||||||
self._counter +%= @truncate(u16, (self.sched.now() - self._start_timestamp) / self.frequency());
|
|
||||||
|
// Counter should hold the value it stopped at meaning we have to calculate it now
|
||||||
|
self._counter +%= @truncate(u16, (self.sched.now() - self._start_timestamp) / self.frequency());
|
||||||
|
}
|
||||||
|
|
||||||
|
// the timer has always been enabled, but the cascade bit which was blocking the timer has been unset
|
||||||
|
if (new.enabled.read() and (self.cnt.cascade.read() and !new.cascade.read())) {
|
||||||
|
// we want to reschedule the timer event, however we won't reload the counter.
|
||||||
|
// the invariant here is that self._counter holds the already calculated paused value
|
||||||
|
|
||||||
|
self.rescheduleTimerExpire(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// the timer was previously disabeld
|
||||||
|
|
||||||
|
if (new.enabled.read()) {
|
||||||
|
// timer should start counting (with a reloaded counter value)
|
||||||
|
self._counter = self._reload;
|
||||||
|
|
||||||
|
// if cascade happens to be set, the timer doesn't actually do anything though
|
||||||
|
if (!new.cascade.read()) self.rescheduleTimerExpire(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The counter is only reloaded on the rising edge of the enable bit
|
|
||||||
if (!self.cnt.enabled.read() and new.enabled.read()) self._counter = self._reload;
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
self.cnt.raw = halfword;
|
self.cnt.raw = halfword;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,23 +205,20 @@ fn Timer(comptime id: u2) type {
|
|||||||
|
|
||||||
// Perform Cascade Behaviour
|
// Perform Cascade Behaviour
|
||||||
switch (id) {
|
switch (id) {
|
||||||
0 => if (cpu.bus.tim[1].cnt.cascade.read()) {
|
inline 0, 1, 2 => |idx| {
|
||||||
cpu.bus.tim[1]._counter +%= 1;
|
const next = idx + 1;
|
||||||
if (cpu.bus.tim[1]._counter == 0) cpu.bus.tim[1].onTimerExpire(cpu, late);
|
|
||||||
|
if (cpu.bus.tim[next].cnt.cascade.read()) {
|
||||||
|
cpu.bus.tim[next]._counter +%= 1;
|
||||||
|
if (cpu.bus.tim[next]._counter == 0) cpu.bus.tim[next].onTimerExpire(cpu, late);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
1 => if (cpu.bus.tim[2].cnt.cascade.read()) {
|
3 => {}, // THere is no timer for TIM3 to cascade to
|
||||||
cpu.bus.tim[2]._counter +%= 1;
|
|
||||||
if (cpu.bus.tim[2]._counter == 0) cpu.bus.tim[2].onTimerExpire(cpu, late);
|
|
||||||
},
|
|
||||||
2 => if (cpu.bus.tim[3].cnt.cascade.read()) {
|
|
||||||
cpu.bus.tim[3]._counter +%= 1;
|
|
||||||
if (cpu.bus.tim[3]._counter == 0) cpu.bus.tim[3].onTimerExpire(cpu, late);
|
|
||||||
},
|
|
||||||
3 => {}, // There is no Timer for TIM3 to "cascade" to,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reschedule Timer if we're not cascading
|
// Reschedule Timer if we're not cascading
|
||||||
if (!self.cnt.cascade.read()) {
|
// TIM0 cascade value is N/A
|
||||||
|
if (id == 0 or !self.cnt.cascade.read()) {
|
||||||
self._counter = self._reload;
|
self._counter = self._reload;
|
||||||
self.rescheduleTimerExpire(late);
|
self.rescheduleTimerExpire(late);
|
||||||
}
|
}
|
||||||
|
321
src/core/cpu.zig
321
src/core/cpu.zig
@@ -1,14 +1,13 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const util = @import("../util.zig");
|
|
||||||
|
|
||||||
const Bus = @import("Bus.zig");
|
const Bus = @import("Bus.zig");
|
||||||
const Bit = @import("bitfield").Bit;
|
const Bit = @import("bitfield").Bit;
|
||||||
const Bitfield = @import("bitfield").Bitfield;
|
const Bitfield = @import("bitfield").Bitfield;
|
||||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
const Scheduler = @import("scheduler.zig").Scheduler;
|
||||||
const FilePaths = @import("../util.zig").FilePaths;
|
|
||||||
const Logger = @import("../util.zig").Logger;
|
const Logger = @import("../util.zig").Logger;
|
||||||
|
|
||||||
const File = std.fs.File;
|
const File = std.fs.File;
|
||||||
|
const log = std.log.scoped(.Arm7Tdmi);
|
||||||
|
|
||||||
// ARM Instructions
|
// ARM Instructions
|
||||||
pub const arm = struct {
|
pub const arm = struct {
|
||||||
@@ -236,8 +235,6 @@ pub const thumb = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const log = std.log.scoped(.Arm7Tdmi);
|
|
||||||
|
|
||||||
pub const Arm7tdmi = struct {
|
pub const Arm7tdmi = struct {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
@@ -248,18 +245,64 @@ pub const Arm7tdmi = struct {
|
|||||||
cpsr: PSR,
|
cpsr: PSR,
|
||||||
spsr: PSR,
|
spsr: PSR,
|
||||||
|
|
||||||
/// Storage for R8_fiq -> R12_fiq and their normal counterparts
|
bank: Bank,
|
||||||
/// e.g [r[0 + 8], fiq_r[0 + 8], r[1 + 8], fiq_r[1 + 8]...]
|
|
||||||
banked_fiq: [2 * 5]u32,
|
|
||||||
|
|
||||||
/// Storage for r13_<mode>, r14_<mode>
|
|
||||||
/// e.g. [r13, r14, r13_svc, r14_svc]
|
|
||||||
banked_r: [2 * 6]u32,
|
|
||||||
|
|
||||||
banked_spsr: [5]PSR,
|
|
||||||
|
|
||||||
logger: ?Logger,
|
logger: ?Logger,
|
||||||
|
|
||||||
|
/// Bank of Registers from other CPU Modes
|
||||||
|
const Bank = struct {
|
||||||
|
/// Storage for r13_<mode>, r14_<mode>
|
||||||
|
/// e.g. [r13, r14, r13_svc, r14_svc]
|
||||||
|
r: [2 * 6]u32,
|
||||||
|
|
||||||
|
/// Storage for R8_fiq -> R12_fiq and their normal counterparts
|
||||||
|
/// e.g [r[0 + 8], fiq_r[0 + 8], r[1 + 8], fiq_r[1 + 8]...]
|
||||||
|
fiq: [2 * 5]u32,
|
||||||
|
|
||||||
|
spsr: [5]PSR,
|
||||||
|
|
||||||
|
const Kind = enum(u1) {
|
||||||
|
R13 = 0,
|
||||||
|
R14,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn create() Bank {
|
||||||
|
return .{
|
||||||
|
.r = [_]u32{0x00} ** 12,
|
||||||
|
.fiq = [_]u32{0x00} ** 10,
|
||||||
|
.spsr = [_]PSR{.{ .raw = 0x0000_0000 }} ** 5,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn regIdx(mode: Mode, kind: Kind) 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 spsrIdx(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 fiqIdx(i: usize, mode: Mode) usize {
|
||||||
|
return (i * 2) + if (mode == .Fiq) @as(usize, 1) else 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub fn init(sched: *Scheduler, bus: *Bus, log_file: ?std.fs.File) Self {
|
pub fn init(sched: *Scheduler, bus: *Bus, log_file: ?std.fs.File) Self {
|
||||||
return Self{
|
return Self{
|
||||||
.r = [_]u32{0x00} ** 16,
|
.r = [_]u32{0x00} ** 16,
|
||||||
@@ -268,41 +311,11 @@ pub const Arm7tdmi = struct {
|
|||||||
.bus = bus,
|
.bus = bus,
|
||||||
.cpsr = .{ .raw = 0x0000_001F },
|
.cpsr = .{ .raw = 0x0000_001F },
|
||||||
.spsr = .{ .raw = 0x0000_0000 },
|
.spsr = .{ .raw = 0x0000_0000 },
|
||||||
.banked_fiq = [_]u32{0x00} ** 10,
|
.bank = Bank.create(),
|
||||||
.banked_r = [_]u32{0x00} ** 12,
|
|
||||||
.banked_spsr = [_]PSR{.{ .raw = 0x0000_0000 }} ** 5,
|
|
||||||
.logger = if (log_file) |file| Logger.init(file) else null,
|
.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 {
|
pub inline fn hasSPSR(self: *const Self) bool {
|
||||||
const mode = getModeChecked(self, self.cpsr.mode.read());
|
const mode = getModeChecked(self, self.cpsr.mode.read());
|
||||||
return switch (mode) {
|
return switch (mode) {
|
||||||
@@ -338,14 +351,14 @@ pub const Arm7tdmi = struct {
|
|||||||
switch (idx) {
|
switch (idx) {
|
||||||
8...12 => {
|
8...12 => {
|
||||||
if (current == .Fiq) {
|
if (current == .Fiq) {
|
||||||
self.banked_fiq[bankedFiqIdx(idx - 8, .User)] = value;
|
self.bank.fiq[Bank.fiqIdx(idx - 8, .User)] = value;
|
||||||
} else self.r[idx] = value;
|
} else self.r[idx] = value;
|
||||||
},
|
},
|
||||||
13, 14 => switch (current) {
|
13, 14 => switch (current) {
|
||||||
.User, .System => self.r[idx] = value,
|
.User, .System => self.r[idx] = value,
|
||||||
else => {
|
else => {
|
||||||
const kind = std.meta.intToEnum(BankedKind, idx - 13) catch unreachable;
|
const kind = std.meta.intToEnum(Bank.Kind, idx - 13) catch unreachable;
|
||||||
self.banked_r[bankedIdx(.User, kind)] = value;
|
self.bank.r[Bank.regIdx(.User, kind)] = value;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
else => self.r[idx] = value, // R0 -> R7 and R15
|
else => self.r[idx] = value, // R0 -> R7 and R15
|
||||||
@@ -356,12 +369,12 @@ pub const Arm7tdmi = struct {
|
|||||||
const current = getModeChecked(self, self.cpsr.mode.read());
|
const current = getModeChecked(self, self.cpsr.mode.read());
|
||||||
|
|
||||||
return switch (idx) {
|
return switch (idx) {
|
||||||
8...12 => if (current == .Fiq) self.banked_fiq[bankedFiqIdx(idx - 8, .User)] else self.r[idx],
|
8...12 => if (current == .Fiq) self.bank.fiq[Bank.fiqIdx(idx - 8, .User)] else self.r[idx],
|
||||||
13, 14 => switch (current) {
|
13, 14 => switch (current) {
|
||||||
.User, .System => self.r[idx],
|
.User, .System => self.r[idx],
|
||||||
else => blk: {
|
else => blk: {
|
||||||
const kind = std.meta.intToEnum(BankedKind, idx - 13) catch unreachable;
|
const kind = std.meta.intToEnum(Bank.Kind, idx - 13) catch unreachable;
|
||||||
break :blk self.banked_r[bankedIdx(.User, kind)];
|
break :blk self.bank.r[Bank.regIdx(.User, kind)];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
else => self.r[idx], // R0 -> R7 and R15
|
else => self.r[idx], // R0 -> R7 and R15
|
||||||
@@ -374,38 +387,38 @@ pub const Arm7tdmi = struct {
|
|||||||
// Bank R8 -> r12
|
// Bank R8 -> r12
|
||||||
var i: usize = 0;
|
var i: usize = 0;
|
||||||
while (i < 5) : (i += 1) {
|
while (i < 5) : (i += 1) {
|
||||||
self.banked_fiq[bankedFiqIdx(i, now)] = self.r[8 + i];
|
self.bank.fiq[Bank.fiqIdx(i, now)] = self.r[8 + i];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bank r13, r14, SPSR
|
// Bank r13, r14, SPSR
|
||||||
switch (now) {
|
switch (now) {
|
||||||
.User, .System => {
|
.User, .System => {
|
||||||
self.banked_r[bankedIdx(now, .R13)] = self.r[13];
|
self.bank.r[Bank.regIdx(now, .R13)] = self.r[13];
|
||||||
self.banked_r[bankedIdx(now, .R14)] = self.r[14];
|
self.bank.r[Bank.regIdx(now, .R14)] = self.r[14];
|
||||||
},
|
},
|
||||||
else => {
|
else => {
|
||||||
self.banked_r[bankedIdx(now, .R13)] = self.r[13];
|
self.bank.r[Bank.regIdx(now, .R13)] = self.r[13];
|
||||||
self.banked_r[bankedIdx(now, .R14)] = self.r[14];
|
self.bank.r[Bank.regIdx(now, .R14)] = self.r[14];
|
||||||
self.banked_spsr[bankedSpsrIndex(now)] = self.spsr;
|
self.bank.spsr[Bank.spsrIdx(now)] = self.spsr;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab R8 -> R12
|
// Grab R8 -> R12
|
||||||
i = 0;
|
i = 0;
|
||||||
while (i < 5) : (i += 1) {
|
while (i < 5) : (i += 1) {
|
||||||
self.r[8 + i] = self.banked_fiq[bankedFiqIdx(i, next)];
|
self.r[8 + i] = self.bank.fiq[Bank.fiqIdx(i, next)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab r13, r14, SPSR
|
// Grab r13, r14, SPSR
|
||||||
switch (next) {
|
switch (next) {
|
||||||
.User, .System => {
|
.User, .System => {
|
||||||
self.r[13] = self.banked_r[bankedIdx(next, .R13)];
|
self.r[13] = self.bank.r[Bank.regIdx(next, .R13)];
|
||||||
self.r[14] = self.banked_r[bankedIdx(next, .R14)];
|
self.r[14] = self.bank.r[Bank.regIdx(next, .R14)];
|
||||||
},
|
},
|
||||||
else => {
|
else => {
|
||||||
self.r[13] = self.banked_r[bankedIdx(next, .R13)];
|
self.r[13] = self.bank.r[Bank.regIdx(next, .R13)];
|
||||||
self.r[14] = self.banked_r[bankedIdx(next, .R14)];
|
self.r[14] = self.bank.r[Bank.regIdx(next, .R14)];
|
||||||
self.spsr = self.banked_spsr[bankedSpsrIndex(next)];
|
self.spsr = self.bank.spsr[Bank.spsrIdx(next)];
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,8 +439,8 @@ pub const Arm7tdmi = struct {
|
|||||||
self.r[13] = 0x0300_7F00;
|
self.r[13] = 0x0300_7F00;
|
||||||
self.r[15] = 0x0800_0000;
|
self.r[15] = 0x0800_0000;
|
||||||
|
|
||||||
self.banked_r[bankedIdx(.Irq, .R13)] = 0x0300_7FA0;
|
self.bank.r[Bank.regIdx(.Irq, .R13)] = 0x0300_7FA0;
|
||||||
self.banked_r[bankedIdx(.Supervisor, .R13)] = 0x0300_7FE0;
|
self.bank.r[Bank.regIdx(.Supervisor, .R13)] = 0x0300_7FE0;
|
||||||
|
|
||||||
// self.cpsr.raw = 0x6000001F;
|
// self.cpsr.raw = 0x6000001F;
|
||||||
self.cpsr.raw = 0x0000_001F;
|
self.cpsr.raw = 0x0000_001F;
|
||||||
@@ -457,29 +470,12 @@ pub const Arm7tdmi = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn stepDmaTransfer(self: *Self) bool {
|
pub fn stepDmaTransfer(self: *Self) bool {
|
||||||
const dma0 = &self.bus.dma[0];
|
comptime var i: usize = 0;
|
||||||
const dma1 = &self.bus.dma[1];
|
inline while (i < 4) : (i += 1) {
|
||||||
const dma2 = &self.bus.dma[2];
|
if (self.bus.dma[i].in_progress) {
|
||||||
const dma3 = &self.bus.dma[3];
|
self.bus.dma[i].step(self);
|
||||||
|
return true;
|
||||||
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;
|
return false;
|
||||||
@@ -534,10 +530,10 @@ pub const Arm7tdmi = struct {
|
|||||||
std.debug.print("R{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\n", .{ i, self.r[i], i_1, self.r[i_1], i_2, self.r[i_2], i_3, self.r[i_3] });
|
std.debug.print("R{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\n", .{ i, self.r[i], i_1, self.r[i_1], i_2, self.r[i_2], i_3, self.r[i_3] });
|
||||||
}
|
}
|
||||||
std.debug.print("cpsr: 0x{X:0>8} ", .{self.cpsr.raw});
|
std.debug.print("cpsr: 0x{X:0>8} ", .{self.cpsr.raw});
|
||||||
prettyPrintPsr(&self.cpsr);
|
self.cpsr.toString();
|
||||||
|
|
||||||
std.debug.print("spsr: 0x{X:0>8} ", .{self.spsr.raw});
|
std.debug.print("spsr: 0x{X:0>8} ", .{self.spsr.raw});
|
||||||
prettyPrintPsr(&self.spsr);
|
self.spsr.toString();
|
||||||
|
|
||||||
std.debug.print("pipeline: {??X:0>8}\n", .{self.pipe.stage});
|
std.debug.print("pipeline: {??X:0>8}\n", .{self.pipe.stage});
|
||||||
|
|
||||||
@@ -555,97 +551,31 @@ pub const Arm7tdmi = struct {
|
|||||||
|
|
||||||
std.debug.panic(format, args);
|
std.debug.panic(format, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prettyPrintPsr(psr: *const PSR) void {
|
|
||||||
std.debug.print("[", .{});
|
|
||||||
|
|
||||||
if (psr.n.read()) std.debug.print("N", .{}) else std.debug.print("-", .{});
|
|
||||||
if (psr.z.read()) std.debug.print("Z", .{}) else std.debug.print("-", .{});
|
|
||||||
if (psr.c.read()) std.debug.print("C", .{}) else std.debug.print("-", .{});
|
|
||||||
if (psr.v.read()) std.debug.print("V", .{}) else std.debug.print("-", .{});
|
|
||||||
if (psr.i.read()) std.debug.print("I", .{}) else std.debug.print("-", .{});
|
|
||||||
if (psr.f.read()) std.debug.print("F", .{}) else std.debug.print("-", .{});
|
|
||||||
if (psr.t.read()) std.debug.print("T", .{}) else std.debug.print("-", .{});
|
|
||||||
std.debug.print("|", .{});
|
|
||||||
if (getMode(psr.mode.read())) |mode| std.debug.print("{s}", .{modeString(mode)}) else std.debug.print("---", .{});
|
|
||||||
|
|
||||||
std.debug.print("]\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn modeString(mode: Mode) []const u8 {
|
|
||||||
return switch (mode) {
|
|
||||||
.User => "usr",
|
|
||||||
.Fiq => "fiq",
|
|
||||||
.Irq => "irq",
|
|
||||||
.Supervisor => "svc",
|
|
||||||
.Abort => "abt",
|
|
||||||
.Undefined => "und",
|
|
||||||
.System => "sys",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mgbaLog(self: *const Self, file: *const File, opcode: u32) !void {
|
|
||||||
const thumb_fmt = "{X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} cpsr: {X:0>8} | {X:0>4}:\n";
|
|
||||||
const arm_fmt = "{X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} cpsr: {X:0>8} | {X:0>8}:\n";
|
|
||||||
var buf: [0x100]u8 = [_]u8{0x00} ** 0x100; // this is larger than it needs to be
|
|
||||||
|
|
||||||
const r0 = self.r[0];
|
|
||||||
const r1 = self.r[1];
|
|
||||||
const r2 = self.r[2];
|
|
||||||
const r3 = self.r[3];
|
|
||||||
const r4 = self.r[4];
|
|
||||||
const r5 = self.r[5];
|
|
||||||
const r6 = self.r[6];
|
|
||||||
const r7 = self.r[7];
|
|
||||||
const r8 = self.r[8];
|
|
||||||
const r9 = self.r[9];
|
|
||||||
const r10 = self.r[10];
|
|
||||||
const r11 = self.r[11];
|
|
||||||
const r12 = self.r[12];
|
|
||||||
const r13 = self.r[13];
|
|
||||||
const r14 = self.r[14];
|
|
||||||
const r15 = self.r[15] -| if (self.cpsr.t.read()) 2 else @as(u32, 4);
|
|
||||||
|
|
||||||
const c_psr = self.cpsr.raw;
|
|
||||||
|
|
||||||
var log_str: []u8 = undefined;
|
|
||||||
if (self.cpsr.t.read()) {
|
|
||||||
if (opcode >> 11 == 0x1E) {
|
|
||||||
// Instruction 1 of a BL Opcode, print in ARM mode
|
|
||||||
const other_half = self.bus.debugRead(u16, self.r[15] - 2);
|
|
||||||
const bl_opcode = @as(u32, opcode) << 16 | other_half;
|
|
||||||
|
|
||||||
log_str = try std.fmt.bufPrint(&buf, arm_fmt, .{ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, c_psr, bl_opcode });
|
|
||||||
} else {
|
|
||||||
log_str = try std.fmt.bufPrint(&buf, thumb_fmt, .{ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, c_psr, opcode });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log_str = try std.fmt.bufPrint(&buf, arm_fmt, .{ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, c_psr, opcode });
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = try file.writeAll(log_str);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn checkCond(cpsr: PSR, cond: u4) bool {
|
const condition_lut = [_]u16{
|
||||||
return switch (cond) {
|
0xF0F0, // EQ - Equal
|
||||||
0x0 => cpsr.z.read(), // EQ - Equal
|
0x0F0F, // NE - Not Equal
|
||||||
0x1 => !cpsr.z.read(), // NE - Not equal
|
0xCCCC, // CS - Unsigned higher or same
|
||||||
0x2 => cpsr.c.read(), // CS - Unsigned higher or same
|
0x3333, // CC - Unsigned lower
|
||||||
0x3 => !cpsr.c.read(), // CC - Unsigned lower
|
0xFF00, // MI - Negative
|
||||||
0x4 => cpsr.n.read(), // MI - Negative
|
0x00FF, // PL - Positive or Zero
|
||||||
0x5 => !cpsr.n.read(), // PL - Positive or zero
|
0xAAAA, // VS - Overflow
|
||||||
0x6 => cpsr.v.read(), // VS - Overflow
|
0x5555, // VC - No Overflow
|
||||||
0x7 => !cpsr.v.read(), // VC - No overflow
|
0x0C0C, // HI - unsigned hierh
|
||||||
0x8 => cpsr.c.read() and !cpsr.z.read(), // HI - unsigned higher
|
0xF3F3, // LS - unsigned lower or same
|
||||||
0x9 => !cpsr.c.read() or cpsr.z.read(), // LS - unsigned lower or same
|
0xAA55, // GE - greater or equal
|
||||||
0xA => cpsr.n.read() == cpsr.v.read(), // GE - Greater or equal
|
0x55AA, // LT - less than
|
||||||
0xB => cpsr.n.read() != cpsr.v.read(), // LT - Less than
|
0x0A05, // GT - greater than
|
||||||
0xC => !cpsr.z.read() and (cpsr.n.read() == cpsr.v.read()), // GT - Greater than
|
0xF5FA, // LE - less than or equal
|
||||||
0xD => cpsr.z.read() or (cpsr.n.read() != cpsr.v.read()), // LE - Less than or equal
|
0xFFFF, // AL - always
|
||||||
0xE => true, // AL - Always
|
0x0000, // NV - never
|
||||||
0xF => false, // NV - Never (reserved in ARMv3 and up, but seems to have not changed?)
|
};
|
||||||
};
|
|
||||||
|
pub inline fn checkCond(cpsr: PSR, cond: u4) bool {
|
||||||
|
const flags = @truncate(u4, cpsr.raw >> 28);
|
||||||
|
|
||||||
|
return condition_lut[cond] & (@as(u16, 1) << flags) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Pipeline = struct {
|
const Pipeline = struct {
|
||||||
@@ -667,9 +597,7 @@ const Pipeline = struct {
|
|||||||
pub fn step(self: *Self, cpu: *Arm7tdmi, comptime T: type) ?u32 {
|
pub fn step(self: *Self, cpu: *Arm7tdmi, comptime T: type) ?u32 {
|
||||||
comptime std.debug.assert(T == u32 or T == u16);
|
comptime std.debug.assert(T == u32 or T == u16);
|
||||||
|
|
||||||
// FIXME: https://github.com/ziglang/zig/issues/12642
|
const opcode = self.stage[0];
|
||||||
var opcode = self.stage[0];
|
|
||||||
|
|
||||||
self.stage[0] = self.stage[1];
|
self.stage[0] = self.stage[1];
|
||||||
self.stage[1] = cpu.fetch(T, cpu.r[15]);
|
self.stage[1] = cpu.fetch(T, cpu.r[15]);
|
||||||
|
|
||||||
@@ -701,6 +629,22 @@ pub const PSR = extern union {
|
|||||||
z: Bit(u32, 30),
|
z: Bit(u32, 30),
|
||||||
n: Bit(u32, 31),
|
n: Bit(u32, 31),
|
||||||
raw: u32,
|
raw: u32,
|
||||||
|
|
||||||
|
fn toString(self: PSR) void {
|
||||||
|
std.debug.print("[", .{});
|
||||||
|
|
||||||
|
if (self.n.read()) std.debug.print("N", .{}) else std.debug.print("-", .{});
|
||||||
|
if (self.z.read()) std.debug.print("Z", .{}) else std.debug.print("-", .{});
|
||||||
|
if (self.c.read()) std.debug.print("C", .{}) else std.debug.print("-", .{});
|
||||||
|
if (self.v.read()) std.debug.print("V", .{}) else std.debug.print("-", .{});
|
||||||
|
if (self.i.read()) std.debug.print("I", .{}) else std.debug.print("-", .{});
|
||||||
|
if (self.f.read()) std.debug.print("F", .{}) else std.debug.print("-", .{});
|
||||||
|
if (self.t.read()) std.debug.print("T", .{}) else std.debug.print("-", .{});
|
||||||
|
std.debug.print("|", .{});
|
||||||
|
if (getMode(self.mode.read())) |m| std.debug.print("{s}", .{m.toString()}) else std.debug.print("---", .{});
|
||||||
|
|
||||||
|
std.debug.print("]\n", .{});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Mode = enum(u5) {
|
const Mode = enum(u5) {
|
||||||
@@ -711,11 +655,18 @@ const Mode = enum(u5) {
|
|||||||
Abort = 0b10111,
|
Abort = 0b10111,
|
||||||
Undefined = 0b11011,
|
Undefined = 0b11011,
|
||||||
System = 0b11111,
|
System = 0b11111,
|
||||||
};
|
|
||||||
|
|
||||||
const BankedKind = enum(u1) {
|
fn toString(self: Mode) []const u8 {
|
||||||
R13 = 0,
|
return switch (self) {
|
||||||
R14,
|
.User => "usr",
|
||||||
|
.Fiq => "fiq",
|
||||||
|
.Irq => "irq",
|
||||||
|
.Supervisor => "svc",
|
||||||
|
.Abort => "abt",
|
||||||
|
.Undefined => "und",
|
||||||
|
.System => "sys",
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fn getMode(bits: u5) ?Mode {
|
fn getMode(bits: u5) ?Mode {
|
||||||
|
@@ -57,7 +57,6 @@ pub fn blockDataTransfer(comptime P: bool, comptime U: bool, comptime S: bool, c
|
|||||||
cpu.r[15] = bus.read(u32, und_addr);
|
cpu.r[15] = bus.read(u32, und_addr);
|
||||||
cpu.pipe.reload(cpu);
|
cpu.pipe.reload(cpu);
|
||||||
} else {
|
} else {
|
||||||
// FIXME: Should r15 on write be +12 ahead?
|
|
||||||
bus.write(u32, und_addr, cpu.r[15] + 4);
|
bus.write(u32, und_addr, cpu.r[15] + 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
const Bus = @import("../../Bus.zig");
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
const Bus = @import("../../Bus.zig");
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||||
@@ -35,11 +33,8 @@ pub fn halfAndSignedDataTransfer(comptime P: bool, comptime U: bool, comptime I:
|
|||||||
},
|
},
|
||||||
0b11 => {
|
0b11 => {
|
||||||
// LDRSH
|
// LDRSH
|
||||||
result = if (address & 1 == 1) blk: {
|
const value = bus.read(u16, address);
|
||||||
break :blk sext(u32, u8, bus.read(u8, address));
|
result = if (address & 1 == 1) sext(u32, u8, @truncate(u8, value >> 8)) else sext(u32, u16, value);
|
||||||
} else blk: {
|
|
||||||
break :blk sext(u32, u16, bus.read(u16, address));
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
0b00 => unreachable, // SWP
|
0b00 => unreachable, // SWP
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
const Bus = @import("../../Bus.zig");
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||||
|
@@ -1,6 +1,3 @@
|
|||||||
const std = @import("std");
|
|
||||||
const util = @import("../../../util.zig");
|
|
||||||
|
|
||||||
const shifter = @import("../barrel_shifter.zig");
|
const shifter = @import("../barrel_shifter.zig");
|
||||||
const Bus = @import("../../Bus.zig");
|
const Bus = @import("../../Bus.zig");
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||||
const CPSR = @import("../cpu.zig").PSR;
|
const CPSR = @import("../cpu.zig").PSR;
|
||||||
|
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
const Bus = @import("../../Bus.zig");
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Bus = @import("../../Bus.zig");
|
const Bus = @import("../../Bus.zig");
|
||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
||||||
@@ -46,11 +44,8 @@ pub fn fmt78(comptime op: u2, comptime T: bool) InstrFn {
|
|||||||
},
|
},
|
||||||
0b11 => {
|
0b11 => {
|
||||||
// LDRSH
|
// LDRSH
|
||||||
cpu.r[rd] = if (address & 1 == 1) blk: {
|
const value = bus.read(u16, address);
|
||||||
break :blk sext(u32, u8, bus.read(u8, address));
|
cpu.r[rd] = if (address & 1 == 1) sext(u32, u8, @truncate(u8, value >> 8)) else sext(u32, u16, value);
|
||||||
} else blk: {
|
|
||||||
break :blk sext(u32, u16, bus.read(u16, address));
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@@ -2,29 +2,30 @@ const std = @import("std");
|
|||||||
const SDL = @import("sdl2");
|
const SDL = @import("sdl2");
|
||||||
const config = @import("../config.zig");
|
const config = @import("../config.zig");
|
||||||
|
|
||||||
const Bus = @import("Bus.zig");
|
|
||||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
const Scheduler = @import("scheduler.zig").Scheduler;
|
||||||
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
||||||
const FpsTracker = @import("../util.zig").FpsTracker;
|
const FpsTracker = @import("../util.zig").FpsTracker;
|
||||||
const FilePaths = @import("../util.zig").FilePaths;
|
const RingBuffer = @import("../util.zig").RingBuffer;
|
||||||
|
|
||||||
const Timer = std.time.Timer;
|
const Timer = std.time.Timer;
|
||||||
const Thread = std.Thread;
|
|
||||||
const Atomic = std.atomic.Atomic;
|
const Atomic = std.atomic.Atomic;
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
// 228 Lines which consist of 308 dots (which are 4 cycles long)
|
/// 4 Cycles in 1 dot
|
||||||
const cycles_per_frame: u64 = 228 * (308 * 4); //280896
|
const cycles_per_dot = 4;
|
||||||
const clock_rate: u64 = 1 << 24; // 16.78MHz
|
|
||||||
|
|
||||||
// TODO: Don't truncate this, be more accurate w/ timing
|
/// The GBA draws 228 Horizontal which each consist 308 dots
|
||||||
// 59.6046447754ns (truncated to just 59ns)
|
/// (note: not all lines are visible)
|
||||||
const clock_period: u64 = std.time.ns_per_s / clock_rate;
|
const cycles_per_frame = 228 * (308 * cycles_per_dot); //280896
|
||||||
const frame_period = (clock_period * cycles_per_frame);
|
|
||||||
|
|
||||||
// 59.7275005696Hz
|
/// The GBA ARM7TDMI runs at 2^24 Hz
|
||||||
pub const frame_rate = @intToFloat(f64, std.time.ns_per_s) /
|
const clock_rate = 1 << 24; // 16.78MHz
|
||||||
((@intToFloat(f64, std.time.ns_per_s) / @intToFloat(f64, clock_rate)) * @intToFloat(f64, cycles_per_frame));
|
|
||||||
|
/// The # of nanoseconds a frame should take
|
||||||
|
const frame_period = (std.time.ns_per_s * cycles_per_frame) / clock_rate;
|
||||||
|
|
||||||
|
/// Exact Value: 59.7275005696Hz
|
||||||
|
/// The inverse of the frame period
|
||||||
|
pub const frame_rate: f64 = @intToFloat(f64, clock_rate) / cycles_per_frame;
|
||||||
|
|
||||||
const log = std.log.scoped(.Emulation);
|
const log = std.log.scoped(.Emulation);
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ const RunKind = enum {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub fn run(quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: *FpsTracker) void {
|
pub fn run(quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: *FpsTracker) void {
|
||||||
const audio_sync = config.config().guest.audio_sync;
|
const audio_sync = config.config().guest.audio_sync and !config.config().host.mute;
|
||||||
if (audio_sync) log.info("Audio sync enabled", .{});
|
if (audio_sync) log.info("Audio sync enabled", .{});
|
||||||
|
|
||||||
if (config.config().guest.video_sync) {
|
if (config.config().guest.video_sync) {
|
||||||
@@ -56,9 +57,9 @@ fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), schedule
|
|||||||
.Unlimited, .UnlimitedFPS => {
|
.Unlimited, .UnlimitedFPS => {
|
||||||
log.info("Emulation w/out video sync", .{});
|
log.info("Emulation w/out video sync", .{});
|
||||||
|
|
||||||
while (!quit.load(.SeqCst)) {
|
while (!quit.load(.Monotonic)) {
|
||||||
runFrame(scheduler, cpu);
|
runFrame(scheduler, cpu);
|
||||||
audioSync(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
|
audioSync(audio_sync, &cpu.bus.apu.sample_queue);
|
||||||
|
|
||||||
if (kind == .UnlimitedFPS) tracker.?.tick();
|
if (kind == .UnlimitedFPS) tracker.?.tick();
|
||||||
}
|
}
|
||||||
@@ -68,7 +69,7 @@ fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), schedule
|
|||||||
var timer = Timer.start() catch @panic("failed to initalize std.timer.Timer");
|
var timer = Timer.start() catch @panic("failed to initalize std.timer.Timer");
|
||||||
var wake_time: u64 = frame_period;
|
var wake_time: u64 = frame_period;
|
||||||
|
|
||||||
while (!quit.load(.SeqCst)) {
|
while (!quit.load(.Monotonic)) {
|
||||||
runFrame(scheduler, cpu);
|
runFrame(scheduler, cpu);
|
||||||
const new_wake_time = videoSync(&timer, wake_time);
|
const new_wake_time = videoSync(&timer, wake_time);
|
||||||
|
|
||||||
@@ -77,7 +78,7 @@ fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), schedule
|
|||||||
// 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, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
|
audioSync(audio_sync, &cpu.bus.apu.sample_queue);
|
||||||
if (!audio_sync) spinLoop(&timer, wake_time);
|
if (!audio_sync) spinLoop(&timer, wake_time);
|
||||||
wake_time = new_wake_time;
|
wake_time = new_wake_time;
|
||||||
|
|
||||||
@@ -104,21 +105,13 @@ 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, sample_queue: *RingBuffer(u16)) void {
|
||||||
const sample_size = 2 * @sizeOf(u16);
|
comptime std.debug.assert(@import("../platform.zig").sample_format == SDL.AUDIO_U16);
|
||||||
const max_buf_size: c_int = 0x400;
|
// const sample_size = 2 * @sizeOf(u16);
|
||||||
|
// const max_buf_size: c_int = 0x400;
|
||||||
|
|
||||||
// Determine whether the APU is busy right at this moment
|
_ = audio_sync;
|
||||||
var still_full: bool = SDL.SDL_AudioStreamAvailable(stream) > sample_size * if (is_buffer_full.*) max_buf_size >> 1 else max_buf_size;
|
_ = sample_queue;
|
||||||
defer is_buffer_full.* = still_full; // Update APU Busy status right before exiting scope
|
|
||||||
|
|
||||||
// If Busy is false, there's no need to sync here
|
|
||||||
if (!still_full) return;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
still_full = SDL.SDL_AudioStreamAvailable(stream) > sample_size * max_buf_size >> 1;
|
|
||||||
if (!audio_sync or !still_full) break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn videoSync(timer: *Timer, wake_time: u64) u64 {
|
fn videoSync(timer: *Timer, wake_time: u64) u64 {
|
||||||
@@ -132,11 +125,10 @@ 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.
|
||||||
const ns_late = timestamp -| wake_time;
|
var ns_late = timestamp -| wake_time;
|
||||||
|
|
||||||
// If we're more than a frame late, skip the rest of this loop
|
// 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
|
||||||
@@ -144,15 +136,18 @@ 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;
|
||||||
|
|
||||||
// // Employ several sleep calls in periods of 10ms
|
const step = 2 * std.time.ns_per_ms; // Granularity of 2ms
|
||||||
// // By doing this the behaviour should average out to be
|
const times = sleep_for / step;
|
||||||
// // more consistent
|
var i: usize = 0;
|
||||||
// const loop_count = sleep_for / step; // How many groups of 10ms
|
|
||||||
|
|
||||||
// var i: usize = 0;
|
while (i < times) : (i += 1) {
|
||||||
// while (i < loop_count) : (i += 1) std.time.sleep(step);
|
std.time.sleep(step);
|
||||||
|
|
||||||
std.time.sleep(sleep_for);
|
// Upon wakeup, check to see if this particular sleep was longer than expected
|
||||||
|
// if so we should exit early, but probably not skip a whole frame period
|
||||||
|
ns_late = timer.read() -| wake_time;
|
||||||
|
if (ns_late > frame_period) return null;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
358
src/core/ppu.zig
358
src/core/ppu.zig
@@ -1,7 +1,7 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const io = @import("bus/io.zig");
|
const io = @import("bus/io.zig");
|
||||||
|
const util = @import("../util.zig");
|
||||||
|
|
||||||
const EventKind = @import("scheduler.zig").EventKind;
|
|
||||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
const Scheduler = @import("scheduler.zig").Scheduler;
|
||||||
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
||||||
|
|
||||||
@@ -10,15 +10,229 @@ const Bitfield = @import("bitfield").Bitfield;
|
|||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const log = std.log.scoped(.PPU);
|
const log = std.log.scoped(.PPU);
|
||||||
const pollDmaOnBlank = @import("bus/dma.zig").pollDmaOnBlank;
|
|
||||||
|
|
||||||
/// This is used to generate byuu / Talurabi's Color Correction algorithm
|
const getHalf = util.getHalf;
|
||||||
const COLOUR_LUT = genColourLut();
|
const setHalf = util.setHalf;
|
||||||
|
const setQuart = util.setQuart;
|
||||||
|
const pollDmaOnBlank = @import("bus/dma.zig").pollDmaOnBlank;
|
||||||
|
|
||||||
pub const width = 240;
|
pub const width = 240;
|
||||||
pub const height = 160;
|
pub const height = 160;
|
||||||
pub const framebuf_pitch = width * @sizeOf(u32);
|
pub const framebuf_pitch = width * @sizeOf(u32);
|
||||||
|
|
||||||
|
pub fn read(comptime T: type, ppu: *const Ppu, addr: u32) ?T {
|
||||||
|
const byte_addr = @truncate(u8, addr);
|
||||||
|
|
||||||
|
return switch (T) {
|
||||||
|
u32 => switch (byte_addr) {
|
||||||
|
0x00 => ppu.dispcnt.raw, // Green Swap is in high half-word
|
||||||
|
0x04 => @as(T, ppu.vcount.raw) << 16 | ppu.dispstat.raw,
|
||||||
|
0x08 => @as(T, ppu.bg[1].bg1Cnt()) << 16 | ppu.bg[0].bg0Cnt(),
|
||||||
|
0x0C => @as(T, ppu.bg[3].cnt.raw) << 16 | ppu.bg[2].cnt.raw,
|
||||||
|
0x10, 0x14, 0x18, 0x1C => null, // BGXHOFS/VOFS
|
||||||
|
0x20, 0x24, 0x28, 0x2C => null, // BG2 Rot/Scaling
|
||||||
|
0x30, 0x34, 0x38, 0x3C => null, // BG3 Rot/Scaling
|
||||||
|
0x40, 0x44 => null, // WINXH/V Registers
|
||||||
|
0x48 => @as(T, ppu.win.getOut()) << 16 | ppu.win.getIn(),
|
||||||
|
0x4C => null, // MOSAIC, undefined in high byte
|
||||||
|
0x50 => @as(T, ppu.bld.getAlpha()) << 16 | ppu.bld.getCnt(),
|
||||||
|
0x54 => null, // BLDY, undefined in high half-wrd
|
||||||
|
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
||||||
|
},
|
||||||
|
u16 => switch (byte_addr) {
|
||||||
|
0x00 => ppu.dispcnt.raw,
|
||||||
|
0x02 => null, // Green Swap
|
||||||
|
0x04 => ppu.dispstat.raw,
|
||||||
|
0x06 => ppu.vcount.raw,
|
||||||
|
0x08 => ppu.bg[0].bg0Cnt(),
|
||||||
|
0x0A => ppu.bg[1].bg1Cnt(),
|
||||||
|
0x0C => ppu.bg[2].cnt.raw,
|
||||||
|
0x0E => ppu.bg[3].cnt.raw,
|
||||||
|
0x10, 0x12, 0x14, 0x16, 0x18, 0x1A, 0x1C, 0x1E => null, // BGXHOFS/VOFS
|
||||||
|
0x20, 0x22, 0x24, 0x26, 0x28, 0x2A, 0x2C, 0x2E => null, // BG2 Rot/Scaling
|
||||||
|
0x30, 0x32, 0x34, 0x36, 0x38, 0x3A, 0x3C, 0x3E => null, // BG3 Rot/Scaling
|
||||||
|
0x40, 0x42, 0x44, 0x46 => null, // WINXH/V Registers
|
||||||
|
0x48 => ppu.win.getIn(),
|
||||||
|
0x4A => ppu.win.getOut(),
|
||||||
|
0x4C => null, // MOSAIC
|
||||||
|
0x4E => null,
|
||||||
|
0x50 => ppu.bld.getCnt(),
|
||||||
|
0x52 => ppu.bld.getAlpha(),
|
||||||
|
0x54 => null, // BLDY
|
||||||
|
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
||||||
|
},
|
||||||
|
u8 => switch (byte_addr) {
|
||||||
|
0x00, 0x01 => @truncate(T, ppu.dispcnt.raw >> getHalf(byte_addr)),
|
||||||
|
0x02, 0x03 => null,
|
||||||
|
0x04, 0x05 => @truncate(T, ppu.dispstat.raw >> getHalf(byte_addr)),
|
||||||
|
0x06, 0x07 => @truncate(T, ppu.vcount.raw >> getHalf(byte_addr)),
|
||||||
|
0x08, 0x09 => @truncate(T, ppu.bg[0].bg0Cnt() >> getHalf(byte_addr)),
|
||||||
|
0x0A, 0x0B => @truncate(T, ppu.bg[1].bg1Cnt() >> getHalf(byte_addr)),
|
||||||
|
0x0C, 0x0D => @truncate(T, ppu.bg[2].cnt.raw >> getHalf(byte_addr)),
|
||||||
|
0x0E, 0x0F => @truncate(T, ppu.bg[3].cnt.raw >> getHalf(byte_addr)),
|
||||||
|
0x10...0x1F => null, // BGXHOFS/VOFS
|
||||||
|
0x20...0x2F => null, // BG2 Rot/Scaling
|
||||||
|
0x30...0x3F => null, // BG3 Rot/Scaling
|
||||||
|
0x40...0x47 => null, // WINXH/V Registers
|
||||||
|
0x48, 0x49 => @truncate(T, ppu.win.getIn() >> getHalf(byte_addr)),
|
||||||
|
0x4A, 0x4B => @truncate(T, ppu.win.getOut() >> getHalf(byte_addr)),
|
||||||
|
0x4C, 0x4D => null, // MOSAIC
|
||||||
|
0x4E, 0x4F => null,
|
||||||
|
0x50, 0x51 => @truncate(T, ppu.bld.getCnt() >> getHalf(byte_addr)),
|
||||||
|
0x52, 0x53 => @truncate(T, ppu.bld.getAlpha() >> getHalf(byte_addr)),
|
||||||
|
0x54, 0x55 => null, // BLDY
|
||||||
|
else => util.io.read.err(T, log, "unexpected {} read from 0x{X:0>8}", .{ T, addr }),
|
||||||
|
},
|
||||||
|
else => @compileError("PPU: Unsupported read width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(comptime T: type, ppu: *Ppu, addr: u32, value: T) void {
|
||||||
|
const byte_addr = @truncate(u8, addr); // prefixed with 0x0400_00
|
||||||
|
|
||||||
|
switch (T) {
|
||||||
|
u32 => switch (byte_addr) {
|
||||||
|
0x00 => ppu.dispcnt.raw = @truncate(u16, value),
|
||||||
|
0x04 => {
|
||||||
|
ppu.dispstat.set(@truncate(u16, value));
|
||||||
|
ppu.vcount.raw = @truncate(u16, value >> 16);
|
||||||
|
},
|
||||||
|
0x08 => ppu.setAdjCnts(0, value),
|
||||||
|
0x0C => ppu.setAdjCnts(2, value),
|
||||||
|
|
||||||
|
0x10 => ppu.setBgOffsets(0, value),
|
||||||
|
0x14 => ppu.setBgOffsets(1, value),
|
||||||
|
0x18 => ppu.setBgOffsets(2, value),
|
||||||
|
0x1C => ppu.setBgOffsets(3, value),
|
||||||
|
|
||||||
|
0x20 => ppu.aff_bg[0].writePaPb(value),
|
||||||
|
0x24 => ppu.aff_bg[0].writePcPd(value),
|
||||||
|
0x28 => ppu.aff_bg[0].setX(ppu.dispstat.vblank.read(), value),
|
||||||
|
0x2C => ppu.aff_bg[0].setY(ppu.dispstat.vblank.read(), value),
|
||||||
|
|
||||||
|
0x30 => ppu.aff_bg[1].writePaPb(value),
|
||||||
|
0x34 => ppu.aff_bg[1].writePcPd(value),
|
||||||
|
0x38 => ppu.aff_bg[1].setX(ppu.dispstat.vblank.read(), value),
|
||||||
|
0x3C => ppu.aff_bg[1].setY(ppu.dispstat.vblank.read(), value),
|
||||||
|
|
||||||
|
0x40 => ppu.win.setH(value),
|
||||||
|
0x44 => ppu.win.setV(value),
|
||||||
|
0x48 => ppu.win.setIo(value),
|
||||||
|
0x4C => log.debug("Wrote 0x{X:0>8} to MOSAIC", .{value}),
|
||||||
|
|
||||||
|
0x50 => {
|
||||||
|
ppu.bld.cnt.raw = @truncate(u16, value);
|
||||||
|
ppu.bld.alpha.raw = @truncate(u16, value >> 16);
|
||||||
|
},
|
||||||
|
0x54 => ppu.bld.y.raw = @truncate(u16, value),
|
||||||
|
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||||
|
},
|
||||||
|
u16 => switch (byte_addr) {
|
||||||
|
0x00 => ppu.dispcnt.raw = value,
|
||||||
|
0x02 => {}, // Green Swap
|
||||||
|
0x04 => ppu.dispstat.set(value),
|
||||||
|
0x06 => {}, // VCOUNT
|
||||||
|
|
||||||
|
0x08 => ppu.bg[0].cnt.raw = value,
|
||||||
|
0x0A => ppu.bg[1].cnt.raw = value,
|
||||||
|
0x0C => ppu.bg[2].cnt.raw = value,
|
||||||
|
0x0E => ppu.bg[3].cnt.raw = value,
|
||||||
|
|
||||||
|
0x10 => ppu.bg[0].hofs.raw = value, // TODO: Don't write out every HOFS / VOFS?
|
||||||
|
0x12 => ppu.bg[0].vofs.raw = value,
|
||||||
|
0x14 => ppu.bg[1].hofs.raw = value,
|
||||||
|
0x16 => ppu.bg[1].vofs.raw = value,
|
||||||
|
0x18 => ppu.bg[2].hofs.raw = value,
|
||||||
|
0x1A => ppu.bg[2].vofs.raw = value,
|
||||||
|
0x1C => ppu.bg[3].hofs.raw = value,
|
||||||
|
0x1E => ppu.bg[3].vofs.raw = value,
|
||||||
|
|
||||||
|
0x20 => ppu.aff_bg[0].pa = @bitCast(i16, value),
|
||||||
|
0x22 => ppu.aff_bg[0].pb = @bitCast(i16, value),
|
||||||
|
0x24 => ppu.aff_bg[0].pc = @bitCast(i16, value),
|
||||||
|
0x26 => ppu.aff_bg[0].pd = @bitCast(i16, value),
|
||||||
|
0x28, 0x2A => ppu.aff_bg[0].x = @bitCast(i32, setHalf(u32, @bitCast(u32, ppu.aff_bg[0].x), byte_addr, value)),
|
||||||
|
0x2C, 0x2E => ppu.aff_bg[0].y = @bitCast(i32, setHalf(u32, @bitCast(u32, ppu.aff_bg[0].y), byte_addr, value)),
|
||||||
|
|
||||||
|
0x30 => ppu.aff_bg[1].pa = @bitCast(i16, value),
|
||||||
|
0x32 => ppu.aff_bg[1].pb = @bitCast(i16, value),
|
||||||
|
0x34 => ppu.aff_bg[1].pc = @bitCast(i16, value),
|
||||||
|
0x36 => ppu.aff_bg[1].pd = @bitCast(i16, value),
|
||||||
|
0x38, 0x3A => ppu.aff_bg[1].x = @bitCast(i32, setHalf(u32, @bitCast(u32, ppu.aff_bg[1].x), byte_addr, value)),
|
||||||
|
0x3C, 0x3E => ppu.aff_bg[1].y = @bitCast(i32, setHalf(u32, @bitCast(u32, ppu.aff_bg[1].y), byte_addr, value)),
|
||||||
|
|
||||||
|
0x40 => ppu.win.h[0].raw = value,
|
||||||
|
0x42 => ppu.win.h[1].raw = value,
|
||||||
|
0x44 => ppu.win.v[0].raw = value,
|
||||||
|
0x46 => ppu.win.v[1].raw = value,
|
||||||
|
0x48 => ppu.win.in.raw = value,
|
||||||
|
0x4A => ppu.win.out.raw = value,
|
||||||
|
0x4C => log.debug("Wrote 0x{X:0>4} to MOSAIC", .{value}),
|
||||||
|
0x4E => {},
|
||||||
|
|
||||||
|
0x50 => ppu.bld.cnt.raw = value,
|
||||||
|
0x52 => ppu.bld.alpha.raw = value,
|
||||||
|
0x54 => ppu.bld.y.raw = value,
|
||||||
|
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||||
|
},
|
||||||
|
u8 => switch (byte_addr) {
|
||||||
|
0x00, 0x01 => ppu.dispcnt.raw = setHalf(u16, ppu.dispcnt.raw, byte_addr, value),
|
||||||
|
0x02, 0x03 => {}, // Green Swap
|
||||||
|
0x04, 0x05 => ppu.dispstat.set(setHalf(u16, ppu.dispstat.raw, byte_addr, value)),
|
||||||
|
0x06, 0x07 => {}, // VCOUNT
|
||||||
|
|
||||||
|
// BGXCNT
|
||||||
|
0x08, 0x09 => ppu.bg[0].cnt.raw = setHalf(u16, ppu.bg[0].cnt.raw, byte_addr, value),
|
||||||
|
0x0A, 0x0B => ppu.bg[1].cnt.raw = setHalf(u16, ppu.bg[1].cnt.raw, byte_addr, value),
|
||||||
|
0x0C, 0x0D => ppu.bg[2].cnt.raw = setHalf(u16, ppu.bg[2].cnt.raw, byte_addr, value),
|
||||||
|
0x0E, 0x0F => ppu.bg[3].cnt.raw = setHalf(u16, ppu.bg[3].cnt.raw, byte_addr, value),
|
||||||
|
|
||||||
|
// BGX HOFS/VOFS
|
||||||
|
0x10, 0x11 => ppu.bg[0].hofs.raw = setHalf(u16, ppu.bg[0].hofs.raw, byte_addr, value),
|
||||||
|
0x12, 0x13 => ppu.bg[0].vofs.raw = setHalf(u16, ppu.bg[0].vofs.raw, byte_addr, value),
|
||||||
|
0x14, 0x15 => ppu.bg[1].hofs.raw = setHalf(u16, ppu.bg[1].hofs.raw, byte_addr, value),
|
||||||
|
0x16, 0x17 => ppu.bg[1].vofs.raw = setHalf(u16, ppu.bg[1].vofs.raw, byte_addr, value),
|
||||||
|
0x18, 0x19 => ppu.bg[2].hofs.raw = setHalf(u16, ppu.bg[2].hofs.raw, byte_addr, value),
|
||||||
|
0x1A, 0x1B => ppu.bg[2].vofs.raw = setHalf(u16, ppu.bg[2].vofs.raw, byte_addr, value),
|
||||||
|
0x1C, 0x1D => ppu.bg[3].hofs.raw = setHalf(u16, ppu.bg[3].hofs.raw, byte_addr, value),
|
||||||
|
0x1E, 0x1F => ppu.bg[3].vofs.raw = setHalf(u16, ppu.bg[3].vofs.raw, byte_addr, value),
|
||||||
|
|
||||||
|
// BG2 Rot/Scaling
|
||||||
|
0x20, 0x21 => ppu.aff_bg[0].pa = @bitCast(i16, setHalf(u16, @bitCast(u16, ppu.aff_bg[0].pa), byte_addr, value)),
|
||||||
|
0x22, 0x23 => ppu.aff_bg[0].pb = @bitCast(i16, setHalf(u16, @bitCast(u16, ppu.aff_bg[0].pb), byte_addr, value)),
|
||||||
|
0x24, 0x25 => ppu.aff_bg[0].pc = @bitCast(i16, setHalf(u16, @bitCast(u16, ppu.aff_bg[0].pc), byte_addr, value)),
|
||||||
|
0x26, 0x27 => ppu.aff_bg[0].pd = @bitCast(i16, setHalf(u16, @bitCast(u16, ppu.aff_bg[0].pd), byte_addr, value)),
|
||||||
|
0x28, 0x29, 0x2A, 0x2B => ppu.aff_bg[0].x = @bitCast(i32, setQuart(@bitCast(u32, ppu.aff_bg[0].x), byte_addr, value)),
|
||||||
|
0x2C, 0x2D, 0x2E, 0x2F => ppu.aff_bg[0].y = @bitCast(i32, setQuart(@bitCast(u32, ppu.aff_bg[0].y), byte_addr, value)),
|
||||||
|
|
||||||
|
// BG3 Rot/Scaling
|
||||||
|
0x30, 0x31 => ppu.aff_bg[1].pa = @bitCast(i16, setHalf(u16, @bitCast(u16, ppu.aff_bg[1].pa), byte_addr, value)),
|
||||||
|
0x32, 0x33 => ppu.aff_bg[1].pb = @bitCast(i16, setHalf(u16, @bitCast(u16, ppu.aff_bg[1].pb), byte_addr, value)),
|
||||||
|
0x34, 0x35 => ppu.aff_bg[1].pc = @bitCast(i16, setHalf(u16, @bitCast(u16, ppu.aff_bg[1].pc), byte_addr, value)),
|
||||||
|
0x36, 0x37 => ppu.aff_bg[1].pd = @bitCast(i16, setHalf(u16, @bitCast(u16, ppu.aff_bg[1].pd), byte_addr, value)),
|
||||||
|
0x38, 0x39, 0x3A, 0x3B => ppu.aff_bg[1].x = @bitCast(i32, setQuart(@bitCast(u32, ppu.aff_bg[1].x), byte_addr, value)),
|
||||||
|
0x3C, 0x3D, 0x3E, 0x3F => ppu.aff_bg[1].y = @bitCast(i32, setQuart(@bitCast(u32, ppu.aff_bg[1].y), byte_addr, value)),
|
||||||
|
|
||||||
|
// Window
|
||||||
|
0x40, 0x41 => ppu.win.h[0].raw = setHalf(u16, ppu.win.h[0].raw, byte_addr, value),
|
||||||
|
0x42, 0x43 => ppu.win.h[1].raw = setHalf(u16, ppu.win.h[1].raw, byte_addr, value),
|
||||||
|
0x44, 0x45 => ppu.win.v[0].raw = setHalf(u16, ppu.win.v[0].raw, byte_addr, value),
|
||||||
|
0x46, 0x47 => ppu.win.v[1].raw = setHalf(u16, ppu.win.v[1].raw, byte_addr, value),
|
||||||
|
0x48, 0x49 => ppu.win.in.raw = setHalf(u16, ppu.win.in.raw, byte_addr, value),
|
||||||
|
0x4A, 0x4B => ppu.win.out.raw = setHalf(u16, ppu.win.out.raw, byte_addr, value),
|
||||||
|
0x4C, 0x4D => log.debug("Wrote 0x{X:0>2} to MOSAIC", .{value}),
|
||||||
|
0x4E, 0x4F => {},
|
||||||
|
|
||||||
|
// Blending
|
||||||
|
0x50, 0x51 => ppu.bld.cnt.raw = setHalf(u16, ppu.bld.cnt.raw, byte_addr, value),
|
||||||
|
0x52, 0x53 => ppu.bld.alpha.raw = setHalf(u16, ppu.bld.alpha.raw, byte_addr, value),
|
||||||
|
0x54, 0x55 => ppu.bld.y.raw = setHalf(u16, ppu.bld.y.raw, byte_addr, value),
|
||||||
|
else => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||||
|
},
|
||||||
|
else => @compileError("PPU: Unsupported write width"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub const Ppu = struct {
|
pub const Ppu = struct {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
@@ -32,9 +246,7 @@ pub const Ppu = struct {
|
|||||||
dispstat: io.DisplayStatus,
|
dispstat: io.DisplayStatus,
|
||||||
vcount: io.VCount,
|
vcount: io.VCount,
|
||||||
|
|
||||||
bldcnt: io.BldCnt,
|
bld: Blend,
|
||||||
bldalpha: io.BldAlpha,
|
|
||||||
bldy: io.BldY,
|
|
||||||
|
|
||||||
vram: Vram,
|
vram: Vram,
|
||||||
palette: Palette,
|
palette: Palette,
|
||||||
@@ -65,12 +277,10 @@ pub const Ppu = struct {
|
|||||||
.win = Window.init(),
|
.win = Window.init(),
|
||||||
.bg = [_]Background{Background.init()} ** 4,
|
.bg = [_]Background{Background.init()} ** 4,
|
||||||
.aff_bg = [_]AffineBackground{AffineBackground.init()} ** 2,
|
.aff_bg = [_]AffineBackground{AffineBackground.init()} ** 2,
|
||||||
|
.bld = Blend.create(),
|
||||||
.dispcnt = .{ .raw = 0x0000 },
|
.dispcnt = .{ .raw = 0x0000 },
|
||||||
.dispstat = .{ .raw = 0x0000 },
|
.dispstat = .{ .raw = 0x0000 },
|
||||||
.vcount = .{ .raw = 0x0000 },
|
.vcount = .{ .raw = 0x0000 },
|
||||||
.bldcnt = .{ .raw = 0x0000 },
|
|
||||||
.bldalpha = .{ .raw = 0x0000 },
|
|
||||||
.bldy = .{ .raw = 0x0000 },
|
|
||||||
|
|
||||||
.scanline = try Scanline.init(allocator),
|
.scanline = try Scanline.init(allocator),
|
||||||
.scanline_sprites = sprites,
|
.scanline_sprites = sprites,
|
||||||
@@ -165,7 +375,7 @@ pub const Ppu = struct {
|
|||||||
const x = (sprite.x() +% i) % width;
|
const x = (sprite.x() +% i) % width;
|
||||||
const ix = @bitCast(i9, x);
|
const ix = @bitCast(i9, x);
|
||||||
|
|
||||||
if (!shouldDrawSprite(self.bldcnt, &self.scanline, x)) continue;
|
if (!shouldDrawSprite(self.bld.cnt, &self.scanline, x)) continue;
|
||||||
|
|
||||||
const sprite_start = sprite.x();
|
const sprite_start = sprite.x();
|
||||||
const isprite_start = @bitCast(i9, sprite_start);
|
const isprite_start = @bitCast(i9, sprite_start);
|
||||||
@@ -194,7 +404,7 @@ pub const Ppu = struct {
|
|||||||
// Sprite Palette starts at 0x0500_0200
|
// Sprite Palette starts at 0x0500_0200
|
||||||
if (pal_id != 0) {
|
if (pal_id != 0) {
|
||||||
const bgr555 = self.palette.read(u16, 0x200 + pal_id * 2);
|
const bgr555 = self.palette.read(u16, 0x200 + pal_id * 2);
|
||||||
copyToSpriteBuffer(self.bldcnt, &self.scanline, x, bgr555);
|
copyToSpriteBuffer(self.bld.cnt, &self.scanline, x, bgr555);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,7 +425,7 @@ pub const Ppu = struct {
|
|||||||
const x = (sprite.x() +% i) % width;
|
const x = (sprite.x() +% i) % width;
|
||||||
const ix = @bitCast(i9, x);
|
const ix = @bitCast(i9, x);
|
||||||
|
|
||||||
if (!shouldDrawSprite(self.bldcnt, &self.scanline, x)) continue;
|
if (!shouldDrawSprite(self.bld.cnt, &self.scanline, x)) continue;
|
||||||
|
|
||||||
const sprite_start = sprite.x();
|
const sprite_start = sprite.x();
|
||||||
const isprite_start = @bitCast(i9, sprite_start);
|
const isprite_start = @bitCast(i9, sprite_start);
|
||||||
@@ -250,7 +460,7 @@ pub const Ppu = struct {
|
|||||||
// Sprite Palette starts at 0x0500_0200
|
// Sprite Palette starts at 0x0500_0200
|
||||||
if (pal_id != 0) {
|
if (pal_id != 0) {
|
||||||
const bgr555 = self.palette.read(u16, 0x200 + pal_id * 2);
|
const bgr555 = self.palette.read(u16, 0x200 + pal_id * 2);
|
||||||
copyToSpriteBuffer(self.bldcnt, &self.scanline, x, bgr555);
|
copyToSpriteBuffer(self.bld.cnt, &self.scanline, x, bgr555);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -277,7 +487,7 @@ pub const Ppu = struct {
|
|||||||
aff_x += self.aff_bg[n - 2].pa;
|
aff_x += self.aff_bg[n - 2].pa;
|
||||||
aff_y += self.aff_bg[n - 2].pc;
|
aff_y += self.aff_bg[n - 2].pc;
|
||||||
|
|
||||||
if (!shouldDrawBackground(n, self.bldcnt, &self.scanline, i)) continue;
|
if (!shouldDrawBackground(n, self.bld.cnt, &self.scanline, i)) continue;
|
||||||
|
|
||||||
if (self.bg[n].cnt.display_overflow.read()) {
|
if (self.bg[n].cnt.display_overflow.read()) {
|
||||||
ix = if (ix > px_width) @rem(ix, px_width) else if (ix < 0) px_width + @rem(ix, px_width) else ix;
|
ix = if (ix > px_width) @rem(ix, px_width) else if (ix < 0) px_width + @rem(ix, px_width) else ix;
|
||||||
@@ -296,7 +506,7 @@ pub const Ppu = struct {
|
|||||||
|
|
||||||
if (pal_id != 0) {
|
if (pal_id != 0) {
|
||||||
const bgr555 = self.palette.read(u16, pal_id * 2);
|
const bgr555 = self.palette.read(u16, pal_id * 2);
|
||||||
copyToBackgroundBuffer(n, self.bldcnt, &self.scanline, i, bgr555);
|
copyToBackgroundBuffer(n, self.bld.cnt, &self.scanline, i, bgr555);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +535,7 @@ pub const Ppu = struct {
|
|||||||
|
|
||||||
var i: u32 = 0;
|
var i: u32 = 0;
|
||||||
while (i < width) : (i += 1) {
|
while (i < width) : (i += 1) {
|
||||||
if (!shouldDrawBackground(n, self.bldcnt, &self.scanline, i)) continue;
|
if (!shouldDrawBackground(n, self.bld.cnt, &self.scanline, i)) continue;
|
||||||
|
|
||||||
const x = hofs + i;
|
const x = hofs + i;
|
||||||
|
|
||||||
@@ -353,7 +563,7 @@ pub const Ppu = struct {
|
|||||||
|
|
||||||
if (pal_id != 0) {
|
if (pal_id != 0) {
|
||||||
const bgr555 = self.palette.read(u16, pal_id * 2);
|
const bgr555 = self.palette.read(u16, pal_id * 2);
|
||||||
copyToBackgroundBuffer(n, self.bldcnt, &self.scanline, i, bgr555);
|
copyToBackgroundBuffer(n, self.bld.cnt, &self.scanline, i, bgr555);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,7 +602,7 @@ pub const Ppu = struct {
|
|||||||
const maybe_btm = self.scanline.btm()[i];
|
const maybe_btm = self.scanline.btm()[i];
|
||||||
|
|
||||||
const bgr555 = self.getBgr555(maybe_top, maybe_btm);
|
const bgr555 = self.getBgr555(maybe_top, maybe_btm);
|
||||||
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
|
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset Current Scanline Pixel Buffer and list of fetched sprites
|
// Reset Current Scanline Pixel Buffer and list of fetched sprites
|
||||||
@@ -419,7 +629,7 @@ pub const Ppu = struct {
|
|||||||
const maybe_btm = self.scanline.btm()[i];
|
const maybe_btm = self.scanline.btm()[i];
|
||||||
|
|
||||||
const bgr555 = self.getBgr555(maybe_top, maybe_btm);
|
const bgr555 = self.getBgr555(maybe_top, maybe_btm);
|
||||||
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
|
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset Current Scanline Pixel Buffer and list of fetched sprites
|
// Reset Current Scanline Pixel Buffer and list of fetched sprites
|
||||||
@@ -445,7 +655,7 @@ pub const Ppu = struct {
|
|||||||
const maybe_btm = self.scanline.btm()[i];
|
const maybe_btm = self.scanline.btm()[i];
|
||||||
|
|
||||||
const bgr555 = self.getBgr555(maybe_top, maybe_btm);
|
const bgr555 = self.getBgr555(maybe_top, maybe_btm);
|
||||||
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
|
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset Current Scanline Pixel Buffer and list of fetched sprites
|
// Reset Current Scanline Pixel Buffer and list of fetched sprites
|
||||||
@@ -460,7 +670,7 @@ pub const Ppu = struct {
|
|||||||
var i: usize = 0;
|
var i: usize = 0;
|
||||||
while (i < width) : (i += 1) {
|
while (i < width) : (i += 1) {
|
||||||
const bgr555 = self.vram.read(u16, vram_base + i * @sizeOf(u16));
|
const bgr555 = self.vram.read(u16, vram_base + i * @sizeOf(u16));
|
||||||
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
|
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
0x4 => {
|
0x4 => {
|
||||||
@@ -471,7 +681,7 @@ pub const Ppu = struct {
|
|||||||
// Render Current Scanline
|
// Render Current Scanline
|
||||||
for (self.vram.buf[vram_base .. vram_base + width]) |byte, i| {
|
for (self.vram.buf[vram_base .. vram_base + width]) |byte, i| {
|
||||||
const bgr555 = self.palette.read(u16, @as(u16, byte) * @sizeOf(u16));
|
const bgr555 = self.palette.read(u16, @as(u16, byte) * @sizeOf(u16));
|
||||||
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
|
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
0x5 => {
|
0x5 => {
|
||||||
@@ -488,7 +698,7 @@ pub const Ppu = struct {
|
|||||||
const bgr555 =
|
const bgr555 =
|
||||||
if (scanline < m5_height and i < m5_width) self.vram.read(u16, vram_base + i * @sizeOf(u16)) else self.palette.getBackdrop();
|
if (scanline < m5_height and i < m5_width) self.vram.read(u16, vram_base + i * @sizeOf(u16)) else self.palette.getBackdrop();
|
||||||
|
|
||||||
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
|
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], rgba888(bgr555));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
else => std.debug.panic("[PPU] TODO: Implement BG Mode {}", .{bg_mode}),
|
else => std.debug.panic("[PPU] TODO: Implement BG Mode {}", .{bg_mode}),
|
||||||
@@ -497,11 +707,11 @@ pub const Ppu = struct {
|
|||||||
|
|
||||||
fn getBgr555(self: *Self, maybe_top: ?u16, maybe_btm: ?u16) u16 {
|
fn getBgr555(self: *Self, maybe_top: ?u16, maybe_btm: ?u16) u16 {
|
||||||
if (maybe_btm) |btm| {
|
if (maybe_btm) |btm| {
|
||||||
return switch (self.bldcnt.mode.read()) {
|
return switch (self.bld.cnt.mode.read()) {
|
||||||
0b00 => if (maybe_top) |top| top else btm,
|
0b00 => if (maybe_top) |top| top else btm,
|
||||||
0b01 => if (maybe_top) |top| alphaBlend(btm, top, self.bldalpha) else btm,
|
0b01 => if (maybe_top) |top| alphaBlend(btm, top, self.bld.alpha) else btm,
|
||||||
0b10 => blk: {
|
0b10 => blk: {
|
||||||
const evy: u16 = self.bldy.evy.read();
|
const evy: u16 = self.bld.y.evy.read();
|
||||||
|
|
||||||
const r = btm & 0x1F;
|
const r = btm & 0x1F;
|
||||||
const g = (btm >> 5) & 0x1F;
|
const g = (btm >> 5) & 0x1F;
|
||||||
@@ -514,7 +724,7 @@ pub const Ppu = struct {
|
|||||||
break :blk (bld_b << 10) | (bld_g << 5) | bld_r;
|
break :blk (bld_b << 10) | (bld_g << 5) | bld_r;
|
||||||
},
|
},
|
||||||
0b11 => blk: {
|
0b11 => blk: {
|
||||||
const evy: u16 = self.bldy.evy.read();
|
const evy: u16 = self.bld.y.evy.read();
|
||||||
|
|
||||||
const btm_r = btm & 0x1F;
|
const btm_r = btm & 0x1F;
|
||||||
const btm_g = (btm >> 5) & 0x1F;
|
const btm_g = (btm >> 5) & 0x1F;
|
||||||
@@ -672,7 +882,7 @@ const Palette = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Vram = struct {
|
pub const Vram = struct {
|
||||||
const vram_size = 0x18000;
|
const vram_size = 0x18000;
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
@@ -723,7 +933,7 @@ const Vram = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mirror(address: usize) usize {
|
pub fn mirror(address: usize) usize {
|
||||||
// Mirrored in steps of 128K (64K + 32K + 32K) (abcc)
|
// Mirrored in steps of 128K (64K + 32K + 32K) (abcc)
|
||||||
const addr = address & 0x1FFFF;
|
const addr = address & 0x1FFFF;
|
||||||
|
|
||||||
@@ -775,6 +985,30 @@ const Oam = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Blend = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
cnt: io.BldCnt,
|
||||||
|
alpha: io.BldAlpha,
|
||||||
|
y: io.BldY,
|
||||||
|
|
||||||
|
pub fn create() Self {
|
||||||
|
return .{
|
||||||
|
.cnt = .{ .raw = 0x000 },
|
||||||
|
.alpha = .{ .raw = 0x000 },
|
||||||
|
.y = .{ .raw = 0x000 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getCnt(self: *const Self) u16 {
|
||||||
|
return self.cnt.raw & 0x3FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getAlpha(self: *const Self) u16 {
|
||||||
|
return self.alpha.raw & 0x1F1F;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const Window = struct {
|
const Window = struct {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
@@ -794,6 +1028,14 @@ const Window = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getIn(self: *const Self) u16 {
|
||||||
|
return self.in.raw & 0x3F3F;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getOut(self: *const Self) u16 {
|
||||||
|
return self.out.raw & 0x3F3F;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn setH(self: *Self, value: u32) void {
|
pub fn setH(self: *Self, value: u32) void {
|
||||||
self.h[0].raw = @truncate(u16, value);
|
self.h[0].raw = @truncate(u16, value);
|
||||||
self.h[1].raw = @truncate(u16, value >> 16);
|
self.h[1].raw = @truncate(u16, value >> 16);
|
||||||
@@ -827,6 +1069,17 @@ const Background = struct {
|
|||||||
.vofs = .{ .raw = 0x0000 },
|
.vofs = .{ .raw = 0x0000 },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// For whatever reason, some higher bits of BG0CNT
|
||||||
|
/// are masked out
|
||||||
|
pub inline fn bg0Cnt(self: *const Self) u16 {
|
||||||
|
return self.cnt.raw & 0xDFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// BG1CNT inherits the same mask as BG0CNTs
|
||||||
|
pub inline fn bg1Cnt(self: *const Self) u16 {
|
||||||
|
return self.bg0Cnt();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const AffineBackground = struct {
|
const AffineBackground = struct {
|
||||||
@@ -1063,7 +1316,7 @@ fn spriteDimensions(shape: u2, size: u2) [2]u8 {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toRgba8888(bgr555: u16) u32 {
|
inline fn rgba888(bgr555: u16) u32 {
|
||||||
const b = @as(u32, bgr555 >> 10 & 0x1F);
|
const b = @as(u32, bgr555 >> 10 & 0x1F);
|
||||||
const g = @as(u32, bgr555 >> 5 & 0x1F);
|
const g = @as(u32, bgr555 >> 5 & 0x1F);
|
||||||
const r = @as(u32, bgr555 & 0x1F);
|
const r = @as(u32, bgr555 & 0x1F);
|
||||||
@@ -1071,39 +1324,6 @@ fn toRgba8888(bgr555: u16) u32 {
|
|||||||
return (r << 3 | r >> 2) << 24 | (g << 3 | g >> 2) << 16 | (b << 3 | b >> 2) << 8 | 0xFF;
|
return (r << 3 | r >> 2) << 24 | (g << 3 | g >> 2) << 16 | (b << 3 | b >> 2) << 8 | 0xFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn genColourLut() [0x8000]u32 {
|
|
||||||
return comptime {
|
|
||||||
@setEvalBranchQuota(0x10001);
|
|
||||||
|
|
||||||
var lut: [0x8000]u32 = undefined;
|
|
||||||
for (lut) |*px, i| px.* = toRgba8888(i);
|
|
||||||
return lut;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: The implementation is incorrect and using it in the LUT crashes the compiler (OOM)
|
|
||||||
/// Implementation courtesy of byuu and Talarubi at https://near.sh/articles/video/color-emulation
|
|
||||||
fn toRgba8888Talarubi(bgr555: u16) u32 {
|
|
||||||
@setRuntimeSafety(false);
|
|
||||||
|
|
||||||
const lcd_gamma: f64 = 4;
|
|
||||||
const out_gamma: f64 = 2.2;
|
|
||||||
|
|
||||||
const b = @as(u32, bgr555 >> 10 & 0x1F);
|
|
||||||
const g = @as(u32, bgr555 >> 5 & 0x1F);
|
|
||||||
const r = @as(u32, bgr555 & 0x1F);
|
|
||||||
|
|
||||||
const lb = std.math.pow(f64, @intToFloat(f64, b << 3 | b >> 2) / 31, lcd_gamma);
|
|
||||||
const lg = std.math.pow(f64, @intToFloat(f64, g << 3 | g >> 2) / 31, lcd_gamma);
|
|
||||||
const lr = std.math.pow(f64, @intToFloat(f64, r << 3 | r >> 2) / 31, lcd_gamma);
|
|
||||||
|
|
||||||
const out_b = std.math.pow(f64, (220 * lb + 10 * lg + 50 * lr) / 255, 1 / out_gamma);
|
|
||||||
const out_g = std.math.pow(f64, (30 * lb + 230 * lg + 10 * lr) / 255, 1 / out_gamma);
|
|
||||||
const out_r = std.math.pow(f64, (0 * lb + 50 * lg + 255 * lr) / 255, 1 / out_gamma);
|
|
||||||
|
|
||||||
return @floatToInt(u32, out_r) << 24 | @floatToInt(u32, out_g) << 16 | @floatToInt(u32, out_b) << 8 | 0xFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn alphaBlend(top: u16, btm: u16, bldalpha: io.BldAlpha) u16 {
|
fn alphaBlend(top: u16, btm: u16, bldalpha: io.BldAlpha) u16 {
|
||||||
const eva: u16 = bldalpha.eva.read();
|
const eva: u16 = bldalpha.eva.read();
|
||||||
const evb: u16 = bldalpha.evb.read();
|
const evb: u16 = bldalpha.evb.read();
|
||||||
@@ -1244,7 +1464,7 @@ const FrameBuffer = struct {
|
|||||||
|
|
||||||
layers: [2][]u8,
|
layers: [2][]u8,
|
||||||
buf: []u8,
|
buf: []u8,
|
||||||
current: u1,
|
current: std.atomic.Atomic(u8),
|
||||||
|
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
|
|
||||||
@@ -1263,7 +1483,7 @@ const FrameBuffer = struct {
|
|||||||
// Front and Back Framebuffers
|
// Front and Back Framebuffers
|
||||||
.layers = [_][]u8{ buf[0..][0..framebuf_len], buf[framebuf_len..][0..framebuf_len] },
|
.layers = [_][]u8{ buf[0..][0..framebuf_len], buf[framebuf_len..][0..framebuf_len] },
|
||||||
.buf = buf,
|
.buf = buf,
|
||||||
.current = 0,
|
.current = std.atomic.Atomic(u8).init(0),
|
||||||
|
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
};
|
};
|
||||||
@@ -1275,10 +1495,12 @@ const FrameBuffer = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn swap(self: *Self) void {
|
pub fn swap(self: *Self) void {
|
||||||
self.current = ~self.current;
|
_ = self.current.fetchXor(1, .Release); // fetchNot(.Release)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(self: *Self, comptime dev: Device) []u8 {
|
pub fn get(self: *Self, comptime dev: Device) []u8 {
|
||||||
return self.layers[if (dev == .Emulator) self.current else ~self.current];
|
const current = @intCast(u1, self.current.load(.Acquire));
|
||||||
|
|
||||||
|
return self.layers[if (dev == .Emulator) current else ~current];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
const Bus = @import("Bus.zig");
|
|
||||||
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
||||||
const Clock = @import("bus/gpio.zig").Clock;
|
const Clock = @import("bus/gpio.zig").Clock;
|
||||||
|
|
||||||
@@ -47,10 +46,7 @@ pub const Scheduler = struct {
|
|||||||
},
|
},
|
||||||
.TimerOverflow => |id| {
|
.TimerOverflow => |id| {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
0 => cpu.bus.tim[0].onTimerExpire(cpu, late),
|
inline 0...3 => |idx| cpu.bus.tim[idx].onTimerExpire(cpu, late),
|
||||||
1 => cpu.bus.tim[1].onTimerExpire(cpu, late),
|
|
||||||
2 => cpu.bus.tim[2].onTimerExpire(cpu, late),
|
|
||||||
3 => cpu.bus.tim[3].onTimerExpire(cpu, late),
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.ApuChannel => |id| {
|
.ApuChannel => |id| {
|
||||||
|
60
src/main.zig
60
src/main.zig
@@ -26,30 +26,44 @@ const params = clap.parseParamsComptime(
|
|||||||
\\
|
\\
|
||||||
);
|
);
|
||||||
|
|
||||||
pub fn main() anyerror!void {
|
pub fn main() void {
|
||||||
// Main Allocator for ZBA
|
// Main Allocator for ZBA
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
defer std.debug.assert(!gpa.deinit());
|
defer std.debug.assert(!gpa.deinit());
|
||||||
|
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
// Determine the Data Directory (stores saves, config file, etc.)
|
// Determine the Data Directory (stores saves)
|
||||||
const data_path = blk: {
|
const data_path = blk: {
|
||||||
const result = known_folders.getPath(allocator, .data);
|
const result = known_folders.getPath(allocator, .data);
|
||||||
const option = result catch |e| exitln("interrupted while attempting to find a data directory: {}", .{e});
|
const option = result catch |e| exitln("interrupted while determining the data folder: {}", .{e});
|
||||||
break :blk option orelse exitln("no valid data directory could be found", .{});
|
const path = option orelse exitln("no valid data folder found", .{});
|
||||||
|
ensureDataDirsExist(path) catch |e| exitln("failed to create folders under \"{s}\": {}", .{ path, e });
|
||||||
|
|
||||||
|
break :blk path;
|
||||||
};
|
};
|
||||||
defer allocator.free(data_path);
|
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, ¶ms, clap.parsers.default, .{}) catch |e| exitln("failed to parse cli: {}", .{e});
|
const result = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{}) catch |e| exitln("failed to parse cli: {}", .{e});
|
||||||
defer result.deinit();
|
defer result.deinit();
|
||||||
|
|
||||||
// TODO: Move config file to XDG Config directory?
|
// TODO: Move config file to XDG Config directory?
|
||||||
const config_path = configFilePath(allocator, data_path) catch |e| exitln("failed to determine the config file path for ZBA: {}", .{e});
|
const cfg_file_path = configFilePath(allocator, config_path) catch |e| exitln("failed to ready config file for access: {}", .{e});
|
||||||
defer allocator.free(config_path);
|
defer allocator.free(cfg_file_path);
|
||||||
|
|
||||||
config.load(allocator, config_path) catch |e| exitln("failed to read config file: {}", .{e});
|
config.load(allocator, cfg_file_path) catch |e| exitln("failed to load config file: {}", .{e});
|
||||||
|
|
||||||
const paths = handleArguments(allocator, data_path, &result) catch |e| exitln("failed to handle cli arguments: {}", .{e});
|
const paths = handleArguments(allocator, data_path, &result) catch |e| exitln("failed to handle cli arguments: {}", .{e});
|
||||||
defer if (paths.save) |path| allocator.free(path);
|
defer if (paths.save) |path| allocator.free(path);
|
||||||
@@ -73,7 +87,7 @@ pub fn main() anyerror!void {
|
|||||||
cpu.fastBoot();
|
cpu.fastBoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
var gui = Gui.init(&bus.pak.title, &bus.apu, width, height);
|
var gui = Gui.init(&bus.pak.title, &bus.apu, width, height) catch |e| exitln("failed to init gui: {}", .{e});
|
||||||
defer gui.deinit();
|
defer gui.deinit();
|
||||||
|
|
||||||
gui.run(&cpu, &scheduler) catch |e| exitln("failed to run gui thread: {}", .{e});
|
gui.run(&cpu, &scheduler) catch |e| exitln("failed to run gui thread: {}", .{e});
|
||||||
@@ -86,7 +100,7 @@ pub fn handleArguments(allocator: Allocator, data_path: []const u8, result: *con
|
|||||||
const bios_path = result.args.bios;
|
const bios_path = result.args.bios;
|
||||||
if (bios_path) |path| log.info("BIOS path: {s}", .{path}) else log.warn("No BIOS provided", .{});
|
if (bios_path) |path| log.info("BIOS path: {s}", .{path}) else log.warn("No BIOS provided", .{});
|
||||||
|
|
||||||
const save_path = try savePath(allocator, data_path);
|
const save_path = try std.fs.path.join(allocator, &[_][]const u8{ data_path, "zba", "save" });
|
||||||
log.info("Save path: {s}", .{save_path});
|
log.info("Save path: {s}", .{save_path});
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
@@ -96,32 +110,38 @@ pub fn handleArguments(allocator: Allocator, data_path: []const u8, result: *con
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn configFilePath(allocator: Allocator, data_path: []const u8) ![]const u8 {
|
fn configFilePath(allocator: Allocator, config_path: []const u8) ![]const u8 {
|
||||||
const path = try std.fs.path.join(allocator, &[_][]const u8{ data_path, "zba", "config.toml" });
|
const path = try std.fs.path.join(allocator, &[_][]const u8{ config_path, "zba", "config.toml" });
|
||||||
errdefer allocator.free(path);
|
errdefer allocator.free(path);
|
||||||
|
|
||||||
// We try to create the file exclusively, meaning that we err out if the file already exists.
|
// We try to create the file exclusively, meaning that we err out if the file already exists.
|
||||||
// All we care about is a file being there so we can just ignore that error in particular and
|
// All we care about is a file being there so we can just ignore that error in particular and
|
||||||
// continue down the happy pathj
|
// continue down the happy pathj
|
||||||
std.fs.accessAbsolute(path, .{}) catch {
|
std.fs.accessAbsolute(path, .{}) catch |e| {
|
||||||
const file_handle = try std.fs.createFileAbsolute(path, .{});
|
if (e != error.FileNotFound) return e;
|
||||||
defer file_handle.close();
|
|
||||||
|
|
||||||
// TODO: Write Default valeus to config file
|
const config_file = std.fs.createFileAbsolute(path, .{}) catch |err| exitln("failed to create \"{s}\": {}", .{ path, err });
|
||||||
|
defer config_file.close();
|
||||||
|
|
||||||
|
try config_file.writeAll(@embedFile("../example.toml"));
|
||||||
};
|
};
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn savePath(allocator: Allocator, data_path: []const u8) ![]const u8 {
|
fn ensureDataDirsExist(data_path: []const u8) !void {
|
||||||
var dir = try std.fs.openDirAbsolute(data_path, .{});
|
var dir = try std.fs.openDirAbsolute(data_path, .{});
|
||||||
defer dir.close();
|
defer dir.close();
|
||||||
|
|
||||||
// Will either make the path recursively, or just exit early since it already exists
|
// Will recursively create directories
|
||||||
try dir.makePath("zba" ++ [_]u8{std.fs.path.sep} ++ "save");
|
try dir.makePath("zba" ++ std.fs.path.sep_str ++ "save");
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: Do we have to allocate? :sad:
|
fn ensureConfigDirExists(config_path: []const u8) !void {
|
||||||
return try std.fs.path.join(allocator, &[_][]const u8{ data_path, "zba", "save" });
|
var dir = try std.fs.openDirAbsolute(config_path, .{});
|
||||||
|
defer dir.close();
|
||||||
|
|
||||||
|
try dir.makePath("zba");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn romPath(result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) []const u8 {
|
fn romPath(result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) []const u8 {
|
||||||
|
289
src/platform.zig
289
src/platform.zig
@@ -1,5 +1,6 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const SDL = @import("sdl2");
|
const SDL = @import("sdl2");
|
||||||
|
const gl = @import("gl");
|
||||||
const emu = @import("core/emu.zig");
|
const emu = @import("core/emu.zig");
|
||||||
const config = @import("config.zig");
|
const config = @import("config.zig");
|
||||||
|
|
||||||
@@ -8,64 +9,170 @@ const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
|
|||||||
const Scheduler = @import("core/scheduler.zig").Scheduler;
|
const Scheduler = @import("core/scheduler.zig").Scheduler;
|
||||||
const FpsTracker = @import("util.zig").FpsTracker;
|
const FpsTracker = @import("util.zig").FpsTracker;
|
||||||
|
|
||||||
const span = @import("util.zig").span;
|
const gba_width = @import("core/ppu.zig").width;
|
||||||
|
const gba_height = @import("core/ppu.zig").height;
|
||||||
|
|
||||||
const pitch = @import("core/ppu.zig").framebuf_pitch;
|
pub const sample_rate = 1 << 16;
|
||||||
const default_title: []const u8 = "ZBA";
|
pub const sample_format = SDL.AUDIO_U16;
|
||||||
|
|
||||||
|
const default_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,
|
||||||
title: []const u8,
|
title: []const u8,
|
||||||
renderer: *SDL.SDL_Renderer,
|
|
||||||
texture: *SDL.SDL_Texture,
|
|
||||||
audio: Audio,
|
audio: Audio,
|
||||||
|
|
||||||
pub fn init(title: *const [12]u8, apu: *Apu, width: i32, height: i32) Self {
|
program_id: gl.GLuint,
|
||||||
const ret = SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO | SDL.SDL_INIT_GAMECONTROLLER);
|
|
||||||
if (ret < 0) panic();
|
pub fn init(title: *const [12]u8, apu: *Apu, width: i32, height: i32) !Self {
|
||||||
|
if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO) < 0) panic();
|
||||||
|
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_PROFILE_MASK, SDL.SDL_GL_CONTEXT_PROFILE_CORE) < 0) panic();
|
||||||
|
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic();
|
||||||
|
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic();
|
||||||
|
|
||||||
const win_scale = @intCast(c_int, config.config().host.win_scale);
|
const win_scale = @intCast(c_int, config.config().host.win_scale);
|
||||||
|
|
||||||
const window = SDL.SDL_CreateWindow(
|
const window = SDL.SDL_CreateWindow(
|
||||||
default_title.ptr,
|
default_title,
|
||||||
SDL.SDL_WINDOWPOS_CENTERED,
|
SDL.SDL_WINDOWPOS_CENTERED,
|
||||||
SDL.SDL_WINDOWPOS_CENTERED,
|
SDL.SDL_WINDOWPOS_CENTERED,
|
||||||
@as(c_int, width * win_scale),
|
@as(c_int, width * win_scale),
|
||||||
@as(c_int, height * win_scale),
|
@as(c_int, height * win_scale),
|
||||||
SDL.SDL_WINDOW_SHOWN,
|
SDL.SDL_WINDOW_OPENGL | SDL.SDL_WINDOW_SHOWN,
|
||||||
) orelse panic();
|
) orelse panic();
|
||||||
|
|
||||||
const renderer_flags = SDL.SDL_RENDERER_ACCELERATED | if (config.config().host.vsync) SDL.SDL_RENDERER_PRESENTVSYNC else 0;
|
const ctx = SDL.SDL_GL_CreateContext(window) orelse panic();
|
||||||
const renderer = SDL.SDL_CreateRenderer(window, -1, @bitCast(u32, renderer_flags)) orelse panic();
|
if (SDL.SDL_GL_MakeCurrent(window, ctx) < 0) panic();
|
||||||
|
|
||||||
const texture = SDL.SDL_CreateTexture(
|
try gl.load(ctx, Self.glGetProcAddress);
|
||||||
renderer,
|
if (SDL.SDL_GL_SetSwapInterval(@boolToInt(config.config().host.vsync)) < 0) panic();
|
||||||
SDL.SDL_PIXELFORMAT_RGBA8888,
|
|
||||||
SDL.SDL_TEXTUREACCESS_STREAMING,
|
const program_id = try compileShaders();
|
||||||
@as(c_int, width),
|
|
||||||
@as(c_int, height),
|
|
||||||
) orelse panic();
|
|
||||||
|
|
||||||
return Self{
|
return Self{
|
||||||
.window = window,
|
.window = window,
|
||||||
.title = span(title),
|
.title = std.mem.sliceTo(title, 0),
|
||||||
.renderer = renderer,
|
.ctx = ctx,
|
||||||
.texture = texture,
|
.program_id = program_id,
|
||||||
.audio = Audio.init(apu),
|
.audio = Audio.init(apu),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn compileShaders() !gl.GLuint {
|
||||||
|
// TODO: Panic on Shader Compiler Failure + Error Message
|
||||||
|
const vert_shader = @embedFile("shader/pixelbuf.vert");
|
||||||
|
const frag_shader = @embedFile("shader/pixelbuf.frag");
|
||||||
|
|
||||||
|
const vs = gl.createShader(gl.VERTEX_SHADER);
|
||||||
|
defer gl.deleteShader(vs);
|
||||||
|
|
||||||
|
gl.shaderSource(vs, 1, &[_][*c]const u8{vert_shader}, 0);
|
||||||
|
gl.compileShader(vs);
|
||||||
|
|
||||||
|
if (!shader.didCompile(vs)) return error.VertexCompileError;
|
||||||
|
|
||||||
|
const fs = gl.createShader(gl.FRAGMENT_SHADER);
|
||||||
|
defer gl.deleteShader(fs);
|
||||||
|
|
||||||
|
gl.shaderSource(fs, 1, &[_][*c]const u8{frag_shader}, 0);
|
||||||
|
gl.compileShader(fs);
|
||||||
|
|
||||||
|
if (!shader.didCompile(fs)) return error.FragmentCompileError;
|
||||||
|
|
||||||
|
const program = gl.createProgram();
|
||||||
|
gl.attachShader(program, vs);
|
||||||
|
gl.attachShader(program, fs);
|
||||||
|
gl.linkProgram(program);
|
||||||
|
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the VAO ID since it's used in run()
|
||||||
|
fn generateBuffers() struct { c_uint, c_uint, c_uint } {
|
||||||
|
var vao_id: c_uint = undefined;
|
||||||
|
var vbo_id: c_uint = undefined;
|
||||||
|
var ebo_id: c_uint = undefined;
|
||||||
|
gl.genVertexArrays(1, &vao_id);
|
||||||
|
gl.genBuffers(1, &vbo_id);
|
||||||
|
gl.genBuffers(1, &ebo_id);
|
||||||
|
|
||||||
|
gl.bindVertexArray(vao_id);
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, vbo_id);
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, @sizeOf(@TypeOf(vertices)), &vertices, gl.STATIC_DRAW);
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo_id);
|
||||||
|
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, @sizeOf(@TypeOf(indices)), &indices, gl.STATIC_DRAW);
|
||||||
|
|
||||||
|
// Position
|
||||||
|
gl.vertexAttribPointer(0, 3, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, 0)); // lmao
|
||||||
|
gl.enableVertexAttribArray(0);
|
||||||
|
// Colour
|
||||||
|
gl.vertexAttribPointer(1, 3, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, (3 * @sizeOf(f32))));
|
||||||
|
gl.enableVertexAttribArray(1);
|
||||||
|
// Texture Coord
|
||||||
|
gl.vertexAttribPointer(2, 2, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, (6 * @sizeOf(f32))));
|
||||||
|
gl.enableVertexAttribArray(2);
|
||||||
|
|
||||||
|
return .{ vao_id, vbo_id, ebo_id };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generateTexture(buf: []const u8) c_uint {
|
||||||
|
var tex_id: c_uint = undefined;
|
||||||
|
gl.genTextures(1, &tex_id);
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, tex_id);
|
||||||
|
|
||||||
|
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||||
|
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||||
|
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||||
|
|
||||||
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gba_width, gba_height, 0, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, buf.ptr);
|
||||||
|
// gl.generateMipmap(gl.TEXTURE_2D); // TODO: Remove?
|
||||||
|
|
||||||
|
return tex_id;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn run(self: *Self, cpu: *Arm7tdmi, scheduler: *Scheduler) !void {
|
pub fn run(self: *Self, cpu: *Arm7tdmi, scheduler: *Scheduler) !void {
|
||||||
var quit = std.atomic.Atomic(bool).init(false);
|
var quit = std.atomic.Atomic(bool).init(false);
|
||||||
var tracker = FpsTracker.init();
|
var tracker = FpsTracker.init();
|
||||||
|
|
||||||
|
var buffer_ids = Self.generateBuffers();
|
||||||
|
defer {
|
||||||
|
gl.deleteBuffers(1, &buffer_ids[2]); // EBO
|
||||||
|
gl.deleteBuffers(1, &buffer_ids[1]); // VBO
|
||||||
|
gl.deleteVertexArrays(1, &buffer_ids[0]); // VAO
|
||||||
|
}
|
||||||
|
const vao_id = buffer_ids[0];
|
||||||
|
|
||||||
|
const tex_id = Self.generateTexture(cpu.bus.ppu.framebuf.get(.Renderer));
|
||||||
|
defer gl.deleteTextures(1, &tex_id);
|
||||||
|
|
||||||
const thread = try std.Thread.spawn(.{}, emu.run, .{ &quit, scheduler, cpu, &tracker });
|
const thread = try std.Thread.spawn(.{}, emu.run, .{ &quit, scheduler, cpu, &tracker });
|
||||||
defer thread.join();
|
defer thread.join();
|
||||||
|
|
||||||
var title_buf: [0x100]u8 = [_]u8{0} ** 0x100;
|
var title_buf: [0x100]u8 = undefined;
|
||||||
|
|
||||||
emu_loop: while (true) {
|
emu_loop: while (true) {
|
||||||
var event: SDL.SDL_Event = undefined;
|
var event: SDL.SDL_Event = undefined;
|
||||||
@@ -73,51 +180,50 @@ pub const Gui = struct {
|
|||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
SDL.SDL_QUIT => break :emu_loop,
|
SDL.SDL_QUIT => break :emu_loop,
|
||||||
SDL.SDL_KEYDOWN => {
|
SDL.SDL_KEYDOWN => {
|
||||||
const io = &cpu.bus.io;
|
|
||||||
const key_code = event.key.keysym.sym;
|
const key_code = event.key.keysym.sym;
|
||||||
|
var keyinput = cpu.bus.io.keyinput.load(.Monotonic);
|
||||||
|
|
||||||
switch (key_code) {
|
switch (key_code) {
|
||||||
SDL.SDLK_UP => io.keyinput.up.unset(),
|
SDL.SDLK_UP => keyinput.up.unset(),
|
||||||
SDL.SDLK_DOWN => io.keyinput.down.unset(),
|
SDL.SDLK_DOWN => keyinput.down.unset(),
|
||||||
SDL.SDLK_LEFT => io.keyinput.left.unset(),
|
SDL.SDLK_LEFT => keyinput.left.unset(),
|
||||||
SDL.SDLK_RIGHT => io.keyinput.right.unset(),
|
SDL.SDLK_RIGHT => keyinput.right.unset(),
|
||||||
SDL.SDLK_x => io.keyinput.a.unset(),
|
SDL.SDLK_x => keyinput.a.unset(),
|
||||||
SDL.SDLK_z => io.keyinput.b.unset(),
|
SDL.SDLK_z => keyinput.b.unset(),
|
||||||
SDL.SDLK_a => io.keyinput.shoulder_l.unset(),
|
SDL.SDLK_a => keyinput.shoulder_l.unset(),
|
||||||
SDL.SDLK_s => io.keyinput.shoulder_r.unset(),
|
SDL.SDLK_s => keyinput.shoulder_r.unset(),
|
||||||
SDL.SDLK_RETURN => io.keyinput.start.unset(),
|
SDL.SDLK_RETURN => keyinput.start.unset(),
|
||||||
SDL.SDLK_RSHIFT => io.keyinput.select.unset(),
|
SDL.SDLK_RSHIFT => keyinput.select.unset(),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cpu.bus.io.keyinput.store(keyinput.raw, .Monotonic);
|
||||||
},
|
},
|
||||||
SDL.SDL_KEYUP => {
|
SDL.SDL_KEYUP => {
|
||||||
const io = &cpu.bus.io;
|
|
||||||
const key_code = event.key.keysym.sym;
|
const key_code = event.key.keysym.sym;
|
||||||
|
var keyinput = cpu.bus.io.keyinput.load(.Monotonic);
|
||||||
|
|
||||||
switch (key_code) {
|
switch (key_code) {
|
||||||
SDL.SDLK_UP => io.keyinput.up.set(),
|
SDL.SDLK_UP => keyinput.up.set(),
|
||||||
SDL.SDLK_DOWN => io.keyinput.down.set(),
|
SDL.SDLK_DOWN => keyinput.down.set(),
|
||||||
SDL.SDLK_LEFT => io.keyinput.left.set(),
|
SDL.SDLK_LEFT => keyinput.left.set(),
|
||||||
SDL.SDLK_RIGHT => io.keyinput.right.set(),
|
SDL.SDLK_RIGHT => keyinput.right.set(),
|
||||||
SDL.SDLK_x => io.keyinput.a.set(),
|
SDL.SDLK_x => keyinput.a.set(),
|
||||||
SDL.SDLK_z => io.keyinput.b.set(),
|
SDL.SDLK_z => keyinput.b.set(),
|
||||||
SDL.SDLK_a => io.keyinput.shoulder_l.set(),
|
SDL.SDLK_a => keyinput.shoulder_l.set(),
|
||||||
SDL.SDLK_s => io.keyinput.shoulder_r.set(),
|
SDL.SDLK_s => keyinput.shoulder_r.set(),
|
||||||
SDL.SDLK_RETURN => io.keyinput.start.set(),
|
SDL.SDLK_RETURN => keyinput.start.set(),
|
||||||
SDL.SDLK_RSHIFT => io.keyinput.select.set(),
|
SDL.SDLK_RSHIFT => keyinput.select.set(),
|
||||||
SDL.SDLK_i => log.err("Sample Count: {}", .{@intCast(u32, SDL.SDL_AudioStreamAvailable(cpu.bus.apu.stream)) / (2 * @sizeOf(u16))}),
|
SDL.SDLK_i => {
|
||||||
SDL.SDLK_j => log.err("Scheduler Capacity: {} | Scheduler Event Count: {}", .{ scheduler.queue.capacity(), scheduler.queue.count() }),
|
comptime std.debug.assert(sample_format == SDL.AUDIO_U16);
|
||||||
SDL.SDLK_k => {
|
log.err("Sample Count: {}", .{cpu.bus.apu.sample_queue.len() / 2});
|
||||||
// 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);
|
|
||||||
},
|
},
|
||||||
|
// SDL.SDLK_j => log.err("Scheduler Capacity: {} | Scheduler Event Count: {}", .{ scheduler.queue.capacity(), scheduler.queue.count() }),
|
||||||
|
SDL.SDLK_k => {},
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cpu.bus.io.keyinput.store(keyinput.raw, .Monotonic);
|
||||||
},
|
},
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
@@ -125,31 +231,38 @@ pub const Gui = struct {
|
|||||||
|
|
||||||
// Emulator has an internal Double Buffer
|
// Emulator has an internal Double Buffer
|
||||||
const framebuf = cpu.bus.ppu.framebuf.get(.Renderer);
|
const framebuf = cpu.bus.ppu.framebuf.get(.Renderer);
|
||||||
_ = SDL.SDL_UpdateTexture(self.texture, null, framebuf.ptr, pitch);
|
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gba_width, gba_height, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, framebuf.ptr);
|
||||||
_ = SDL.SDL_RenderCopy(self.renderer, self.texture, null, null);
|
|
||||||
SDL.SDL_RenderPresent(self.renderer);
|
|
||||||
|
|
||||||
const dyn_title = std.fmt.bufPrint(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, tracker.value() }) catch unreachable;
|
gl.useProgram(self.program_id);
|
||||||
|
gl.bindVertexArray(vao_id);
|
||||||
|
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, null);
|
||||||
|
SDL.SDL_GL_SwapWindow(self.window);
|
||||||
|
|
||||||
|
const dyn_title = std.fmt.bufPrintZ(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, tracker.value() }) catch unreachable;
|
||||||
SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr);
|
SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
quit.store(true, .SeqCst); // Terminate Emulator Thread
|
quit.store(true, .Monotonic); // Terminate Emulator Thread
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
pub fn deinit(self: *Self) void {
|
||||||
self.audio.deinit();
|
self.audio.deinit();
|
||||||
SDL.SDL_DestroyTexture(self.texture);
|
gl.deleteProgram(self.program_id);
|
||||||
SDL.SDL_DestroyRenderer(self.renderer);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn glGetProcAddress(ctx: SDL.SDL_GLContext, proc: [:0]const u8) ?*anyopaque {
|
||||||
|
_ = ctx;
|
||||||
|
return SDL.SDL_GL_GetProcAddress(proc.ptr);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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,
|
||||||
|
|
||||||
@@ -157,16 +270,22 @@ 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 = SDL.AUDIO_U16;
|
want.format = sample_format;
|
||||||
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
|
if (!config.config().host.mute) {
|
||||||
|
SDL.SDL_PauseAudioDevice(device, 0); // Unpause Audio
|
||||||
|
log.info("Unpaused Device", .{});
|
||||||
|
}
|
||||||
|
|
||||||
return .{ .device = device };
|
return .{ .device = device };
|
||||||
}
|
}
|
||||||
@@ -177,18 +296,40 @@ 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 = @ptrCast(*Apu, @alignCast(@alignOf(*Apu), userdata));
|
const T = *Apu;
|
||||||
|
const apu = @ptrCast(T, @alignCast(@alignOf(T), userdata));
|
||||||
|
|
||||||
// TODO: Find a better way to mute this
|
comptime std.debug.assert(sample_format == SDL.AUDIO_U16);
|
||||||
if (!config.config().host.mute) {
|
const sample_buf = @ptrCast([*]u16, @alignCast(@alignOf(u16), stream))[0 .. @intCast(u32, len) / @sizeOf(u16)];
|
||||||
_ = SDL.SDL_AudioStreamGet(apu.stream, stream, len);
|
|
||||||
} else {
|
var previous: u16 = 0x8000;
|
||||||
// FIXME: I don't think this hack to remove DC Offset is acceptable :thinking:
|
for (sample_buf) |*sample| {
|
||||||
std.mem.set(u8, stream[0..@intCast(usize, len)], 0x40);
|
if (apu.sample_queue.pop()) |value| previous = value;
|
||||||
|
|
||||||
|
sample.* = previous;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// If we don't write anything, play silence otherwise garbage will be played
|
const shader = struct {
|
||||||
// if (written == 0) std.mem.set(u8, stream[0..@intCast(usize, len)], 0x40);
|
const Kind = enum { vertex, fragment };
|
||||||
|
const log = std.log.scoped(.Shader);
|
||||||
|
|
||||||
|
fn didCompile(id: gl.GLuint) bool {
|
||||||
|
var success: gl.GLint = undefined;
|
||||||
|
gl.getShaderiv(id, gl.COMPILE_STATUS, &success);
|
||||||
|
|
||||||
|
if (success == 0) err(id);
|
||||||
|
|
||||||
|
return success == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn err(id: gl.GLuint) void {
|
||||||
|
const buf_len = 512;
|
||||||
|
var error_msg: [buf_len]u8 = undefined;
|
||||||
|
|
||||||
|
gl.getShaderInfoLog(id, buf_len, 0, &error_msg);
|
||||||
|
log.err("{s}", .{std.mem.sliceTo(&error_msg, 0)});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
25
src/shader/pixelbuf.frag
Normal file
25
src/shader/pixelbuf.frag
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#version 330 core
|
||||||
|
out vec4 frag_color;
|
||||||
|
|
||||||
|
in vec3 color;
|
||||||
|
in vec2 uv;
|
||||||
|
|
||||||
|
uniform sampler2D screen;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// https://near.sh/video/color-emulation
|
||||||
|
// Thanks to Talarubi + Near for the Colour Correction
|
||||||
|
// Thanks to fleur + mattrb for the Shader Impl
|
||||||
|
|
||||||
|
vec4 color = texture(screen, uv);
|
||||||
|
color.rgb = pow(color.rgb, vec3(4.0)); // LCD Gamma
|
||||||
|
|
||||||
|
frag_color = vec4(
|
||||||
|
pow(vec3(
|
||||||
|
0 * color.b + 50 * color.g + 255 * color.r,
|
||||||
|
30 * color.b + 230 * color.g + 10 * color.r,
|
||||||
|
220 * color.b + 10 * color.g + 50 * color.r
|
||||||
|
) / 255, vec3(1.0 / 2.2)), // Out Gamma
|
||||||
|
1.0);
|
||||||
|
}
|
||||||
|
|
13
src/shader/pixelbuf.vert
Normal file
13
src/shader/pixelbuf.vert
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#version 330 core
|
||||||
|
layout (location = 0) in vec3 pos;
|
||||||
|
layout (location = 1) in vec3 in_color;
|
||||||
|
layout (location = 2) in vec2 in_uv;
|
||||||
|
|
||||||
|
out vec3 color;
|
||||||
|
out vec2 uv;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
color = in_color;
|
||||||
|
uv = in_uv;
|
||||||
|
gl_Position = vec4(pos, 1.0);
|
||||||
|
}
|
218
src/util.zig
218
src/util.zig
@@ -12,9 +12,9 @@ pub fn sext(comptime T: type, comptime U: type, value: T) T {
|
|||||||
|
|
||||||
const iT = std.meta.Int(.signed, @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 ExtU = if (@typeInfo(U).Int.signedness == .unsigned) T else iT;
|
||||||
const shift = @intCast(Log2Int(T), @typeInfo(T).Int.bits - @typeInfo(U).Int.bits);
|
const shift_amt = @intCast(Log2Int(T), @typeInfo(T).Int.bits - @typeInfo(U).Int.bits);
|
||||||
|
|
||||||
return @bitCast(T, @bitCast(iT, @as(ExtU, @truncate(U, value)) << shift) >> shift);
|
return @bitCast(T, @bitCast(iT, @as(ExtU, @truncate(U, value)) << shift_amt) >> shift_amt);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// See https://godbolt.org/z/W3en9Eche
|
/// See https://godbolt.org/z/W3en9Eche
|
||||||
@@ -47,7 +47,7 @@ pub const FpsTracker = struct {
|
|||||||
|
|
||||||
pub fn value(self: *Self) u32 {
|
pub fn value(self: *Self) u32 {
|
||||||
if (self.timer.read() >= std.time.ns_per_s) {
|
if (self.timer.read() >= std.time.ns_per_s) {
|
||||||
self.fps = self.count.swap(0, .SeqCst);
|
self.fps = self.count.swap(0, .Monotonic);
|
||||||
self.timer.reset();
|
self.timer.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,57 +66,6 @@ pub fn intToBytes(comptime T: type, value: anytype) [@sizeOf(T)]u8 {
|
|||||||
return result;
|
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
|
||||||
@@ -143,7 +92,9 @@ pub const io = struct {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn undef(comptime T: type, log: anytype, comptime format: []const u8, args: anytype) ?T {
|
pub fn undef(comptime T: type, comptime log: anytype, comptime format: []const u8, args: anytype) ?T {
|
||||||
|
@setCold(true);
|
||||||
|
|
||||||
const unhandled_io = config.config().debug.unhandled_io;
|
const unhandled_io = config.config().debug.unhandled_io;
|
||||||
|
|
||||||
log.warn(format, args);
|
log.warn(format, args);
|
||||||
@@ -151,6 +102,13 @@ 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 {
|
||||||
@@ -187,7 +145,7 @@ pub const Logger = struct {
|
|||||||
if (cpu.cpsr.t.read()) {
|
if (cpu.cpsr.t.read()) {
|
||||||
if (opcode >> 11 == 0x1E) {
|
if (opcode >> 11 == 0x1E) {
|
||||||
// Instruction 1 of a BL Opcode, print in ARM mode
|
// Instruction 1 of a BL Opcode, print in ARM mode
|
||||||
const low = cpu.bus.dbgRead(u16, cpu.r[15]);
|
const low = cpu.bus.dbgRead(u16, cpu.r[15] - 2);
|
||||||
const bl_opcode = @as(u32, opcode) << 16 | low;
|
const bl_opcode = @as(u32, opcode) << 16 | low;
|
||||||
|
|
||||||
self.print(arm_fmt, Self.fmtArgs(cpu, bl_opcode)) catch @panic("failed to write to log file");
|
self.print(arm_fmt, Self.fmtArgs(cpu, bl_opcode)) catch @panic("failed to write to log file");
|
||||||
@@ -223,7 +181,7 @@ pub const Logger = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const FmtArgTuple = std.meta.Tuple(&.{ u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32 });
|
const FmtArgTuple = struct { u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32 };
|
||||||
|
|
||||||
pub const audio = struct {
|
pub const audio = struct {
|
||||||
const _io = @import("core/bus/io.zig");
|
const _io = @import("core/bus/io.zig");
|
||||||
@@ -274,22 +232,37 @@ pub const audio = struct {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Sets the high bits of an integer to a value
|
/// Sets a quarter (8) of the bits of the u32 `left` to the value of u8 `right`
|
||||||
pub inline fn setHi(comptime T: type, left: T, right: HalfInt(T)) T {
|
pub inline fn setQuart(left: u32, addr: u8, right: u8) u32 {
|
||||||
return switch (T) {
|
const offset = @truncate(u2, addr);
|
||||||
u32 => (left & 0xFFFF_0000) | right,
|
|
||||||
u16 => (left & 0xFF00) | right,
|
return switch (offset) {
|
||||||
u8 => (left & 0xF0) | right,
|
0b00 => (left & 0xFFFF_FF00) | right,
|
||||||
else => @compileError("unsupported type"),
|
0b01 => (left & 0xFFFF_00FF) | @as(u32, right) << 8,
|
||||||
|
0b10 => (left & 0xFF00_FFFF) | @as(u32, right) << 16,
|
||||||
|
0b11 => (left & 0x00FF_FFFF) | @as(u32, right) << 24,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// sets the low bits of an integer to a value
|
/// Calculates the correct shift offset for an aligned/unaligned u8 read
|
||||||
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 @truncate(u4, byte & 1) << 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn setHalf(comptime T: type, left: T, addr: u8, right: HalfInt(T)) T {
|
||||||
|
const offset = @truncate(u1, addr >> if (T == u32) 1 else 0);
|
||||||
|
|
||||||
return switch (T) {
|
return switch (T) {
|
||||||
u32 => (left & 0x0000_FFFF) | @as(u32, right) << 16,
|
u32 => switch (offset) {
|
||||||
u16 => (left & 0x00FF) | @as(u16, right) << 8,
|
0b0 => (left & 0xFFFF_0000) | right,
|
||||||
u8 => (left & 0x0F) | @as(u8, right) << 4,
|
0b1 => (left & 0x0000_FFFF) | @as(u32, right) << 16,
|
||||||
|
},
|
||||||
|
u16 => switch (offset) {
|
||||||
|
0b0 => (left & 0xFF00) | right,
|
||||||
|
0b1 => (left & 0x00FF) | @as(u16, right) << 8,
|
||||||
|
},
|
||||||
else => @compileError("unsupported type"),
|
else => @compileError("unsupported type"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -302,3 +275,110 @@ fn HalfInt(comptime T: type) type {
|
|||||||
|
|
||||||
return std.meta.Int(type_info.Int.signedness, type_info.Int.bits >> 1);
|
return std.meta.Int(type_info.Int.signedness, type_info.Int.bits >> 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Mutex = std.Thread.Mutex;
|
||||||
|
|
||||||
|
pub fn RingBuffer(comptime T: type) type {
|
||||||
|
return struct {
|
||||||
|
const Self = @This();
|
||||||
|
const Index = usize;
|
||||||
|
const max_capacity = (@as(Index, 1) << @typeInfo(Index).Int.bits - 1) - 1; // half the range of index type
|
||||||
|
|
||||||
|
const log = std.log.scoped(.RingBuffer);
|
||||||
|
|
||||||
|
read: Index,
|
||||||
|
write: Index,
|
||||||
|
|
||||||
|
buf: []T,
|
||||||
|
|
||||||
|
mutex: Mutex,
|
||||||
|
|
||||||
|
const Error = error{buffer_full};
|
||||||
|
|
||||||
|
pub fn init(buf: []T) Self {
|
||||||
|
std.mem.set(T, buf, 0);
|
||||||
|
|
||||||
|
std.debug.assert(std.math.isPowerOfTwo(buf.len)); // capacity must be a power of two
|
||||||
|
std.debug.assert(buf.len <= max_capacity);
|
||||||
|
|
||||||
|
return .{ .read = 0, .write = 0, .buf = buf, .mutex = .{} };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(self: *Self, left: T, right: T) Error!void {
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
|
try self._push(left);
|
||||||
|
self._push(right) catch |e| {
|
||||||
|
self.write -= 1; // undo the previous write;
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pop(self: *Self) ?T {
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
|
return self._pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(self: *Self) Index {
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
|
return self._len();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _push(self: *Self, value: T) Error!void {
|
||||||
|
if (self.isFull()) return error.buffer_full;
|
||||||
|
defer self.write += 1;
|
||||||
|
|
||||||
|
self.buf[self.mask(self.write)] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _pop(self: *Self) ?T {
|
||||||
|
if (self.isEmpty()) return null;
|
||||||
|
defer self.read += 1;
|
||||||
|
|
||||||
|
return self.buf[self.mask(self.read)];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _len(self: *const Self) Index {
|
||||||
|
return self.write - self.read;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isFull(self: *const Self) bool {
|
||||||
|
return self._len() == self.buf.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isEmpty(self: *const Self) bool {
|
||||||
|
return self.read == self.write;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mask(self: *const Self, idx: Index) Index {
|
||||||
|
return idx & (self.buf.len - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test "RingBuffer" {
|
||||||
|
const Queue = RingBuffer(u8);
|
||||||
|
|
||||||
|
var buf: [4]u8 = undefined;
|
||||||
|
var queue = Queue.init(&buf);
|
||||||
|
|
||||||
|
try queue.push(1, 2);
|
||||||
|
try std.testing.expectEqual(@as(?u8, 1), queue.pop());
|
||||||
|
|
||||||
|
try queue.push(3, 4);
|
||||||
|
try std.testing.expectError(Queue.Error.buffer_full, queue.push(5, 6));
|
||||||
|
try std.testing.expectEqual(@as(?u8, 2), queue.pop());
|
||||||
|
|
||||||
|
try queue.push(7, 8);
|
||||||
|
|
||||||
|
try std.testing.expectEqual(@as(?u8, 3), queue.pop());
|
||||||
|
try std.testing.expectEqual(@as(?u8, 4), queue.pop());
|
||||||
|
try std.testing.expectEqual(@as(?u8, 7), queue.pop());
|
||||||
|
try std.testing.expectEqual(@as(?u8, 8), queue.pop());
|
||||||
|
try std.testing.expectEqual(@as(?u8, null), queue.pop());
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user