Compare commits
4 Commits
apu-things
...
047ab445ca
| Author | SHA1 | Date | |
|---|---|---|---|
| 047ab445ca | |||
| 523828226f | |||
| a774570370 | |||
| 52da1f0406 |
58
.github/workflows/main.yml
vendored
58
.github/workflows/main.yml
vendored
@@ -1,58 +0,0 @@
|
|||||||
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
|
||||||
|
|||||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -7,9 +7,3 @@
|
|||||||
[submodule "lib/known-folders"]
|
[submodule "lib/known-folders"]
|
||||||
path = lib/known-folders
|
path = lib/known-folders
|
||||||
url = https://github.com/ziglibs/known-folders
|
url = https://github.com/ziglibs/known-folders
|
||||||
[submodule "lib/zig-datetime"]
|
|
||||||
path = lib/zig-datetime
|
|
||||||
url = https://github.com/frmdstryr/zig-datetime
|
|
||||||
[submodule "lib/zig-toml"]
|
|
||||||
path = lib/zig-toml
|
|
||||||
url = https://github.com/aeronavery/zig-toml
|
|
||||||
|
|||||||
130
README.md
130
README.md
@@ -1,110 +1,64 @@
|
|||||||
# ZBA (working title)
|
# ZBA (working title)
|
||||||
|
An in-progress Gameboy Advance Emulator written in Zig ⚡!
|
||||||
A Game Boy Advance Emulator written in Zig ⚡!
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
I'm hardly the first to write a Game Boy Advance Emulator nor will I be the last. This project isn't going to compete with the GOATs like [mGBA](https://github.com/mgba-emu) or [NanoBoyAdvance](https://github.com/nba-emu/NanoBoyAdvance). There aren't any interesting ideas either like in [DSHBA](https://github.com/DenSinH/DSHBA).
|
|
||||||
|
|
||||||
This is a simple (read: incomplete) for-fun long-term project. I hope to get "mostly there", which to me means that I'm not missing any major hardware features and the set of possible improvements would be in memory timing or in UI/UX. With respect to that goal, here's what's outstanding:
|
|
||||||
|
|
||||||
### TODO
|
|
||||||
|
|
||||||
- [ ] Affine Sprites
|
|
||||||
- [ ] Windowing (see [this branch](https://git.musuka.dev/paoda/zba/src/branch/window))
|
|
||||||
- [ ] Audio Resampler (Having issues with SDL2's)
|
|
||||||
- [ ] Immediate Mode GUI
|
|
||||||
- [ ] Refactoring for easy-ish perf boosts
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
As it currently exists, ZBA is run from the terminal. In your console of choice, type `./zba --help` to see what you can do.
|
|
||||||
|
|
||||||
I typically find myself typing `./zba -b ./bin/bios.bin ./bin/test/suite.gba` to see how badly my "cool new feature" broke everything else.
|
|
||||||
|
|
||||||
Need a BIOS? Why not try using the open-source [Cult-Of-GBA BIOS](https://github.com/Cult-of-GBA/BIOS) written by [fleroviux](https://github.com/fleroviux) and [DenSinH](https://github.com/DenSinH)?
|
|
||||||
|
|
||||||
Finally it's worth noting that ZBA uses a TOML config file it'll store in your OS's data directory. See `example.toml` to learn about the defaults and what exactly you can mess around with.
|
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
- [ ] [jsmolka's GBA Test Collection](https://github.com/jsmolka/gba-tests)
|
||||||
GBA Tests | [jsmolka](https://github.com/jsmolka/)
|
- [x] `arm.gba` and `thumb.gba`
|
||||||
--- | ---
|
- [x] `flash64.gba`, `flash128.gba`, `none.gba`, and `sram.gba`
|
||||||
`arm.gba`, `thumb.gba` | PASS
|
- [x] `hello.gba`, `shades.gba`, and `stripes.gba`
|
||||||
`memory.gba`, `bios.gba` | PASS
|
- [x] `memory.gba`
|
||||||
`flash64.gba`, `flash128.gba` | PASS
|
- [x] `bios.gba`
|
||||||
`sram.gba` | PASS
|
- [ ] `nes.gba`
|
||||||
`none.gba` | PASS
|
- [ ] [DenSinH's GBA ROMs](https://github.com/DenSinH/GBARoms)
|
||||||
`hello.gba`, `shades.gba`, `stripes.gba` | PASS
|
- [x] `eeprom-test`
|
||||||
`nes.gba` | PASS
|
- [x] `flash-test`
|
||||||
|
- [x] `midikey2freq`
|
||||||
GBARoms | [DenSinH](https://github.com/DenSinH/)
|
- [ ] `swi-tests-random`
|
||||||
--- | ---
|
- [ ] [destoer's GBA Tests](https://github.com/destoer/gba_tests)
|
||||||
`eeprom-test`, `flash-test` | PASS
|
- [x] `cond_invalid.gba`
|
||||||
`midikey2freq` | PASS
|
- [x] `dma_priority.gba`
|
||||||
`swi-tests-random` | FAIL
|
- [x] `hello_world.gba`
|
||||||
|
- [x] `if_ack.gba`
|
||||||
gba_tests | [destoer](https://github.com/destoer/)
|
- [ ] `line_timing.gba`
|
||||||
--- | ---
|
- [ ] `lyc_midline.gba`
|
||||||
`cond_invalid.gba` | PASS
|
- [ ] `window_midframe.gba`
|
||||||
`dma_priority.gba` | PASS
|
- [x] [ladystarbreeze's GBA Test Collection](https://github.com/ladystarbreeze/GBA-Test-Collection)
|
||||||
`hello_world.gba` | PASS
|
- [x] `retAddr.gba`
|
||||||
`if_ack.gba` | PASS
|
- [x] `helloWorld.gba`
|
||||||
`line_timing.gba` | FAIL
|
- [x] `helloAudio.gba`
|
||||||
`lyc_midline.gba` | FAIL
|
- [x] [`armwrestler-gba-fixed.gba`](https://github.com/destoer/armwrestler-gba-fixed)
|
||||||
`window_midframe.gba` | FAIL
|
- [x] [FuzzARM](https://github.com/DenSinH/FuzzARM)
|
||||||
|
|
||||||
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)
|
||||||
- [GBATEK](https://problemkaputt.de/gbatek.htm)
|
* [TONC](https://coranac.com/tonc/text/toc.htm)
|
||||||
- [TONC](https://coranac.com/tonc/text/toc.htm)
|
* [ARM Architecture Reference Manual](https://www.intel.com/content/dam/www/programmable/us/en/pdfs/literature/third-party/ddi0100e_arm_arm.pdf)
|
||||||
- [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)
|
||||||
- [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.2978+803376708](https://github.com/ziglang/zig/tree/803376708)
|
||||||
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)
|
||||||
|
* [`bitfields.zig`](https://github.com/FlorenceOS/Florence/blob/f6044db788d35d43d66c1d7e58ef1e3c79f10d6f/lib/util/bitfields.zig)
|
||||||
|
|
||||||
Dependency | Source
|
`bitfields.zig` from [FlorenceOS](https://github.com/FlorenceOS) is included under `lib/util/bitfield.zig`.
|
||||||
--- | ---
|
|
||||||
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`, and `known-folders`
|
||||||
|
|
||||||
Be sure to provide SDL2 using:
|
Be sure to provide SDL2 using:
|
||||||
|
* Linux: Your distro's package manager
|
||||||
- Linux: Your distro's package manager
|
* MacOS: ¯\\\_(ツ)_/¯
|
||||||
- MacOS: ¯\\\_(ツ)_/¯
|
* Windows: [`vcpkg`](https://github.com/Microsoft/vcpkg) (install `sdl2:x64-windows`)
|
||||||
- Windows: [`vcpkg`](https://github.com/Microsoft/vcpkg) (install `sdl2:x64-windows`)
|
|
||||||
|
|
||||||
`SDL.zig` will provide a helpful compile error if the zig compiler is unable to find SDL2.
|
`SDL.zig` will provide a helpful compile error if the zig compiler is unable to find SDL2.
|
||||||
|
|
||||||
Once you've got all the dependencies, execute `zig build -Drelease-fast`. The executable is located at `zig-out/bin/`.
|
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
|
||||||
|
|||||||
21
build.zig
21
build.zig
@@ -1,15 +1,7 @@
|
|||||||
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
|
||||||
@@ -21,15 +13,10 @@ 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);
|
|
||||||
|
|
||||||
// Known Folders (%APPDATA%, XDG, etc.)
|
// Known Folders (%APPDATA%, XDG, etc.)
|
||||||
exe.addPackagePath("known_folders", "lib/known-folders/known-folders.zig");
|
exe.addPackagePath("known_folders", "lib/known-folders/known-folders.zig");
|
||||||
|
|
||||||
// DateTime Library
|
|
||||||
exe.addPackagePath("datetime", "lib/zig-datetime/src/main.zig");
|
|
||||||
|
|
||||||
// Bitfield type from FlorenceOS: https://github.com/FlorenceOS/
|
// Bitfield type from FlorenceOS: https://github.com/FlorenceOS/
|
||||||
// exe.addPackage(.{ .name = "bitfield", .path = .{ .path = "lib/util/bitfield.zig" } });
|
// exe.addPackage(.{ .name = "bitfield", .path = .{ .path = "lib/util/bitfield.zig" } });
|
||||||
exe.addPackagePath("bitfield", "lib/util/bitfield.zig");
|
exe.addPackagePath("bitfield", "lib/util/bitfield.zig");
|
||||||
@@ -37,17 +24,13 @@ pub fn build(b: *std.build.Builder) void {
|
|||||||
// Argument Parsing Library
|
// Argument Parsing Library
|
||||||
exe.addPackagePath("clap", "lib/zig-clap/clap.zig");
|
exe.addPackagePath("clap", "lib/zig-clap/clap.zig");
|
||||||
|
|
||||||
// TOML Library
|
|
||||||
exe.addPackagePath("toml", "lib/zig-toml/src/toml.zig");
|
|
||||||
|
|
||||||
// OpenGL 3.3 Bindings
|
|
||||||
exe.addPackagePath("gl", "lib/gl.zig");
|
|
||||||
|
|
||||||
// Zig SDL Bindings: https://github.com/MasterQ32/SDL.zig
|
// 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);
|
||||||
|
|
||||||
exe.addPackage(sdk.getNativePackage("sdl2"));
|
exe.addPackage(sdk.getNativePackage("sdl2"));
|
||||||
|
|
||||||
|
exe.setTarget(target);
|
||||||
exe.setBuildMode(mode);
|
exe.setBuildMode(mode);
|
||||||
exe.install();
|
exe.install();
|
||||||
|
|
||||||
|
|||||||
25
example.toml
25
example.toml
@@ -1,25 +0,0 @@
|
|||||||
[Host]
|
|
||||||
# Using nearest-neighbour scaling, how many times the native resolution
|
|
||||||
# of the game bow should the screen be?
|
|
||||||
win_scale = 3
|
|
||||||
# Enable VSYNC on the UI thread
|
|
||||||
vsync = true
|
|
||||||
# Mute ZBA
|
|
||||||
mute = false
|
|
||||||
|
|
||||||
[Guest]
|
|
||||||
# Sync Emulation to Audio
|
|
||||||
audio_sync = true
|
|
||||||
# Sync Emulation to Video
|
|
||||||
video_sync = true
|
|
||||||
# Force RTC support
|
|
||||||
force_rtc = false
|
|
||||||
# Skip BIOS
|
|
||||||
skip_bios = false
|
|
||||||
|
|
||||||
[Debug]
|
|
||||||
# Enable detailed CPU logs
|
|
||||||
cpu_trace = false
|
|
||||||
# When false and builtin.mode == .Debug, ZBA will panic
|
|
||||||
# on unknown I/O reads
|
|
||||||
unhandled_io = true
|
|
||||||
Submodule lib/SDL.zig updated: 00b4356885...401c50ff3d
5028
lib/gl.zig
5028
lib/gl.zig
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ fn PtrCastPreserveCV(comptime T: type, comptime PtrToT: type, comptime NewT: typ
|
|||||||
fn BitType(comptime FieldType: type, comptime ValueType: type, comptime shamt: usize) type {
|
fn BitType(comptime FieldType: type, comptime ValueType: type, comptime shamt: usize) type {
|
||||||
const self_bit: FieldType = (1 << shamt);
|
const self_bit: FieldType = (1 << shamt);
|
||||||
|
|
||||||
return extern struct {
|
return struct {
|
||||||
bits: Bitfield(FieldType, shamt, 1),
|
bits: Bitfield(FieldType, shamt, 1),
|
||||||
|
|
||||||
pub fn set(self: anytype) void {
|
pub fn set(self: anytype) void {
|
||||||
@@ -63,7 +63,7 @@ pub fn Bitfield(comptime FieldType: type, comptime shamt: usize, comptime num_bi
|
|||||||
|
|
||||||
const ValueType = std.meta.Int(.unsigned, num_bits);
|
const ValueType = std.meta.Int(.unsigned, num_bits);
|
||||||
|
|
||||||
return extern struct {
|
return struct {
|
||||||
dummy: FieldType,
|
dummy: FieldType,
|
||||||
|
|
||||||
fn field(self: anytype) PtrCastPreserveCV(@This(), @TypeOf(self), FieldType) {
|
fn field(self: anytype) PtrCastPreserveCV(@This(), @TypeOf(self), FieldType) {
|
||||||
|
|||||||
Submodule lib/zig-clap updated: a1b01ffeab...1c09e0dc31
Submodule lib/zig-datetime deleted from 932d284521
Submodule lib/zig-toml deleted from 016b8bcf98
193
src/Gui.zig
Normal file
193
src/Gui.zig
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const SDL = @import("sdl2");
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
const Apu = @import("core/apu.zig").Apu;
|
||||||
|
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
|
||||||
|
const Scheduler = @import("core/scheduler.zig").Scheduler;
|
||||||
|
const FpsTracker = @import("core/util.zig").FpsTracker;
|
||||||
|
|
||||||
|
const pitch = @import("core/ppu.zig").framebuf_pitch;
|
||||||
|
const scale = @import("core/emu.zig").win_scale;
|
||||||
|
|
||||||
|
const emu = @import("core/emu.zig");
|
||||||
|
const asString = @import("core/util.zig").asString;
|
||||||
|
const log = std.log.scoped(.GUI);
|
||||||
|
|
||||||
|
const default_title: []const u8 = "ZBA";
|
||||||
|
|
||||||
|
window: *SDL.SDL_Window,
|
||||||
|
base_title: [12]u8,
|
||||||
|
renderer: *SDL.SDL_Renderer,
|
||||||
|
texture: *SDL.SDL_Texture,
|
||||||
|
audio: ?Audio,
|
||||||
|
|
||||||
|
pub fn init(title: [12]u8, width: i32, height: i32) Self {
|
||||||
|
const ret = SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO | SDL.SDL_INIT_GAMECONTROLLER);
|
||||||
|
if (ret < 0) panic();
|
||||||
|
|
||||||
|
const window = SDL.SDL_CreateWindow(
|
||||||
|
default_title.ptr,
|
||||||
|
SDL.SDL_WINDOWPOS_CENTERED,
|
||||||
|
SDL.SDL_WINDOWPOS_CENTERED,
|
||||||
|
@as(c_int, width * scale),
|
||||||
|
@as(c_int, height * scale),
|
||||||
|
SDL.SDL_WINDOW_SHOWN,
|
||||||
|
) orelse panic();
|
||||||
|
|
||||||
|
const renderer = SDL.SDL_CreateRenderer(window, -1, SDL.SDL_RENDERER_ACCELERATED | SDL.SDL_RENDERER_PRESENTVSYNC) orelse panic();
|
||||||
|
|
||||||
|
const texture = SDL.SDL_CreateTexture(
|
||||||
|
renderer,
|
||||||
|
SDL.SDL_PIXELFORMAT_RGBA8888,
|
||||||
|
SDL.SDL_TEXTUREACCESS_STREAMING,
|
||||||
|
@as(c_int, width),
|
||||||
|
@as(c_int, height),
|
||||||
|
) orelse panic();
|
||||||
|
|
||||||
|
return Self{
|
||||||
|
.window = window,
|
||||||
|
.base_title = title,
|
||||||
|
.renderer = renderer,
|
||||||
|
.texture = texture,
|
||||||
|
.audio = null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(self: *Self, arm7tdmi: *Arm7tdmi, scheduler: *Scheduler) !void {
|
||||||
|
var quit = std.atomic.Atomic(bool).init(false);
|
||||||
|
var frame_rate = FpsTracker.init();
|
||||||
|
|
||||||
|
const thread = try std.Thread.spawn(.{}, emu.run, .{ &quit, &frame_rate, scheduler, arm7tdmi });
|
||||||
|
defer thread.join();
|
||||||
|
|
||||||
|
var title_buf: [0x100]u8 = [_]u8{0} ** 0x100;
|
||||||
|
|
||||||
|
emu_loop: while (true) {
|
||||||
|
var event: SDL.SDL_Event = undefined;
|
||||||
|
while (SDL.SDL_PollEvent(&event) != 0) {
|
||||||
|
switch (event.type) {
|
||||||
|
SDL.SDL_QUIT => break :emu_loop,
|
||||||
|
SDL.SDL_KEYDOWN => {
|
||||||
|
const io = &arm7tdmi.bus.io;
|
||||||
|
const key_code = event.key.keysym.sym;
|
||||||
|
|
||||||
|
switch (key_code) {
|
||||||
|
SDL.SDLK_UP => io.keyinput.up.unset(),
|
||||||
|
SDL.SDLK_DOWN => io.keyinput.down.unset(),
|
||||||
|
SDL.SDLK_LEFT => io.keyinput.left.unset(),
|
||||||
|
SDL.SDLK_RIGHT => io.keyinput.right.unset(),
|
||||||
|
SDL.SDLK_x => io.keyinput.a.unset(),
|
||||||
|
SDL.SDLK_z => io.keyinput.b.unset(),
|
||||||
|
SDL.SDLK_a => io.keyinput.shoulder_l.unset(),
|
||||||
|
SDL.SDLK_s => io.keyinput.shoulder_r.unset(),
|
||||||
|
SDL.SDLK_RETURN => io.keyinput.start.unset(),
|
||||||
|
SDL.SDLK_RSHIFT => io.keyinput.select.unset(),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SDL.SDL_KEYUP => {
|
||||||
|
const io = &arm7tdmi.bus.io;
|
||||||
|
const key_code = event.key.keysym.sym;
|
||||||
|
|
||||||
|
switch (key_code) {
|
||||||
|
SDL.SDLK_UP => io.keyinput.up.set(),
|
||||||
|
SDL.SDLK_DOWN => io.keyinput.down.set(),
|
||||||
|
SDL.SDLK_LEFT => io.keyinput.left.set(),
|
||||||
|
SDL.SDLK_RIGHT => io.keyinput.right.set(),
|
||||||
|
SDL.SDLK_x => io.keyinput.a.set(),
|
||||||
|
SDL.SDLK_z => io.keyinput.b.set(),
|
||||||
|
SDL.SDLK_a => io.keyinput.shoulder_l.set(),
|
||||||
|
SDL.SDLK_s => io.keyinput.shoulder_r.set(),
|
||||||
|
SDL.SDLK_RETURN => io.keyinput.start.set(),
|
||||||
|
SDL.SDLK_RSHIFT => io.keyinput.select.set(),
|
||||||
|
SDL.SDLK_i => log.err("Sample Count: {}", .{@intCast(u32, SDL.SDL_AudioStreamAvailable(arm7tdmi.bus.apu.stream)) / (2 * @sizeOf(u16))}),
|
||||||
|
SDL.SDLK_j => log.err("Scheduler Capacity: {} | Scheduler Event Count: {}", .{ scheduler.queue.capacity(), scheduler.queue.count() }),
|
||||||
|
SDL.SDLK_k => {
|
||||||
|
// Dump IWRAM to file
|
||||||
|
log.info("PC: 0x{X:0>8}", .{arm7tdmi.r[15]});
|
||||||
|
log.info("LR: 0x{X:0>8}", .{arm7tdmi.r[14]});
|
||||||
|
// const iwram_file = try std.fs.cwd().createFile("iwram.bin", .{});
|
||||||
|
// defer iwram_file.close();
|
||||||
|
|
||||||
|
// try iwram_file.writeAll(cpu.bus.iwram.buf);
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emulator has an internal Double Buffer
|
||||||
|
const framebuf = arm7tdmi.bus.ppu.framebuf.get(.Renderer);
|
||||||
|
_ = SDL.SDL_UpdateTexture(self.texture, null, framebuf.ptr, pitch);
|
||||||
|
_ = SDL.SDL_RenderCopy(self.renderer, self.texture, null, null);
|
||||||
|
SDL.SDL_RenderPresent(self.renderer);
|
||||||
|
|
||||||
|
const title = std.fmt.bufPrint(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.base_title, frame_rate.value() }) catch unreachable;
|
||||||
|
SDL.SDL_SetWindowTitle(self.window, title.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
quit.store(true, .SeqCst); // Terminate Emulator Thread
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initAudio(self: *Self, apu: *Apu) void {
|
||||||
|
self.audio = Audio.init(apu);
|
||||||
|
self.audio.?.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: Self) void {
|
||||||
|
if (self.audio) |aud| aud.deinit();
|
||||||
|
SDL.SDL_DestroyTexture(self.texture);
|
||||||
|
SDL.SDL_DestroyRenderer(self.renderer);
|
||||||
|
SDL.SDL_DestroyWindow(self.window);
|
||||||
|
SDL.SDL_Quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
const Audio = struct {
|
||||||
|
const This = @This();
|
||||||
|
const sample_rate = @import("core/apu.zig").host_sample_rate;
|
||||||
|
|
||||||
|
device: SDL.SDL_AudioDeviceID,
|
||||||
|
|
||||||
|
fn init(apu: *Apu) This {
|
||||||
|
var have: SDL.SDL_AudioSpec = undefined;
|
||||||
|
var want: SDL.SDL_AudioSpec = std.mem.zeroes(SDL.SDL_AudioSpec);
|
||||||
|
want.freq = sample_rate;
|
||||||
|
want.format = SDL.AUDIO_U16;
|
||||||
|
want.channels = 2;
|
||||||
|
want.samples = 0x100;
|
||||||
|
want.callback = This.callback;
|
||||||
|
want.userdata = apu;
|
||||||
|
|
||||||
|
const device = SDL.SDL_OpenAudioDevice(null, 0, &want, &have, 0);
|
||||||
|
if (device == 0) panic();
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.device = device,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinit(this: This) void {
|
||||||
|
SDL.SDL_CloseAudioDevice(this.device);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn play(this: *This) void {
|
||||||
|
SDL.SDL_PauseAudioDevice(this.device, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn callback(userdata: ?*anyopaque, stream: [*c]u8, len: c_int) void {
|
||||||
|
const apu = @ptrCast(*Apu, @alignCast(@alignOf(*Apu), userdata));
|
||||||
|
const written = SDL.SDL_AudioStreamGet(apu.stream, stream, len);
|
||||||
|
|
||||||
|
// If we don't write anything, play silence otherwise garbage will be played
|
||||||
|
// FIXME: I don't think this hack to remove DC Offset is acceptable :thinking:
|
||||||
|
if (written == 0) std.mem.set(u8, stream[0..@intCast(usize, len)], 0x40);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn panic() noreturn {
|
||||||
|
const str = @as(?[*:0]const u8, SDL.SDL_GetError()) orelse "unknown error";
|
||||||
|
@panic(std.mem.sliceTo(str, 0));
|
||||||
|
}
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const toml = @import("toml");
|
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const log = std.log.scoped(.Config);
|
|
||||||
var state: Config = .{};
|
|
||||||
|
|
||||||
const Config = struct {
|
|
||||||
host: Host = .{},
|
|
||||||
guest: Guest = .{},
|
|
||||||
debug: Debug = .{},
|
|
||||||
|
|
||||||
/// Settings related to the Computer the Emulator is being run on
|
|
||||||
const Host = struct {
|
|
||||||
/// Using Nearest-Neighbor, multiply the resolution of the GBA Window
|
|
||||||
win_scale: i64 = 3,
|
|
||||||
/// Enable Vsync
|
|
||||||
///
|
|
||||||
/// Note: This does not affect whether Emulation is synced to 59Hz
|
|
||||||
vsync: bool = true,
|
|
||||||
/// Mute ZBA
|
|
||||||
mute: bool = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Settings realted to the emulation itself
|
|
||||||
const Guest = struct {
|
|
||||||
/// Whether Emulation thread to sync to Audio Callbacks
|
|
||||||
audio_sync: bool = true,
|
|
||||||
/// Whether Emulation thread should sync to 59Hz
|
|
||||||
video_sync: bool = true,
|
|
||||||
/// Whether RTC I/O should always be enabled
|
|
||||||
force_rtc: bool = false,
|
|
||||||
/// Skip BIOS
|
|
||||||
skip_bios: bool = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Settings related to debugging ZBA
|
|
||||||
const Debug = struct {
|
|
||||||
/// Enable CPU Trace logs
|
|
||||||
cpu_trace: bool = false,
|
|
||||||
/// If false and ZBA is built in debug mode, ZBA will panic on unhandled I/O
|
|
||||||
unhandled_io: bool = true,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn config() *const Config {
|
|
||||||
return &state;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads a config file and then loads it into the global state
|
|
||||||
pub fn load(allocator: Allocator, file_path: []const u8) !void {
|
|
||||||
var config_file = try std.fs.cwd().openFile(file_path, .{});
|
|
||||||
defer config_file.close();
|
|
||||||
|
|
||||||
log.info("loaded from {s}", .{file_path});
|
|
||||||
|
|
||||||
const contents = try config_file.readToEndAlloc(allocator, try config_file.getEndPos());
|
|
||||||
defer allocator.free(contents);
|
|
||||||
|
|
||||||
var parser = try toml.parseFile(allocator, file_path);
|
|
||||||
defer parser.deinit();
|
|
||||||
|
|
||||||
const table = try parser.parse();
|
|
||||||
defer table.deinit();
|
|
||||||
|
|
||||||
// TODO: Report unknown config options
|
|
||||||
|
|
||||||
if (table.keys.get("Host")) |host| {
|
|
||||||
if (host.Table.keys.get("win_scale")) |scale| state.host.win_scale = scale.Integer;
|
|
||||||
if (host.Table.keys.get("vsync")) |vsync| state.host.vsync = vsync.Boolean;
|
|
||||||
if (host.Table.keys.get("mute")) |mute| state.host.mute = mute.Boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (table.keys.get("Guest")) |guest| {
|
|
||||||
if (guest.Table.keys.get("audio_sync")) |sync| state.guest.audio_sync = sync.Boolean;
|
|
||||||
if (guest.Table.keys.get("video_sync")) |sync| state.guest.video_sync = sync.Boolean;
|
|
||||||
if (guest.Table.keys.get("force_rtc")) |forced| state.guest.force_rtc = forced.Boolean;
|
|
||||||
if (guest.Table.keys.get("skip_bios")) |skip| state.guest.skip_bios = skip.Boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (table.keys.get("Debug")) |debug| {
|
|
||||||
if (debug.Table.keys.get("cpu_trace")) |trace| state.debug.cpu_trace = trace.Boolean;
|
|
||||||
if (debug.Table.keys.get("unhandled_io")) |unhandled| state.debug.unhandled_io = unhandled.Boolean;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
365
src/core/Bus.zig
365
src/core/Bus.zig
@@ -1,5 +1,6 @@
|
|||||||
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");
|
||||||
@@ -11,7 +12,7 @@ const Apu = @import("apu.zig").Apu;
|
|||||||
const DmaTuple = @import("bus/dma.zig").DmaTuple;
|
const DmaTuple = @import("bus/dma.zig").DmaTuple;
|
||||||
const TimerTuple = @import("bus/timer.zig").TimerTuple;
|
const TimerTuple = @import("bus/timer.zig").TimerTuple;
|
||||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
const Scheduler = @import("scheduler.zig").Scheduler;
|
||||||
const FilePaths = @import("../util.zig").FilePaths;
|
const FilePaths = @import("util.zig").FilePaths;
|
||||||
|
|
||||||
const io = @import("bus/io.zig");
|
const io = @import("bus/io.zig");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
@@ -19,7 +20,7 @@ const log = std.log.scoped(.Bus);
|
|||||||
|
|
||||||
const createDmaTuple = @import("bus/dma.zig").create;
|
const createDmaTuple = @import("bus/dma.zig").create;
|
||||||
const createTimerTuple = @import("bus/timer.zig").create;
|
const createTimerTuple = @import("bus/timer.zig").create;
|
||||||
const rotr = @import("../util.zig").rotr;
|
const rotr = @import("util.zig").rotr;
|
||||||
|
|
||||||
const timings: [2][0x10]u8 = [_][0x10]u8{
|
const timings: [2][0x10]u8 = [_][0x10]u8{
|
||||||
// BIOS, Unused, EWRAM, IWRAM, I/0, PALRAM, VRAM, OAM, ROM0, ROM0, ROM1, ROM1, ROM2, ROM2, SRAM, Unused
|
// BIOS, Unused, EWRAM, IWRAM, I/0, PALRAM, VRAM, OAM, ROM0, ROM0, ROM1, ROM1, ROM2, ROM2, SRAM, Unused
|
||||||
@@ -33,11 +34,6 @@ pub const fetch_timings: [2][0x10]u8 = [_][0x10]u8{
|
|||||||
[_]u8{ 1, 1, 6, 1, 1, 2, 2, 1, 4, 4, 4, 4, 4, 4, 8, 8 }, // 32-bit
|
[_]u8{ 1, 1, 6, 1, 1, 2, 2, 1, 4, 4, 4, 4, 4, 4, 8, 8 }, // 32-bit
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fastmem Related
|
|
||||||
const page_size = 1 * 0x400; // 1KiB
|
|
||||||
const address_space_size = 0x1000_0000;
|
|
||||||
const table_len = address_space_size / page_size;
|
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
pak: GamePak,
|
pak: GamePak,
|
||||||
@@ -50,179 +46,62 @@ iwram: Iwram,
|
|||||||
ewram: Ewram,
|
ewram: Ewram,
|
||||||
io: Io,
|
io: Io,
|
||||||
|
|
||||||
cpu: *Arm7tdmi,
|
cpu: ?*Arm7tdmi,
|
||||||
sched: *Scheduler,
|
sched: *Scheduler,
|
||||||
|
|
||||||
read_table: *const [table_len]?*const anyopaque,
|
pub fn init(alloc: Allocator, sched: *Scheduler, paths: FilePaths) !Self {
|
||||||
write_tables: [2]*const [table_len]?*anyopaque,
|
return Self{
|
||||||
allocator: Allocator,
|
.pak = try GamePak.init(alloc, paths.rom, paths.save),
|
||||||
|
.bios = try Bios.init(alloc, paths.bios),
|
||||||
pub fn init(self: *Self, allocator: Allocator, sched: *Scheduler, cpu: *Arm7tdmi, paths: FilePaths) !void {
|
.ppu = try Ppu.init(alloc, sched),
|
||||||
const tables = try allocator.alloc(?*anyopaque, 3 * table_len); // Allocate all tables
|
|
||||||
|
|
||||||
const read_table: *[table_len]?*const anyopaque = tables[0..table_len];
|
|
||||||
const left_write: *[table_len]?*anyopaque = tables[table_len .. 2 * table_len];
|
|
||||||
const right_write: *[table_len]?*anyopaque = tables[2 * table_len .. 3 * table_len];
|
|
||||||
|
|
||||||
self.* = .{
|
|
||||||
.pak = try GamePak.init(allocator, cpu, paths.rom, paths.save),
|
|
||||||
.bios = try Bios.init(allocator, paths.bios),
|
|
||||||
.ppu = try Ppu.init(allocator, sched),
|
|
||||||
.apu = Apu.init(sched),
|
.apu = Apu.init(sched),
|
||||||
.iwram = try Iwram.init(allocator),
|
.iwram = try Iwram.init(alloc),
|
||||||
.ewram = try Ewram.init(allocator),
|
.ewram = try Ewram.init(alloc),
|
||||||
.dma = createDmaTuple(),
|
.dma = createDmaTuple(),
|
||||||
.tim = createTimerTuple(sched),
|
.tim = createTimerTuple(sched),
|
||||||
.io = Io.init(),
|
.io = Io.init(),
|
||||||
.cpu = cpu,
|
.cpu = null,
|
||||||
.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 {
|
||||||
self.iwram.deinit();
|
self.iwram.deinit();
|
||||||
self.ewram.deinit();
|
self.ewram.deinit();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fillReadTable(bus: *Self, table: *[table_len]?*const anyopaque) void {
|
pub fn attach(self: *Self, cpu: *Arm7tdmi) void {
|
||||||
const vramMirror = @import("ppu.zig").Vram.mirror;
|
self.cpu = cpu;
|
||||||
|
|
||||||
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 {
|
pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
|
||||||
comptime std.debug.assert(T == u32 or T == u16 or T == u8);
|
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 => 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], address);
|
break :blk self.bios.dbgRead(T, self.cpu.?.r[15], aligned_addr);
|
||||||
|
|
||||||
break :blk self.openBus(T, address);
|
break :blk self.readOpenBus(T, address);
|
||||||
},
|
},
|
||||||
0x02 => self.ewram.read(T, address),
|
0x02 => self.ewram.read(T, aligned_addr),
|
||||||
0x03 => self.iwram.read(T, address),
|
0x03 => self.iwram.read(T, aligned_addr),
|
||||||
0x04 => self.readIo(T, address),
|
0x04 => io.read(self, T, aligned_addr),
|
||||||
|
|
||||||
// Internal Display Memory
|
// Internal Display Memory
|
||||||
0x05 => self.ppu.palette.read(T, address),
|
0x05 => self.ppu.palette.read(T, aligned_addr),
|
||||||
0x06 => self.ppu.vram.read(T, address),
|
0x06 => self.ppu.vram.read(T, aligned_addr),
|
||||||
0x07 => self.ppu.oam.read(T, address),
|
0x07 => self.ppu.oam.read(T, aligned_addr),
|
||||||
|
|
||||||
// External Memory (Game Pak)
|
// External Memory (Game Pak)
|
||||||
0x08...0x0D => self.pak.dbgRead(T, address),
|
0x08...0x0D => self.pak.dbgRead(T, aligned_addr),
|
||||||
0x0E...0x0F => blk: {
|
0x0E...0x0F => blk: {
|
||||||
const value = self.pak.backup.read(unaligned_address);
|
const value = self.pak.backup.read(address);
|
||||||
|
|
||||||
const multiplier = switch (T) {
|
const multiplier = switch (T) {
|
||||||
u32 => 0x01010101,
|
u32 => 0x01010101,
|
||||||
@@ -233,127 +112,66 @@ pub fn dbgRead(self: *const Self, comptime T: type, unaligned_address: u32) T {
|
|||||||
|
|
||||||
break :blk @as(T, value) * multiplier;
|
break :blk @as(T, value) * multiplier;
|
||||||
},
|
},
|
||||||
else => self.openBus(T, address),
|
else => self.readOpenBus(T, address),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn readIo(self: *const Self, comptime T: type, address: u32) T {
|
fn readOpenBus(self: *const Self, comptime T: type, address: u32) T {
|
||||||
return io.read(self, T, address) orelse self.openBus(T, address);
|
const r15 = self.cpu.?.r[15];
|
||||||
}
|
|
||||||
|
|
||||||
fn openBus(self: *const Self, comptime T: type, address: u32) T {
|
|
||||||
@setCold(true);
|
|
||||||
const r15 = self.cpu.r[15];
|
|
||||||
|
|
||||||
const word = blk: {
|
|
||||||
// If Arm, get the most recently fetched instruction (PC + 8)
|
|
||||||
//
|
|
||||||
// FIXME: This is most likely a faulty assumption.
|
|
||||||
// I think what *actually* happens is that the Bus has a latch for the most
|
|
||||||
// recently fetched piece of data, which is then returned during Open Bus (also DMA open bus?)
|
|
||||||
// I can "get away" with this because it's very statistically likely that the most recently latched value is
|
|
||||||
// the most recently fetched instruction by the pipeline
|
|
||||||
if (!self.cpu.cpsr.t.read()) break :blk self.cpu.pipe.stage[1].?;
|
|
||||||
|
|
||||||
|
const word = if (self.cpu.?.cpsr.t.read()) blk: {
|
||||||
const page = @truncate(u8, r15 >> 24);
|
const page = @truncate(u8, r15 >> 24);
|
||||||
|
|
||||||
// PC + 2 = stage[0]
|
|
||||||
// PC + 4 = stage[1]
|
|
||||||
// PC + 6 = Need a Debug Read for this?
|
|
||||||
|
|
||||||
switch (page) {
|
switch (page) {
|
||||||
// EWRAM, PALRAM, VRAM, and Game ROM (16-bit)
|
// EWRAM, PALRAM, VRAM, and Game ROM (16-bit)
|
||||||
0x02, 0x05, 0x06, 0x08...0x0D => {
|
0x02, 0x05, 0x06, 0x08...0x0D => {
|
||||||
const halfword: u32 = @truncate(u16, self.cpu.pipe.stage[1].?);
|
const halfword = self.dbgRead(u16, r15 + 2);
|
||||||
break :blk halfword << 16 | halfword;
|
break :blk @as(u32, halfword) << 16 | halfword;
|
||||||
},
|
},
|
||||||
|
|
||||||
// BIOS or OAM (32-bit)
|
// BIOS or OAM (32-bit)
|
||||||
0x00, 0x07 => {
|
0x00, 0x07 => {
|
||||||
// Aligned: (PC + 6) | (PC + 4)
|
const offset: u32 = if (address & 3 == 0b00) 2 else 0;
|
||||||
// Unaligned: (PC + 4) | (PC + 2)
|
break :blk @as(u32, self.dbgRead(u16, (r15 + 2) + offset)) << 16 | self.dbgRead(u16, r15 + offset);
|
||||||
const aligned = address & 3 == 0b00;
|
|
||||||
|
|
||||||
// TODO: What to do on PC + 6?
|
|
||||||
const high: u32 = if (aligned) self.dbgRead(u16, r15 + 4) else @truncate(u16, self.cpu.pipe.stage[1].?);
|
|
||||||
const low: u32 = @truncate(u16, self.cpu.pipe.stage[@boolToInt(aligned)].?);
|
|
||||||
|
|
||||||
break :blk high << 16 | low;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// IWRAM (16-bit but special)
|
// IWRAM (16-bit but special)
|
||||||
0x03 => {
|
0x03 => {
|
||||||
// Aligned: (PC + 2) | (PC + 4)
|
const offset: u32 = if (address & 3 == 0b00) 2 else 0;
|
||||||
// Unaligned: (PC + 4) | (PC + 2)
|
break :blk @as(u32, self.dbgRead(u16, (r15 + 2) - offset)) << 16 | self.dbgRead(u16, r15 + offset);
|
||||||
const aligned = address & 3 == 0b00;
|
|
||||||
|
|
||||||
const high: u32 = @truncate(u16, self.cpu.pipe.stage[1 - @boolToInt(aligned)].?);
|
|
||||||
const low: u32 = @truncate(u16, self.cpu.pipe.stage[@boolToInt(aligned)].?);
|
|
||||||
|
|
||||||
break :blk high << 16 | low;
|
|
||||||
},
|
|
||||||
else => {
|
|
||||||
log.err("THUMB open bus read from 0x{X:0>2} page @0x{X:0>8}", .{ page, address });
|
|
||||||
@panic("invariant most-likely broken");
|
|
||||||
},
|
},
|
||||||
|
else => unreachable,
|
||||||
}
|
}
|
||||||
};
|
} else self.dbgRead(u32, r15 + 4);
|
||||||
|
|
||||||
return @truncate(T, word);
|
return @truncate(T, rotr(u32, word, 8 * (address & 3)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read(self: *Self, comptime T: type, unaligned_address: u32) T {
|
pub fn read(self: *Self, comptime T: type, address: u32) T {
|
||||||
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits;
|
const page = @truncate(u8, address >> 24);
|
||||||
const page = unaligned_address >> bits;
|
const aligned_addr = forceAlign(T, address);
|
||||||
const offset = unaligned_address & (page_size - 1);
|
|
||||||
|
|
||||||
// whether or not we do this in slowmem or fastmem, we should advance the scheduler
|
self.sched.tick += timings[@boolToInt(T == u32)][@truncate(u4, page)];
|
||||||
self.sched.tick += timings[@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], address);
|
break :blk self.bios.read(T, self.cpu.?.r[15], aligned_addr);
|
||||||
|
|
||||||
break :blk self.openBus(T, address);
|
break :blk self.readOpenBus(T, address);
|
||||||
},
|
},
|
||||||
0x02 => unreachable, // completely handled by fastmeme
|
0x02 => self.ewram.read(T, aligned_addr),
|
||||||
0x03 => unreachable, // completely handled by fastmeme
|
0x03 => self.iwram.read(T, aligned_addr),
|
||||||
0x04 => self.readIo(T, address),
|
0x04 => io.read(self, T, aligned_addr),
|
||||||
|
|
||||||
// Internal Display Memory
|
// Internal Display Memory
|
||||||
0x05 => unreachable, // completely handled by fastmeme
|
0x05 => self.ppu.palette.read(T, aligned_addr),
|
||||||
0x06 => unreachable, // completely handled by fastmeme
|
0x06 => self.ppu.vram.read(T, aligned_addr),
|
||||||
0x07 => unreachable, // completely handled by fastmeme
|
0x07 => self.ppu.oam.read(T, aligned_addr),
|
||||||
|
|
||||||
// External Memory (Game Pak)
|
// External Memory (Game Pak)
|
||||||
0x08...0x0D => self.pak.read(T, address),
|
0x08...0x0D => self.pak.read(T, aligned_addr),
|
||||||
0x0E...0x0F => blk: {
|
0x0E...0x0F => blk: {
|
||||||
const value = self.pak.backup.read(unaligned_address);
|
const value = self.pak.backup.read(address);
|
||||||
|
|
||||||
const multiplier = switch (T) {
|
const multiplier = switch (T) {
|
||||||
u32 => 0x01010101,
|
u32 => 0x01010101,
|
||||||
@@ -364,75 +182,48 @@ fn slowRead(self: *Self, comptime T: type, unaligned_address: u32) T {
|
|||||||
|
|
||||||
break :blk @as(T, value) * multiplier;
|
break :blk @as(T, value) * multiplier;
|
||||||
},
|
},
|
||||||
else => self.openBus(T, address),
|
else => self.readOpenBus(T, address),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
|
pub fn write(self: *Self, comptime T: type, address: u32, value: T) void {
|
||||||
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits;
|
const page = @truncate(u8, address >> 24);
|
||||||
const page = unaligned_address >> bits;
|
const aligned_addr = forceAlign(T, address);
|
||||||
const offset = unaligned_address & (page_size - 1);
|
|
||||||
|
|
||||||
// whether or not we do this in slowmem or fastmem, we should advance the scheduler
|
self.sched.tick += timings[@boolToInt(T == u32)][@truncate(u4, page)];
|
||||||
self.sched.tick += timings[@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, address, value),
|
0x00 => self.bios.write(T, aligned_addr, value),
|
||||||
0x02 => unreachable, // completely handled by fastmem
|
0x02 => self.ewram.write(T, aligned_addr, value),
|
||||||
0x03 => unreachable, // completely handled by fastmem
|
0x03 => self.iwram.write(T, aligned_addr, value),
|
||||||
0x04 => io.write(self, T, address, value),
|
0x04 => io.write(self, T, aligned_addr, value),
|
||||||
|
|
||||||
// Internal Display Memory
|
// Internal Display Memory
|
||||||
0x05 => self.ppu.palette.write(T, address, value),
|
0x05 => self.ppu.palette.write(T, aligned_addr, value),
|
||||||
0x06 => self.ppu.vram.write(T, self.ppu.dispcnt, address, value),
|
0x06 => self.ppu.vram.write(T, self.ppu.dispcnt, aligned_addr, value),
|
||||||
0x07 => unreachable, // completely handled by fastmem
|
0x07 => self.ppu.oam.write(T, aligned_addr, value),
|
||||||
|
|
||||||
// External Memory (Game Pak)
|
// External Memory (Game Pak)
|
||||||
0x08...0x0D => self.pak.write(T, self.dma[3].word_count, address, value),
|
0x08...0x0D => self.pak.write(T, self.dma[3].word_count, aligned_addr, value),
|
||||||
0x0E...0x0F => self.pak.backup.write(unaligned_address, @truncate(u8, rotr(T, value, 8 * rotateBy(T, unaligned_address)))),
|
0x0E...0x0F => {
|
||||||
else => {},
|
const rotate_by = switch (T) {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fn rotateBy(comptime T: type, address: u32) u32 {
|
|
||||||
return switch (T) {
|
|
||||||
u32 => address & 3,
|
u32 => address & 3,
|
||||||
u16 => address & 1,
|
u16 => address & 1,
|
||||||
u8 => 0,
|
u8 => 0,
|
||||||
else => @compileError("Backup: Unsupported write width"),
|
else => @compileError("Backup: Unsupported write width"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.pak.backup.write(address, @truncate(u8, rotr(T, value, 8 * rotate_by)));
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fn forceAlign(comptime T: type, address: u32) u32 {
|
fn forceAlign(comptime T: type, address: u32) u32 {
|
||||||
return switch (T) {
|
return switch (T) {
|
||||||
u32 => address & ~@as(u32, 3),
|
u32 => address & 0xFFFF_FFFC,
|
||||||
u16 => address & ~@as(u32, 1),
|
u16 => address & 0xFFFF_FFFE,
|
||||||
u8 => address,
|
u8 => address,
|
||||||
else => @compileError("Bus: Invalid read/write type"),
|
else => @compileError("Bus: Invalid read/write type"),
|
||||||
};
|
};
|
||||||
|
|||||||
1348
src/core/apu.zig
1348
src/core/apu.zig
File diff suppressed because it is too large
Load Diff
@@ -1,145 +0,0 @@
|
|||||||
const io = @import("../bus/io.zig");
|
|
||||||
const util = @import("../../util.zig");
|
|
||||||
|
|
||||||
const Scheduler = @import("../scheduler.zig").Scheduler;
|
|
||||||
const FrameSequencer = @import("../apu.zig").FrameSequencer;
|
|
||||||
const Tick = @import("../apu.zig").Apu.Tick;
|
|
||||||
const Envelope = @import("device/Envelope.zig");
|
|
||||||
const Length = @import("device/Length.zig");
|
|
||||||
const Lfsr = @import("signal/Lfsr.zig");
|
|
||||||
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
/// Write-only
|
|
||||||
/// NR41
|
|
||||||
len: u6,
|
|
||||||
/// NR42
|
|
||||||
envelope: io.Envelope,
|
|
||||||
/// NR43
|
|
||||||
poly: io.PolyCounter,
|
|
||||||
/// NR44
|
|
||||||
cnt: io.NoiseControl,
|
|
||||||
|
|
||||||
/// Length Functionarlity
|
|
||||||
len_dev: Length,
|
|
||||||
|
|
||||||
/// Envelope Functionality
|
|
||||||
env_dev: Envelope,
|
|
||||||
|
|
||||||
// Linear Feedback Shift Register
|
|
||||||
lfsr: Lfsr,
|
|
||||||
|
|
||||||
enabled: bool,
|
|
||||||
sample: i8,
|
|
||||||
|
|
||||||
pub fn init(sched: *Scheduler) Self {
|
|
||||||
return .{
|
|
||||||
.len = 0,
|
|
||||||
.envelope = .{ .raw = 0 },
|
|
||||||
.poly = .{ .raw = 0 },
|
|
||||||
.cnt = .{ .raw = 0 },
|
|
||||||
.enabled = false,
|
|
||||||
|
|
||||||
.len_dev = Length.create(),
|
|
||||||
.env_dev = Envelope.create(),
|
|
||||||
.lfsr = Lfsr.create(sched),
|
|
||||||
|
|
||||||
.sample = 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset(self: *Self) void {
|
|
||||||
self.len = 0; // NR41
|
|
||||||
self.envelope.raw = 0; // NR42
|
|
||||||
self.poly.raw = 0; // NR43
|
|
||||||
self.cnt.raw = 0; // NR44
|
|
||||||
|
|
||||||
self.len_dev.reset();
|
|
||||||
self.env_dev.reset();
|
|
||||||
|
|
||||||
self.sample = 0;
|
|
||||||
self.enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tick(self: *Self, comptime kind: Tick) void {
|
|
||||||
switch (kind) {
|
|
||||||
.Length => self.len_dev.tick(self.cnt.length_enable.read(), &self.enabled),
|
|
||||||
.Envelope => self.env_dev.tick(self.envelope),
|
|
||||||
.Sweep => @compileError("Channel 4 does not implement Sweep"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR41, NR42
|
|
||||||
pub fn sound4CntL(self: *const Self) u16 {
|
|
||||||
return @as(u16, self.envelope.raw) << 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR41, NR42
|
|
||||||
pub fn setSound4CntL(self: *Self, value: u16) void {
|
|
||||||
self.setNr41(@truncate(u8, value));
|
|
||||||
self.setNr42(@truncate(u8, value >> 8));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR41
|
|
||||||
pub fn setNr41(self: *Self, len: u8) void {
|
|
||||||
self.len = @truncate(u6, len);
|
|
||||||
self.len_dev.timer = @as(u7, 64) - @truncate(u6, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR42
|
|
||||||
pub fn setNr42(self: *Self, value: u8) void {
|
|
||||||
self.envelope.raw = value;
|
|
||||||
if (!self.isDacEnabled()) self.enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR43, NR44
|
|
||||||
pub fn sound4CntH(self: *const Self) u16 {
|
|
||||||
return @as(u16, self.poly.raw & 0x40) << 8 | self.cnt.raw;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR43, NR44
|
|
||||||
pub fn setSound4CntH(self: *Self, fs: *const FrameSequencer, value: u16) void {
|
|
||||||
self.poly.raw = @truncate(u8, value);
|
|
||||||
self.setNr44(fs, @truncate(u8, value >> 8));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR44
|
|
||||||
pub fn setNr44(self: *Self, fs: *const FrameSequencer, byte: u8) void {
|
|
||||||
var new: io.NoiseControl = .{ .raw = byte };
|
|
||||||
|
|
||||||
if (new.trigger.read()) {
|
|
||||||
self.enabled = true;
|
|
||||||
|
|
||||||
if (self.len_dev.timer == 0) {
|
|
||||||
self.len_dev.timer =
|
|
||||||
if (!fs.isLengthNext() and new.length_enable.read()) 63 else 64;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update The Frequency Timer
|
|
||||||
self.lfsr.reload(self.poly);
|
|
||||||
self.lfsr.shift = 0x7FFF;
|
|
||||||
|
|
||||||
// Update Envelope and Volume
|
|
||||||
self.env_dev.timer = self.envelope.period.read();
|
|
||||||
if (fs.isEnvelopeNext() and self.env_dev.timer != 0b111) self.env_dev.timer += 1;
|
|
||||||
|
|
||||||
self.env_dev.vol = self.envelope.init_vol.read();
|
|
||||||
|
|
||||||
self.enabled = self.isDacEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
util.audio.length.ch4.update(self, fs, new);
|
|
||||||
self.cnt = new;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn onNoiseEvent(self: *Self, late: u64) void {
|
|
||||||
self.lfsr.onLfsrTimerExpire(self.poly, late);
|
|
||||||
|
|
||||||
self.sample = 0;
|
|
||||||
if (!self.isDacEnabled()) return;
|
|
||||||
self.sample = if (self.enabled) self.lfsr.sample() * @as(i8, self.env_dev.vol) else 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isDacEnabled(self: *const Self) bool {
|
|
||||||
return self.envelope.raw & 0xF8 != 0x00;
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
const io = @import("../bus/io.zig");
|
|
||||||
const util = @import("../../util.zig");
|
|
||||||
|
|
||||||
const Scheduler = @import("../scheduler.zig").Scheduler;
|
|
||||||
const FrameSequencer = @import("../apu.zig").FrameSequencer;
|
|
||||||
const Tick = @import("../apu.zig").Apu.Tick;
|
|
||||||
const Length = @import("device/Length.zig");
|
|
||||||
const Envelope = @import("device/Envelope.zig");
|
|
||||||
const Square = @import("signal/Square.zig");
|
|
||||||
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
/// NR21
|
|
||||||
duty: io.Duty,
|
|
||||||
/// NR22
|
|
||||||
envelope: io.Envelope,
|
|
||||||
/// NR23, NR24
|
|
||||||
freq: io.Frequency,
|
|
||||||
|
|
||||||
/// Length Functionarlity
|
|
||||||
len_dev: Length,
|
|
||||||
/// Envelope Functionality
|
|
||||||
env_dev: Envelope,
|
|
||||||
/// FrequencyTimer Functionality
|
|
||||||
square: Square,
|
|
||||||
|
|
||||||
enabled: bool,
|
|
||||||
sample: i8,
|
|
||||||
|
|
||||||
pub fn init(sched: *Scheduler) Self {
|
|
||||||
return .{
|
|
||||||
.duty = .{ .raw = 0 },
|
|
||||||
.envelope = .{ .raw = 0 },
|
|
||||||
.freq = .{ .raw = 0 },
|
|
||||||
.enabled = false,
|
|
||||||
|
|
||||||
.square = Square.init(sched),
|
|
||||||
.len_dev = Length.create(),
|
|
||||||
.env_dev = Envelope.create(),
|
|
||||||
|
|
||||||
.sample = 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset(self: *Self) void {
|
|
||||||
self.duty.raw = 0; // NR21
|
|
||||||
self.envelope.raw = 0; // NR22
|
|
||||||
self.freq.raw = 0; // NR32, NR24
|
|
||||||
|
|
||||||
self.len_dev.reset();
|
|
||||||
self.env_dev.reset();
|
|
||||||
|
|
||||||
self.sample = 0;
|
|
||||||
self.enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tick(self: *Self, comptime kind: Tick) void {
|
|
||||||
switch (kind) {
|
|
||||||
.Length => self.len_dev.tick(self.freq.length_enable.read(), &self.enabled),
|
|
||||||
.Envelope => self.env_dev.tick(self.envelope),
|
|
||||||
.Sweep => @compileError("Channel 2 does not implement Sweep"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn onToneEvent(self: *Self, late: u64) void {
|
|
||||||
self.square.onSquareTimerExpire(Self, self.freq, late);
|
|
||||||
|
|
||||||
self.sample = 0;
|
|
||||||
if (!self.isDacEnabled()) return;
|
|
||||||
self.sample = if (self.enabled) self.square.sample(self.duty) * @as(i8, self.env_dev.vol) else 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR21, NR22
|
|
||||||
pub fn sound2CntL(self: *const Self) u16 {
|
|
||||||
return @as(u16, self.envelope.raw) << 8 | (self.duty.raw & 0xC0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR21, NR22
|
|
||||||
pub fn setSound2CntL(self: *Self, value: u16) void {
|
|
||||||
self.setNr21(@truncate(u8, value));
|
|
||||||
self.setNr22(@truncate(u8, value >> 8));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR21
|
|
||||||
pub fn setNr21(self: *Self, value: u8) void {
|
|
||||||
self.duty.raw = value;
|
|
||||||
self.len_dev.timer = @as(u7, 64) - @truncate(u6, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR22
|
|
||||||
pub fn setNr22(self: *Self, value: u8) void {
|
|
||||||
self.envelope.raw = value;
|
|
||||||
if (!self.isDacEnabled()) self.enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR23, NR24
|
|
||||||
pub fn sound2CntH(self: *const Self) u16 {
|
|
||||||
return self.freq.raw & 0x4000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR23, NR24
|
|
||||||
pub fn setSound2CntH(self: *Self, fs: *const FrameSequencer, value: u16) void {
|
|
||||||
self.setNr23(@truncate(u8, value));
|
|
||||||
self.setNr24(fs, @truncate(u8, value >> 8));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR23
|
|
||||||
pub fn setNr23(self: *Self, byte: u8) void {
|
|
||||||
self.freq.raw = (self.freq.raw & 0xFF00) | byte;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR24
|
|
||||||
pub fn setNr24(self: *Self, fs: *const FrameSequencer, byte: u8) void {
|
|
||||||
var new: io.Frequency = .{ .raw = (@as(u16, byte) << 8) | (self.freq.raw & 0xFF) };
|
|
||||||
|
|
||||||
if (new.trigger.read()) {
|
|
||||||
self.enabled = true;
|
|
||||||
|
|
||||||
if (self.len_dev.timer == 0) {
|
|
||||||
self.len_dev.timer =
|
|
||||||
if (!fs.isLengthNext() and new.length_enable.read()) 63 else 64;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.square.reload(Self, self.freq.frequency.read());
|
|
||||||
|
|
||||||
// Reload Envelope period and timer
|
|
||||||
self.env_dev.timer = self.envelope.period.read();
|
|
||||||
if (fs.isEnvelopeNext() and self.env_dev.timer != 0b111) self.env_dev.timer += 1;
|
|
||||||
|
|
||||||
self.env_dev.vol = self.envelope.init_vol.read();
|
|
||||||
|
|
||||||
self.enabled = self.isDacEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
util.audio.length.update(Self, self, fs, new);
|
|
||||||
self.freq = new;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isDacEnabled(self: *const Self) bool {
|
|
||||||
return self.envelope.raw & 0xF8 != 0;
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
const io = @import("../bus/io.zig");
|
|
||||||
const util = @import("../../util.zig");
|
|
||||||
|
|
||||||
const Scheduler = @import("../scheduler.zig").Scheduler;
|
|
||||||
const FrameSequencer = @import("../apu.zig").FrameSequencer;
|
|
||||||
const Length = @import("device/Length.zig");
|
|
||||||
const Envelope = @import("device/Envelope.zig");
|
|
||||||
const Sweep = @import("device/Sweep.zig");
|
|
||||||
const Square = @import("signal/Square.zig");
|
|
||||||
|
|
||||||
const Tick = @import("../apu.zig").Apu.Tick;
|
|
||||||
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
/// NR10
|
|
||||||
sweep: io.Sweep,
|
|
||||||
/// NR11
|
|
||||||
duty: io.Duty,
|
|
||||||
/// NR12
|
|
||||||
envelope: io.Envelope,
|
|
||||||
/// NR13, NR14
|
|
||||||
freq: io.Frequency,
|
|
||||||
|
|
||||||
/// Length Functionality
|
|
||||||
len_dev: Length,
|
|
||||||
/// Sweep Functionality
|
|
||||||
sweep_dev: Sweep,
|
|
||||||
/// Envelope Functionality
|
|
||||||
env_dev: Envelope,
|
|
||||||
/// Frequency Timer Functionality
|
|
||||||
square: Square,
|
|
||||||
enabled: bool,
|
|
||||||
|
|
||||||
sample: i8,
|
|
||||||
|
|
||||||
pub fn init(sched: *Scheduler) Self {
|
|
||||||
return .{
|
|
||||||
.sweep = .{ .raw = 0 },
|
|
||||||
.duty = .{ .raw = 0 },
|
|
||||||
.envelope = .{ .raw = 0 },
|
|
||||||
.freq = .{ .raw = 0 },
|
|
||||||
.sample = 0,
|
|
||||||
.enabled = false,
|
|
||||||
|
|
||||||
.square = Square.init(sched),
|
|
||||||
.len_dev = Length.create(),
|
|
||||||
.sweep_dev = Sweep.create(),
|
|
||||||
.env_dev = Envelope.create(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset(self: *Self) void {
|
|
||||||
self.sweep.raw = 0; // NR10
|
|
||||||
self.duty.raw = 0; // NR11
|
|
||||||
self.envelope.raw = 0; // NR12
|
|
||||||
self.freq.raw = 0; // NR13, NR14
|
|
||||||
|
|
||||||
self.len_dev.reset();
|
|
||||||
self.sweep_dev.reset();
|
|
||||||
self.env_dev.reset();
|
|
||||||
|
|
||||||
self.sample = 0;
|
|
||||||
self.enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tick(self: *Self, comptime kind: Tick) void {
|
|
||||||
switch (kind) {
|
|
||||||
.Length => self.len_dev.tick(self.freq.length_enable.read(), &self.enabled),
|
|
||||||
.Envelope => self.env_dev.tick(self.envelope),
|
|
||||||
.Sweep => self.sweep_dev.tick(self),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn onToneSweepEvent(self: *Self, late: u64) void {
|
|
||||||
self.square.onSquareTimerExpire(Self, self.freq, late);
|
|
||||||
|
|
||||||
self.sample = 0;
|
|
||||||
if (!self.isDacEnabled()) return;
|
|
||||||
self.sample = if (self.enabled) self.square.sample(self.duty) * @as(i8, self.env_dev.vol) else 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR10, NR11, NR12
|
|
||||||
pub fn setSound1Cnt(self: *Self, value: u32) void {
|
|
||||||
self.setSound1CntL(@truncate(u8, value));
|
|
||||||
self.setSound1CntH(@truncate(u16, value >> 16));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR10
|
|
||||||
pub fn sound1CntL(self: *const Self) u8 {
|
|
||||||
return self.sweep.raw & 0x7F;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR10
|
|
||||||
pub fn setSound1CntL(self: *Self, value: u8) void {
|
|
||||||
const new = io.Sweep{ .raw = value };
|
|
||||||
|
|
||||||
if (!new.direction.read()) {
|
|
||||||
// If at least one (1) sweep calculation has been made with
|
|
||||||
// the negate bit set (since last trigger), disable the channel
|
|
||||||
|
|
||||||
if (self.sweep_dev.calc_performed) self.enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.sweep.raw = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR11, NR12
|
|
||||||
pub fn sound1CntH(self: *const Self) u16 {
|
|
||||||
return @as(u16, self.envelope.raw) << 8 | (self.duty.raw & 0xC0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR11, NR12
|
|
||||||
pub fn setSound1CntH(self: *Self, value: u16) void {
|
|
||||||
self.setNr11(@truncate(u8, value));
|
|
||||||
self.setNr12(@truncate(u8, value >> 8));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR11
|
|
||||||
pub fn setNr11(self: *Self, value: u8) void {
|
|
||||||
self.duty.raw = value;
|
|
||||||
self.len_dev.timer = @as(u7, 64) - @truncate(u6, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR12
|
|
||||||
pub fn setNr12(self: *Self, value: u8) void {
|
|
||||||
self.envelope.raw = value;
|
|
||||||
if (!self.isDacEnabled()) self.enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR13, NR14
|
|
||||||
pub fn sound1CntX(self: *const Self) u16 {
|
|
||||||
return self.freq.raw & 0x4000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR13, NR14
|
|
||||||
pub fn setSound1CntX(self: *Self, fs: *const FrameSequencer, value: u16) void {
|
|
||||||
self.setNr13(@truncate(u8, value));
|
|
||||||
self.setNr14(fs, @truncate(u8, value >> 8));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR13
|
|
||||||
pub fn setNr13(self: *Self, byte: u8) void {
|
|
||||||
self.freq.raw = (self.freq.raw & 0xFF00) | byte;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR14
|
|
||||||
pub fn setNr14(self: *Self, fs: *const FrameSequencer, byte: u8) void {
|
|
||||||
var new: io.Frequency = .{ .raw = (@as(u16, byte) << 8) | (self.freq.raw & 0xFF) };
|
|
||||||
|
|
||||||
if (new.trigger.read()) {
|
|
||||||
self.enabled = true;
|
|
||||||
|
|
||||||
if (self.len_dev.timer == 0) {
|
|
||||||
self.len_dev.timer =
|
|
||||||
if (!fs.isLengthNext() and new.length_enable.read()) 63 else 64;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.square.reload(Self, self.freq.frequency.read());
|
|
||||||
|
|
||||||
// Reload Envelope period and timer
|
|
||||||
self.env_dev.timer = self.envelope.period.read();
|
|
||||||
if (fs.isEnvelopeNext() and self.env_dev.timer != 0b111) self.env_dev.timer += 1;
|
|
||||||
|
|
||||||
self.env_dev.vol = self.envelope.init_vol.read();
|
|
||||||
|
|
||||||
// Sweep Trigger Behaviour
|
|
||||||
const sw_period = self.sweep.period.read();
|
|
||||||
const sw_shift = self.sweep.shift.read();
|
|
||||||
|
|
||||||
self.sweep_dev.calc_performed = false;
|
|
||||||
self.sweep_dev.shadow = self.freq.frequency.read();
|
|
||||||
self.sweep_dev.timer = if (sw_period == 0) 8 else sw_period;
|
|
||||||
self.sweep_dev.enabled = sw_period != 0 or sw_shift != 0;
|
|
||||||
if (sw_shift != 0) _ = self.sweep_dev.calculate(self.sweep, &self.enabled);
|
|
||||||
|
|
||||||
self.enabled = self.isDacEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
util.audio.length.update(Self, self, fs, new);
|
|
||||||
self.freq = new;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isDacEnabled(self: *const Self) bool {
|
|
||||||
return self.envelope.raw & 0xF8 != 0;
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
const io = @import("../bus/io.zig");
|
|
||||||
const util = @import("../../util.zig");
|
|
||||||
|
|
||||||
const Scheduler = @import("../scheduler.zig").Scheduler;
|
|
||||||
const FrameSequencer = @import("../apu.zig").FrameSequencer;
|
|
||||||
const Tick = @import("../apu.zig").Apu.Tick;
|
|
||||||
|
|
||||||
const Length = @import("device/Length.zig");
|
|
||||||
const Wave = @import("signal/Wave.zig");
|
|
||||||
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
/// Write-only
|
|
||||||
/// NR30
|
|
||||||
select: io.WaveSelect,
|
|
||||||
/// NR31
|
|
||||||
length: u8,
|
|
||||||
/// NR32
|
|
||||||
vol: io.WaveVolume,
|
|
||||||
/// NR33, NR34
|
|
||||||
freq: io.Frequency,
|
|
||||||
|
|
||||||
/// Length Functionarlity
|
|
||||||
len_dev: Length,
|
|
||||||
wave_dev: Wave,
|
|
||||||
|
|
||||||
enabled: bool,
|
|
||||||
sample: i8,
|
|
||||||
|
|
||||||
pub fn init(sched: *Scheduler) Self {
|
|
||||||
return .{
|
|
||||||
.select = .{ .raw = 0 },
|
|
||||||
.vol = .{ .raw = 0 },
|
|
||||||
.freq = .{ .raw = 0 },
|
|
||||||
.length = 0,
|
|
||||||
|
|
||||||
.len_dev = Length.create(),
|
|
||||||
.wave_dev = Wave.init(sched),
|
|
||||||
.enabled = false,
|
|
||||||
.sample = 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset(self: *Self) void {
|
|
||||||
self.select.raw = 0; // NR30
|
|
||||||
self.length = 0; // NR31
|
|
||||||
self.vol.raw = 0; // NR32
|
|
||||||
self.freq.raw = 0; // NR33, NR34
|
|
||||||
|
|
||||||
self.len_dev.reset();
|
|
||||||
self.wave_dev.reset();
|
|
||||||
|
|
||||||
self.sample = 0;
|
|
||||||
self.enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tick(self: *Self, comptime kind: Tick) void {
|
|
||||||
switch (kind) {
|
|
||||||
.Length => self.len_dev.tick(self.freq.length_enable.read(), &self.enabled),
|
|
||||||
.Envelope => @compileError("Channel 3 does not implement Envelope"),
|
|
||||||
.Sweep => @compileError("Channel 3 does not implement Sweep"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR30, NR31, NR32
|
|
||||||
pub fn setSound3Cnt(self: *Self, value: u32) void {
|
|
||||||
self.setSound3CntL(@truncate(u8, value));
|
|
||||||
self.setSound3CntH(@truncate(u16, value >> 16));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR30
|
|
||||||
pub fn setSound3CntL(self: *Self, value: u8) void {
|
|
||||||
self.select.raw = value;
|
|
||||||
if (!self.select.enabled.read()) self.enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR30
|
|
||||||
pub fn sound3CntL(self: *const Self) u8 {
|
|
||||||
return self.select.raw & 0xE0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR31, NR32
|
|
||||||
pub fn sound3CntH(self: *const Self) u16 {
|
|
||||||
return @as(u16, self.length & 0xE0) << 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR31, NR32
|
|
||||||
pub fn setSound3CntH(self: *Self, value: u16) void {
|
|
||||||
self.setNr31(@truncate(u8, value));
|
|
||||||
self.vol.raw = (@truncate(u8, value >> 8));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR31
|
|
||||||
pub fn setNr31(self: *Self, len: u8) void {
|
|
||||||
self.length = len;
|
|
||||||
self.len_dev.timer = 256 - @as(u9, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR33, NR34
|
|
||||||
pub fn setSound3CntX(self: *Self, fs: *const FrameSequencer, value: u16) void {
|
|
||||||
self.setNr33(@truncate(u8, value));
|
|
||||||
self.setNr34(fs, @truncate(u8, value >> 8));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR33, NR34
|
|
||||||
pub fn sound3CntX(self: *const Self) u16 {
|
|
||||||
return self.freq.raw & 0x4000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR33
|
|
||||||
pub fn setNr33(self: *Self, byte: u8) void {
|
|
||||||
self.freq.raw = (self.freq.raw & 0xFF00) | byte;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NR34
|
|
||||||
pub fn setNr34(self: *Self, fs: *const FrameSequencer, byte: u8) void {
|
|
||||||
var new: io.Frequency = .{ .raw = (@as(u16, byte) << 8) | (self.freq.raw & 0xFF) };
|
|
||||||
|
|
||||||
if (new.trigger.read()) {
|
|
||||||
self.enabled = true;
|
|
||||||
|
|
||||||
if (self.len_dev.timer == 0) {
|
|
||||||
self.len_dev.timer =
|
|
||||||
if (!fs.isLengthNext() and new.length_enable.read()) 255 else 256;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update The Frequency Timer
|
|
||||||
self.wave_dev.reload(self.freq.frequency.read());
|
|
||||||
self.wave_dev.offset = 0;
|
|
||||||
|
|
||||||
self.enabled = self.select.enabled.read();
|
|
||||||
}
|
|
||||||
|
|
||||||
util.audio.length.update(Self, self, fs, new);
|
|
||||||
self.freq = new;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn onWaveEvent(self: *Self, late: u64) void {
|
|
||||||
self.wave_dev.onWaveTimerExpire(self.freq, self.select, late);
|
|
||||||
|
|
||||||
self.sample = 0;
|
|
||||||
if (!self.select.enabled.read()) return;
|
|
||||||
// Convert unsigned 4-bit wave sample to signed 8-bit sample
|
|
||||||
self.sample = (2 * @as(i8, self.wave_dev.sample(self.select)) - 15) >> self.wave_dev.shift(self.vol);
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
const io = @import("../../bus/io.zig");
|
|
||||||
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
/// Period Timer
|
|
||||||
timer: u3,
|
|
||||||
/// Current Volume
|
|
||||||
vol: u4,
|
|
||||||
|
|
||||||
pub fn create() Self {
|
|
||||||
return .{ .timer = 0, .vol = 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset(self: *Self) void {
|
|
||||||
self.timer = 0;
|
|
||||||
self.vol = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tick(self: *Self, nrx2: io.Envelope) void {
|
|
||||||
if (nrx2.period.read() != 0) {
|
|
||||||
if (self.timer != 0) self.timer -= 1;
|
|
||||||
|
|
||||||
if (self.timer == 0) {
|
|
||||||
self.timer = nrx2.period.read();
|
|
||||||
|
|
||||||
if (nrx2.direction.read()) {
|
|
||||||
if (self.vol < 0xF) self.vol += 1;
|
|
||||||
} else {
|
|
||||||
if (self.vol > 0x0) self.vol -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
const Self = @This();
|
|
||||||
|
|
||||||
timer: u9,
|
|
||||||
|
|
||||||
pub fn create() Self {
|
|
||||||
return .{ .timer = 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset(self: *Self) void {
|
|
||||||
self.timer = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tick(self: *Self, enabled: bool, ch_enable: *bool) void {
|
|
||||||
if (enabled) {
|
|
||||||
if (self.timer == 0) return;
|
|
||||||
self.timer -= 1;
|
|
||||||
|
|
||||||
// By returning early if timer == 0, this is only
|
|
||||||
// true if timer == 0 because of the decrement we just did
|
|
||||||
if (self.timer == 0) ch_enable.* = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
const io = @import("../../bus/io.zig");
|
|
||||||
const ToneSweep = @import("../ToneSweep.zig");
|
|
||||||
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
timer: u8,
|
|
||||||
enabled: bool,
|
|
||||||
shadow: u11,
|
|
||||||
|
|
||||||
calc_performed: bool,
|
|
||||||
|
|
||||||
pub fn create() Self {
|
|
||||||
return .{
|
|
||||||
.timer = 0,
|
|
||||||
.enabled = false,
|
|
||||||
.shadow = 0,
|
|
||||||
.calc_performed = false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset(self: *Self) void {
|
|
||||||
self.timer = 0;
|
|
||||||
self.enabled = false;
|
|
||||||
self.shadow = 0;
|
|
||||||
self.calc_performed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tick(self: *Self, ch1: *ToneSweep) void {
|
|
||||||
if (self.timer != 0) self.timer -= 1;
|
|
||||||
|
|
||||||
if (self.timer == 0) {
|
|
||||||
const period = ch1.sweep.period.read();
|
|
||||||
self.timer = if (period == 0) 8 else period;
|
|
||||||
|
|
||||||
if (self.enabled and period != 0) {
|
|
||||||
const new_freq = self.calculate(ch1.sweep, &ch1.enabled);
|
|
||||||
|
|
||||||
if (new_freq <= 0x7FF and ch1.sweep.shift.read() != 0) {
|
|
||||||
ch1.freq.frequency.write(@truncate(u11, new_freq));
|
|
||||||
self.shadow = @truncate(u11, new_freq);
|
|
||||||
|
|
||||||
_ = self.calculate(ch1.sweep, &ch1.enabled);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculates the Sweep Frequency
|
|
||||||
pub fn calculate(self: *Self, sweep: io.Sweep, ch_enable: *bool) u12 {
|
|
||||||
const shadow = @as(u12, self.shadow);
|
|
||||||
const shadow_shifted = shadow >> sweep.shift.read();
|
|
||||||
const decrease = sweep.direction.read();
|
|
||||||
|
|
||||||
const freq = if (decrease) blk: {
|
|
||||||
self.calc_performed = true;
|
|
||||||
break :blk shadow - shadow_shifted;
|
|
||||||
} else shadow + shadow_shifted;
|
|
||||||
if (freq > 0x7FF) ch_enable.* = false;
|
|
||||||
|
|
||||||
return freq;
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
//! Linear Feedback Shift Register
|
|
||||||
const io = @import("../../bus/io.zig");
|
|
||||||
|
|
||||||
const Scheduler = @import("../../scheduler.zig").Scheduler;
|
|
||||||
|
|
||||||
const Self = @This();
|
|
||||||
pub const interval: u64 = (1 << 24) / (1 << 22);
|
|
||||||
|
|
||||||
shift: u15,
|
|
||||||
timer: u16,
|
|
||||||
|
|
||||||
sched: *Scheduler,
|
|
||||||
|
|
||||||
pub fn create(sched: *Scheduler) Self {
|
|
||||||
return .{
|
|
||||||
.shift = 0,
|
|
||||||
.timer = 0,
|
|
||||||
.sched = sched,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset(self: *Self) void {
|
|
||||||
self.shift = 0;
|
|
||||||
self.timer = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sample(self: *const Self) i8 {
|
|
||||||
return if ((~self.shift & 1) == 1) 1 else -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reload LFSR Timer
|
|
||||||
pub fn reload(self: *Self, poly: io.PolyCounter) void {
|
|
||||||
self.sched.removeScheduledEvent(.{ .ApuChannel = 3 });
|
|
||||||
|
|
||||||
const div = Self.divisor(poly.div_ratio.read());
|
|
||||||
const timer = div << poly.shift.read();
|
|
||||||
self.sched.push(.{ .ApuChannel = 3 }, @as(u64, timer) * interval);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scheduler Event Handler for LFSR Timer Expire
|
|
||||||
/// FIXME: This gets called a lot, slowing down the scheduler
|
|
||||||
pub fn onLfsrTimerExpire(self: *Self, poly: io.PolyCounter, late: u64) void {
|
|
||||||
// Obscure: "Using a noise channel clock shift of 14 or 15
|
|
||||||
// results in the LFSR receiving no clocks."
|
|
||||||
if (poly.shift.read() >= 14) return;
|
|
||||||
|
|
||||||
const div = Self.divisor(poly.div_ratio.read());
|
|
||||||
const timer = div << poly.shift.read();
|
|
||||||
|
|
||||||
const tmp = (self.shift & 1) ^ ((self.shift & 2) >> 1);
|
|
||||||
self.shift = (self.shift >> 1) | (tmp << 14);
|
|
||||||
|
|
||||||
if (poly.width.read())
|
|
||||||
self.shift = (self.shift & ~@as(u15, 0x40)) | tmp << 6;
|
|
||||||
|
|
||||||
self.sched.push(.{ .ApuChannel = 3 }, @as(u64, timer) * interval -| late);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn divisor(code: u3) u16 {
|
|
||||||
if (code == 0) return 8;
|
|
||||||
return @as(u16, code) << 4;
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const io = @import("../../bus/io.zig");
|
|
||||||
|
|
||||||
const Scheduler = @import("../../scheduler.zig").Scheduler;
|
|
||||||
const ToneSweep = @import("../ToneSweep.zig");
|
|
||||||
const Tone = @import("../Tone.zig");
|
|
||||||
|
|
||||||
const Self = @This();
|
|
||||||
pub const interval: u64 = (1 << 24) / (1 << 22);
|
|
||||||
|
|
||||||
pos: u3,
|
|
||||||
sched: *Scheduler,
|
|
||||||
timer: u16,
|
|
||||||
|
|
||||||
pub fn init(sched: *Scheduler) Self {
|
|
||||||
return .{
|
|
||||||
.timer = 0,
|
|
||||||
.pos = 0,
|
|
||||||
.sched = sched,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset(self: *Self) void {
|
|
||||||
self.timer = 0;
|
|
||||||
self.pos = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scheduler Event Handler for Square Synth Timer Expire
|
|
||||||
pub fn onSquareTimerExpire(self: *Self, comptime T: type, nrx34: io.Frequency, late: u64) void {
|
|
||||||
comptime std.debug.assert(T == ToneSweep or T == Tone);
|
|
||||||
self.pos +%= 1;
|
|
||||||
|
|
||||||
self.timer = (@as(u16, 2048) - nrx34.frequency.read()) * 4;
|
|
||||||
self.sched.push(.{ .ApuChannel = if (T == ToneSweep) 0 else 1 }, @as(u64, self.timer) * interval -| late);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reload Square Wave Timer
|
|
||||||
pub fn reload(self: *Self, comptime T: type, value: u11) void {
|
|
||||||
comptime std.debug.assert(T == ToneSweep or T == Tone);
|
|
||||||
const channel = if (T == ToneSweep) 0 else 1;
|
|
||||||
|
|
||||||
self.sched.removeScheduledEvent(.{ .ApuChannel = channel });
|
|
||||||
|
|
||||||
const tmp = (@as(u16, 2048) - value) * 4; // What Freq Timer should be assuming no weird behaviour
|
|
||||||
self.timer = (tmp & ~@as(u16, 0x3)) | self.timer & 0x3; // Keep the last two bits from the old timer;
|
|
||||||
|
|
||||||
self.sched.push(.{ .ApuChannel = channel }, @as(u64, self.timer) * interval);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sample(self: *const Self, nrx1: io.Duty) i8 {
|
|
||||||
const pattern = nrx1.pattern.read();
|
|
||||||
|
|
||||||
const i = self.pos ^ 7; // index of 0 should get highest bit
|
|
||||||
const result = switch (pattern) {
|
|
||||||
0b00 => @as(u8, 0b00000001) >> i, // 12.5%
|
|
||||||
0b01 => @as(u8, 0b00000011) >> i, // 25%
|
|
||||||
0b10 => @as(u8, 0b00001111) >> i, // 50%
|
|
||||||
0b11 => @as(u8, 0b11111100) >> i, // 75%
|
|
||||||
};
|
|
||||||
|
|
||||||
return if (result & 1 == 1) 1 else -1;
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const io = @import("../../bus/io.zig");
|
|
||||||
|
|
||||||
const Scheduler = @import("../../scheduler.zig").Scheduler;
|
|
||||||
|
|
||||||
const buf_len = 0x20;
|
|
||||||
pub const interval: u64 = (1 << 24) / (1 << 22);
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
buf: [buf_len]u8,
|
|
||||||
timer: u16,
|
|
||||||
offset: u12,
|
|
||||||
|
|
||||||
sched: *Scheduler,
|
|
||||||
|
|
||||||
pub fn read(self: *const Self, comptime T: type, nr30: io.WaveSelect, addr: u32) T {
|
|
||||||
// TODO: Handle reads when Channel 3 is disabled
|
|
||||||
const base = if (!nr30.bank.read()) @as(u32, 0x10) else 0; // Read from the Opposite Bank in Use
|
|
||||||
|
|
||||||
const i = base + addr - 0x0400_0090;
|
|
||||||
return std.mem.readIntSliceLittle(T, self.buf[i..][0..@sizeOf(T)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write(self: *Self, comptime T: type, nr30: io.WaveSelect, addr: u32, value: T) void {
|
|
||||||
// TODO: Handle writes when Channel 3 is disabled
|
|
||||||
const base = if (!nr30.bank.read()) @as(u32, 0x10) else 0; // Write to the Opposite Bank in Use
|
|
||||||
|
|
||||||
const i = base + addr - 0x0400_0090;
|
|
||||||
std.mem.writeIntSliceLittle(T, self.buf[i..][0..@sizeOf(T)], value);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init(sched: *Scheduler) Self {
|
|
||||||
return .{
|
|
||||||
.buf = [_]u8{0x00} ** buf_len,
|
|
||||||
.timer = 0,
|
|
||||||
.offset = 0,
|
|
||||||
.sched = sched,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset(self: *Self) void {
|
|
||||||
self.timer = 0;
|
|
||||||
self.offset = 0;
|
|
||||||
|
|
||||||
// sample buffer isn't reset because it's outside of the range of what NR52{7}'s effects
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reload internal Wave Timer
|
|
||||||
pub fn reload(self: *Self, value: u11) void {
|
|
||||||
self.sched.removeScheduledEvent(.{ .ApuChannel = 2 });
|
|
||||||
|
|
||||||
self.timer = (@as(u16, 2048) - value) * 2;
|
|
||||||
self.sched.push(.{ .ApuChannel = 2 }, @as(u64, self.timer) * interval);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scheduler Event Handler
|
|
||||||
pub fn onWaveTimerExpire(self: *Self, nrx34: io.Frequency, nr30: io.WaveSelect, late: u64) void {
|
|
||||||
if (nr30.dimension.read()) {
|
|
||||||
self.offset = (self.offset + 1) % 0x40; // 0x20 bytes (both banks), which contain 2 samples each
|
|
||||||
} else {
|
|
||||||
self.offset = (self.offset + 1) % 0x20; // 0x10 bytes, which contain 2 samples each
|
|
||||||
}
|
|
||||||
|
|
||||||
self.timer = (@as(u16, 2048) - nrx34.frequency.read()) * 2;
|
|
||||||
self.sched.push(.{ .ApuChannel = 2 }, @as(u64, self.timer) * interval -| late);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate Sample from Wave Synth
|
|
||||||
pub fn sample(self: *const Self, nr30: io.WaveSelect) u4 {
|
|
||||||
const base = if (nr30.bank.read()) @as(u32, 0x10) else 0;
|
|
||||||
|
|
||||||
const value = self.buf[base + self.offset / 2];
|
|
||||||
return if (self.offset & 1 == 0) @truncate(u4, value >> 4) else @truncate(u4, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// TODO: Write comment
|
|
||||||
pub fn shift(_: *const Self, nr32: io.WaveVolume) u2 {
|
|
||||||
return switch (nr32.kind.read()) {
|
|
||||||
0b00 => 3, // Mute / Zero
|
|
||||||
0b01 => 0, // 100% Volume
|
|
||||||
0b10 => 1, // 50% Volume
|
|
||||||
0b11 => 2, // 25% Volume
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -8,56 +8,56 @@ pub const size = 0x4000;
|
|||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
buf: ?[]u8,
|
buf: ?[]u8,
|
||||||
allocator: Allocator,
|
alloc: Allocator,
|
||||||
|
|
||||||
addr_latch: u32,
|
addr_latch: u32,
|
||||||
|
|
||||||
|
pub fn init(alloc: Allocator, maybe_path: ?[]const u8) !Self {
|
||||||
|
var buf: ?[]u8 = null;
|
||||||
|
if (maybe_path) |path| {
|
||||||
|
const file = try std.fs.cwd().openFile(path, .{});
|
||||||
|
defer file.close();
|
||||||
|
buf = try file.readToEndAlloc(alloc, try file.getEndPos());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Self{
|
||||||
|
.buf = buf,
|
||||||
|
.alloc = alloc,
|
||||||
|
.addr_latch = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: Self) void {
|
||||||
|
if (self.buf) |buf| self.alloc.free(buf);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read(self: *Self, comptime T: type, r15: u32, addr: u32) T {
|
pub fn read(self: *Self, comptime T: type, r15: u32, addr: u32) T {
|
||||||
if (r15 < Self.size) {
|
if (r15 < Self.size) {
|
||||||
self.addr_latch = addr;
|
self.addr_latch = addr;
|
||||||
return self._read(T, addr);
|
return self.uncheckedRead(T, addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
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));
|
return @truncate(T, self.uncheckedRead(T, self.addr_latch + 8));
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
if (r15 < Self.size) return self._read(T, addr);
|
if (r15 < Self.size) return self.uncheckedRead(T, addr);
|
||||||
return @truncate(T, self._read(T, self.addr_latch + 8));
|
return @truncate(T, self.uncheckedRead(T, self.addr_latch + 8));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read without the GBA safety checks
|
fn uncheckedRead(self: *const Self, comptime T: type, addr: u32) T {
|
||||||
fn _read(self: *const Self, comptime T: type, addr: u32) T {
|
if (self.buf) |buf| {
|
||||||
const buf = self.buf orelse std.debug.panic("[BIOS] ZBA tried to read {} from 0x{X:0>8} but not BIOS was present", .{ T, addr });
|
|
||||||
|
|
||||||
return switch (T) {
|
return switch (T) {
|
||||||
u32, u16, u8 => std.mem.readIntSliceLittle(T, buf[addr..][0..@sizeOf(T)]),
|
u32, u16, u8 => std.mem.readIntSliceLittle(T, buf[addr..][0..@sizeOf(T)]),
|
||||||
else => @compileError("BIOS: Unsupported read width"),
|
else => @compileError("BIOS: Unsupported read width"),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
std.debug.panic("[BIOS] ZBA tried to read {} from 0x{X:0>8} but not BIOS was present", .{ T, addr });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write(_: *Self, comptime T: type, addr: u32, value: T) void {
|
pub fn write(_: *Self, comptime T: type, addr: u32, value: T) void {
|
||||||
@setCold(true);
|
@setCold(true);
|
||||||
log.debug("Tried to write {} 0x{X:} to 0x{X:0>8} ", .{ T, value, addr });
|
log.debug("Tried to write {} 0x{X:} to 0x{X:0>8} ", .{ T, value, addr });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, maybe_path: ?[]const u8) !Self {
|
|
||||||
const buf: ?[]u8 = if (maybe_path) |path| blk: {
|
|
||||||
const file = try std.fs.cwd().openFile(path, .{});
|
|
||||||
defer file.close();
|
|
||||||
|
|
||||||
break :blk try file.readToEndAlloc(allocator, try file.getEndPos());
|
|
||||||
} else null;
|
|
||||||
|
|
||||||
return Self{
|
|
||||||
.buf = buf,
|
|
||||||
.allocator = allocator,
|
|
||||||
.addr_latch = 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
|
||||||
if (self.buf) |buf| self.allocator.free(buf);
|
|
||||||
self.* = undefined;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,21 @@ const ewram_size = 0x40000;
|
|||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
buf: []u8,
|
buf: []u8,
|
||||||
allocator: Allocator,
|
alloc: Allocator,
|
||||||
|
|
||||||
|
pub fn init(alloc: Allocator) !Self {
|
||||||
|
const buf = try alloc.alloc(u8, ewram_size);
|
||||||
|
std.mem.set(u8, buf, 0);
|
||||||
|
|
||||||
|
return Self{
|
||||||
|
.buf = buf,
|
||||||
|
.alloc = alloc,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: Self) void {
|
||||||
|
self.alloc.free(self.buf);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read(self: *const Self, comptime T: type, address: usize) T {
|
pub fn read(self: *const Self, comptime T: type, address: usize) T {
|
||||||
const addr = address & 0x3FFFF;
|
const addr = address & 0x3FFFF;
|
||||||
@@ -24,18 +38,3 @@ pub fn write(self: *const Self, comptime T: type, address: usize, value: T) void
|
|||||||
else => @compileError("EWRAM: Unsupported write width"),
|
else => @compileError("EWRAM: Unsupported write width"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(allocator: Allocator) !Self {
|
|
||||||
const buf = try allocator.alloc(u8, ewram_size);
|
|
||||||
std.mem.set(u8, buf, 0);
|
|
||||||
|
|
||||||
return Self{
|
|
||||||
.buf = buf,
|
|
||||||
.allocator = allocator,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
|
||||||
self.allocator.free(self.buf);
|
|
||||||
self.* = undefined;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,26 +1,73 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const config = @import("../../config.zig");
|
|
||||||
|
|
||||||
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 Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const log = std.log.scoped(.GamePak);
|
const log = std.log.scoped(.GamePak);
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
title: [12]u8,
|
title: [12]u8,
|
||||||
buf: []u8,
|
buf: []u8,
|
||||||
allocator: Allocator,
|
alloc: Allocator,
|
||||||
backup: Backup,
|
backup: Backup,
|
||||||
gpio: *Gpio,
|
|
||||||
|
pub fn init(alloc: Allocator, rom_path: []const u8, save_path: ?[]const u8) !Self {
|
||||||
|
const file = try std.fs.cwd().openFile(rom_path, .{});
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
const file_buf = try file.readToEndAlloc(alloc, try file.getEndPos());
|
||||||
|
const title = parseTitle(file_buf);
|
||||||
|
const kind = Backup.guessKind(file_buf) orelse .None;
|
||||||
|
|
||||||
|
const pak = Self{
|
||||||
|
.buf = file_buf,
|
||||||
|
.alloc = alloc,
|
||||||
|
.title = title,
|
||||||
|
.backup = try Backup.init(alloc, kind, title, save_path),
|
||||||
|
};
|
||||||
|
pak.parseHeader();
|
||||||
|
|
||||||
|
return pak;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseHeader(self: *const Self) void {
|
||||||
|
const title = parseTitle(self.buf);
|
||||||
|
const code = self.buf[0xAC..0xB0];
|
||||||
|
const maker = self.buf[0xB0..0xB2];
|
||||||
|
const version = self.buf[0xBC];
|
||||||
|
|
||||||
|
log.info("Title: {s}", .{title});
|
||||||
|
if (version != 0) log.info("Version: {}", .{version});
|
||||||
|
log.info("Game Code: {s}", .{code});
|
||||||
|
if (lookupMaker(maker)) |c| log.info("Maker: {s}", .{c}) else log.info("Maker Code: {s}", .{maker});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseTitle(buf: []u8) [12]u8 {
|
||||||
|
return buf[0xA0..0xAC].*;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lookupMaker(slice: *const [2]u8) ?[]const u8 {
|
||||||
|
const id = @as(u16, slice[1]) << 8 | @as(u16, slice[0]);
|
||||||
|
return switch (id) {
|
||||||
|
0x3130 => "Nintendo",
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn isLarge(self: *const Self) bool {
|
||||||
|
return self.buf.len > 0x100_0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: Self) void {
|
||||||
|
self.alloc.free(self.buf);
|
||||||
|
self.backup.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read(self: *Self, comptime T: type, address: u32) T {
|
pub fn read(self: *Self, comptime T: type, address: u32) T {
|
||||||
const addr = address & 0x1FF_FFFF;
|
const addr = address & 0x1FF_FFFF;
|
||||||
|
|
||||||
if (self.backup.kind == .Eeprom) {
|
if (self.backup.kind == .Eeprom) {
|
||||||
if (self.buf.len > 0x100_0000) { // Large
|
if (self.isLarge()) {
|
||||||
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
|
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
|
||||||
// * Backup type is EEPROM
|
// * Backup type is EEPROM
|
||||||
// * Large ROM (Size is greater than 16MB)
|
// * Large ROM (Size is greater than 16MB)
|
||||||
@@ -35,35 +82,6 @@ pub fn read(self: *Self, comptime T: type, address: u32) T {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.gpio.cnt == 1) {
|
|
||||||
// GPIO Can be read from
|
|
||||||
// We assume that this will only be true when a ROM actually does want something from GPIO
|
|
||||||
|
|
||||||
switch (T) {
|
|
||||||
u32 => switch (address) {
|
|
||||||
// TODO: Do I even need to implement these?
|
|
||||||
0x0800_00C4 => std.debug.panic("Handle 32-bit GPIO Data/Direction Reads", .{}),
|
|
||||||
0x0800_00C6 => std.debug.panic("Handle 32-bit GPIO Direction/Control Reads", .{}),
|
|
||||||
0x0800_00C8 => std.debug.panic("Handle 32-bit GPIO Control Reads", .{}),
|
|
||||||
else => {},
|
|
||||||
},
|
|
||||||
u16 => switch (address) {
|
|
||||||
// FIXME: What do 16-bit GPIO Reads look like?
|
|
||||||
0x0800_00C4 => return self.gpio.read(.Data),
|
|
||||||
0x0800_00C6 => return self.gpio.read(.Direction),
|
|
||||||
0x0800_00C8 => return self.gpio.read(.Control),
|
|
||||||
else => {},
|
|
||||||
},
|
|
||||||
u8 => switch (address) {
|
|
||||||
0x0800_00C4 => return self.gpio.read(.Data),
|
|
||||||
0x0800_00C6 => return self.gpio.read(.Direction),
|
|
||||||
0x0800_00C8 => return self.gpio.read(.Control),
|
|
||||||
else => {},
|
|
||||||
},
|
|
||||||
else => @compileError("GamePak[GPIO]: Unsupported read width"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return switch (T) {
|
return switch (T) {
|
||||||
u32 => (@as(T, self.get(addr + 3)) << 24) | (@as(T, self.get(addr + 2)) << 16) | (@as(T, self.get(addr + 1)) << 8) | (@as(T, self.get(addr))),
|
u32 => (@as(T, self.get(addr + 3)) << 24) | (@as(T, self.get(addr + 2)) << 16) | (@as(T, self.get(addr + 1)) << 8) | (@as(T, self.get(addr))),
|
||||||
u16 => (@as(T, self.get(addr + 1)) << 8) | @as(T, self.get(addr)),
|
u16 => (@as(T, self.get(addr + 1)) << 8) | @as(T, self.get(addr)),
|
||||||
@@ -72,19 +90,11 @@ pub fn read(self: *Self, comptime T: type, address: u32) T {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fn get(self: *const Self, i: u32) u8 {
|
|
||||||
@setRuntimeSafety(false);
|
|
||||||
if (i < self.buf.len) return self.buf[i];
|
|
||||||
|
|
||||||
const lhs = i >> 1 & 0xFFFF;
|
|
||||||
return @truncate(u8, lhs >> 8 * @truncate(u5, i & 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
|
pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
|
||||||
const addr = address & 0x1FF_FFFF;
|
const addr = address & 0x1FF_FFFF;
|
||||||
|
|
||||||
if (self.backup.kind == .Eeprom) {
|
if (self.backup.kind == .Eeprom) {
|
||||||
if (self.buf.len > 0x100_0000) { // Large
|
if (self.isLarge()) {
|
||||||
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
|
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
|
||||||
// * Backup type is EEPROM
|
// * Backup type is EEPROM
|
||||||
// * Large ROM (Size is greater than 16MB)
|
// * Large ROM (Size is greater than 16MB)
|
||||||
@@ -99,34 +109,6 @@ pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.gpio.cnt == 1) {
|
|
||||||
// GPIO Can be read from
|
|
||||||
// We assume that this will only be true when a ROM actually does want something from GPIO
|
|
||||||
|
|
||||||
switch (T) {
|
|
||||||
u32 => switch (address) {
|
|
||||||
// FIXME: Do I even need to implement these?
|
|
||||||
0x0800_00C4 => std.debug.panic("Handle 32-bit GPIO Data/Direction Reads", .{}),
|
|
||||||
0x0800_00C6 => std.debug.panic("Handle 32-bit GPIO Direction/Control Reads", .{}),
|
|
||||||
0x0800_00C8 => std.debug.panic("Handle 32-bit GPIO Control Reads", .{}),
|
|
||||||
else => {},
|
|
||||||
},
|
|
||||||
u16 => switch (address) {
|
|
||||||
0x0800_00C4 => return self.gpio.read(.Data),
|
|
||||||
0x0800_00C6 => return self.gpio.read(.Direction),
|
|
||||||
0x0800_00C8 => return self.gpio.read(.Control),
|
|
||||||
else => {},
|
|
||||||
},
|
|
||||||
u8 => switch (address) {
|
|
||||||
0x0800_00C4 => return self.gpio.read(.Data),
|
|
||||||
0x0800_00C6 => return self.gpio.read(.Direction),
|
|
||||||
0x0800_00C8 => return self.gpio.read(.Control),
|
|
||||||
else => {},
|
|
||||||
},
|
|
||||||
else => @compileError("GamePak[GPIO]: Unsupported read width"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return switch (T) {
|
return switch (T) {
|
||||||
u32 => (@as(T, self.get(addr + 3)) << 24) | (@as(T, self.get(addr + 2)) << 16) | (@as(T, self.get(addr + 1)) << 8) | (@as(T, self.get(addr))),
|
u32 => (@as(T, self.get(addr + 3)) << 24) | (@as(T, self.get(addr + 2)) << 16) | (@as(T, self.get(addr + 1)) << 8) | (@as(T, self.get(addr))),
|
||||||
u16 => (@as(T, self.get(addr + 1)) << 8) | @as(T, self.get(addr)),
|
u16 => (@as(T, self.get(addr + 1)) << 8) | @as(T, self.get(addr)),
|
||||||
@@ -141,7 +123,7 @@ pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value
|
|||||||
if (self.backup.kind == .Eeprom) {
|
if (self.backup.kind == .Eeprom) {
|
||||||
const bit = @truncate(u1, value);
|
const bit = @truncate(u1, value);
|
||||||
|
|
||||||
if (self.buf.len > 0x100_0000) { // Large
|
if (self.isLarge()) {
|
||||||
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
|
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
|
||||||
// * Backup type is EEPROM
|
// * Backup type is EEPROM
|
||||||
// * Large ROM (Size is greater than 16MB)
|
// * Large ROM (Size is greater than 16MB)
|
||||||
@@ -158,80 +140,27 @@ pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value
|
|||||||
|
|
||||||
switch (T) {
|
switch (T) {
|
||||||
u32 => switch (address) {
|
u32 => switch (address) {
|
||||||
0x0800_00C4 => {
|
0x0800_00C4 => log.debug("Wrote {} 0x{X:} to I/O Port Data and Direction", .{ T, value }),
|
||||||
self.gpio.write(.Data, @truncate(u4, value));
|
0x0800_00C6 => log.debug("Wrote {} 0x{X:} to I/O Port Direction and Control", .{ T, value }),
|
||||||
self.gpio.write(.Direction, @truncate(u4, value >> 16));
|
else => {},
|
||||||
},
|
|
||||||
0x0800_00C6 => {
|
|
||||||
self.gpio.write(.Direction, @truncate(u4, value));
|
|
||||||
self.gpio.write(.Control, @truncate(u1, value >> 16));
|
|
||||||
},
|
|
||||||
else => log.err("Wrote {} 0x{X:0>8} to 0x{X:0>8}, Unhandled", .{ T, value, address }),
|
|
||||||
},
|
},
|
||||||
u16 => switch (address) {
|
u16 => switch (address) {
|
||||||
0x0800_00C4 => self.gpio.write(.Data, @truncate(u4, value)),
|
0x0800_00C4 => log.debug("Wrote {} 0x{X:} to I/O Port Data", .{ T, value }),
|
||||||
0x0800_00C6 => self.gpio.write(.Direction, @truncate(u4, value)),
|
0x0800_00C6 => log.debug("Wrote {} 0x{X:} to I/O Port Direction", .{ T, value }),
|
||||||
0x0800_00C8 => self.gpio.write(.Control, @truncate(u1, value)),
|
0x0800_00C8 => log.debug("Wrote {} 0x{X:} to I/O Port Control", .{ T, value }),
|
||||||
else => log.err("Wrote {} 0x{X:0>4} to 0x{X:0>8}, Unhandled", .{ T, value, address }),
|
else => {},
|
||||||
},
|
},
|
||||||
u8 => log.debug("Wrote {} 0x{X:0>2} to 0x{X:0>8}, Ignored.", .{ T, value, address }),
|
u8 => log.debug("Wrote {} 0x{X:} to 0x{X:0>8}, Ignored.", .{ T, value, address }),
|
||||||
else => @compileError("GamePak: Unsupported write width"),
|
else => @compileError("GamePak: Unsupported write width"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, rom_path: []const u8, save_path: ?[]const u8) !Self {
|
fn get(self: *const Self, i: u32) u8 {
|
||||||
const file = try std.fs.cwd().openFile(rom_path, .{});
|
@setRuntimeSafety(false);
|
||||||
defer file.close();
|
if (i < self.buf.len) return self.buf[i];
|
||||||
|
|
||||||
const file_buf = try file.readToEndAlloc(allocator, try file.getEndPos());
|
const lhs = i >> 1 & 0xFFFF;
|
||||||
const title = file_buf[0xA0..0xAC].*;
|
return @truncate(u8, lhs >> 8 * @truncate(u5, i & 1));
|
||||||
const kind = Backup.guess(file_buf);
|
|
||||||
const device = if (config.config().guest.force_rtc) .Rtc else guessDevice(file_buf);
|
|
||||||
|
|
||||||
logHeader(file_buf, &title);
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.buf = file_buf,
|
|
||||||
.allocator = allocator,
|
|
||||||
.title = title,
|
|
||||||
.backup = try Backup.init(allocator, kind, title, save_path),
|
|
||||||
.gpio = try Gpio.init(allocator, cpu, device),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
|
||||||
self.backup.deinit();
|
|
||||||
self.gpio.deinit(self.allocator);
|
|
||||||
self.allocator.destroy(self.gpio);
|
|
||||||
self.allocator.free(self.buf);
|
|
||||||
self.* = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Searches the ROM to see if it can determine whether the ROM it's searching uses
|
|
||||||
/// any GPIO device, like a RTC for example.
|
|
||||||
fn guessDevice(buf: []const u8) Gpio.Device.Kind {
|
|
||||||
// Try to Guess if ROM uses RTC
|
|
||||||
const needle = "RTC_V"; // I was told SIIRTC_V, though Pokemen Firered (USA) is a false negative
|
|
||||||
|
|
||||||
var i: usize = 0;
|
|
||||||
while ((i + needle.len) < buf.len) : (i += 1) {
|
|
||||||
if (std.mem.eql(u8, needle, buf[i..(i + needle.len)])) return .Rtc;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Detect other GPIO devices
|
|
||||||
|
|
||||||
return .None;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn logHeader(buf: []const u8, title: *const [12]u8) void {
|
|
||||||
const code = buf[0xAC..0xB0];
|
|
||||||
const maker = buf[0xB0..0xB2];
|
|
||||||
const version = buf[0xBC];
|
|
||||||
|
|
||||||
log.info("Title: {s}", .{title});
|
|
||||||
if (version != 0) log.info("Version: {}", .{version});
|
|
||||||
log.info("Game Code: {s}", .{code});
|
|
||||||
log.info("Maker Code: {s}", .{maker});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test "OOB Access" {
|
test "OOB Access" {
|
||||||
|
|||||||
@@ -5,7 +5,21 @@ const iwram_size = 0x8000;
|
|||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
buf: []u8,
|
buf: []u8,
|
||||||
allocator: Allocator,
|
alloc: Allocator,
|
||||||
|
|
||||||
|
pub fn init(alloc: Allocator) !Self {
|
||||||
|
const buf = try alloc.alloc(u8, iwram_size);
|
||||||
|
std.mem.set(u8, buf, 0);
|
||||||
|
|
||||||
|
return Self{
|
||||||
|
.buf = buf,
|
||||||
|
.alloc = alloc,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: Self) void {
|
||||||
|
self.alloc.free(self.buf);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read(self: *const Self, comptime T: type, address: usize) T {
|
pub fn read(self: *const Self, comptime T: type, address: usize) T {
|
||||||
const addr = address & 0x7FFF;
|
const addr = address & 0x7FFF;
|
||||||
@@ -24,18 +38,3 @@ pub fn write(self: *const Self, comptime T: type, address: usize, value: T) void
|
|||||||
else => @compileError("IWRAM: Unsupported write width"),
|
else => @compileError("IWRAM: Unsupported write width"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(allocator: Allocator) !Self {
|
|
||||||
const buf = try allocator.alloc(u8, iwram_size);
|
|
||||||
std.mem.set(u8, buf, 0);
|
|
||||||
|
|
||||||
return Self{
|
|
||||||
.buf = buf,
|
|
||||||
.allocator = allocator,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
|
||||||
self.allocator.free(self.buf);
|
|
||||||
self.* = undefined;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,29 +2,23 @@ const std = @import("std");
|
|||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const log = std.log.scoped(.Backup);
|
const log = std.log.scoped(.Backup);
|
||||||
|
|
||||||
const Eeprom = @import("backup/eeprom.zig").Eeprom;
|
const escape = @import("../util.zig").escape;
|
||||||
const Flash = @import("backup/Flash.zig");
|
const asString = @import("../util.zig").asString;
|
||||||
|
|
||||||
const escape = @import("../../util.zig").escape;
|
const backup_kinds = [5]Needle{
|
||||||
|
|
||||||
const Needle = struct { str: []const u8, kind: Backup.Kind };
|
|
||||||
const backup_kinds = [6]Needle{
|
|
||||||
.{ .str = "EEPROM_V", .kind = .Eeprom },
|
.{ .str = "EEPROM_V", .kind = .Eeprom },
|
||||||
.{ .str = "SRAM_V", .kind = .Sram },
|
.{ .str = "SRAM_V", .kind = .Sram },
|
||||||
.{ .str = "SRAM_F_V", .kind = .Sram },
|
|
||||||
.{ .str = "FLASH_V", .kind = .Flash },
|
.{ .str = "FLASH_V", .kind = .Flash },
|
||||||
.{ .str = "FLASH512_V", .kind = .Flash },
|
.{ .str = "FLASH512_V", .kind = .Flash },
|
||||||
.{ .str = "FLASH1M_V", .kind = .Flash1M },
|
.{ .str = "FLASH1M_V", .kind = .Flash1M },
|
||||||
};
|
};
|
||||||
|
|
||||||
const SaveError = error{Unsupported};
|
|
||||||
|
|
||||||
pub const Backup = struct {
|
pub const Backup = struct {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
buf: []u8,
|
buf: []u8,
|
||||||
allocator: Allocator,
|
alloc: Allocator,
|
||||||
kind: Kind,
|
kind: BackupKind,
|
||||||
|
|
||||||
title: [12]u8,
|
title: [12]u8,
|
||||||
save_path: ?[]const u8,
|
save_path: ?[]const u8,
|
||||||
@@ -32,14 +26,121 @@ pub const Backup = struct {
|
|||||||
flash: Flash,
|
flash: Flash,
|
||||||
eeprom: Eeprom,
|
eeprom: Eeprom,
|
||||||
|
|
||||||
const Kind = enum {
|
pub fn init(alloc: Allocator, kind: BackupKind, title: [12]u8, path: ?[]const u8) !Self {
|
||||||
Eeprom,
|
log.info("Kind: {}", .{kind});
|
||||||
Sram,
|
|
||||||
Flash,
|
const buf_size: usize = switch (kind) {
|
||||||
Flash1M,
|
.Sram => 0x8000, // 32K
|
||||||
None,
|
.Flash => 0x10000, // 64K
|
||||||
|
.Flash1M => 0x20000, // 128K
|
||||||
|
.None, .Eeprom => 0, // EEPROM is handled upon first Read Request to it
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buf = try alloc.alloc(u8, buf_size);
|
||||||
|
std.mem.set(u8, buf, 0xFF);
|
||||||
|
|
||||||
|
var backup = Self{
|
||||||
|
.buf = buf,
|
||||||
|
.alloc = alloc,
|
||||||
|
.kind = kind,
|
||||||
|
.title = title,
|
||||||
|
.save_path = path,
|
||||||
|
.flash = Flash.init(),
|
||||||
|
.eeprom = Eeprom.init(alloc),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (backup.save_path) |p| backup.loadSaveFromDisk(p) catch |e| log.err("Failed to load save: {}", .{e});
|
||||||
|
return backup;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn guessKind(rom: []const u8) ?BackupKind {
|
||||||
|
for (backup_kinds) |needle| {
|
||||||
|
const needle_len = needle.str.len;
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
while ((i + needle_len) < rom.len) : (i += 1) {
|
||||||
|
if (std.mem.eql(u8, needle.str, rom[i..][0..needle_len])) return needle.kind;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: Self) void {
|
||||||
|
if (self.save_path) |path| self.writeSaveToDisk(path) catch |e| log.err("Failed to write save: {}", .{e});
|
||||||
|
self.alloc.free(self.buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn loadSaveFromDisk(self: *Self, path: []const u8) !void {
|
||||||
|
const file_path = try self.getSaveFilePath(path);
|
||||||
|
defer self.alloc.free(file_path);
|
||||||
|
|
||||||
|
// FIXME: Don't rely on this lol
|
||||||
|
if (std.mem.eql(u8, file_path[file_path.len - 12 .. file_path.len], "untitled.sav")) {
|
||||||
|
return log.err("ROM header lacks title, no save loaded", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
const file: std.fs.File = try std.fs.openFileAbsolute(file_path, .{});
|
||||||
|
const file_buf = try file.readToEndAlloc(self.alloc, try file.getEndPos());
|
||||||
|
defer self.alloc.free(file_buf);
|
||||||
|
|
||||||
|
switch (self.kind) {
|
||||||
|
.Sram, .Flash, .Flash1M => {
|
||||||
|
if (self.buf.len == file_buf.len) {
|
||||||
|
std.mem.copy(u8, self.buf, file_buf);
|
||||||
|
return log.info("Loaded Save from {s}", .{file_path});
|
||||||
|
}
|
||||||
|
|
||||||
|
log.err("{s} is {} bytes, but we expected {} bytes", .{ file_path, file_buf.len, self.buf.len });
|
||||||
|
},
|
||||||
|
.Eeprom => {
|
||||||
|
if (file_buf.len == 0x200 or file_buf.len == 0x2000) {
|
||||||
|
self.eeprom.kind = if (file_buf.len == 0x200) .Small else .Large;
|
||||||
|
|
||||||
|
self.buf = try self.alloc.alloc(u8, file_buf.len);
|
||||||
|
std.mem.copy(u8, self.buf, file_buf);
|
||||||
|
return log.info("Loaded Save from {s}", .{file_path});
|
||||||
|
}
|
||||||
|
|
||||||
|
log.err("EEPROM can either be 0x200 bytes or 0x2000 byes, but {s} was {X:} bytes", .{
|
||||||
|
file_path,
|
||||||
|
file_buf.len,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
.None => return SaveError.UnsupportedBackupKind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getSaveFilePath(self: *const Self, path: []const u8) ![]const u8 {
|
||||||
|
const filename = try self.getSaveFilename();
|
||||||
|
defer self.alloc.free(filename);
|
||||||
|
|
||||||
|
return try std.fs.path.join(self.alloc, &[_][]const u8{ path, filename });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getSaveFilename(self: *const Self) ![]const u8 {
|
||||||
|
const title = asString(escape(self.title));
|
||||||
|
const name = if (title.len != 0) title else "untitled";
|
||||||
|
|
||||||
|
return try std.mem.concat(self.alloc, u8, &[_][]const u8{ name, ".sav" });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeSaveToDisk(self: Self, path: []const u8) !void {
|
||||||
|
const file_path = try self.getSaveFilePath(path);
|
||||||
|
defer self.alloc.free(file_path);
|
||||||
|
|
||||||
|
switch (self.kind) {
|
||||||
|
.Sram, .Flash, .Flash1M, .Eeprom => {
|
||||||
|
const file = try std.fs.createFileAbsolute(file_path, .{});
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
try file.writeAll(self.buf);
|
||||||
|
log.info("Wrote Save to {s}", .{file_path});
|
||||||
|
},
|
||||||
|
else => return SaveError.UnsupportedBackupKind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read(self: *const Self, address: usize) u8 {
|
pub fn read(self: *const Self, address: usize) u8 {
|
||||||
const addr = address & 0xFFFF;
|
const addr = address & 0xFFFF;
|
||||||
|
|
||||||
@@ -73,7 +174,7 @@ pub const Backup = struct {
|
|||||||
switch (self.kind) {
|
switch (self.kind) {
|
||||||
.Flash, .Flash1M => {
|
.Flash, .Flash1M => {
|
||||||
if (self.flash.prep_write) return self.flash.write(self.buf, addr, byte);
|
if (self.flash.prep_write) return self.flash.write(self.buf, addr, byte);
|
||||||
if (self.flash.shouldEraseSector(addr, byte)) return self.flash.erase(self.buf, addr);
|
if (self.flash.shouldEraseSector(addr, byte)) return self.flash.eraseSector(self.buf, addr);
|
||||||
|
|
||||||
switch (addr) {
|
switch (addr) {
|
||||||
0x0000 => if (self.kind == .Flash1M and self.flash.set_bank) {
|
0x0000 => if (self.kind == .Flash1M and self.flash.set_bank) {
|
||||||
@@ -98,121 +199,366 @@ pub const Backup = struct {
|
|||||||
.None, .Eeprom => {},
|
.None, .Eeprom => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, kind: Kind, title: [12]u8, path: ?[]const u8) !Self {
|
const BackupKind = enum {
|
||||||
log.info("Kind: {}", .{kind});
|
Eeprom,
|
||||||
|
Sram,
|
||||||
|
Flash,
|
||||||
|
Flash1M,
|
||||||
|
None,
|
||||||
|
};
|
||||||
|
|
||||||
const buf_size: usize = switch (kind) {
|
const Needle = struct {
|
||||||
.Sram => 0x8000, // 32K
|
const Self = @This();
|
||||||
.Flash => 0x10000, // 64K
|
|
||||||
.Flash1M => 0x20000, // 128K
|
|
||||||
.None, .Eeprom => 0, // EEPROM is handled upon first Read Request to it
|
|
||||||
};
|
|
||||||
|
|
||||||
const buf = try allocator.alloc(u8, buf_size);
|
str: []const u8,
|
||||||
std.mem.set(u8, buf, 0xFF);
|
kind: BackupKind,
|
||||||
|
|
||||||
var backup = Self{
|
fn init(str: []const u8, kind: BackupKind) Self {
|
||||||
.buf = buf,
|
return .{
|
||||||
.allocator = allocator,
|
.str = str,
|
||||||
.kind = kind,
|
.kind = kind,
|
||||||
.title = title,
|
|
||||||
.save_path = path,
|
|
||||||
.flash = Flash.create(),
|
|
||||||
.eeprom = Eeprom.create(allocator),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (backup.save_path) |p| backup.readSave(allocator, p) catch |e| log.err("Failed to load save: {}", .{e});
|
|
||||||
return backup;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
|
||||||
if (self.save_path) |path| self.writeSave(self.allocator, path) catch |e| log.err("Failed to write save: {}", .{e});
|
|
||||||
self.allocator.free(self.buf);
|
|
||||||
self.* = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Guesses the Backup Kind of a GBA ROM
|
|
||||||
pub fn guess(rom: []const u8) Kind {
|
|
||||||
for (backup_kinds) |needle| {
|
|
||||||
const needle_len = needle.str.len;
|
|
||||||
|
|
||||||
var i: usize = 0;
|
|
||||||
while ((i + needle_len) < rom.len) : (i += 1) {
|
|
||||||
if (std.mem.eql(u8, needle.str, rom[i..][0..needle_len])) return needle.kind;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return .None;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn readSave(self: *Self, allocator: Allocator, path: []const u8) !void {
|
|
||||||
const file_path = try self.savePath(allocator, path);
|
|
||||||
defer allocator.free(file_path);
|
|
||||||
|
|
||||||
const expected = "untitled.sav";
|
|
||||||
if (std.mem.eql(u8, file_path[file_path.len - expected.len .. file_path.len], expected)) {
|
|
||||||
return log.err("ROM header lacks title, no save loaded", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
const file: std.fs.File = try std.fs.openFileAbsolute(file_path, .{});
|
|
||||||
const file_buf = try file.readToEndAlloc(allocator, try file.getEndPos());
|
|
||||||
defer allocator.free(file_buf);
|
|
||||||
|
|
||||||
switch (self.kind) {
|
|
||||||
.Sram, .Flash, .Flash1M => {
|
|
||||||
if (self.buf.len == file_buf.len) {
|
|
||||||
std.mem.copy(u8, self.buf, file_buf);
|
|
||||||
return log.info("Loaded Save from {s}", .{file_path});
|
|
||||||
}
|
|
||||||
|
|
||||||
log.err("{s} is {} bytes, but we expected {} bytes", .{ file_path, file_buf.len, self.buf.len });
|
|
||||||
},
|
|
||||||
.Eeprom => {
|
|
||||||
if (file_buf.len == 0x200 or file_buf.len == 0x2000) {
|
|
||||||
self.eeprom.kind = if (file_buf.len == 0x200) .Small else .Large;
|
|
||||||
|
|
||||||
self.buf = try allocator.alloc(u8, file_buf.len);
|
|
||||||
std.mem.copy(u8, self.buf, file_buf);
|
|
||||||
return log.info("Loaded Save from {s}", .{file_path});
|
|
||||||
}
|
|
||||||
|
|
||||||
log.err("EEPROM can either be 0x200 bytes or 0x2000 byes, but {s} was {X:} bytes", .{
|
|
||||||
file_path,
|
|
||||||
file_buf.len,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
.None => return SaveError.Unsupported,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn savePath(self: *const Self, allocator: Allocator, path: []const u8) ![]const u8 {
|
|
||||||
const filename = try self.saveName(allocator);
|
|
||||||
defer allocator.free(filename);
|
|
||||||
|
|
||||||
return try std.fs.path.join(allocator, &[_][]const u8{ path, filename });
|
|
||||||
}
|
|
||||||
|
|
||||||
fn saveName(self: *const Self, allocator: Allocator) ![]const u8 {
|
|
||||||
const title_str = std.mem.sliceTo(&escape(self.title), 0);
|
|
||||||
const name = if (title_str.len != 0) title_str else "untitled";
|
|
||||||
|
|
||||||
return try std.mem.concat(allocator, u8, &[_][]const u8{ name, ".sav" });
|
|
||||||
}
|
|
||||||
|
|
||||||
fn writeSave(self: Self, allocator: Allocator, path: []const u8) !void {
|
|
||||||
const file_path = try self.savePath(allocator, path);
|
|
||||||
defer allocator.free(file_path);
|
|
||||||
|
|
||||||
switch (self.kind) {
|
|
||||||
.Sram, .Flash, .Flash1M, .Eeprom => {
|
|
||||||
const file = try std.fs.createFileAbsolute(file_path, .{});
|
|
||||||
defer file.close();
|
|
||||||
|
|
||||||
try file.writeAll(self.buf);
|
|
||||||
log.info("Wrote Save to {s}", .{file_path});
|
|
||||||
},
|
|
||||||
else => return SaveError.Unsupported,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SaveError = error{
|
||||||
|
UnsupportedBackupKind,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Flash = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
state: FlashState,
|
||||||
|
|
||||||
|
id_mode: bool,
|
||||||
|
set_bank: bool,
|
||||||
|
prep_erase: bool,
|
||||||
|
prep_write: bool,
|
||||||
|
|
||||||
|
bank: u1,
|
||||||
|
|
||||||
|
fn init() Self {
|
||||||
|
return .{
|
||||||
|
.state = .Ready,
|
||||||
|
.id_mode = false,
|
||||||
|
.set_bank = false,
|
||||||
|
.prep_erase = false,
|
||||||
|
.prep_write = false,
|
||||||
|
.bank = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handleCommand(self: *Self, buf: []u8, byte: u8) void {
|
||||||
|
switch (byte) {
|
||||||
|
0x90 => self.id_mode = true,
|
||||||
|
0xF0 => self.id_mode = false,
|
||||||
|
0xB0 => self.set_bank = true,
|
||||||
|
0x80 => self.prep_erase = true,
|
||||||
|
0x10 => {
|
||||||
|
std.mem.set(u8, buf, 0xFF);
|
||||||
|
self.prep_erase = false;
|
||||||
|
},
|
||||||
|
0xA0 => self.prep_write = true,
|
||||||
|
else => std.debug.panic("Unhandled Flash Command: 0x{X:0>2}", .{byte}),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state = .Ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shouldEraseSector(self: *const Self, addr: usize, byte: u8) bool {
|
||||||
|
return self.state == .Command and self.prep_erase and byte == 0x30 and addr & 0xFFF == 0x000;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(self: *Self, buf: []u8, idx: usize, byte: u8) void {
|
||||||
|
buf[self.baseAddress() + idx] = byte;
|
||||||
|
self.prep_write = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read(self: *const Self, buf: []u8, idx: usize) u8 {
|
||||||
|
return buf[self.baseAddress() + idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eraseSector(self: *Self, buf: []u8, idx: usize) void {
|
||||||
|
const start = self.baseAddress() + (idx & 0xF000);
|
||||||
|
|
||||||
|
std.mem.set(u8, buf[start..][0..0x1000], 0xFF);
|
||||||
|
self.prep_erase = false;
|
||||||
|
self.state = .Ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn baseAddress(self: *const Self) usize {
|
||||||
|
return if (self.bank == 1) 0x10000 else @as(usize, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const FlashState = enum {
|
||||||
|
Ready,
|
||||||
|
Set,
|
||||||
|
Command,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Eeprom = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
addr: u14,
|
||||||
|
|
||||||
|
kind: Kind,
|
||||||
|
state: State,
|
||||||
|
writer: Writer,
|
||||||
|
reader: Reader,
|
||||||
|
|
||||||
|
alloc: Allocator,
|
||||||
|
|
||||||
|
const Kind = enum {
|
||||||
|
Unknown,
|
||||||
|
Small, // 512B
|
||||||
|
Large, // 8KB
|
||||||
|
};
|
||||||
|
|
||||||
|
const State = enum {
|
||||||
|
Ready,
|
||||||
|
Read,
|
||||||
|
Write,
|
||||||
|
WriteTransfer,
|
||||||
|
RequestEnd,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn init(alloc: Allocator) Self {
|
||||||
|
return .{
|
||||||
|
.kind = .Unknown,
|
||||||
|
.state = .Ready,
|
||||||
|
.writer = Writer.init(),
|
||||||
|
.reader = Reader.init(),
|
||||||
|
.addr = 0,
|
||||||
|
.alloc = alloc,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(self: *Self) u1 {
|
||||||
|
return self.reader.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dbgRead(self: *const Self) u1 {
|
||||||
|
return self.reader.dbgRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(self: *Self, word_count: u16, buf: *[]u8, bit: u1) void {
|
||||||
|
if (self.guessKind(word_count)) |found| {
|
||||||
|
log.info("EEPROM Kind: {}", .{found});
|
||||||
|
self.kind = found;
|
||||||
|
|
||||||
|
// buf.len will not equal zero when a save file was found and loaded.
|
||||||
|
// Right now, we assume that the save file is of the correct size which
|
||||||
|
// isn't necessarily true, since we can't trust anything a user can influence
|
||||||
|
// TODO: use ?[]u8 instead of a 0-sized slice?
|
||||||
|
if (buf.len == 0) {
|
||||||
|
const len: usize = switch (found) {
|
||||||
|
.Small => 0x200,
|
||||||
|
.Large => 0x2000,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
|
||||||
|
buf.* = self.alloc.alloc(u8, len) catch |e| {
|
||||||
|
log.err("Failed to resize EEPROM buf to {} bytes", .{len});
|
||||||
|
std.debug.panic("EEPROM entered irrecoverable state {}", .{e});
|
||||||
|
};
|
||||||
|
std.mem.set(u8, buf.*, 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.state == .RequestEnd) {
|
||||||
|
if (bit != 0) log.debug("EEPROM Request did not end in 0u1. TODO: is this ok?", .{});
|
||||||
|
self.state = .Ready;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (self.state) {
|
||||||
|
.Ready => self.writer.requestWrite(bit),
|
||||||
|
.Read, .Write => self.writer.addressWrite(self.kind, bit),
|
||||||
|
.WriteTransfer => self.writer.dataWrite(bit),
|
||||||
|
.RequestEnd => unreachable, // We return early just above this block
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tick(buf.*);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn guessKind(self: *const Self, word_count: u16) ?Kind {
|
||||||
|
if (self.kind != .Unknown or self.state != .Read) return null;
|
||||||
|
|
||||||
|
return switch (word_count) {
|
||||||
|
17 => .Large,
|
||||||
|
9 => .Small,
|
||||||
|
else => blk: {
|
||||||
|
log.err("Unexpected length of DMA3 Transfer upon initial EEPROM read: {}", .{word_count});
|
||||||
|
break :blk null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick(self: *Self, buf: []u8) void {
|
||||||
|
switch (self.state) {
|
||||||
|
.Ready => {
|
||||||
|
if (self.writer.len() == 2) {
|
||||||
|
const req = @intCast(u2, self.writer.finish());
|
||||||
|
switch (req) {
|
||||||
|
0b11 => self.state = .Read,
|
||||||
|
0b10 => self.state = .Write,
|
||||||
|
else => log.err("Unknown EEPROM Request 0b{b:0>2}", .{req}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Read => {
|
||||||
|
switch (self.kind) {
|
||||||
|
.Large => {
|
||||||
|
if (self.writer.len() == 14) {
|
||||||
|
const addr = @intCast(u10, self.writer.finish());
|
||||||
|
const value = std.mem.readIntSliceLittle(u64, buf[@as(u13, addr) * 8 ..][0..8]);
|
||||||
|
|
||||||
|
self.reader.configure(value);
|
||||||
|
self.state = .RequestEnd;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Small => {
|
||||||
|
if (self.writer.len() == 6) {
|
||||||
|
// FIXME: Duplicated code from above
|
||||||
|
const addr = @intCast(u6, self.writer.finish());
|
||||||
|
const value = std.mem.readIntSliceLittle(u64, buf[@as(u13, addr) * 8 ..][0..8]);
|
||||||
|
|
||||||
|
self.reader.configure(value);
|
||||||
|
self.state = .RequestEnd;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => log.err("Unable to calculate EEPROM read address. EEPROM size UNKNOWN", .{}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Write => {
|
||||||
|
switch (self.kind) {
|
||||||
|
.Large => {
|
||||||
|
if (self.writer.len() == 14) {
|
||||||
|
self.addr = @intCast(u10, self.writer.finish());
|
||||||
|
self.state = .WriteTransfer;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Small => {
|
||||||
|
if (self.writer.len() == 6) {
|
||||||
|
self.addr = @intCast(u6, self.writer.finish());
|
||||||
|
self.state = .WriteTransfer;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => log.err("Unable to calculate EEPROM write address. EEPROM size UNKNOWN", .{}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.WriteTransfer => {
|
||||||
|
if (self.writer.len() == 64) {
|
||||||
|
std.mem.writeIntSliceLittle(u64, buf[self.addr * 8 ..][0..8], self.writer.finish());
|
||||||
|
self.state = .RequestEnd;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.RequestEnd => unreachable, // We return early in write() if state is .RequestEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Reader = struct {
|
||||||
|
const This = @This();
|
||||||
|
|
||||||
|
data: u64,
|
||||||
|
i: u8,
|
||||||
|
enabled: bool,
|
||||||
|
|
||||||
|
fn init() This {
|
||||||
|
return .{
|
||||||
|
.data = 0,
|
||||||
|
.i = 0,
|
||||||
|
.enabled = false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure(self: *This, value: u64) void {
|
||||||
|
self.data = value;
|
||||||
|
self.i = 0;
|
||||||
|
self.enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read(self: *This) u1 {
|
||||||
|
if (!self.enabled) return 1;
|
||||||
|
|
||||||
|
const bit = if (self.i < 4) blk: {
|
||||||
|
break :blk 0;
|
||||||
|
} else blk: {
|
||||||
|
const idx = @intCast(u6, 63 - (self.i - 4));
|
||||||
|
break :blk @truncate(u1, self.data >> idx);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.i = (self.i + 1) % (64 + 4);
|
||||||
|
if (self.i == 0) self.enabled = false;
|
||||||
|
|
||||||
|
return bit;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dbgRead(self: *const This) u1 {
|
||||||
|
if (!self.enabled) return 1;
|
||||||
|
|
||||||
|
const bit = if (self.i < 4) blk: {
|
||||||
|
break :blk 0;
|
||||||
|
} else blk: {
|
||||||
|
const idx = @intCast(u6, 63 - (self.i - 4));
|
||||||
|
break :blk @truncate(u1, self.data >> idx);
|
||||||
|
};
|
||||||
|
|
||||||
|
return bit;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Writer = struct {
|
||||||
|
const This = @This();
|
||||||
|
|
||||||
|
data: u64,
|
||||||
|
i: u8,
|
||||||
|
|
||||||
|
fn init() This {
|
||||||
|
return .{ .data = 0, .i = 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn requestWrite(self: *This, bit: u1) void {
|
||||||
|
const idx = @intCast(u1, 1 - self.i);
|
||||||
|
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn addressWrite(self: *This, kind: Eeprom.Kind, bit: u1) void {
|
||||||
|
if (kind == .Unknown) return;
|
||||||
|
|
||||||
|
const size: u4 = switch (kind) {
|
||||||
|
.Large => 13,
|
||||||
|
.Small => 5,
|
||||||
|
.Unknown => unreachable,
|
||||||
|
};
|
||||||
|
|
||||||
|
const idx = @intCast(u4, size - self.i);
|
||||||
|
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dataWrite(self: *This, bit: u1) void {
|
||||||
|
const idx = @intCast(u6, 63 - self.i);
|
||||||
|
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn len(self: *const This) u8 {
|
||||||
|
return self.i;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(self: *This) u64 {
|
||||||
|
defer self.reset();
|
||||||
|
return self.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(self: *This) void {
|
||||||
|
self.i = 0;
|
||||||
|
self.data = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
state: State,
|
|
||||||
|
|
||||||
id_mode: bool,
|
|
||||||
set_bank: bool,
|
|
||||||
prep_erase: bool,
|
|
||||||
prep_write: bool,
|
|
||||||
|
|
||||||
bank: u1,
|
|
||||||
|
|
||||||
const State = enum {
|
|
||||||
Ready,
|
|
||||||
Set,
|
|
||||||
Command,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn read(self: *const Self, buf: []u8, idx: usize) u8 {
|
|
||||||
return buf[self.address() + idx];
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write(self: *Self, buf: []u8, idx: usize, byte: u8) void {
|
|
||||||
buf[self.address() + idx] = byte;
|
|
||||||
self.prep_write = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create() Self {
|
|
||||||
return .{
|
|
||||||
.state = .Ready,
|
|
||||||
.id_mode = false,
|
|
||||||
.set_bank = false,
|
|
||||||
.prep_erase = false,
|
|
||||||
.prep_write = false,
|
|
||||||
.bank = 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handleCommand(self: *Self, buf: []u8, byte: u8) void {
|
|
||||||
switch (byte) {
|
|
||||||
0x90 => self.id_mode = true,
|
|
||||||
0xF0 => self.id_mode = false,
|
|
||||||
0xB0 => self.set_bank = true,
|
|
||||||
0x80 => self.prep_erase = true,
|
|
||||||
0x10 => {
|
|
||||||
std.mem.set(u8, buf, 0xFF);
|
|
||||||
self.prep_erase = false;
|
|
||||||
},
|
|
||||||
0xA0 => self.prep_write = true,
|
|
||||||
else => std.debug.panic("Unhandled Flash Command: 0x{X:0>2}", .{byte}),
|
|
||||||
}
|
|
||||||
|
|
||||||
self.state = .Ready;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn shouldEraseSector(self: *const Self, addr: usize, byte: u8) bool {
|
|
||||||
return self.state == .Command and self.prep_erase and byte == 0x30 and addr & 0xFFF == 0x000;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn erase(self: *Self, buf: []u8, sector: usize) void {
|
|
||||||
const start = self.address() + (sector & 0xF000);
|
|
||||||
|
|
||||||
std.mem.set(u8, buf[start..][0..0x1000], 0xFF);
|
|
||||||
self.prep_erase = false;
|
|
||||||
self.state = .Ready;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Base Address
|
|
||||||
inline fn address(self: *const Self) usize {
|
|
||||||
return if (self.bank == 1) 0x10000 else @as(usize, 0);
|
|
||||||
}
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const log = std.log.scoped(.Eeprom);
|
|
||||||
|
|
||||||
pub const Eeprom = struct {
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
addr: u14,
|
|
||||||
|
|
||||||
kind: Kind,
|
|
||||||
state: State,
|
|
||||||
writer: Writer,
|
|
||||||
reader: Reader,
|
|
||||||
|
|
||||||
allocator: Allocator,
|
|
||||||
|
|
||||||
const Kind = enum {
|
|
||||||
Unknown,
|
|
||||||
Small, // 512B
|
|
||||||
Large, // 8KB
|
|
||||||
};
|
|
||||||
|
|
||||||
const State = enum {
|
|
||||||
Ready,
|
|
||||||
Read,
|
|
||||||
Write,
|
|
||||||
WriteTransfer,
|
|
||||||
RequestEnd,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn read(self: *Self) u1 {
|
|
||||||
return self.reader.read();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dbgRead(self: *const Self) u1 {
|
|
||||||
return self.reader.dbgRead();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write(self: *Self, word_count: u16, buf: *[]u8, bit: u1) void {
|
|
||||||
if (self.guessKind(word_count)) |found| {
|
|
||||||
log.info("EEPROM Kind: {}", .{found});
|
|
||||||
self.kind = found;
|
|
||||||
|
|
||||||
// buf.len will not equal zero when a save file was found and loaded.
|
|
||||||
// Right now, we assume that the save file is of the correct size which
|
|
||||||
// isn't necessarily true, since we can't trust anything a user can influence
|
|
||||||
// TODO: use ?[]u8 instead of a 0-sized slice?
|
|
||||||
if (buf.len == 0) {
|
|
||||||
const len: usize = switch (found) {
|
|
||||||
.Small => 0x200,
|
|
||||||
.Large => 0x2000,
|
|
||||||
else => unreachable,
|
|
||||||
};
|
|
||||||
|
|
||||||
buf.* = self.allocator.alloc(u8, len) catch |e| {
|
|
||||||
log.err("Failed to resize EEPROM buf to {} bytes", .{len});
|
|
||||||
std.debug.panic("EEPROM entered irrecoverable state {}", .{e});
|
|
||||||
};
|
|
||||||
std.mem.set(u8, buf.*, 0xFF);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.state == .RequestEnd) {
|
|
||||||
// if (bit != 0) log.debug("EEPROM Request did not end in 0u1. TODO: is this ok?", .{});
|
|
||||||
self.state = .Ready;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (self.state) {
|
|
||||||
.Ready => self.writer.requestWrite(bit),
|
|
||||||
.Read, .Write => self.writer.addressWrite(self.kind, bit),
|
|
||||||
.WriteTransfer => self.writer.dataWrite(bit),
|
|
||||||
.RequestEnd => unreachable, // We return early just above this block
|
|
||||||
}
|
|
||||||
|
|
||||||
self.tick(buf.*);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create(allocator: Allocator) Self {
|
|
||||||
return .{
|
|
||||||
.kind = .Unknown,
|
|
||||||
.state = .Ready,
|
|
||||||
.writer = Writer.create(),
|
|
||||||
.reader = Reader.create(),
|
|
||||||
.addr = 0,
|
|
||||||
.allocator = allocator,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn guessKind(self: *const Self, word_count: u16) ?Kind {
|
|
||||||
if (self.kind != .Unknown or self.state != .Read) return null;
|
|
||||||
|
|
||||||
return switch (word_count) {
|
|
||||||
17 => .Large,
|
|
||||||
9 => .Small,
|
|
||||||
else => blk: {
|
|
||||||
log.err("Unexpected length of DMA3 Transfer upon initial EEPROM read: {}", .{word_count});
|
|
||||||
break :blk null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tick(self: *Self, buf: []u8) void {
|
|
||||||
switch (self.state) {
|
|
||||||
.Ready => {
|
|
||||||
if (self.writer.len() == 2) {
|
|
||||||
const req = @intCast(u2, self.writer.finish());
|
|
||||||
switch (req) {
|
|
||||||
0b11 => self.state = .Read,
|
|
||||||
0b10 => self.state = .Write,
|
|
||||||
else => log.err("Unknown EEPROM Request 0b{b:0>2}", .{req}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.Read => {
|
|
||||||
switch (self.kind) {
|
|
||||||
.Large => {
|
|
||||||
if (self.writer.len() == 14) {
|
|
||||||
const addr = @intCast(u10, self.writer.finish());
|
|
||||||
const value = std.mem.readIntSliceLittle(u64, buf[@as(u13, addr) * 8 ..][0..8]);
|
|
||||||
|
|
||||||
self.reader.configure(value);
|
|
||||||
self.state = .RequestEnd;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.Small => {
|
|
||||||
if (self.writer.len() == 6) {
|
|
||||||
// FIXME: Duplicated code from above
|
|
||||||
const addr = @intCast(u6, self.writer.finish());
|
|
||||||
const value = std.mem.readIntSliceLittle(u64, buf[@as(u13, addr) * 8 ..][0..8]);
|
|
||||||
|
|
||||||
self.reader.configure(value);
|
|
||||||
self.state = .RequestEnd;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
else => log.err("Unable to calculate EEPROM read address. EEPROM size UNKNOWN", .{}),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.Write => {
|
|
||||||
switch (self.kind) {
|
|
||||||
.Large => {
|
|
||||||
if (self.writer.len() == 14) {
|
|
||||||
self.addr = @intCast(u10, self.writer.finish());
|
|
||||||
self.state = .WriteTransfer;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.Small => {
|
|
||||||
if (self.writer.len() == 6) {
|
|
||||||
self.addr = @intCast(u6, self.writer.finish());
|
|
||||||
self.state = .WriteTransfer;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
else => log.err("Unable to calculate EEPROM write address. EEPROM size UNKNOWN", .{}),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.WriteTransfer => {
|
|
||||||
if (self.writer.len() == 64) {
|
|
||||||
std.mem.writeIntSliceLittle(u64, buf[self.addr * 8 ..][0..8], self.writer.finish());
|
|
||||||
self.state = .RequestEnd;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.RequestEnd => unreachable, // We return early in write() if state is .RequestEnd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Reader = struct {
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
data: u64,
|
|
||||||
i: u8,
|
|
||||||
enabled: bool,
|
|
||||||
|
|
||||||
fn create() Self {
|
|
||||||
return .{
|
|
||||||
.data = 0,
|
|
||||||
.i = 0,
|
|
||||||
.enabled = false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read(self: *Self) u1 {
|
|
||||||
if (!self.enabled) return 1;
|
|
||||||
|
|
||||||
const bit = if (self.i < 4) blk: {
|
|
||||||
break :blk 0;
|
|
||||||
} else blk: {
|
|
||||||
const idx = @intCast(u6, 63 - (self.i - 4));
|
|
||||||
break :blk @truncate(u1, self.data >> idx);
|
|
||||||
};
|
|
||||||
|
|
||||||
self.i = (self.i + 1) % (64 + 4);
|
|
||||||
if (self.i == 0) self.enabled = false;
|
|
||||||
|
|
||||||
return bit;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dbgRead(self: *const Self) u1 {
|
|
||||||
if (!self.enabled) return 1;
|
|
||||||
|
|
||||||
const bit = if (self.i < 4) blk: {
|
|
||||||
break :blk 0;
|
|
||||||
} else blk: {
|
|
||||||
const idx = @intCast(u6, 63 - (self.i - 4));
|
|
||||||
break :blk @truncate(u1, self.data >> idx);
|
|
||||||
};
|
|
||||||
|
|
||||||
return bit;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn configure(self: *Self, value: u64) void {
|
|
||||||
self.data = value;
|
|
||||||
self.i = 0;
|
|
||||||
self.enabled = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Writer = struct {
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
data: u64,
|
|
||||||
i: u8,
|
|
||||||
|
|
||||||
fn create() Self {
|
|
||||||
return .{ .data = 0, .i = 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn requestWrite(self: *Self, bit: u1) void {
|
|
||||||
const idx = @intCast(u1, 1 - self.i);
|
|
||||||
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
|
|
||||||
self.i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn addressWrite(self: *Self, kind: Eeprom.Kind, bit: u1) void {
|
|
||||||
if (kind == .Unknown) return;
|
|
||||||
|
|
||||||
const size: u4 = switch (kind) {
|
|
||||||
.Large => 13,
|
|
||||||
.Small => 5,
|
|
||||||
.Unknown => unreachable,
|
|
||||||
};
|
|
||||||
|
|
||||||
const idx = @intCast(u4, size - self.i);
|
|
||||||
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
|
|
||||||
self.i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dataWrite(self: *Self, bit: u1) void {
|
|
||||||
const idx = @intCast(u6, 63 - self.i);
|
|
||||||
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
|
|
||||||
self.i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn len(self: *const Self) u8 {
|
|
||||||
return self.i;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finish(self: *Self) u64 {
|
|
||||||
defer self.reset();
|
|
||||||
return self.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset(self: *Self) void {
|
|
||||||
self.i = 0;
|
|
||||||
self.data = 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,144 +1,91 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const util = @import("../../util.zig");
|
|
||||||
|
|
||||||
const DmaControl = @import("io.zig").DmaControl;
|
const DmaControl = @import("io.zig").DmaControl;
|
||||||
const Bus = @import("../Bus.zig");
|
const Bus = @import("../Bus.zig");
|
||||||
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||||
|
|
||||||
pub const DmaTuple = struct { DmaController(0), DmaController(1), DmaController(2), DmaController(3) };
|
const readUndefined = @import("../util.zig").readUndefined;
|
||||||
|
const writeUndefined = @import("../util.zig").writeUndefined;
|
||||||
|
pub const DmaTuple = std.meta.Tuple(&[_]type{ DmaController(0), DmaController(1), DmaController(2), DmaController(3) });
|
||||||
const log = std.log.scoped(.DmaTransfer);
|
const log = std.log.scoped(.DmaTransfer);
|
||||||
|
|
||||||
const getHalf = util.getHalf;
|
|
||||||
const 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_addr = @truncate(u8, addr);
|
const byte = @truncate(u8, addr);
|
||||||
|
|
||||||
return switch (T) {
|
return switch (T) {
|
||||||
u32 => switch (byte_addr) {
|
u32 => switch (byte) {
|
||||||
0xB0, 0xB4 => null, // DMA0SAD, DMA0DAD,
|
0xB8 => @as(T, dma.*[0].cnt.raw) << 16,
|
||||||
0xB8 => @as(T, dma.*[0].dmacntH()) << 16, // DMA0CNT_L is write-only
|
0xC4 => @as(T, dma.*[1].cnt.raw) << 16,
|
||||||
0xBC, 0xC0 => null, // DMA1SAD, DMA1DAD
|
0xD0 => @as(T, dma.*[2].cnt.raw) << 16,
|
||||||
0xC4 => @as(T, dma.*[1].dmacntH()) << 16, // DMA1CNT_L is write-only
|
0xDC => @as(T, dma.*[3].cnt.raw) << 16,
|
||||||
0xC8, 0xCC => null, // DMA2SAD, DMA2DAD
|
else => readUndefined(log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
||||||
0xD0 => @as(T, dma.*[2].dmacntH()) << 16, // DMA2CNT_L is write-only
|
|
||||||
0xD4, 0xD8 => null, // DMA3SAD, DMA3DAD
|
|
||||||
0xDC => @as(T, dma.*[3].dmacntH()) << 16, // DMA3CNT_L is write-only
|
|
||||||
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
|
||||||
},
|
},
|
||||||
u16 => switch (byte_addr) {
|
u16 => switch (byte) {
|
||||||
0xB0, 0xB2, 0xB4, 0xB6 => null, // DMA0SAD, DMA0DAD
|
0xBA => dma.*[0].cnt.raw,
|
||||||
0xB8 => 0x0000, // DMA0CNT_L, suite.gba expects 0x0000 instead of 0xDEAD
|
0xC6 => dma.*[1].cnt.raw,
|
||||||
0xBA => dma.*[0].dmacntH(),
|
0xD2 => dma.*[2].cnt.raw,
|
||||||
|
0xDE => dma.*[3].cnt.raw,
|
||||||
0xBC, 0xBE, 0xC0, 0xC2 => null, // DMA1SAD, DMA1DAD
|
else => readUndefined(log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
||||||
0xC4 => 0x0000, // DMA1CNT_L
|
|
||||||
0xC6 => dma.*[1].dmacntH(),
|
|
||||||
|
|
||||||
0xC8, 0xCA, 0xCC, 0xCE => null, // DMA2SAD, DMA2DAD
|
|
||||||
0xD0 => 0x0000, // DMA2CNT_L
|
|
||||||
0xD2 => dma.*[2].dmacntH(),
|
|
||||||
|
|
||||||
0xD4, 0xD6, 0xD8, 0xDA => null, // DMA3SAD, DMA3DAD
|
|
||||||
0xDC => 0x0000, // DMA3CNT_L
|
|
||||||
0xDE => dma.*[3].dmacntH(),
|
|
||||||
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
|
||||||
},
|
|
||||||
u8 => switch (byte_addr) {
|
|
||||||
0xB0...0xB7 => null, // DMA0SAD, DMA0DAD
|
|
||||||
0xB8, 0xB9 => 0x00, // DMA0CNT_L
|
|
||||||
0xBA, 0xBB => @truncate(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 => readUndefined(log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
||||||
else => @compileError("DMA: Unsupported read width"),
|
else => @compileError("DMA: Unsupported read width"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write(comptime T: type, dma: *DmaTuple, addr: u32, value: T) void {
|
pub fn write(comptime T: type, dma: *DmaTuple, addr: u32, value: T) void {
|
||||||
const byte_addr = @truncate(u8, addr);
|
const byte = @truncate(u8, addr);
|
||||||
|
|
||||||
switch (T) {
|
switch (T) {
|
||||||
u32 => switch (byte_addr) {
|
u32 => switch (byte) {
|
||||||
0xB0 => dma.*[0].setDmasad(value),
|
0xB0 => dma.*[0].setSad(value),
|
||||||
0xB4 => dma.*[0].setDmadad(value),
|
0xB4 => dma.*[0].setDad(value),
|
||||||
0xB8 => dma.*[0].setDmacnt(value),
|
0xB8 => dma.*[0].setCnt(value),
|
||||||
|
0xBC => dma.*[1].setSad(value),
|
||||||
0xBC => dma.*[1].setDmasad(value),
|
0xC0 => dma.*[1].setDad(value),
|
||||||
0xC0 => dma.*[1].setDmadad(value),
|
0xC4 => dma.*[1].setCnt(value),
|
||||||
0xC4 => dma.*[1].setDmacnt(value),
|
0xC8 => dma.*[2].setSad(value),
|
||||||
|
0xCC => dma.*[2].setDad(value),
|
||||||
0xC8 => dma.*[2].setDmasad(value),
|
0xD0 => dma.*[2].setCnt(value),
|
||||||
0xCC => dma.*[2].setDmadad(value),
|
0xD4 => dma.*[3].setSad(value),
|
||||||
0xD0 => dma.*[2].setDmacnt(value),
|
0xD8 => dma.*[3].setDad(value),
|
||||||
|
0xDC => dma.*[3].setCnt(value),
|
||||||
0xD4 => dma.*[3].setDmasad(value),
|
else => writeUndefined(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||||
0xD8 => dma.*[3].setDmadad(value),
|
|
||||||
0xDC => dma.*[3].setDmacnt(value),
|
|
||||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
|
|
||||||
},
|
},
|
||||||
u16 => switch (byte_addr) {
|
u16 => switch (byte) {
|
||||||
0xB0, 0xB2 => dma.*[0].setDmasad(setHalf(u32, dma.*[0].sad, byte_addr, value)),
|
0xB0 => dma.*[0].setSad(setU32L(dma.*[0].sad, value)),
|
||||||
0xB4, 0xB6 => dma.*[0].setDmadad(setHalf(u32, dma.*[0].dad, byte_addr, value)),
|
0xB2 => dma.*[0].setSad(setU32H(dma.*[0].sad, value)),
|
||||||
0xB8 => dma.*[0].setDmacntL(value),
|
0xB4 => dma.*[0].setDad(setU32L(dma.*[0].dad, value)),
|
||||||
0xBA => dma.*[0].setDmacntH(value),
|
0xB6 => dma.*[0].setDad(setU32H(dma.*[0].dad, value)),
|
||||||
|
0xB8 => dma.*[0].setCntL(value),
|
||||||
|
0xBA => dma.*[0].setCntH(value),
|
||||||
|
|
||||||
0xBC, 0xBE => dma.*[1].setDmasad(setHalf(u32, dma.*[1].sad, byte_addr, value)),
|
0xBC => dma.*[1].setSad(setU32L(dma.*[1].sad, value)),
|
||||||
0xC0, 0xC2 => dma.*[1].setDmadad(setHalf(u32, dma.*[1].dad, byte_addr, value)),
|
0xBE => dma.*[1].setSad(setU32H(dma.*[1].sad, value)),
|
||||||
0xC4 => dma.*[1].setDmacntL(value),
|
0xC0 => dma.*[1].setDad(setU32L(dma.*[1].dad, value)),
|
||||||
0xC6 => dma.*[1].setDmacntH(value),
|
0xC2 => dma.*[1].setDad(setU32H(dma.*[1].dad, value)),
|
||||||
|
0xC4 => dma.*[1].setCntL(value),
|
||||||
|
0xC6 => dma.*[1].setCntH(value),
|
||||||
|
|
||||||
0xC8, 0xCA => dma.*[2].setDmasad(setHalf(u32, dma.*[2].sad, byte_addr, value)),
|
0xC8 => dma.*[2].setSad(setU32L(dma.*[2].sad, value)),
|
||||||
0xCC, 0xCE => dma.*[2].setDmadad(setHalf(u32, dma.*[2].dad, byte_addr, value)),
|
0xCA => dma.*[2].setSad(setU32H(dma.*[2].sad, value)),
|
||||||
0xD0 => dma.*[2].setDmacntL(value),
|
0xCC => dma.*[2].setDad(setU32L(dma.*[2].dad, value)),
|
||||||
0xD2 => dma.*[2].setDmacntH(value),
|
0xCE => dma.*[2].setDad(setU32H(dma.*[2].dad, value)),
|
||||||
|
0xD0 => dma.*[2].setCntL(value),
|
||||||
|
0xD2 => dma.*[2].setCntH(value),
|
||||||
|
|
||||||
0xD4, 0xD6 => dma.*[3].setDmasad(setHalf(u32, dma.*[3].sad, byte_addr, value)),
|
0xD4 => dma.*[3].setSad(setU32L(dma.*[3].sad, value)),
|
||||||
0xD8, 0xDA => dma.*[3].setDmadad(setHalf(u32, dma.*[3].dad, byte_addr, value)),
|
0xD6 => dma.*[3].setSad(setU32H(dma.*[3].sad, value)),
|
||||||
0xDC => dma.*[3].setDmacntL(value),
|
0xD8 => dma.*[3].setDad(setU32L(dma.*[3].dad, value)),
|
||||||
0xDE => dma.*[3].setDmacntH(value),
|
0xDA => dma.*[3].setDad(setU32H(dma.*[3].dad, value)),
|
||||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
|
0xDC => dma.*[3].setCntL(value),
|
||||||
},
|
0xDE => dma.*[3].setCntH(value),
|
||||||
u8 => switch (byte_addr) {
|
else => writeUndefined(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||||
0xB0, 0xB1, 0xB2, 0xB3 => dma.*[0].setDmasad(setQuart(dma.*[0].sad, byte_addr, value)),
|
|
||||||
0xB4, 0xB5, 0xB6, 0xB7 => dma.*[0].setDmadad(setQuart(dma.*[0].dad, byte_addr, value)),
|
|
||||||
0xB8, 0xB9 => dma.*[0].setDmacntL(setHalf(u16, dma.*[0].word_count, byte_addr, value)),
|
|
||||||
0xBA, 0xBB => dma.*[0].setDmacntH(setHalf(u16, dma.*[0].cnt.raw, byte_addr, value)),
|
|
||||||
|
|
||||||
0xBC, 0xBD, 0xBE, 0xBF => dma.*[1].setDmasad(setQuart(dma.*[1].sad, byte_addr, value)),
|
|
||||||
0xC0, 0xC1, 0xC2, 0xC3 => dma.*[1].setDmadad(setQuart(dma.*[1].dad, byte_addr, value)),
|
|
||||||
0xC4, 0xC5 => dma.*[1].setDmacntL(setHalf(u16, dma.*[1].word_count, byte_addr, value)),
|
|
||||||
0xC6, 0xC7 => dma.*[1].setDmacntH(setHalf(u16, dma.*[1].cnt.raw, byte_addr, value)),
|
|
||||||
|
|
||||||
0xC8, 0xC9, 0xCA, 0xCB => dma.*[2].setDmasad(setQuart(dma.*[2].sad, byte_addr, value)),
|
|
||||||
0xCC, 0xCD, 0xCE, 0xCF => dma.*[2].setDmadad(setQuart(dma.*[2].dad, byte_addr, value)),
|
|
||||||
0xD0, 0xD1 => dma.*[2].setDmacntL(setHalf(u16, dma.*[2].word_count, byte_addr, value)),
|
|
||||||
0xD2, 0xD3 => dma.*[2].setDmacntH(setHalf(u16, dma.*[2].cnt.raw, byte_addr, value)),
|
|
||||||
|
|
||||||
0xD4, 0xD5, 0xD6, 0xD7 => dma.*[3].setDmasad(setQuart(dma.*[3].sad, byte_addr, value)),
|
|
||||||
0xD8, 0xD9, 0xDA, 0xDB => dma.*[3].setDmadad(setQuart(dma.*[3].dad, byte_addr, value)),
|
|
||||||
0xDC, 0xDD => dma.*[3].setDmacntL(setHalf(u16, dma.*[3].word_count, byte_addr, value)),
|
|
||||||
0xDE, 0xDF => dma.*[3].setDmacntH(setHalf(u16, dma.*[3].cnt.raw, byte_addr, value)),
|
|
||||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
|
|
||||||
},
|
},
|
||||||
|
u8 => writeUndefined(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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,7 +97,6 @@ fn DmaController(comptime id: u2) type {
|
|||||||
|
|
||||||
const sad_mask: u32 = if (id == 0) 0x07FF_FFFF else 0x0FFF_FFFF;
|
const sad_mask: u32 = if (id == 0) 0x07FF_FFFF else 0x0FFF_FFFF;
|
||||||
const dad_mask: u32 = if (id != 3) 0x07FF_FFFF else 0x0FFF_FFFF;
|
const dad_mask: u32 = if (id != 3) 0x07FF_FFFF else 0x0FFF_FFFF;
|
||||||
const WordCount = if (id == 3) u16 else u14;
|
|
||||||
|
|
||||||
/// Write-only. The first address in a DMA transfer. (DMASAD)
|
/// Write-only. The first address in a DMA transfer. (DMASAD)
|
||||||
/// Note: use writeSrc instead of manipulating src_addr directly
|
/// Note: use writeSrc instead of manipulating src_addr directly
|
||||||
@@ -159,19 +105,20 @@ fn DmaController(comptime id: u2) type {
|
|||||||
/// Note: Use writeDst instead of manipulatig dst_addr directly
|
/// Note: Use writeDst instead of manipulatig dst_addr directly
|
||||||
dad: u32,
|
dad: u32,
|
||||||
/// Write-only. The Word Count for the DMA Transfer (DMACNT_L)
|
/// Write-only. The Word Count for the DMA Transfer (DMACNT_L)
|
||||||
word_count: WordCount,
|
word_count: if (id == 3) u16 else u14,
|
||||||
/// Read / Write. DMACNT_H
|
/// Read / Write. DMACNT_H
|
||||||
/// Note: Use writeControl instead of manipulating cnt directly.
|
/// Note: Use writeControl instead of manipulating cnt directly.
|
||||||
cnt: DmaControl,
|
cnt: DmaControl,
|
||||||
|
|
||||||
/// Internal. The last successfully read value
|
|
||||||
data_latch: u32,
|
|
||||||
/// Internal. Currrent Source Address
|
/// Internal. Currrent Source Address
|
||||||
sad_latch: u32,
|
_sad: u32,
|
||||||
/// Internal. Current Destination Address
|
/// Internal. Current Destination Address
|
||||||
dad_latch: u32,
|
_dad: u32,
|
||||||
/// Internal. Word Count
|
/// Internal. Word Count
|
||||||
_word_count: WordCount,
|
_word_count: if (id == 3) u16 else u14,
|
||||||
|
|
||||||
|
// Internal. FIFO Word Count
|
||||||
|
_fifo_word_count: u8,
|
||||||
|
|
||||||
/// Some DMA Transfers are enabled during Hblank / VBlank and / or
|
/// 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
|
||||||
@@ -186,39 +133,34 @@ fn DmaController(comptime id: u2) type {
|
|||||||
.cnt = .{ .raw = 0x000 },
|
.cnt = .{ .raw = 0x000 },
|
||||||
|
|
||||||
// Internals
|
// Internals
|
||||||
.sad_latch = 0,
|
._sad = 0,
|
||||||
.dad_latch = 0,
|
._dad = 0,
|
||||||
.data_latch = 0,
|
|
||||||
|
|
||||||
._word_count = 0,
|
._word_count = 0,
|
||||||
|
._fifo_word_count = 4,
|
||||||
.in_progress = false,
|
.in_progress = false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setDmasad(self: *Self, addr: u32) void {
|
pub fn setSad(self: *Self, addr: u32) void {
|
||||||
self.sad = addr & sad_mask;
|
self.sad = addr & sad_mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setDmadad(self: *Self, addr: u32) void {
|
pub fn setDad(self: *Self, addr: u32) void {
|
||||||
self.dad = addr & dad_mask;
|
self.dad = addr & dad_mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setDmacntL(self: *Self, halfword: u16) void {
|
pub fn setCntL(self: *Self, halfword: u16) void {
|
||||||
self.word_count = @truncate(@TypeOf(self.word_count), halfword);
|
self.word_count = @truncate(@TypeOf(self.word_count), halfword);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dmacntH(self: *const Self) u16 {
|
pub fn setCntH(self: *Self, halfword: u16) void {
|
||||||
return self.cnt.raw & if (id == 3) 0xFFE0 else 0xF7E0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setDmacntH(self: *Self, halfword: u16) void {
|
|
||||||
const new = DmaControl{ .raw = halfword };
|
const new = DmaControl{ .raw = halfword };
|
||||||
|
|
||||||
if (!self.cnt.enabled.read() and new.enabled.read()) {
|
if (!self.cnt.enabled.read() and new.enabled.read()) {
|
||||||
// Reload Internals on Rising Edge.
|
// Reload Internals on Rising Edge.
|
||||||
self.sad_latch = self.sad;
|
self._sad = self.sad;
|
||||||
self.dad_latch = self.dad;
|
self._dad = self.dad;
|
||||||
self._word_count = if (self.word_count == 0) std.math.maxInt(WordCount) else self.word_count;
|
self._word_count = if (self.word_count == 0) std.math.maxInt(@TypeOf(self._word_count)) else self.word_count;
|
||||||
|
|
||||||
// Only a Start Timing of 00 has a DMA Transfer immediately begin
|
// Only a Start Timing of 00 has a DMA Transfer immediately begin
|
||||||
self.in_progress = new.start_timing.read() == 0b00;
|
self.in_progress = new.start_timing.read() == 0b00;
|
||||||
@@ -227,50 +169,38 @@ fn DmaController(comptime id: u2) type {
|
|||||||
self.cnt.raw = halfword;
|
self.cnt.raw = halfword;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setDmacnt(self: *Self, word: u32) void {
|
pub fn setCnt(self: *Self, word: u32) void {
|
||||||
self.setDmacntL(@truncate(u16, word));
|
self.setCntL(@truncate(u16, word));
|
||||||
self.setDmacntH(@truncate(u16, word >> 16));
|
self.setCntH(@truncate(u16, word >> 16));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn step(self: *Self, cpu: *Arm7tdmi) void {
|
pub fn step(self: *Self, cpu: *Arm7tdmi) void {
|
||||||
const is_fifo = (id == 1 or id == 2) and self.cnt.start_timing.read() == 0b11;
|
const is_fifo = (id == 1 or id == 2) and self.cnt.start_timing.read() == 0b11;
|
||||||
const sad_adj = @intToEnum(Adjustment, self.cnt.sad_adj.read());
|
const sad_adj = Self.adjustment(self.cnt.sad_adj.read());
|
||||||
const dad_adj = if (is_fifo) .Fixed else @intToEnum(Adjustment, self.cnt.dad_adj.read());
|
const dad_adj = if (is_fifo) .Fixed else Self.adjustment(self.cnt.dad_adj.read());
|
||||||
|
|
||||||
const transfer_type = is_fifo or self.cnt.transfer_type.read();
|
const transfer_type = is_fifo or self.cnt.transfer_type.read();
|
||||||
const offset: u32 = if (transfer_type) @sizeOf(u32) else @sizeOf(u16);
|
const offset: u32 = if (transfer_type) @sizeOf(u32) else @sizeOf(u16);
|
||||||
|
|
||||||
const mask = if (transfer_type) ~@as(u32, 3) else ~@as(u32, 1);
|
const mask = if (transfer_type) ~@as(u32, 3) else ~@as(u32, 1);
|
||||||
const sad_addr = self.sad_latch & mask;
|
|
||||||
const dad_addr = self.dad_latch & mask;
|
|
||||||
|
|
||||||
if (transfer_type) {
|
if (transfer_type) {
|
||||||
if (sad_addr >= 0x0200_0000) self.data_latch = cpu.bus.read(u32, sad_addr);
|
cpu.bus.write(u32, self._dad & mask, cpu.bus.read(u32, self._sad & mask));
|
||||||
cpu.bus.write(u32, dad_addr, self.data_latch);
|
|
||||||
} else {
|
} else {
|
||||||
if (sad_addr >= 0x0200_0000) {
|
cpu.bus.write(u16, self._dad & mask, cpu.bus.read(u16, self._sad & mask));
|
||||||
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) {
|
||||||
}
|
.Increment => self._sad +%= offset,
|
||||||
|
.Decrement => self._sad -%= offset,
|
||||||
switch (@truncate(u8, sad_addr >> 24)) {
|
// TODO: Is just ignoring this ok?
|
||||||
// according to fleroviux, DMAs with a source address in ROM misbehave
|
|
||||||
// the resultant behaviour is that the source address will increment despite what DMAXCNT says
|
|
||||||
0x08...0x0D => self.sad_latch +%= offset, // obscure behaviour
|
|
||||||
else => switch (sad_adj) {
|
|
||||||
.Increment => self.sad_latch +%= offset,
|
|
||||||
.Decrement => self.sad_latch -%= offset,
|
|
||||||
.IncrementReload => log.err("{} is a prohibited adjustment on SAD", .{sad_adj}),
|
.IncrementReload => log.err("{} is a prohibited adjustment on SAD", .{sad_adj}),
|
||||||
.Fixed => {},
|
.Fixed => {},
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (dad_adj) {
|
switch (dad_adj) {
|
||||||
.Increment, .IncrementReload => self.dad_latch +%= offset,
|
.Increment, .IncrementReload => self._dad +%= offset,
|
||||||
.Decrement => self.dad_latch -%= offset,
|
.Decrement => self._dad -%= offset,
|
||||||
.Fixed => {},
|
.Fixed => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +228,7 @@ fn DmaController(comptime id: u2) type {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn poll(self: *Self, comptime kind: DmaKind) void {
|
pub fn pollBlankingDma(self: *Self, comptime kind: DmaKind) void {
|
||||||
if (self.in_progress) return; // If there's an ongoing DMA Transfer, exit early
|
if (self.in_progress) return; // If there's an ongoing DMA Transfer, exit early
|
||||||
|
|
||||||
// No ongoing DMA Transfer, We want to check if we should repeat an existing one
|
// No ongoing DMA Transfer, We want to check if we should repeat an existing one
|
||||||
@@ -314,11 +244,11 @@ fn DmaController(comptime id: u2) type {
|
|||||||
// Reload internal DAD latch if we are in IncrementRelaod
|
// Reload internal DAD latch if we are in IncrementRelaod
|
||||||
if (self.in_progress) {
|
if (self.in_progress) {
|
||||||
self._word_count = if (self.word_count == 0) std.math.maxInt(@TypeOf(self._word_count)) else self.word_count;
|
self._word_count = if (self.word_count == 0) std.math.maxInt(@TypeOf(self._word_count)) else self.word_count;
|
||||||
if (@intToEnum(Adjustment, self.cnt.dad_adj.read()) == .IncrementReload) self.dad_latch = self.dad;
|
if (Self.adjustment(self.cnt.dad_adj.read()) == .IncrementReload) self._dad = self.dad;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn requestAudio(self: *Self, _: u32) void {
|
pub fn requestSoundDma(self: *Self, _: u32) void {
|
||||||
comptime std.debug.assert(id == 1 or id == 2);
|
comptime std.debug.assert(id == 1 or id == 2);
|
||||||
if (self.in_progress) return; // APU must wait their turn
|
if (self.in_progress) return; // APU must wait their turn
|
||||||
|
|
||||||
@@ -330,19 +260,23 @@ fn DmaController(comptime id: u2) type {
|
|||||||
// We Assume DMACNT_L is set to 4
|
// We Assume DMACNT_L is set to 4
|
||||||
|
|
||||||
// FIXME: Safe to just assume whatever DAD is set to is the FIFO Address?
|
// FIXME: Safe to just assume whatever DAD is set to is the FIFO Address?
|
||||||
// self.dad_latch = fifo_addr;
|
// self._dad = fifo_addr;
|
||||||
self.cnt.repeat.set();
|
self.cnt.repeat.set();
|
||||||
self._word_count = 4;
|
self._word_count = 4;
|
||||||
self.in_progress = true;
|
self.in_progress = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn adjustment(idx: u2) Adjustment {
|
||||||
|
return std.meta.intToEnum(Adjustment, idx) catch unreachable;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pollDmaOnBlank(bus: *Bus, comptime kind: DmaKind) void {
|
pub fn pollBlankingDma(bus: *Bus, comptime kind: DmaKind) void {
|
||||||
comptime var i: usize = 0;
|
bus.dma[0].pollBlankingDma(kind);
|
||||||
inline while (i < 4) : (i += 1) {
|
bus.dma[1].pollBlankingDma(kind);
|
||||||
bus.dma[i].poll(kind);
|
bus.dma[2].pollBlankingDma(kind);
|
||||||
}
|
bus.dma[3].pollBlankingDma(kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Adjustment = enum(u2) {
|
const Adjustment = enum(u2) {
|
||||||
@@ -358,3 +292,11 @@ const DmaKind = enum(u2) {
|
|||||||
VBlank,
|
VBlank,
|
||||||
Special,
|
Special,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn setU32L(left: u32, right: u16) u32 {
|
||||||
|
return (left & 0xFFFF_0000) | right;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setU32H(left: u32, right: u16) u32 {
|
||||||
|
return (left & 0x0000_FFFF) | (@as(u32, right) << 16);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,464 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const Bit = @import("bitfield").Bit;
|
|
||||||
const DateTime = @import("datetime").datetime.Datetime;
|
|
||||||
|
|
||||||
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
/// GPIO Register Implementation
|
|
||||||
pub const Gpio = struct {
|
|
||||||
const Self = @This();
|
|
||||||
const log = std.log.scoped(.Gpio);
|
|
||||||
|
|
||||||
data: u4,
|
|
||||||
direction: u4,
|
|
||||||
cnt: u1,
|
|
||||||
|
|
||||||
device: Device,
|
|
||||||
|
|
||||||
const Register = enum { Data, Direction, Control };
|
|
||||||
|
|
||||||
pub const Device = struct {
|
|
||||||
ptr: ?*anyopaque,
|
|
||||||
kind: Kind, // TODO: Make comptime known?
|
|
||||||
|
|
||||||
pub const Kind = enum { Rtc, None };
|
|
||||||
|
|
||||||
fn step(self: *Device, value: u4) u4 {
|
|
||||||
return switch (self.kind) {
|
|
||||||
.Rtc => blk: {
|
|
||||||
const clock = @ptrCast(*Clock, @alignCast(@alignOf(*Clock), self.ptr.?));
|
|
||||||
break :blk clock.step(Clock.Data{ .raw = value });
|
|
||||||
},
|
|
||||||
.None => value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init(kind: Kind, ptr: ?*anyopaque) Device {
|
|
||||||
return .{ .kind = kind, .ptr = ptr };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn write(self: *Self, comptime reg: Register, value: if (reg == .Control) u1 else u4) void {
|
|
||||||
switch (reg) {
|
|
||||||
.Data => {
|
|
||||||
const masked_value = value & self.direction;
|
|
||||||
|
|
||||||
// The value which is actually stored in the GPIO register
|
|
||||||
// might be modified by the device implementing the GPIO interface e.g. RTC reads
|
|
||||||
self.data = self.device.step(masked_value);
|
|
||||||
},
|
|
||||||
.Direction => self.direction = value,
|
|
||||||
.Control => self.cnt = value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read(self: *const Self, comptime reg: Register) if (reg == .Control) u1 else u4 {
|
|
||||||
if (self.cnt == 0) return 0;
|
|
||||||
|
|
||||||
return switch (reg) {
|
|
||||||
.Data => self.data & ~self.direction,
|
|
||||||
.Direction => self.direction,
|
|
||||||
.Control => self.cnt,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, kind: Device.Kind) !*Self {
|
|
||||||
log.info("Device: {}", .{kind});
|
|
||||||
|
|
||||||
const self = try allocator.create(Self);
|
|
||||||
errdefer allocator.destroy(self);
|
|
||||||
|
|
||||||
self.* = .{
|
|
||||||
.data = 0b0000,
|
|
||||||
.direction = 0b1111, // TODO: What is GPIO Direction set to by default?
|
|
||||||
.cnt = 0b0,
|
|
||||||
|
|
||||||
.device = switch (kind) {
|
|
||||||
.Rtc => blk: {
|
|
||||||
const clock = try allocator.create(Clock);
|
|
||||||
clock.init(cpu, self);
|
|
||||||
|
|
||||||
break :blk Device{ .kind = kind, .ptr = clock };
|
|
||||||
},
|
|
||||||
.None => Device{ .kind = kind, .ptr = null },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Self, allocator: Allocator) void {
|
|
||||||
switch (self.device.kind) {
|
|
||||||
.Rtc => allocator.destroy(@ptrCast(*Clock, @alignCast(@alignOf(*Clock), self.device.ptr.?))),
|
|
||||||
.None => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
self.* = undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// GBA Real Time Clock
|
|
||||||
pub const Clock = struct {
|
|
||||||
const Self = @This();
|
|
||||||
const log = std.log.scoped(.Rtc);
|
|
||||||
|
|
||||||
writer: Writer,
|
|
||||||
reader: Reader,
|
|
||||||
state: State,
|
|
||||||
cnt: Control,
|
|
||||||
|
|
||||||
year: u8,
|
|
||||||
month: u5,
|
|
||||||
day: u6,
|
|
||||||
weekday: u3,
|
|
||||||
hour: u6,
|
|
||||||
minute: u7,
|
|
||||||
second: u7,
|
|
||||||
|
|
||||||
cpu: *Arm7tdmi,
|
|
||||||
gpio: *const Gpio,
|
|
||||||
|
|
||||||
const Register = enum {
|
|
||||||
Control,
|
|
||||||
DateTime,
|
|
||||||
Time,
|
|
||||||
};
|
|
||||||
|
|
||||||
const State = union(enum) {
|
|
||||||
Idle,
|
|
||||||
Command,
|
|
||||||
Write: Register,
|
|
||||||
Read: Register,
|
|
||||||
};
|
|
||||||
|
|
||||||
const Reader = struct {
|
|
||||||
i: u4,
|
|
||||||
count: u8,
|
|
||||||
|
|
||||||
/// Reads a bit from RTC registers. Which bit it reads is dependent on
|
|
||||||
///
|
|
||||||
/// 1. The RTC State Machine, whitch tells us which register we're accessing
|
|
||||||
/// 2. A `count`, which keeps track of which byte is currently being read
|
|
||||||
/// 3. An index, which keeps track of which bit of the byte determined by `count` is being read
|
|
||||||
fn read(self: *Reader, clock: *const Clock, register: Register) u1 {
|
|
||||||
const idx = @intCast(u3, self.i);
|
|
||||||
defer self.i += 1;
|
|
||||||
|
|
||||||
// FIXME: What do I do about the unused bits?
|
|
||||||
return switch (register) {
|
|
||||||
.Control => @truncate(u1, switch (self.count) {
|
|
||||||
0 => clock.cnt.raw >> idx,
|
|
||||||
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 1 byte)", .{ self.count, register }),
|
|
||||||
}),
|
|
||||||
.DateTime => @truncate(u1, switch (self.count) {
|
|
||||||
// Date
|
|
||||||
0 => clock.year >> idx,
|
|
||||||
1 => @as(u8, clock.month) >> idx,
|
|
||||||
2 => @as(u8, clock.day) >> idx,
|
|
||||||
3 => @as(u8, clock.weekday) >> idx,
|
|
||||||
|
|
||||||
// Time
|
|
||||||
4 => @as(u8, clock.hour) >> idx,
|
|
||||||
5 => @as(u8, clock.minute) >> idx,
|
|
||||||
6 => @as(u8, clock.second) >> idx,
|
|
||||||
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 7 bytes)", .{ self.count, register }),
|
|
||||||
}),
|
|
||||||
.Time => @truncate(u1, switch (self.count) {
|
|
||||||
0 => @as(u8, clock.hour) >> idx,
|
|
||||||
1 => @as(u8, clock.minute) >> idx,
|
|
||||||
2 => @as(u8, clock.second) >> idx,
|
|
||||||
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 3 bytes)", .{ self.count, register }),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Is true when a Reader has read a u8's worth of bits
|
|
||||||
fn finished(self: *const Reader) bool {
|
|
||||||
return self.i >= 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resets the index used to shift bits out of RTC registers
|
|
||||||
/// and `count`, which is used to keep track of which byte we're reading
|
|
||||||
/// is incremeneted
|
|
||||||
fn lap(self: *Reader) void {
|
|
||||||
self.i = 0;
|
|
||||||
self.count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resets the state of a `Reader` in preparation for a future
|
|
||||||
/// read command
|
|
||||||
fn reset(self: *Reader) void {
|
|
||||||
self.i = 0;
|
|
||||||
self.count = 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Writer = struct {
|
|
||||||
buf: u8,
|
|
||||||
i: u4,
|
|
||||||
|
|
||||||
/// The Number of bytes written since last reset
|
|
||||||
count: u8,
|
|
||||||
|
|
||||||
/// Append a bit to the internal bit buffer (aka an integer)
|
|
||||||
fn push(self: *Writer, value: u1) void {
|
|
||||||
const idx = @intCast(u3, self.i);
|
|
||||||
self.buf = (self.buf & ~(@as(u8, 1) << idx)) | @as(u8, value) << idx;
|
|
||||||
self.i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Takes the contents of the internal buffer and writes it to an RTC register
|
|
||||||
/// Where it writes to is dependent on:
|
|
||||||
///
|
|
||||||
/// 1. The RTC State Machine, whitch tells us which register we're accessing
|
|
||||||
/// 2. A `count`, which keeps track of which byte is currently being read
|
|
||||||
fn write(self: *const Writer, clock: *Clock, register: Register) void {
|
|
||||||
// FIXME: What do do about unused bits?
|
|
||||||
switch (register) {
|
|
||||||
.Control => switch (self.count) {
|
|
||||||
0 => clock.cnt.raw = (clock.cnt.raw & 0x80) | (self.buf & 0x7F), // Bit 7 read-only
|
|
||||||
else => std.debug.panic("Tried to write to byte #{} of {} (hint: there's only 1 byte)", .{ self.count, register }),
|
|
||||||
},
|
|
||||||
.DateTime, .Time => log.debug("Ignoring {} write", .{register}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Is true when 8 bits have been shifted into the internal buffer
|
|
||||||
fn finished(self: *const Writer) bool {
|
|
||||||
return self.i >= 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resets the internal buffer
|
|
||||||
/// resets the index used to shift bits into the internal buffer
|
|
||||||
/// increments `count` (which keeps track of byte offsets) by one
|
|
||||||
fn lap(self: *Writer) void {
|
|
||||||
self.buf = 0;
|
|
||||||
self.i = 0;
|
|
||||||
self.count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resets `Writer` to a clean state in preparation for a future write command
|
|
||||||
fn reset(self: *Writer) void {
|
|
||||||
self.buf = 0;
|
|
||||||
self.i = 0;
|
|
||||||
self.count = 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Data = extern union {
|
|
||||||
sck: Bit(u8, 0),
|
|
||||||
sio: Bit(u8, 1),
|
|
||||||
cs: Bit(u8, 2),
|
|
||||||
raw: u8,
|
|
||||||
};
|
|
||||||
|
|
||||||
const Control = extern union {
|
|
||||||
/// Unknown, value should be preserved though
|
|
||||||
unk: Bit(u8, 1),
|
|
||||||
/// Per-minute IRQ
|
|
||||||
/// If set, fire a Gamepak IRQ every 30s,
|
|
||||||
irq: Bit(u8, 3),
|
|
||||||
/// 12/24 Hour Bit
|
|
||||||
/// If set, 12h mode
|
|
||||||
/// If cleared, 24h mode
|
|
||||||
mode: Bit(u8, 6),
|
|
||||||
/// Read-Only, bit cleared on read
|
|
||||||
/// If is set, means that there has been a failure / time has been lost
|
|
||||||
off: Bit(u8, 7),
|
|
||||||
raw: u8,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn init(ptr: *Self, cpu: *Arm7tdmi, gpio: *const Gpio) void {
|
|
||||||
ptr.* = .{
|
|
||||||
.writer = .{ .buf = 0, .i = 0, .count = 0 },
|
|
||||||
.reader = .{ .i = 0, .count = 0 },
|
|
||||||
.state = .Idle,
|
|
||||||
.cnt = .{ .raw = 0 },
|
|
||||||
.year = 0x01,
|
|
||||||
.month = 0x6,
|
|
||||||
.day = 0x13,
|
|
||||||
.weekday = 0x3,
|
|
||||||
.hour = 0x23,
|
|
||||||
.minute = 0x59,
|
|
||||||
.second = 0x59,
|
|
||||||
.cpu = cpu,
|
|
||||||
.gpio = gpio, // Can't use Arm7tdmi ptr b/c not initialized yet
|
|
||||||
};
|
|
||||||
|
|
||||||
cpu.sched.push(.RealTimeClock, 1 << 24); // Every Second
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn onClockUpdate(self: *Self, late: u64) void {
|
|
||||||
self.cpu.sched.push(.RealTimeClock, (1 << 24) -| late); // Reschedule
|
|
||||||
|
|
||||||
const now = DateTime.now();
|
|
||||||
self.year = bcd(u8, @intCast(u8, now.date.year - 2000));
|
|
||||||
self.month = bcd(u5, now.date.month);
|
|
||||||
self.day = bcd(u6, now.date.day);
|
|
||||||
self.weekday = bcd(u3, (now.date.weekday() + 1) % 7); // API is Monday = 0, Sunday = 6. We want Sunday = 0, Saturday = 6
|
|
||||||
self.hour = bcd(u6, now.time.hour);
|
|
||||||
self.minute = bcd(u7, now.time.minute);
|
|
||||||
self.second = bcd(u7, now.time.second);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn step(self: *Self, value: Data) u4 {
|
|
||||||
const cache: Data = .{ .raw = self.gpio.data };
|
|
||||||
|
|
||||||
return switch (self.state) {
|
|
||||||
.Idle => blk: {
|
|
||||||
// FIXME: Maybe check incoming value to see if SCK is also high?
|
|
||||||
if (cache.sck.read()) {
|
|
||||||
if (!cache.cs.read() and value.cs.read()) {
|
|
||||||
log.debug("Entering Command Mode", .{});
|
|
||||||
self.state = .Command;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break :blk @truncate(u4, value.raw);
|
|
||||||
},
|
|
||||||
.Command => blk: {
|
|
||||||
if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state});
|
|
||||||
|
|
||||||
// If SCK rises, sample SIO
|
|
||||||
if (!cache.sck.read() and value.sck.read()) {
|
|
||||||
self.writer.push(@boolToInt(value.sio.read()));
|
|
||||||
|
|
||||||
if (self.writer.finished()) {
|
|
||||||
self.state = self.processCommand(self.writer.buf);
|
|
||||||
self.writer.reset();
|
|
||||||
|
|
||||||
log.debug("Switching to {}", .{self.state});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break :blk @truncate(u4, value.raw);
|
|
||||||
},
|
|
||||||
.Write => |register| blk: {
|
|
||||||
if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state});
|
|
||||||
|
|
||||||
// If SCK rises, sample SIO
|
|
||||||
if (!cache.sck.read() and value.sck.read()) {
|
|
||||||
self.writer.push(@boolToInt(value.sio.read()));
|
|
||||||
|
|
||||||
const register_width: u32 = switch (register) {
|
|
||||||
.Control => 1,
|
|
||||||
.DateTime => 7,
|
|
||||||
.Time => 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (self.writer.finished()) {
|
|
||||||
self.writer.write(self, register); // write inner buffer to RTC register
|
|
||||||
self.writer.lap();
|
|
||||||
|
|
||||||
if (self.writer.count == register_width) {
|
|
||||||
self.writer.reset();
|
|
||||||
self.state = .Idle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break :blk @truncate(u4, value.raw);
|
|
||||||
},
|
|
||||||
.Read => |register| blk: {
|
|
||||||
if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state});
|
|
||||||
var ret = value;
|
|
||||||
|
|
||||||
// if SCK rises, sample SIO
|
|
||||||
if (!cache.sck.read() and value.sck.read()) {
|
|
||||||
ret.sio.write(self.reader.read(self, register) == 0b1);
|
|
||||||
|
|
||||||
const register_width: u32 = switch (register) {
|
|
||||||
.Control => 1,
|
|
||||||
.DateTime => 7,
|
|
||||||
.Time => 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (self.reader.finished()) {
|
|
||||||
self.reader.lap();
|
|
||||||
|
|
||||||
if (self.reader.count == register_width) {
|
|
||||||
self.reader.reset();
|
|
||||||
self.state = .Idle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break :blk @truncate(u4, ret.raw);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset(self: *Self) void {
|
|
||||||
// mGBA and NBA only zero the control register. We will do the same
|
|
||||||
log.debug("Reset (control register was zeroed)", .{});
|
|
||||||
|
|
||||||
self.cnt.raw = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn irq(self: *Self) void {
|
|
||||||
// TODO: Confirm that this is the right behaviour
|
|
||||||
log.debug("Force GamePak IRQ", .{});
|
|
||||||
|
|
||||||
self.cpu.bus.io.irq.game_pak.set();
|
|
||||||
self.cpu.handleInterrupt();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn processCommand(self: *Self, raw_command: u8) State {
|
|
||||||
const command = blk: {
|
|
||||||
// If High Nybble is 0x6, no need to switch the endianness
|
|
||||||
if (raw_command >> 4 & 0xF == 0x6) break :blk raw_command;
|
|
||||||
|
|
||||||
// Turns out reversing the order of bits isn't trivial at all
|
|
||||||
// https://stackoverflow.com/questions/2602823/in-c-c-whats-the-simplest-way-to-reverse-the-order-of-bits-in-a-byte
|
|
||||||
var ret = raw_command;
|
|
||||||
ret = (ret & 0xF0) >> 4 | (ret & 0x0F) << 4;
|
|
||||||
ret = (ret & 0xCC) >> 2 | (ret & 0x33) << 2;
|
|
||||||
ret = (ret & 0xAA) >> 1 | (ret & 0x55) << 1;
|
|
||||||
|
|
||||||
break :blk ret;
|
|
||||||
};
|
|
||||||
log.debug("Handling Command 0x{X:0>2} [0b{b:0>8}]", .{ command, command });
|
|
||||||
|
|
||||||
const is_write = command & 1 == 0;
|
|
||||||
const rtc_register = @truncate(u3, command >> 1 & 0x7);
|
|
||||||
|
|
||||||
if (is_write) {
|
|
||||||
return switch (rtc_register) {
|
|
||||||
0 => blk: {
|
|
||||||
self.reset();
|
|
||||||
break :blk .Idle;
|
|
||||||
},
|
|
||||||
1 => .{ .Write = .Control },
|
|
||||||
2 => .{ .Write = .DateTime },
|
|
||||||
3 => .{ .Write = .Time },
|
|
||||||
6 => blk: {
|
|
||||||
self.irq();
|
|
||||||
break :blk .Idle;
|
|
||||||
},
|
|
||||||
4, 5, 7 => .Idle,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return switch (rtc_register) {
|
|
||||||
1 => .{ .Read = .Control },
|
|
||||||
2 => .{ .Read = .DateTime },
|
|
||||||
3 => .{ .Read = .Time },
|
|
||||||
0, 4, 5, 6, 7 => .Idle, // Do Nothing
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fn bcd(comptime T: type, value: u8) T {
|
|
||||||
var input = value;
|
|
||||||
var ret: u8 = 0;
|
|
||||||
var shift: u3 = 0;
|
|
||||||
|
|
||||||
while (input > 0) {
|
|
||||||
ret |= (input % 10) << (shift << 2);
|
|
||||||
shift += 1;
|
|
||||||
input /= 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
return @truncate(T, ret);
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const timer = @import("timer.zig");
|
const builtin = @import("builtin");
|
||||||
const dma = @import("dma.zig");
|
|
||||||
const apu = @import("../apu.zig");
|
|
||||||
const ppu = @import("../ppu.zig");
|
|
||||||
const util = @import("../../util.zig");
|
|
||||||
|
|
||||||
const Bit = @import("bitfield").Bit;
|
const Bit = @import("bitfield").Bit;
|
||||||
const Bitfield = @import("bitfield").Bitfield;
|
const Bitfield = @import("bitfield").Bitfield;
|
||||||
const Bus = @import("../Bus.zig");
|
const Bus = @import("../Bus.zig");
|
||||||
|
const DmaController = @import("dma.zig").DmaController;
|
||||||
|
const Scheduler = @import("../scheduler.zig").Scheduler;
|
||||||
|
|
||||||
const getHalf = util.getHalf;
|
const timer = @import("timer.zig");
|
||||||
const setHalf = util.setHalf;
|
const dma = @import("dma.zig");
|
||||||
|
const apu = @import("../apu.zig");
|
||||||
|
|
||||||
|
const readUndefined = @import("../util.zig").readUndefined;
|
||||||
|
const writeUndefined = @import("../util.zig").writeUndefined;
|
||||||
const log = std.log.scoped(.@"I/O");
|
const log = std.log.scoped(.@"I/O");
|
||||||
|
|
||||||
pub const Io = struct {
|
pub const Io = struct {
|
||||||
@@ -22,17 +23,15 @@ pub const Io = struct {
|
|||||||
ie: InterruptEnable,
|
ie: InterruptEnable,
|
||||||
irq: InterruptRequest,
|
irq: InterruptRequest,
|
||||||
postflg: PostFlag,
|
postflg: PostFlag,
|
||||||
waitcnt: WaitControl,
|
|
||||||
haltcnt: HaltControl,
|
haltcnt: HaltControl,
|
||||||
keyinput: AtomicKeyInput,
|
keyinput: KeyInput,
|
||||||
|
|
||||||
pub fn init() Self {
|
pub fn init() Self {
|
||||||
return .{
|
return .{
|
||||||
.ime = false,
|
.ime = false,
|
||||||
.ie = .{ .raw = 0x0000 },
|
.ie = .{ .raw = 0x0000 },
|
||||||
.irq = .{ .raw = 0x0000 },
|
.irq = .{ .raw = 0x0000 },
|
||||||
.keyinput = AtomicKeyInput.init(.{ .raw = 0x03FF }),
|
.keyinput = .{ .raw = 0x03FF },
|
||||||
.waitcnt = .{ .raw = 0x0000_0000 }, // Bit 15 == 0 for GBA
|
|
||||||
.postflg = .FirstBoot,
|
.postflg = .FirstBoot,
|
||||||
.haltcnt = .Execute,
|
.haltcnt = .Execute,
|
||||||
};
|
};
|
||||||
@@ -44,14 +43,13 @@ pub const Io = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn read(bus: *const Bus, comptime T: type, address: u32) ?T {
|
pub fn read(bus: *const Bus, comptime T: type, address: u32) T {
|
||||||
return switch (T) {
|
return switch (T) {
|
||||||
u32 => switch (address) {
|
u32 => switch (address) {
|
||||||
// Display
|
// Display
|
||||||
0x0400_0000...0x0400_0054 => ppu.read(T, &bus.ppu, address),
|
0x0400_0000 => bus.ppu.dispcnt.raw,
|
||||||
|
0x0400_0004 => @as(T, bus.ppu.vcount.raw) << 16 | bus.ppu.dispstat.raw,
|
||||||
// Sound
|
0x0400_0006 => @as(T, bus.ppu.bg[0].cnt.raw) << 16 | bus.ppu.vcount.raw,
|
||||||
0x0400_0060...0x0400_00A4 => apu.read(T, &bus.apu, address),
|
|
||||||
|
|
||||||
// DMA Transfers
|
// DMA Transfers
|
||||||
0x0400_00B0...0x0400_00DC => dma.read(T, &bus.dma, address),
|
0x0400_00B0...0x0400_00DC => dma.read(T, &bus.dma, address),
|
||||||
@@ -60,27 +58,33 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) ?T {
|
|||||||
0x0400_0100...0x0400_010C => timer.read(T, &bus.tim, address),
|
0x0400_0100...0x0400_010C => timer.read(T, &bus.tim, address),
|
||||||
|
|
||||||
// Serial Communication 1
|
// Serial Communication 1
|
||||||
0x0400_0128 => util.io.read.todo(log, "Read {} from SIOCNT and SIOMLT_SEND", .{T}),
|
0x0400_0128 => readTodo("Read {} from SIOCNT and SIOMLT_SEND", .{T}),
|
||||||
|
|
||||||
// Keypad Input
|
// Keypad Input
|
||||||
0x0400_0130 => util.io.read.todo(log, "Read {} from KEYINPUT", .{T}),
|
0x0400_0130 => readTodo("Read {} from KEYINPUT", .{T}),
|
||||||
|
|
||||||
// Serial Communication 2
|
// Serial Communication 2
|
||||||
0x0400_0150 => util.io.read.todo(log, "Read {} from JOY_RECV", .{T}),
|
0x0400_0150 => readTodo("Read {} from JOY_RECV", .{T}),
|
||||||
|
|
||||||
// Interrupts
|
// Interrupts
|
||||||
0x0400_0200 => @as(u32, bus.io.irq.raw) << 16 | bus.io.ie.raw,
|
0x0400_0200 => @as(T, bus.io.irq.raw) << 16 | bus.io.ie.raw,
|
||||||
0x0400_0204 => bus.io.waitcnt.raw,
|
|
||||||
0x0400_0208 => @boolToInt(bus.io.ime),
|
0x0400_0208 => @boolToInt(bus.io.ime),
|
||||||
0x0400_0300 => @enumToInt(bus.io.postflg),
|
else => readUndefined(log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
|
||||||
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
|
|
||||||
},
|
},
|
||||||
u16 => switch (address) {
|
u16 => switch (address) {
|
||||||
// Display
|
// Display
|
||||||
0x0400_0000...0x0400_0054 => ppu.read(T, &bus.ppu, address),
|
0x0400_0000 => bus.ppu.dispcnt.raw,
|
||||||
|
0x0400_0004 => bus.ppu.dispstat.raw,
|
||||||
|
0x0400_0006 => bus.ppu.vcount.raw,
|
||||||
|
0x0400_0008 => bus.ppu.bg[0].cnt.raw,
|
||||||
|
0x0400_000A => bus.ppu.bg[1].cnt.raw,
|
||||||
|
0x0400_000C => bus.ppu.bg[2].cnt.raw,
|
||||||
|
0x0400_000E => bus.ppu.bg[3].cnt.raw,
|
||||||
|
0x0400_004C => readTodo("Read {} from MOSAIC", .{T}),
|
||||||
|
0x0400_0050 => bus.ppu.bldcnt.raw,
|
||||||
|
|
||||||
// Sound
|
// Sound
|
||||||
0x0400_0060...0x0400_00A6 => apu.read(T, &bus.apu, address),
|
0x0400_0060...0x0400_009E => apu.read(T, &bus.apu, address),
|
||||||
|
|
||||||
// DMA Transfers
|
// DMA Transfers
|
||||||
0x0400_00B0...0x0400_00DE => dma.read(T, &bus.dma, address),
|
0x0400_00B0...0x0400_00DE => dma.read(T, &bus.dma, address),
|
||||||
@@ -89,64 +93,48 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) ?T {
|
|||||||
0x0400_0100...0x0400_010E => timer.read(T, &bus.tim, address),
|
0x0400_0100...0x0400_010E => timer.read(T, &bus.tim, address),
|
||||||
|
|
||||||
// Serial Communication 1
|
// Serial Communication 1
|
||||||
0x0400_0128 => util.io.read.todo(log, "Read {} from SIOCNT", .{T}),
|
0x0400_0128 => readTodo("Read {} from SIOCNT", .{T}),
|
||||||
|
|
||||||
// Keypad Input
|
// Keypad Input
|
||||||
0x0400_0130 => bus.io.keyinput.load(.Monotonic).raw,
|
0x0400_0130 => bus.io.keyinput.raw,
|
||||||
|
|
||||||
// Serial Communication 2
|
// Serial Communication 2
|
||||||
0x0400_0134 => util.io.read.todo(log, "Read {} from RCNT", .{T}),
|
0x0400_0134 => readTodo("Read {} from RCNT", .{T}),
|
||||||
0x0400_0136 => 0x0000,
|
|
||||||
0x0400_0142 => 0x0000,
|
|
||||||
0x0400_015A => 0x0000,
|
|
||||||
|
|
||||||
// Interrupts
|
// Interrupts
|
||||||
0x0400_0200 => bus.io.ie.raw,
|
0x0400_0200 => bus.io.ie.raw,
|
||||||
0x0400_0202 => bus.io.irq.raw,
|
0x0400_0202 => bus.io.irq.raw,
|
||||||
0x0400_0204 => bus.io.waitcnt.raw,
|
0x0400_0204 => readTodo("Read {} from WAITCNT", .{T}),
|
||||||
0x0400_0206 => 0x0000,
|
|
||||||
0x0400_0208 => @boolToInt(bus.io.ime),
|
0x0400_0208 => @boolToInt(bus.io.ime),
|
||||||
0x0400_020A => 0x0000,
|
else => readUndefined(log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
|
||||||
0x0400_0300 => @enumToInt(bus.io.postflg),
|
|
||||||
0x0400_0302 => 0x0000,
|
|
||||||
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
|
|
||||||
},
|
},
|
||||||
u8 => return switch (address) {
|
u8 => return switch (address) {
|
||||||
// Display
|
// Display
|
||||||
0x0400_0000...0x0400_0055 => ppu.read(T, &bus.ppu, address),
|
0x0400_0000 => @truncate(T, bus.ppu.dispcnt.raw),
|
||||||
|
0x0400_0004 => @truncate(T, bus.ppu.dispstat.raw),
|
||||||
|
0x0400_0005 => @truncate(T, bus.ppu.dispcnt.raw >> 8),
|
||||||
|
0x0400_0006 => @truncate(T, bus.ppu.vcount.raw),
|
||||||
|
0x0400_0008 => @truncate(T, bus.ppu.bg[0].cnt.raw),
|
||||||
|
0x0400_0009 => @truncate(T, bus.ppu.bg[0].cnt.raw >> 8),
|
||||||
|
0x0400_000A => @truncate(T, bus.ppu.bg[1].cnt.raw),
|
||||||
|
0x0400_000B => @truncate(T, bus.ppu.bg[1].cnt.raw >> 8),
|
||||||
|
|
||||||
// Sound
|
// Sound
|
||||||
0x0400_0060...0x0400_00A7 => apu.read(T, &bus.apu, address),
|
0x0400_0060...0x0400_00A7 => apu.read(T, &bus.apu, address),
|
||||||
|
|
||||||
// DMA Transfers
|
|
||||||
0x0400_00B0...0x0400_00DF => dma.read(T, &bus.dma, address),
|
|
||||||
|
|
||||||
// Timers
|
|
||||||
0x0400_0100...0x0400_010F => timer.read(T, &bus.tim, address),
|
|
||||||
|
|
||||||
// Serial Communication 1
|
// Serial Communication 1
|
||||||
0x0400_0128 => util.io.read.todo(log, "Read {} from SIOCNT_L", .{T}),
|
0x0400_0128 => readTodo("Read {} from SIOCNT_L", .{T}),
|
||||||
|
|
||||||
// Keypad Input
|
// Keypad Input
|
||||||
0x0400_0130 => util.io.read.todo(log, "read {} from KEYINPUT_L", .{T}),
|
0x0400_0130 => readTodo("read {} from KEYINPUT_L", .{T}),
|
||||||
|
|
||||||
// Serial Communication 2
|
// Serial Communication 2
|
||||||
0x0400_0135 => util.io.read.todo(log, "Read {} from RCNT_H", .{T}),
|
0x0400_0135 => readTodo("Read {} from RCNT_H", .{T}),
|
||||||
0x0400_0136, 0x0400_0137 => 0x00,
|
|
||||||
0x0400_0142, 0x0400_0143 => 0x00,
|
|
||||||
0x0400_015A, 0x0400_015B => 0x00,
|
|
||||||
|
|
||||||
// Interrupts
|
// Interrupts
|
||||||
0x0400_0200, 0x0400_0201 => @truncate(T, bus.io.ie.raw >> getHalf(@truncate(u8, address))),
|
0x0400_0200 => @truncate(T, bus.io.ie.raw),
|
||||||
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,
|
else => readUndefined(log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
|
||||||
0x0400_0302, 0x0400_0303 => 0x00,
|
|
||||||
else => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
|
|
||||||
},
|
},
|
||||||
else => @compileError("I/O: Unsupported read width"),
|
else => @compileError("I/O: Unsupported read width"),
|
||||||
};
|
};
|
||||||
@@ -156,7 +144,34 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
|
|||||||
return switch (T) {
|
return switch (T) {
|
||||||
u32 => switch (address) {
|
u32 => switch (address) {
|
||||||
// Display
|
// Display
|
||||||
0x0400_0000...0x0400_0054 => ppu.write(T, &bus.ppu, address, value),
|
0x0400_0000 => bus.ppu.dispcnt.raw = @truncate(u16, value),
|
||||||
|
0x0400_0004 => {
|
||||||
|
bus.ppu.dispstat.raw = @truncate(u16, value);
|
||||||
|
bus.ppu.vcount.raw = @truncate(u16, value >> 16);
|
||||||
|
},
|
||||||
|
0x0400_0008 => bus.ppu.setAdjCnts(0, value),
|
||||||
|
0x0400_000C => bus.ppu.setAdjCnts(2, value),
|
||||||
|
0x0400_0010 => bus.ppu.setBgOffsets(0, value),
|
||||||
|
0x0400_0014 => bus.ppu.setBgOffsets(1, value),
|
||||||
|
0x0400_0018 => bus.ppu.setBgOffsets(2, value),
|
||||||
|
0x0400_001C => bus.ppu.setBgOffsets(3, value),
|
||||||
|
0x0400_0020 => bus.ppu.aff_bg[0].writePaPb(value),
|
||||||
|
0x0400_0024 => bus.ppu.aff_bg[0].writePcPd(value),
|
||||||
|
0x0400_0028 => bus.ppu.aff_bg[0].setX(bus.ppu.dispstat.vblank.read(), value),
|
||||||
|
0x0400_002C => bus.ppu.aff_bg[0].setY(bus.ppu.dispstat.vblank.read(), value),
|
||||||
|
0x0400_0030 => bus.ppu.aff_bg[1].writePaPb(value),
|
||||||
|
0x0400_0034 => bus.ppu.aff_bg[1].writePcPd(value),
|
||||||
|
0x0400_0038 => bus.ppu.aff_bg[1].setX(bus.ppu.dispstat.vblank.read(), value),
|
||||||
|
0x0400_003C => bus.ppu.aff_bg[1].setY(bus.ppu.dispstat.vblank.read(), value),
|
||||||
|
0x0400_0040 => bus.ppu.win.setH(value),
|
||||||
|
0x0400_0044 => bus.ppu.win.setV(value),
|
||||||
|
0x0400_0048 => bus.ppu.win.setIo(value),
|
||||||
|
0x0400_004C => log.debug("Wrote 0x{X:0>8} to MOSAIC", .{value}),
|
||||||
|
0x0400_0050 => {
|
||||||
|
bus.ppu.bldcnt.raw = @truncate(u16, value);
|
||||||
|
bus.ppu.bldalpha.raw = @truncate(u16, value >> 16);
|
||||||
|
},
|
||||||
|
0x0400_0054 => bus.ppu.bldy.raw = @truncate(u16, value),
|
||||||
0x0400_0058...0x0400_005C => {}, // Unused
|
0x0400_0058...0x0400_005C => {}, // Unused
|
||||||
|
|
||||||
// Sound
|
// Sound
|
||||||
@@ -192,28 +207,65 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
|
|||||||
|
|
||||||
// Interrupts
|
// Interrupts
|
||||||
0x0400_0200 => bus.io.setIrqs(value),
|
0x0400_0200 => bus.io.setIrqs(value),
|
||||||
0x0400_0204 => bus.io.waitcnt.set(@truncate(u16, value)),
|
0x0400_0204 => log.debug("Wrote 0x{X:0>8} to WAITCNT", .{value}),
|
||||||
0x0400_0208 => bus.io.ime = value & 1 == 1,
|
0x0400_0208 => bus.io.ime = value & 1 == 1,
|
||||||
0x0400_0300 => {
|
0x0400_020C...0x0400_021C => {}, // Unused
|
||||||
bus.io.postflg = @intToEnum(PostFlag, value & 1);
|
else => writeUndefined(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, address }),
|
||||||
bus.io.haltcnt = if (value >> 15 & 1 == 0) .Halt else @panic("TODO: Implement STOP");
|
|
||||||
},
|
|
||||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, address }),
|
|
||||||
},
|
},
|
||||||
u16 => switch (address) {
|
u16 => switch (address) {
|
||||||
// Display
|
// Display
|
||||||
0x0400_0000...0x0400_0054 => ppu.write(T, &bus.ppu, address, value),
|
0x0400_0000 => bus.ppu.dispcnt.raw = value,
|
||||||
0x0400_0056 => {}, // Not used
|
0x0400_0004 => bus.ppu.dispstat.raw = value,
|
||||||
|
0x0400_0006 => {}, // vcount is read-only
|
||||||
|
0x0400_0008 => bus.ppu.bg[0].cnt.raw = value,
|
||||||
|
0x0400_000A => bus.ppu.bg[1].cnt.raw = value,
|
||||||
|
0x0400_000C => bus.ppu.bg[2].cnt.raw = value,
|
||||||
|
0x0400_000E => bus.ppu.bg[3].cnt.raw = value,
|
||||||
|
0x0400_0010 => bus.ppu.bg[0].hofs.raw = value, // TODO: Don't write out every HOFS / VOFS?
|
||||||
|
0x0400_0012 => bus.ppu.bg[0].vofs.raw = value,
|
||||||
|
0x0400_0014 => bus.ppu.bg[1].hofs.raw = value,
|
||||||
|
0x0400_0016 => bus.ppu.bg[1].vofs.raw = value,
|
||||||
|
0x0400_0018 => bus.ppu.bg[2].hofs.raw = value,
|
||||||
|
0x0400_001A => bus.ppu.bg[2].vofs.raw = value,
|
||||||
|
0x0400_001C => bus.ppu.bg[3].hofs.raw = value,
|
||||||
|
0x0400_001E => bus.ppu.bg[3].vofs.raw = value,
|
||||||
|
0x0400_0020 => bus.ppu.aff_bg[0].pa = @bitCast(i16, value),
|
||||||
|
0x0400_0022 => bus.ppu.aff_bg[0].pb = @bitCast(i16, value),
|
||||||
|
0x0400_0024 => bus.ppu.aff_bg[0].pc = @bitCast(i16, value),
|
||||||
|
0x0400_0026 => bus.ppu.aff_bg[0].pd = @bitCast(i16, value),
|
||||||
|
0x0400_0028 => bus.ppu.aff_bg[0].x = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[0].x) & 0xFFFF_0000 | value),
|
||||||
|
0x0400_002A => bus.ppu.aff_bg[0].x = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[0].x) & 0x0000_FFFF | (@as(u32, value) << 16)),
|
||||||
|
0x0400_002C => bus.ppu.aff_bg[0].y = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[0].y) & 0xFFFF_0000 | value),
|
||||||
|
0x0400_002E => bus.ppu.aff_bg[0].y = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[0].y) & 0x0000_FFFF | (@as(u32, value) << 16)),
|
||||||
|
0x0400_0030 => bus.ppu.aff_bg[1].pa = @bitCast(i16, value),
|
||||||
|
0x0400_0032 => bus.ppu.aff_bg[1].pb = @bitCast(i16, value),
|
||||||
|
0x0400_0034 => bus.ppu.aff_bg[1].pc = @bitCast(i16, value),
|
||||||
|
0x0400_0036 => bus.ppu.aff_bg[1].pd = @bitCast(i16, value),
|
||||||
|
0x0400_0038 => bus.ppu.aff_bg[1].x = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[1].x) & 0xFFFF_0000 | value),
|
||||||
|
0x0400_003A => bus.ppu.aff_bg[1].x = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[1].x) & 0x0000_FFFF | (@as(u32, value) << 16)),
|
||||||
|
0x0400_003C => bus.ppu.aff_bg[1].y = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[1].y) & 0xFFFF_0000 | value),
|
||||||
|
0x0400_003E => bus.ppu.aff_bg[1].y = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[1].y) & 0x0000_FFFF | (@as(u32, value) << 16)),
|
||||||
|
0x0400_0040 => bus.ppu.win.h[0].raw = value,
|
||||||
|
0x0400_0042 => bus.ppu.win.h[1].raw = value,
|
||||||
|
0x0400_0044 => bus.ppu.win.v[0].raw = value,
|
||||||
|
0x0400_0046 => bus.ppu.win.v[1].raw = value,
|
||||||
|
0x0400_0048 => bus.ppu.win.in.raw = value,
|
||||||
|
0x0400_004A => bus.ppu.win.out.raw = value,
|
||||||
|
0x0400_004C => log.debug("Wrote 0x{X:0>4} to MOSAIC", .{value}),
|
||||||
|
0x0400_0050 => bus.ppu.bldcnt.raw = value,
|
||||||
|
0x0400_0052 => bus.ppu.bldalpha.raw = value,
|
||||||
|
0x0400_0054 => bus.ppu.bldy.raw = value,
|
||||||
|
0x0400_004E, 0x0400_0056 => {}, // Not used
|
||||||
|
|
||||||
// Sound
|
// Sound
|
||||||
0x0400_0060...0x0400_00A6 => apu.write(T, &bus.apu, address, value),
|
0x0400_0060...0x0400_009E => apu.write(T, &bus.apu, address, value),
|
||||||
|
|
||||||
// Dma Transfers
|
// Dma Transfers
|
||||||
0x0400_00B0...0x0400_00DE => dma.write(T, &bus.dma, address, value),
|
0x0400_00B0...0x0400_00DE => dma.write(T, &bus.dma, address, value),
|
||||||
|
|
||||||
// Timers
|
// Timers
|
||||||
0x0400_0100...0x0400_010E => timer.write(T, &bus.tim, address, value),
|
0x0400_0100...0x0400_010E => timer.write(T, &bus.tim, address, value),
|
||||||
0x0400_0114 => {},
|
0x0400_0114 => {}, // TODO: Gyakuten Saiban writes 0x8000 to 0x0400_0114
|
||||||
0x0400_0110 => {}, // Not Used,
|
0x0400_0110 => {}, // Not Used,
|
||||||
|
|
||||||
// Serial Communication 1
|
// Serial Communication 1
|
||||||
@@ -237,29 +289,27 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
|
|||||||
// Interrupts
|
// Interrupts
|
||||||
0x0400_0200 => bus.io.ie.raw = value,
|
0x0400_0200 => bus.io.ie.raw = value,
|
||||||
0x0400_0202 => bus.io.irq.raw &= ~value,
|
0x0400_0202 => bus.io.irq.raw &= ~value,
|
||||||
0x0400_0204 => bus.io.waitcnt.set(value),
|
0x0400_0204 => log.debug("Wrote 0x{X:0>4} to WAITCNT", .{value}),
|
||||||
0x0400_0206 => {},
|
|
||||||
0x0400_0208 => bus.io.ime = value & 1 == 1,
|
0x0400_0208 => bus.io.ime = value & 1 == 1,
|
||||||
0x0400_020A => {},
|
0x0400_0206, 0x0400_020A => {}, // Not Used
|
||||||
0x0400_0300 => {
|
else => writeUndefined(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, address }),
|
||||||
bus.io.postflg = @intToEnum(PostFlag, value & 1);
|
|
||||||
bus.io.haltcnt = if (value >> 15 & 1 == 0) .Halt else @panic("TODO: Implement STOP");
|
|
||||||
},
|
|
||||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, address }),
|
|
||||||
},
|
},
|
||||||
u8 => switch (address) {
|
u8 => switch (address) {
|
||||||
// Display
|
// Display
|
||||||
0x0400_0000...0x0400_0055 => ppu.write(T, &bus.ppu, address, value),
|
0x0400_0004 => bus.ppu.dispstat.raw = (bus.ppu.dispstat.raw & 0xFF00) | value,
|
||||||
|
0x0400_0005 => bus.ppu.dispstat.raw = (@as(u16, value) << 8) | (bus.ppu.dispstat.raw & 0xFF),
|
||||||
|
0x0400_0008 => bus.ppu.bg[0].cnt.raw = (bus.ppu.bg[0].cnt.raw & 0xFF00) | value,
|
||||||
|
0x0400_0009 => bus.ppu.bg[0].cnt.raw = (@as(u16, value) << 8) | (bus.ppu.bg[0].cnt.raw & 0xFF),
|
||||||
|
0x0400_000A => bus.ppu.bg[1].cnt.raw = (bus.ppu.bg[1].cnt.raw & 0xFF00) | value,
|
||||||
|
0x0400_000B => bus.ppu.bg[1].cnt.raw = (@as(u16, value) << 8) | (bus.ppu.bg[1].cnt.raw & 0xFF),
|
||||||
|
0x0400_0048 => bus.ppu.win.setInL(value),
|
||||||
|
0x0400_0049 => bus.ppu.win.setInH(value),
|
||||||
|
0x0400_004A => bus.ppu.win.setOutL(value),
|
||||||
|
0x0400_0054 => bus.ppu.bldy.raw = (bus.ppu.bldy.raw & 0xFF00) | value,
|
||||||
|
|
||||||
// Sound
|
// 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}),
|
||||||
@@ -269,25 +319,23 @@ 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_0209 => {},
|
0x0400_0300 => bus.io.postflg = std.meta.intToEnum(PostFlag, value & 1) catch unreachable,
|
||||||
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 }),
|
||||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, address }),
|
else => writeUndefined(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, address }),
|
||||||
},
|
},
|
||||||
else => @compileError("I/O: Unsupported write width"),
|
else => @compileError("I/O: Unsupported write width"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn readTodo(comptime format: []const u8, args: anytype) u8 {
|
||||||
|
log.debug(format, args);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/// Read / Write
|
/// Read / Write
|
||||||
pub const PostFlag = enum(u1) {
|
pub const PostFlag = enum(u1) {
|
||||||
FirstBoot = 0,
|
FirstBoot = 0,
|
||||||
@@ -317,22 +365,14 @@ pub const DisplayControl = extern union {
|
|||||||
|
|
||||||
/// Read / Write
|
/// Read / Write
|
||||||
pub const DisplayStatus = extern union {
|
pub const DisplayStatus = extern union {
|
||||||
/// read-only
|
|
||||||
vblank: Bit(u16, 0),
|
vblank: Bit(u16, 0),
|
||||||
/// read-only
|
|
||||||
hblank: Bit(u16, 1),
|
hblank: Bit(u16, 1),
|
||||||
// read-only
|
|
||||||
coincidence: Bit(u16, 2),
|
coincidence: Bit(u16, 2),
|
||||||
vblank_irq: Bit(u16, 3),
|
vblank_irq: Bit(u16, 3),
|
||||||
hblank_irq: Bit(u16, 4),
|
hblank_irq: Bit(u16, 4),
|
||||||
vcount_irq: Bit(u16, 5),
|
vcount_irq: Bit(u16, 5),
|
||||||
vcount_trigger: Bitfield(u16, 8, 8),
|
vcount_trigger: Bitfield(u16, 8, 8),
|
||||||
raw: u16,
|
raw: u16,
|
||||||
|
|
||||||
pub fn set(self: *DisplayStatus, value: u16) void {
|
|
||||||
const mask: u16 = 0x00C7; // set bits are read-only
|
|
||||||
self.raw = (self.raw & mask) | (value & ~mask);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Read Only
|
/// Read Only
|
||||||
@@ -376,31 +416,6 @@ 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),
|
||||||
@@ -648,24 +663,3 @@ pub const SoundBias = extern union {
|
|||||||
sampling_cycle: Bitfield(u16, 14, 2),
|
sampling_cycle: Bitfield(u16, 14, 2),
|
||||||
raw: u16,
|
raw: u16,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Read / Write
|
|
||||||
pub const WaitControl = extern union {
|
|
||||||
sram_cnt: Bitfield(u16, 0, 2),
|
|
||||||
s0_first: Bitfield(u16, 2, 2),
|
|
||||||
s0_second: Bit(u16, 4),
|
|
||||||
s1_first: Bitfield(u16, 5, 2),
|
|
||||||
s1_second: Bit(u16, 7),
|
|
||||||
s2_first: Bitfield(u16, 8, 2),
|
|
||||||
s2_second: Bit(u16, 10),
|
|
||||||
phi_out: Bitfield(u16, 11, 2),
|
|
||||||
|
|
||||||
prefetch_enable: Bit(u16, 14),
|
|
||||||
pak_kind: Bit(u16, 15),
|
|
||||||
raw: u16,
|
|
||||||
|
|
||||||
pub fn set(self: *WaitControl, value: u16) void {
|
|
||||||
const mask: u16 = 0x8000; // set bits are read-only
|
|
||||||
self.raw = (self.raw & mask) | (value & ~mask);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,100 +1,70 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
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 = struct { Timer(0), Timer(1), Timer(2), Timer(3) };
|
const readUndefined = @import("../util.zig").readUndefined;
|
||||||
|
const writeUndefined = @import("../util.zig").writeUndefined;
|
||||||
|
pub const TimerTuple = std.meta.Tuple(&[_]type{ Timer(0), Timer(1), Timer(2), Timer(3) });
|
||||||
const log = std.log.scoped(.Timer);
|
const log = std.log.scoped(.Timer);
|
||||||
|
|
||||||
const getHalf = util.getHalf;
|
|
||||||
const setHalf = util.setHalf;
|
|
||||||
|
|
||||||
pub fn create(sched: *Scheduler) TimerTuple {
|
pub fn create(sched: *Scheduler) TimerTuple {
|
||||||
return .{ Timer(0).init(sched), Timer(1).init(sched), Timer(2).init(sched), Timer(3).init(sched) };
|
return .{ Timer(0).init(sched), Timer(1).init(sched), Timer(2).init(sched), Timer(3).init(sched) };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read(comptime T: type, tim: *const TimerTuple, addr: u32) ?T {
|
pub fn read(comptime T: type, tim: *const TimerTuple, addr: u32) T {
|
||||||
const nybble_addr = @truncate(u4, addr);
|
const nybble = @truncate(u4, addr);
|
||||||
|
|
||||||
return switch (T) {
|
return switch (T) {
|
||||||
u32 => switch (nybble_addr) {
|
u32 => switch (nybble) {
|
||||||
0x0 => @as(T, tim.*[0].cnt.raw) << 16 | tim.*[0].timcntL(),
|
0x0 => @as(T, tim.*[0].cnt.raw) << 16 | tim.*[0].getCntL(),
|
||||||
0x4 => @as(T, tim.*[1].cnt.raw) << 16 | tim.*[1].timcntL(),
|
0x4 => @as(T, tim.*[1].cnt.raw) << 16 | tim.*[1].getCntL(),
|
||||||
0x8 => @as(T, tim.*[2].cnt.raw) << 16 | tim.*[2].timcntL(),
|
0x8 => @as(T, tim.*[2].cnt.raw) << 16 | tim.*[2].getCntL(),
|
||||||
0xC => @as(T, tim.*[3].cnt.raw) << 16 | tim.*[3].timcntL(),
|
0xC => @as(T, tim.*[3].cnt.raw) << 16 | tim.*[3].getCntL(),
|
||||||
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
else => readUndefined(log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
||||||
},
|
},
|
||||||
u16 => switch (nybble_addr) {
|
u16 => switch (nybble) {
|
||||||
0x0 => tim.*[0].timcntL(),
|
0x0 => tim.*[0].getCntL(),
|
||||||
0x2 => tim.*[0].cnt.raw,
|
0x2 => tim.*[0].cnt.raw,
|
||||||
|
0x4 => tim.*[1].getCntL(),
|
||||||
0x4 => tim.*[1].timcntL(),
|
|
||||||
0x6 => tim.*[1].cnt.raw,
|
0x6 => tim.*[1].cnt.raw,
|
||||||
|
0x8 => tim.*[2].getCntL(),
|
||||||
0x8 => tim.*[2].timcntL(),
|
|
||||||
0xA => tim.*[2].cnt.raw,
|
0xA => tim.*[2].cnt.raw,
|
||||||
|
0xC => tim.*[3].getCntL(),
|
||||||
0xC => tim.*[3].timcntL(),
|
|
||||||
0xE => tim.*[3].cnt.raw,
|
0xE => tim.*[3].cnt.raw,
|
||||||
else => util.io.read.err(T, log, "unaligned {} read from 0x{X:0>8}", .{ T, addr }),
|
else => readUndefined(log, "Tried to perform a {} read to 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 => readUndefined(log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
||||||
else => @compileError("TIM: Unsupported read width"),
|
else => @compileError("TIM: Unsupported read width"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write(comptime T: type, tim: *TimerTuple, addr: u32, value: T) void {
|
pub fn write(comptime T: type, tim: *TimerTuple, addr: u32, value: T) void {
|
||||||
const nybble_addr = @truncate(u4, addr);
|
const nybble = @truncate(u4, addr);
|
||||||
|
|
||||||
return switch (T) {
|
return switch (T) {
|
||||||
u32 => switch (nybble_addr) {
|
u32 => switch (nybble) {
|
||||||
0x0 => tim.*[0].setTimcnt(value),
|
0x0 => tim.*[0].setCnt(value),
|
||||||
0x4 => tim.*[1].setTimcnt(value),
|
0x4 => tim.*[1].setCnt(value),
|
||||||
0x8 => tim.*[2].setTimcnt(value),
|
0x8 => tim.*[2].setCnt(value),
|
||||||
0xC => tim.*[3].setTimcnt(value),
|
0xC => tim.*[3].setCnt(value),
|
||||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
|
else => writeUndefined(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||||
},
|
},
|
||||||
u16 => switch (nybble_addr) {
|
u16 => switch (nybble) {
|
||||||
0x0 => tim.*[0].setTimcntL(value),
|
0x0 => tim.*[0].setCntL(value),
|
||||||
0x2 => tim.*[0].setTimcntH(value),
|
0x2 => tim.*[0].setCntH(value),
|
||||||
|
0x4 => tim.*[1].setCntL(value),
|
||||||
0x4 => tim.*[1].setTimcntL(value),
|
0x6 => tim.*[1].setCntH(value),
|
||||||
0x6 => tim.*[1].setTimcntH(value),
|
0x8 => tim.*[2].setCntL(value),
|
||||||
|
0xA => tim.*[2].setCntH(value),
|
||||||
0x8 => tim.*[2].setTimcntL(value),
|
0xC => tim.*[3].setCntL(value),
|
||||||
0xA => tim.*[2].setTimcntH(value),
|
0xE => tim.*[3].setCntH(value),
|
||||||
|
else => writeUndefined(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||||
0xC => tim.*[3].setTimcntL(value),
|
|
||||||
0xE => tim.*[3].setTimcntH(value),
|
|
||||||
else => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
|
|
||||||
},
|
|
||||||
u8 => switch (nybble_addr) {
|
|
||||||
0x0, 0x1 => tim.*[0].setTimcntL(setHalf(u16, tim.*[0]._reload, nybble_addr, value)),
|
|
||||||
0x2, 0x3 => tim.*[0].setTimcntH(setHalf(u16, tim.*[0].cnt.raw, nybble_addr, value)),
|
|
||||||
|
|
||||||
0x4, 0x5 => tim.*[1].setTimcntL(setHalf(u16, tim.*[1]._reload, nybble_addr, value)),
|
|
||||||
0x6, 0x7 => tim.*[1].setTimcntH(setHalf(u16, tim.*[1].cnt.raw, nybble_addr, value)),
|
|
||||||
|
|
||||||
0x8, 0x9 => tim.*[2].setTimcntL(setHalf(u16, tim.*[2]._reload, nybble_addr, value)),
|
|
||||||
0xA, 0xB => tim.*[2].setTimcntH(setHalf(u16, tim.*[2].cnt.raw, nybble_addr, value)),
|
|
||||||
|
|
||||||
0xC, 0xD => tim.*[3].setTimcntL(setHalf(u16, tim.*[3]._reload, nybble_addr, value)),
|
|
||||||
0xE, 0xF => tim.*[3].setTimcntH(setHalf(u16, tim.*[3].cnt.raw, nybble_addr, value)),
|
|
||||||
},
|
},
|
||||||
|
u8 => writeUndefined(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||||
else => @compileError("TIM: Unsupported write width"),
|
else => @compileError("TIM: Unsupported write width"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -103,13 +73,13 @@ fn Timer(comptime id: u2) type {
|
|||||||
return struct {
|
return struct {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
/// Read Only, Internal. Please use self.timcntL()
|
/// Read Only, Internal. Please use self.getCntL()
|
||||||
_counter: u16,
|
_counter: u16,
|
||||||
|
|
||||||
/// Write Only, Internal. Please use self.setTimcntL()
|
/// Write Only, Internal. Please use self.setCntL()
|
||||||
_reload: u16,
|
_reload: u16,
|
||||||
|
|
||||||
/// Write Only, Internal. Please use self.setTimcntH()
|
/// Write Only, Internal. Please use self.setCntH()
|
||||||
cnt: TimerControl,
|
cnt: TimerControl,
|
||||||
|
|
||||||
/// Internal.
|
/// Internal.
|
||||||
@@ -128,62 +98,47 @@ fn Timer(comptime id: u2) type {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TIMCNT_L Getter
|
/// TIMCNT_L
|
||||||
pub fn timcntL(self: *const Self) u16 {
|
pub fn getCntL(self: *const Self) u16 {
|
||||||
if (self.cnt.cascade.read() or !self.cnt.enabled.read()) return self._counter;
|
if (self.cnt.cascade.read() or !self.cnt.enabled.read()) return self._counter;
|
||||||
|
|
||||||
return self._counter +% @truncate(u16, (self.sched.now() - self._start_timestamp) / self.frequency());
|
return self._counter +% @truncate(u16, (self.sched.now() - self._start_timestamp) / self.frequency());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TIMCNT_L Setter
|
/// TIMCNT_L
|
||||||
pub fn setTimcntL(self: *Self, halfword: u16) void {
|
pub fn setCntL(self: *Self, halfword: u16) void {
|
||||||
self._reload = halfword;
|
self._reload = halfword;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TIMCNT_L & TIMCNT_H
|
/// TIMCNT_L & TIMCNT_H
|
||||||
pub fn setTimcnt(self: *Self, word: u32) void {
|
pub fn setCnt(self: *Self, word: u32) void {
|
||||||
self.setTimcntL(@truncate(u16, word));
|
self.setCntL(@truncate(u16, word));
|
||||||
self.setTimcntH(@truncate(u16, word >> 16));
|
self.setCntH(@truncate(u16, word >> 16));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TIMCNT_H
|
/// TIMCNT_H
|
||||||
pub fn setTimcntH(self: *Self, halfword: u16) void {
|
pub fn setCntH(self: *Self, halfword: u16) void {
|
||||||
const new = TimerControl{ .raw = halfword };
|
const new = TimerControl{ .raw = halfword };
|
||||||
|
|
||||||
if (self.cnt.enabled.read()) {
|
// If Timer happens to be enabled, It will either be resheduled or disabled
|
||||||
// timer was already enabled
|
|
||||||
|
|
||||||
// If enabled falling edge or cascade falling edge, timer is paused
|
|
||||||
if (!new.enabled.read() or (!self.cnt.cascade.read() and new.cascade.read())) {
|
|
||||||
self.sched.removeScheduledEvent(.{ .TimerOverflow = id });
|
self.sched.removeScheduledEvent(.{ .TimerOverflow = id });
|
||||||
|
|
||||||
// Counter should hold the value it stopped at meaning we have to calculate it now
|
if (self.cnt.enabled.read() and (new.cascade.read() or !new.enabled.read())) {
|
||||||
|
// Either through the cascade bit or the enable bit, the timer has effectively been disabled
|
||||||
|
// The Counter should hold whatever value it should have been at when it was disabled
|
||||||
self._counter +%= @truncate(u16, (self.sched.now() - self._start_timestamp) / self.frequency());
|
self._counter +%= @truncate(u16, (self.sched.now() - self._start_timestamp) / self.frequency());
|
||||||
}
|
}
|
||||||
|
|
||||||
// the timer has always been enabled, but the cascade bit which was blocking the timer has been unset
|
// The counter is only reloaded on the rising edge of the enable bit
|
||||||
if (new.enabled.read() and (self.cnt.cascade.read() and !new.cascade.read())) {
|
if (!self.cnt.enabled.read() and new.enabled.read()) self._counter = self._reload;
|
||||||
// we want to reschedule the timer event, however we won't reload the counter.
|
|
||||||
// the invariant here is that self._counter holds the already calculated paused value
|
|
||||||
|
|
||||||
self.rescheduleTimerExpire(0);
|
// If Timer is enabled and we're not cascading, we need to schedule an overflow event
|
||||||
}
|
if (new.enabled.read() and !new.cascade.read()) self.scheduleOverflow(0);
|
||||||
} else {
|
|
||||||
// the timer was previously disabeld
|
|
||||||
|
|
||||||
if (new.enabled.read()) {
|
|
||||||
// timer should start counting (with a reloaded counter value)
|
|
||||||
self._counter = self._reload;
|
|
||||||
|
|
||||||
// if cascade happens to be set, the timer doesn't actually do anything though
|
|
||||||
if (!new.cascade.read()) self.rescheduleTimerExpire(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.cnt.raw = halfword;
|
self.cnt.raw = halfword;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn onTimerExpire(self: *Self, cpu: *Arm7tdmi, late: u64) void {
|
pub fn handleOverflow(self: *Self, cpu: *Arm7tdmi, late: u64) void {
|
||||||
// Fire IRQ if enabled
|
// Fire IRQ if enabled
|
||||||
const io = &cpu.bus.io;
|
const io = &cpu.bus.io;
|
||||||
|
|
||||||
@@ -200,31 +155,34 @@ fn Timer(comptime id: u2) type {
|
|||||||
|
|
||||||
// DMA Sound Things
|
// DMA Sound Things
|
||||||
if (id == 0 or id == 1) {
|
if (id == 0 or id == 1) {
|
||||||
cpu.bus.apu.onDmaAudioSampleRequest(cpu, id);
|
cpu.bus.apu.handleTimerOverflow(cpu, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform Cascade Behaviour
|
// Perform Cascade Behaviour
|
||||||
switch (id) {
|
switch (id) {
|
||||||
inline 0, 1, 2 => |idx| {
|
0 => if (cpu.bus.tim[1].cnt.cascade.read()) {
|
||||||
const next = idx + 1;
|
cpu.bus.tim[1]._counter +%= 1;
|
||||||
|
if (cpu.bus.tim[1]._counter == 0) cpu.bus.tim[1].handleOverflow(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);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
3 => {}, // THere is no timer for TIM3 to cascade to
|
1 => if (cpu.bus.tim[2].cnt.cascade.read()) {
|
||||||
|
cpu.bus.tim[2]._counter +%= 1;
|
||||||
|
if (cpu.bus.tim[2]._counter == 0) cpu.bus.tim[2].handleOverflow(cpu, late);
|
||||||
|
},
|
||||||
|
2 => if (cpu.bus.tim[3].cnt.cascade.read()) {
|
||||||
|
cpu.bus.tim[3]._counter +%= 1;
|
||||||
|
if (cpu.bus.tim[3]._counter == 0) cpu.bus.tim[3].handleOverflow(cpu, late);
|
||||||
|
},
|
||||||
|
3 => {}, // There is no Timer for TIM3 to "cascade" to,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reschedule Timer if we're not cascading
|
// Reschedule Timer if we're not cascading
|
||||||
// TIM0 cascade value is N/A
|
if (!self.cnt.cascade.read()) {
|
||||||
if (id == 0 or !self.cnt.cascade.read()) {
|
|
||||||
self._counter = self._reload;
|
self._counter = self._reload;
|
||||||
self.rescheduleTimerExpire(late);
|
self.scheduleOverflow(late);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rescheduleTimerExpire(self: *Self, late: u64) void {
|
fn scheduleOverflow(self: *Self, late: u64) void {
|
||||||
const when = (@as(u64, 0x10000) - self._counter) * self.frequency();
|
const when = (@as(u64, 0x10000) - self._counter) * self.frequency();
|
||||||
|
|
||||||
self._start_timestamp = self.sched.now();
|
self._start_timestamp = self.sched.now();
|
||||||
|
|||||||
437
src/core/cpu.zig
437
src/core/cpu.zig
@@ -1,17 +1,18 @@
|
|||||||
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 Logger = @import("../util.zig").Logger;
|
const FilePaths = @import("util.zig").FilePaths;
|
||||||
|
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 {
|
||||||
pub const InstrFn = *const fn (*Arm7tdmi, *Bus, u32) void;
|
pub const InstrFn = fn (*Arm7tdmi, *Bus, u32) void;
|
||||||
const lut: [0x1000]InstrFn = populate();
|
const lut: [0x1000]InstrFn = populate();
|
||||||
|
|
||||||
const processing = @import("cpu/arm/data_processing.zig").dataProcessing;
|
const processing = @import("cpu/arm/data_processing.zig").dataProcessing;
|
||||||
@@ -27,14 +28,9 @@ pub const arm = struct {
|
|||||||
const multiply = @import("cpu/arm/multiply.zig").multiply;
|
const multiply = @import("cpu/arm/multiply.zig").multiply;
|
||||||
const multiplyLong = @import("cpu/arm/multiply.zig").multiplyLong;
|
const multiplyLong = @import("cpu/arm/multiply.zig").multiplyLong;
|
||||||
|
|
||||||
/// Determine index into ARM InstrFn LUT
|
|
||||||
fn idx(opcode: u32) u12 {
|
|
||||||
return @truncate(u12, opcode >> 20 & 0xFF) << 4 | @truncate(u12, opcode >> 4 & 0xF);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Undefined ARM Instruction handler
|
// Undefined ARM Instruction handler
|
||||||
fn und(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
fn und(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
||||||
const id = idx(opcode);
|
const id = armIdx(opcode);
|
||||||
cpu.panic("[CPU/Decode] ID: 0x{X:0>3} 0x{X:0>8} is an illegal opcode", .{ id, opcode });
|
cpu.panic("[CPU/Decode] ID: 0x{X:0>3} 0x{X:0>8} is an illegal opcode", .{ id, opcode });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +110,7 @@ pub const arm = struct {
|
|||||||
|
|
||||||
// THUMB Instructions
|
// THUMB Instructions
|
||||||
pub const thumb = struct {
|
pub const thumb = struct {
|
||||||
pub const InstrFn = *const fn (*Arm7tdmi, *Bus, u16) void;
|
pub const InstrFn = fn (*Arm7tdmi, *Bus, u16) void;
|
||||||
const lut: [0x400]InstrFn = populate();
|
const lut: [0x400]InstrFn = populate();
|
||||||
|
|
||||||
const processing = @import("cpu/thumb/data_processing.zig");
|
const processing = @import("cpu/thumb/data_processing.zig");
|
||||||
@@ -124,14 +120,9 @@ pub const thumb = struct {
|
|||||||
const swi = @import("cpu/thumb/software_interrupt.zig").fmt17;
|
const swi = @import("cpu/thumb/software_interrupt.zig").fmt17;
|
||||||
const branch = @import("cpu/thumb/branch.zig");
|
const branch = @import("cpu/thumb/branch.zig");
|
||||||
|
|
||||||
/// Determine index into THUMB InstrFn LUT
|
|
||||||
fn idx(opcode: u16) u10 {
|
|
||||||
return @truncate(u10, opcode >> 6);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Undefined THUMB Instruction Handler
|
/// Undefined THUMB Instruction Handler
|
||||||
fn und(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
fn und(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||||
const id = idx(opcode);
|
const id = thumbIdx(opcode);
|
||||||
cpu.panic("[CPU/Decode] ID: 0b{b:0>10} 0x{X:0>2} is an illegal opcode", .{ id, opcode });
|
cpu.panic("[CPU/Decode] ID: 0b{b:0>10} 0x{X:0>2} is an illegal opcode", .{ id, opcode });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,46 +226,51 @@ pub const thumb = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cpu_logging = @import("emu.zig").cpu_logging;
|
||||||
|
const log = std.log.scoped(.Arm7Tdmi);
|
||||||
|
|
||||||
pub const Arm7tdmi = struct {
|
pub const Arm7tdmi = struct {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
r: [16]u32,
|
r: [16]u32,
|
||||||
pipe: Pipeline,
|
pipe: Pipline,
|
||||||
sched: *Scheduler,
|
sched: *Scheduler,
|
||||||
bus: *Bus,
|
bus: *Bus,
|
||||||
cpsr: PSR,
|
cpsr: PSR,
|
||||||
spsr: PSR,
|
spsr: PSR,
|
||||||
|
|
||||||
bank: Bank,
|
/// Storage for R8_fiq -> R12_fiq and their normal counterparts
|
||||||
|
/// e.g [r[0 + 8], fiq_r[0 + 8], r[1 + 8], fiq_r[1 + 8]...]
|
||||||
|
banked_fiq: [2 * 5]u32,
|
||||||
|
|
||||||
|
/// Storage for r13_<mode>, r14_<mode>
|
||||||
|
/// e.g. [r13, r14, r13_svc, r14_svc]
|
||||||
|
banked_r: [2 * 6]u32,
|
||||||
|
|
||||||
|
banked_spsr: [5]PSR,
|
||||||
|
|
||||||
logger: ?Logger,
|
logger: ?Logger,
|
||||||
|
|
||||||
/// Bank of Registers from other CPU Modes
|
pub fn init(sched: *Scheduler, bus: *Bus) Self {
|
||||||
const Bank = struct {
|
return Self{
|
||||||
/// Storage for r13_<mode>, r14_<mode>
|
.r = [_]u32{0x00} ** 16,
|
||||||
/// e.g. [r13, r14, r13_svc, r14_svc]
|
.pipe = Pipline.init(),
|
||||||
r: [2 * 6]u32,
|
.sched = sched,
|
||||||
|
.bus = bus,
|
||||||
/// Storage for R8_fiq -> R12_fiq and their normal counterparts
|
.cpsr = .{ .raw = 0x0000_001F },
|
||||||
/// e.g [r[0 + 8], fiq_r[0 + 8], r[1 + 8], fiq_r[1 + 8]...]
|
.spsr = .{ .raw = 0x0000_0000 },
|
||||||
fiq: [2 * 5]u32,
|
.banked_fiq = [_]u32{0x00} ** 10,
|
||||||
|
.banked_r = [_]u32{0x00} ** 12,
|
||||||
spsr: [5]PSR,
|
.banked_spsr = [_]PSR{.{ .raw = 0x0000_0000 }} ** 5,
|
||||||
|
.logger = null,
|
||||||
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 {
|
pub fn attach(self: *Self, log_file: std.fs.File) void {
|
||||||
|
self.logger = Logger.init(log_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn bankedIdx(mode: Mode, kind: BankedKind) usize {
|
||||||
const idx: usize = switch (mode) {
|
const idx: usize = switch (mode) {
|
||||||
.User, .System => 0,
|
.User, .System => 0,
|
||||||
.Supervisor => 1,
|
.Supervisor => 1,
|
||||||
@@ -287,7 +283,7 @@ pub const Arm7tdmi = struct {
|
|||||||
return (idx * 2) + if (kind == .R14) @as(usize, 1) else 0;
|
return (idx * 2) + if (kind == .R14) @as(usize, 1) else 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fn spsrIdx(mode: Mode) usize {
|
inline fn bankedSpsrIndex(mode: Mode) usize {
|
||||||
return switch (mode) {
|
return switch (mode) {
|
||||||
.Supervisor => 0,
|
.Supervisor => 0,
|
||||||
.Abort => 1,
|
.Abort => 1,
|
||||||
@@ -298,23 +294,9 @@ pub const Arm7tdmi = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fn fiqIdx(i: usize, mode: Mode) usize {
|
inline fn bankedFiqIdx(i: usize, mode: Mode) usize {
|
||||||
return (i * 2) + if (mode == .Fiq) @as(usize, 1) else 0;
|
return (i * 2) + if (mode == .Fiq) @as(usize, 1) else 0;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(sched: *Scheduler, bus: *Bus, log_file: ?std.fs.File) Self {
|
|
||||||
return Self{
|
|
||||||
.r = [_]u32{0x00} ** 16,
|
|
||||||
.pipe = Pipeline.init(),
|
|
||||||
.sched = sched,
|
|
||||||
.bus = bus,
|
|
||||||
.cpsr = .{ .raw = 0x0000_001F },
|
|
||||||
.spsr = .{ .raw = 0x0000_0000 },
|
|
||||||
.bank = Bank.create(),
|
|
||||||
.logger = if (log_file) |file| Logger.init(file) else null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub inline fn hasSPSR(self: *const Self) bool {
|
pub inline fn hasSPSR(self: *const Self) bool {
|
||||||
const mode = getModeChecked(self, self.cpsr.mode.read());
|
const mode = getModeChecked(self, self.cpsr.mode.read());
|
||||||
@@ -336,8 +318,21 @@ pub const Arm7tdmi = struct {
|
|||||||
return self.bus.io.haltcnt == .Halt;
|
return self.bus.io.haltcnt == .Halt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn setCpsrNoFlush(self: *Self, value: u32) void {
|
||||||
|
if (value & 0x1F != self.cpsr.raw & 0x1F) self.changeModeFromIdx(@truncate(u5, value & 0x1F));
|
||||||
|
self.cpsr.raw = value;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn setCpsr(self: *Self, value: u32) void {
|
pub fn setCpsr(self: *Self, value: u32) void {
|
||||||
if (value & 0x1F != self.cpsr.raw & 0x1F) self.changeModeFromIdx(@truncate(u5, value & 0x1F));
|
if (value & 0x1F != self.cpsr.raw & 0x1F) self.changeModeFromIdx(@truncate(u5, value & 0x1F));
|
||||||
|
|
||||||
|
const new: PSR = .{ .raw = value };
|
||||||
|
if (self.cpsr.t.read() != new.t.read()) {
|
||||||
|
// If THUMB to ARM or ARM to THUMB, flush pipeline
|
||||||
|
self.r[15] &= if (new.t.read()) ~@as(u32, 1) else ~@as(u32, 3);
|
||||||
|
self.pipe.flush();
|
||||||
|
}
|
||||||
|
|
||||||
self.cpsr.raw = value;
|
self.cpsr.raw = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,14 +346,14 @@ pub const Arm7tdmi = struct {
|
|||||||
switch (idx) {
|
switch (idx) {
|
||||||
8...12 => {
|
8...12 => {
|
||||||
if (current == .Fiq) {
|
if (current == .Fiq) {
|
||||||
self.bank.fiq[Bank.fiqIdx(idx - 8, .User)] = value;
|
self.banked_fiq[bankedFiqIdx(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(Bank.Kind, idx - 13) catch unreachable;
|
const kind = std.meta.intToEnum(BankedKind, idx - 13) catch unreachable;
|
||||||
self.bank.r[Bank.regIdx(.User, kind)] = value;
|
self.banked_r[bankedIdx(.User, kind)] = value;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
else => self.r[idx] = value, // R0 -> R7 and R15
|
else => self.r[idx] = value, // R0 -> R7 and R15
|
||||||
@@ -369,12 +364,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.bank.fiq[Bank.fiqIdx(idx - 8, .User)] else self.r[idx],
|
8...12 => if (current == .Fiq) self.banked_fiq[bankedFiqIdx(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(Bank.Kind, idx - 13) catch unreachable;
|
const kind = std.meta.intToEnum(BankedKind, idx - 13) catch unreachable;
|
||||||
break :blk self.bank.r[Bank.regIdx(.User, kind)];
|
break :blk self.banked_r[bankedIdx(.User, kind)];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
else => self.r[idx], // R0 -> R7 and R15
|
else => self.r[idx], // R0 -> R7 and R15
|
||||||
@@ -387,95 +382,101 @@ 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.bank.fiq[Bank.fiqIdx(i, now)] = self.r[8 + i];
|
self.banked_fiq[bankedFiqIdx(i, now)] = self.r[8 + i];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bank r13, r14, SPSR
|
// Bank r13, r14, SPSR
|
||||||
switch (now) {
|
switch (now) {
|
||||||
.User, .System => {
|
.User, .System => {
|
||||||
self.bank.r[Bank.regIdx(now, .R13)] = self.r[13];
|
self.banked_r[bankedIdx(now, .R13)] = self.r[13];
|
||||||
self.bank.r[Bank.regIdx(now, .R14)] = self.r[14];
|
self.banked_r[bankedIdx(now, .R14)] = self.r[14];
|
||||||
},
|
},
|
||||||
else => {
|
else => {
|
||||||
self.bank.r[Bank.regIdx(now, .R13)] = self.r[13];
|
self.banked_r[bankedIdx(now, .R13)] = self.r[13];
|
||||||
self.bank.r[Bank.regIdx(now, .R14)] = self.r[14];
|
self.banked_r[bankedIdx(now, .R14)] = self.r[14];
|
||||||
self.bank.spsr[Bank.spsrIdx(now)] = self.spsr;
|
self.banked_spsr[bankedSpsrIndex(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.bank.fiq[Bank.fiqIdx(i, next)];
|
self.r[8 + i] = self.banked_fiq[bankedFiqIdx(i, next)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab r13, r14, SPSR
|
// Grab r13, r14, SPSR
|
||||||
switch (next) {
|
switch (next) {
|
||||||
.User, .System => {
|
.User, .System => {
|
||||||
self.r[13] = self.bank.r[Bank.regIdx(next, .R13)];
|
self.r[13] = self.banked_r[bankedIdx(next, .R13)];
|
||||||
self.r[14] = self.bank.r[Bank.regIdx(next, .R14)];
|
self.r[14] = self.banked_r[bankedIdx(next, .R14)];
|
||||||
},
|
},
|
||||||
else => {
|
else => {
|
||||||
self.r[13] = self.bank.r[Bank.regIdx(next, .R13)];
|
self.r[13] = self.banked_r[bankedIdx(next, .R13)];
|
||||||
self.r[14] = self.bank.r[Bank.regIdx(next, .R14)];
|
self.r[14] = self.banked_r[bankedIdx(next, .R14)];
|
||||||
self.spsr = self.bank.spsr[Bank.spsrIdx(next)];
|
self.spsr = self.banked_spsr[bankedSpsrIndex(next)];
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
self.cpsr.mode.write(@enumToInt(next));
|
self.cpsr.mode.write(@enumToInt(next));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Advances state so that the BIOS is skipped
|
|
||||||
///
|
|
||||||
/// Note: This accesses the CPU's bus ptr so it only may be called
|
|
||||||
/// once the Bus has been properly initialized
|
|
||||||
///
|
|
||||||
/// TODO: Make above notice impossible to do in code
|
|
||||||
pub fn fastBoot(self: *Self) void {
|
pub fn fastBoot(self: *Self) void {
|
||||||
self.r = std.mem.zeroes([16]u32);
|
self.r = std.mem.zeroes([16]u32);
|
||||||
|
|
||||||
// self.r[0] = 0x08000000;
|
self.r[0] = 0x08000000;
|
||||||
// self.r[1] = 0x000000EA;
|
self.r[1] = 0x000000EA;
|
||||||
self.r[13] = 0x0300_7F00;
|
self.r[13] = 0x0300_7F00;
|
||||||
self.r[15] = 0x0800_0000;
|
self.r[15] = 0x0800_0000;
|
||||||
|
|
||||||
self.bank.r[Bank.regIdx(.Irq, .R13)] = 0x0300_7FA0;
|
self.banked_r[bankedIdx(.Irq, .R13)] = 0x0300_7FA0;
|
||||||
self.bank.r[Bank.regIdx(.Supervisor, .R13)] = 0x0300_7FE0;
|
self.banked_r[bankedIdx(.Supervisor, .R13)] = 0x0300_7FE0;
|
||||||
|
|
||||||
// self.cpsr.raw = 0x6000001F;
|
self.cpsr.raw = 0x6000001F;
|
||||||
self.cpsr.raw = 0x0000_001F;
|
|
||||||
|
|
||||||
self.bus.bios.addr_latch = 0x0000_00DC + 8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn step(self: *Self) void {
|
pub fn step(self: *Self) void {
|
||||||
defer {
|
if (self.cpsr.t.read()) blk: {
|
||||||
|
const opcode = @truncate(u16, self.pipe.step(self, u16) orelse break :blk);
|
||||||
|
if (cpu_logging) self.logger.?.mgbaLog(self, opcode);
|
||||||
|
|
||||||
|
thumb.lut[thumbIdx(opcode)](self, self.bus, opcode);
|
||||||
|
} else blk: {
|
||||||
|
const opcode = self.pipe.step(self, u32) orelse break :blk;
|
||||||
|
if (cpu_logging) self.logger.?.mgbaLog(self, opcode);
|
||||||
|
|
||||||
|
if (checkCond(self.cpsr, @truncate(u4, opcode >> 28))) {
|
||||||
|
arm.lut[armIdx(opcode)](self, self.bus, opcode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!self.pipe.flushed) self.r[15] += if (self.cpsr.t.read()) 2 else @as(u32, 4);
|
if (!self.pipe.flushed) self.r[15] += if (self.cpsr.t.read()) 2 else @as(u32, 4);
|
||||||
self.pipe.flushed = false;
|
self.pipe.flushed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.cpsr.t.read()) {
|
|
||||||
const opcode = @truncate(u16, self.pipe.step(self, u16) orelse return);
|
|
||||||
if (self.logger) |*trace| trace.mgbaLog(self, opcode);
|
|
||||||
|
|
||||||
thumb.lut[thumb.idx(opcode)](self, self.bus, opcode);
|
|
||||||
} else {
|
|
||||||
const opcode = self.pipe.step(self, u32) orelse return;
|
|
||||||
if (self.logger) |*trace| trace.mgbaLog(self, opcode);
|
|
||||||
|
|
||||||
if (checkCond(self.cpsr, @truncate(u4, opcode >> 28))) {
|
|
||||||
arm.lut[arm.idx(opcode)](self, self.bus, opcode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stepDmaTransfer(self: *Self) bool {
|
pub fn stepDmaTransfer(self: *Self) bool {
|
||||||
comptime var i: usize = 0;
|
const dma0 = &self.bus.dma[0];
|
||||||
inline while (i < 4) : (i += 1) {
|
const dma1 = &self.bus.dma[1];
|
||||||
if (self.bus.dma[i].in_progress) {
|
const dma2 = &self.bus.dma[2];
|
||||||
self.bus.dma[i].step(self);
|
const dma3 = &self.bus.dma[3];
|
||||||
|
|
||||||
|
if (dma0.in_progress) {
|
||||||
|
dma0.step(self);
|
||||||
return true;
|
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;
|
||||||
@@ -487,14 +488,13 @@ pub const Arm7tdmi = struct {
|
|||||||
// Return if IME is disabled, CPSR I is set or there is nothing to handle
|
// Return if IME is disabled, CPSR I is set or there is nothing to handle
|
||||||
if (!self.bus.io.ime or self.cpsr.i.read() or should_handle == 0) return;
|
if (!self.bus.io.ime or self.cpsr.i.read() or should_handle == 0) return;
|
||||||
|
|
||||||
// If Pipeline isn't full, we have a bug
|
// If pipeline isn't full, return but reschedule the handling of the event
|
||||||
std.debug.assert(self.pipe.isFull());
|
if (!self.pipe.isFull()) return;
|
||||||
|
|
||||||
// log.debug("Handling Interrupt!", .{});
|
// log.debug("Handling Interrupt!", .{});
|
||||||
self.bus.io.haltcnt = .Execute;
|
self.bus.io.haltcnt = .Execute;
|
||||||
|
|
||||||
// FIXME: This seems weird, but retAddr.gba suggests I need to make these changes
|
const ret_addr = self.r[15] - if (self.cpsr.t.read()) 2 else @as(u32, 4);
|
||||||
const ret_addr = self.r[15] - if (self.cpsr.t.read()) 0 else @as(u32, 4);
|
|
||||||
const new_spsr = self.cpsr.raw;
|
const new_spsr = self.cpsr.raw;
|
||||||
|
|
||||||
self.changeMode(.Irq);
|
self.changeMode(.Irq);
|
||||||
@@ -504,21 +504,26 @@ pub const Arm7tdmi = struct {
|
|||||||
self.r[14] = ret_addr;
|
self.r[14] = ret_addr;
|
||||||
self.spsr.raw = new_spsr;
|
self.spsr.raw = new_spsr;
|
||||||
self.r[15] = 0x0000_0018;
|
self.r[15] = 0x0000_0018;
|
||||||
self.pipe.reload(self);
|
self.pipe.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fn fetch(self: *Self, comptime T: type, address: u32) T {
|
inline fn fetch(self: *Self, comptime T: type) T {
|
||||||
comptime std.debug.assert(T == u32 or T == u16); // Opcode may be 32-bit (ARM) or 16-bit (THUMB)
|
comptime std.debug.assert(T == u32 or T == u16); // Opcode may be 32-bit (ARM) or 16-bit (THUMB)
|
||||||
|
defer self.r[15] += if (T == u32) 4 else 2;
|
||||||
|
|
||||||
// Bus.read will advance the scheduler. There are different timings for CPU fetches,
|
// FIXME: You better hope this is optimized out
|
||||||
// so we want to undo what Bus.read will apply. We can do this by caching the current tick
|
|
||||||
// This is very dumb.
|
|
||||||
//
|
|
||||||
// FIXME: Please rework this
|
|
||||||
const tick_cache = self.sched.tick;
|
const tick_cache = self.sched.tick;
|
||||||
defer self.sched.tick = tick_cache + Bus.fetch_timings[@boolToInt(T == u32)][@truncate(u4, address >> 24)];
|
defer self.sched.tick = tick_cache + Bus.fetch_timings[@boolToInt(T == u32)][@truncate(u4, self.r[15] >> 24)];
|
||||||
|
|
||||||
return self.bus.read(T, address);
|
return self.bus.read(T, self.r[15]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debug_log(self: *const Self, file: *const File, opcode: u32) void {
|
||||||
|
if (self.binary_log) {
|
||||||
|
self.skyLog(file) catch unreachable;
|
||||||
|
} else {
|
||||||
|
self.mgbaLog(file, opcode) catch unreachable;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn panic(self: *const Self, comptime format: []const u8, args: anytype) noreturn {
|
pub fn panic(self: *const Self, comptime format: []const u8, args: anytype) noreturn {
|
||||||
@@ -530,20 +535,18 @@ 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});
|
||||||
self.cpsr.toString();
|
prettyPrintPsr(&self.cpsr);
|
||||||
|
|
||||||
std.debug.print("spsr: 0x{X:0>8} ", .{self.spsr.raw});
|
std.debug.print("spsr: 0x{X:0>8} ", .{self.spsr.raw});
|
||||||
self.spsr.toString();
|
prettyPrintPsr(&self.spsr);
|
||||||
|
|
||||||
std.debug.print("pipeline: {??X:0>8}\n", .{self.pipe.stage});
|
|
||||||
|
|
||||||
if (self.cpsr.t.read()) {
|
if (self.cpsr.t.read()) {
|
||||||
const opcode = self.bus.dbgRead(u16, self.r[15] - 4);
|
const opcode = self.bus.dbgRead(u16, self.r[15] - 4);
|
||||||
const id = thumb.idx(opcode);
|
const id = thumbIdx(opcode);
|
||||||
std.debug.print("opcode: ID: 0x{b:0>10} 0x{X:0>4}\n", .{ id, opcode });
|
std.debug.print("opcode: ID: 0x{b:0>10} 0x{X:0>4}\n", .{ id, opcode });
|
||||||
} else {
|
} else {
|
||||||
const opcode = self.bus.dbgRead(u32, self.r[15] - 4);
|
const opcode = self.bus.dbgRead(u32, self.r[15] - 4);
|
||||||
const id = arm.idx(opcode);
|
const id = armIdx(opcode);
|
||||||
std.debug.print("opcode: ID: 0x{X:0>3} 0x{X:0>8}\n", .{ id, opcode });
|
std.debug.print("opcode: ID: 0x{X:0>3} 0x{X:0>8}\n", .{ id, opcode });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,34 +554,108 @@ 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);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const condition_lut = [_]u16{
|
inline fn armIdx(opcode: u32) u12 {
|
||||||
0xF0F0, // EQ - Equal
|
return @truncate(u12, opcode >> 20 & 0xFF) << 4 | @truncate(u12, opcode >> 4 & 0xF);
|
||||||
0x0F0F, // NE - Not Equal
|
|
||||||
0xCCCC, // CS - Unsigned higher or same
|
|
||||||
0x3333, // CC - Unsigned lower
|
|
||||||
0xFF00, // MI - Negative
|
|
||||||
0x00FF, // PL - Positive or Zero
|
|
||||||
0xAAAA, // VS - Overflow
|
|
||||||
0x5555, // VC - No Overflow
|
|
||||||
0x0C0C, // HI - unsigned hierh
|
|
||||||
0xF3F3, // LS - unsigned lower or same
|
|
||||||
0xAA55, // GE - greater or equal
|
|
||||||
0x55AA, // LT - less than
|
|
||||||
0x0A05, // GT - greater than
|
|
||||||
0xF5FA, // LE - less than or equal
|
|
||||||
0xFFFF, // AL - always
|
|
||||||
0x0000, // NV - never
|
|
||||||
};
|
|
||||||
|
|
||||||
pub inline fn checkCond(cpsr: PSR, cond: u4) bool {
|
|
||||||
const flags = @truncate(u4, cpsr.raw >> 28);
|
|
||||||
|
|
||||||
return condition_lut[cond] & (@as(u16, 1) << flags) != 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Pipeline = struct {
|
inline fn thumbIdx(opcode: u16) u10 {
|
||||||
|
return @truncate(u10, opcode >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn checkCond(cpsr: PSR, cond: u4) bool {
|
||||||
|
return switch (cond) {
|
||||||
|
0x0 => cpsr.z.read(), // EQ - Equal
|
||||||
|
0x1 => !cpsr.z.read(), // NE - Not equal
|
||||||
|
0x2 => cpsr.c.read(), // CS - Unsigned higher or same
|
||||||
|
0x3 => !cpsr.c.read(), // CC - Unsigned lower
|
||||||
|
0x4 => cpsr.n.read(), // MI - Negative
|
||||||
|
0x5 => !cpsr.n.read(), // PL - Positive or zero
|
||||||
|
0x6 => cpsr.v.read(), // VS - Overflow
|
||||||
|
0x7 => !cpsr.v.read(), // VC - No overflow
|
||||||
|
0x8 => cpsr.c.read() and !cpsr.z.read(), // HI - unsigned higher
|
||||||
|
0x9 => !cpsr.c.read() or cpsr.z.read(), // LS - unsigned lower or same
|
||||||
|
0xA => cpsr.n.read() == cpsr.v.read(), // GE - Greater or equal
|
||||||
|
0xB => cpsr.n.read() != cpsr.v.read(), // LT - Less than
|
||||||
|
0xC => !cpsr.z.read() and (cpsr.n.read() == cpsr.v.read()), // GT - Greater than
|
||||||
|
0xD => cpsr.z.read() or (cpsr.n.read() != cpsr.v.read()), // LE - Less than or equal
|
||||||
|
0xE => true, // AL - Always
|
||||||
|
0xF => false, // NV - Never (reserved in ARMv3 and up, but seems to have not changed?)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const Pipline = struct {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
stage: [2]?u32,
|
stage: [2]?u32,
|
||||||
flushed: bool,
|
flushed: bool,
|
||||||
@@ -590,6 +667,11 @@ const Pipeline = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn flush(self: *Self) void {
|
||||||
|
for (self.stage) |*opcode| opcode.* = null;
|
||||||
|
self.flushed = true;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn isFull(self: *const Self) bool {
|
pub fn isFull(self: *const Self) bool {
|
||||||
return self.stage[0] != null and self.stage[1] != null;
|
return self.stage[0] != null and self.stage[1] != null;
|
||||||
}
|
}
|
||||||
@@ -598,24 +680,20 @@ const Pipeline = struct {
|
|||||||
comptime std.debug.assert(T == u32 or T == u16);
|
comptime std.debug.assert(T == u32 or T == u16);
|
||||||
|
|
||||||
const opcode = self.stage[0];
|
const 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.bus.read(T, cpu.r[15]);
|
||||||
|
|
||||||
return opcode;
|
return opcode;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reload(self: *Self, cpu: *Arm7tdmi) void {
|
fn reload(self: *Self, cpu: *Arm7tdmi, comptime T: type) void {
|
||||||
if (cpu.cpsr.t.read()) {
|
comptime std.debug.assert(T == u32 or T == u16);
|
||||||
self.stage[0] = cpu.fetch(u16, cpu.r[15]);
|
const inc = if (T == u32) 4 else 2;
|
||||||
self.stage[1] = cpu.fetch(u16, cpu.r[15] + 2);
|
|
||||||
cpu.r[15] += 4;
|
|
||||||
} else {
|
|
||||||
self.stage[0] = cpu.fetch(u32, cpu.r[15]);
|
|
||||||
self.stage[1] = cpu.fetch(u32, cpu.r[15] + 4);
|
|
||||||
cpu.r[15] += 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.flushed = true;
|
self.stage[0] = cpu.bus.read(T, cpu.r[15]);
|
||||||
|
self.stage[1] = cpu.bus.read(T, cpu.r[15] + inc);
|
||||||
|
cpu.r[15] += inc * 2;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -629,22 +707,6 @@ 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) {
|
||||||
@@ -655,18 +717,11 @@ const Mode = enum(u5) {
|
|||||||
Abort = 0b10111,
|
Abort = 0b10111,
|
||||||
Undefined = 0b11011,
|
Undefined = 0b11011,
|
||||||
System = 0b11111,
|
System = 0b11111,
|
||||||
|
};
|
||||||
|
|
||||||
fn toString(self: Mode) []const u8 {
|
const BankedKind = enum(u1) {
|
||||||
return switch (self) {
|
R13 = 0,
|
||||||
.User => "usr",
|
R14,
|
||||||
.Fiq => "fiq",
|
|
||||||
.Irq => "irq",
|
|
||||||
.Supervisor => "svc",
|
|
||||||
.Abort => "abt",
|
|
||||||
.Undefined => "und",
|
|
||||||
.System => "sys",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fn getMode(bits: u5) ?Mode {
|
fn getMode(bits: u5) ?Mode {
|
||||||
|
|||||||
@@ -55,8 +55,9 @@ pub fn blockDataTransfer(comptime P: bool, comptime U: bool, comptime S: bool, c
|
|||||||
|
|
||||||
if (L) {
|
if (L) {
|
||||||
cpu.r[15] = bus.read(u32, und_addr);
|
cpu.r[15] = bus.read(u32, und_addr);
|
||||||
cpu.pipe.reload(cpu);
|
cpu.pipe.flush();
|
||||||
} 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ pub fn blockDataTransfer(comptime P: bool, comptime U: bool, comptime S: bool, c
|
|||||||
cpu.r[i] = value;
|
cpu.r[i] = value;
|
||||||
if (i == 0xF) {
|
if (i == 0xF) {
|
||||||
cpu.r[i] &= ~@as(u32, 3); // Align r15
|
cpu.r[i] &= ~@as(u32, 3); // Align r15
|
||||||
cpu.pipe.reload(cpu);
|
cpu.pipe.flush();
|
||||||
|
|
||||||
if (S) cpu.setCpsr(cpu.spsr.raw);
|
if (S) cpu.setCpsr(cpu.spsr.raw);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
const sext = @import("../../../util.zig").sext;
|
const sext = @import("../../util.zig").sext;
|
||||||
|
|
||||||
pub fn branch(comptime L: bool) InstrFn {
|
pub fn branch(comptime L: bool) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
@@ -10,7 +12,7 @@ pub fn branch(comptime L: bool) InstrFn {
|
|||||||
if (L) cpu.r[14] = cpu.r[15] - 4;
|
if (L) cpu.r[14] = cpu.r[15] - 4;
|
||||||
|
|
||||||
cpu.r[15] +%= sext(u32, u24, opcode) << 2;
|
cpu.r[15] +%= sext(u32, u24, opcode) << 2;
|
||||||
cpu.pipe.reload(cpu);
|
cpu.pipe.flush();
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
}
|
}
|
||||||
@@ -20,7 +22,6 @@ pub fn branchAndExchange(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
|||||||
|
|
||||||
const thumb = cpu.r[rn] & 1 == 1;
|
const thumb = cpu.r[rn] & 1 == 1;
|
||||||
cpu.r[15] = cpu.r[rn] & if (thumb) ~@as(u32, 1) else ~@as(u32, 3);
|
cpu.r[15] = cpu.r[rn] & if (thumb) ~@as(u32, 1) else ~@as(u32, 3);
|
||||||
|
|
||||||
cpu.cpsr.t.write(thumb);
|
cpu.cpsr.t.write(thumb);
|
||||||
cpu.pipe.reload(cpu);
|
cpu.pipe.flush();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ 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;
|
||||||
|
|
||||||
const exec = @import("../barrel_shifter.zig").exec;
|
const rotateRight = @import("../barrel_shifter.zig").rotateRight;
|
||||||
const ror = @import("../barrel_shifter.zig").ror;
|
const execute = @import("../barrel_shifter.zig").execute;
|
||||||
|
|
||||||
pub fn dataProcessing(comptime I: bool, comptime S: bool, comptime kind: u4) InstrFn {
|
pub fn dataProcessing(comptime I: bool, comptime S: bool, comptime instrKind: u4) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
||||||
const rd = @truncate(u4, opcode >> 12 & 0xF);
|
const rd = @truncate(u4, opcode >> 12 & 0xF);
|
||||||
@@ -13,171 +13,261 @@ pub fn dataProcessing(comptime I: bool, comptime S: bool, comptime kind: u4) Ins
|
|||||||
const old_carry = @boolToInt(cpu.cpsr.c.read());
|
const old_carry = @boolToInt(cpu.cpsr.c.read());
|
||||||
|
|
||||||
// If certain conditions are met, PC is 12 ahead instead of 8
|
// If certain conditions are met, PC is 12 ahead instead of 8
|
||||||
// TODO: Why these conditions?
|
// TODO: What are these conditions? I can't remember
|
||||||
if (!I and opcode >> 4 & 1 == 1) cpu.r[15] += 4;
|
if (!I and opcode >> 4 & 1 == 1) cpu.r[15] += 4;
|
||||||
const op1 = cpu.r[rn];
|
const op1 = cpu.r[rn];
|
||||||
|
|
||||||
const amount = @truncate(u8, (opcode >> 8 & 0xF) << 1);
|
const amount = @truncate(u8, (opcode >> 8 & 0xF) << 1);
|
||||||
const op2 = if (I) ror(S, &cpu.cpsr, opcode & 0xFF, amount) else exec(S, cpu, opcode);
|
const op2 = if (I) rotateRight(S, &cpu.cpsr, opcode & 0xFF, amount) else execute(S, cpu, opcode);
|
||||||
|
|
||||||
// Undo special condition from above
|
// Undo special condition from above
|
||||||
if (!I and opcode >> 4 & 1 == 1) cpu.r[15] -= 4;
|
if (!I and opcode >> 4 & 1 == 1) cpu.r[15] -= 4;
|
||||||
|
|
||||||
var result: u32 = undefined;
|
switch (instrKind) {
|
||||||
var overflow: bool = undefined;
|
0x0 => {
|
||||||
|
// AND
|
||||||
// Perform Data Processing Logic
|
const result = op1 & op2;
|
||||||
switch (kind) {
|
cpu.r[rd] = result;
|
||||||
0x0 => result = op1 & op2, // AND
|
setArmLogicOpFlags(S, cpu, rd, result);
|
||||||
0x1 => result = op1 ^ op2, // EOR
|
},
|
||||||
0x2 => result = op1 -% op2, // SUB
|
0x1 => {
|
||||||
0x3 => result = op2 -% op1, // RSB
|
// EOR
|
||||||
0x4 => result = add(&overflow, op1, op2), // ADD
|
const result = op1 ^ op2;
|
||||||
0x5 => result = adc(&overflow, op1, op2, old_carry), // ADC
|
cpu.r[rd] = result;
|
||||||
0x6 => result = sbc(op1, op2, old_carry), // SBC
|
setArmLogicOpFlags(S, cpu, rd, result);
|
||||||
0x7 => result = sbc(op2, op1, old_carry), // RSC
|
},
|
||||||
|
0x2 => {
|
||||||
|
// SUB
|
||||||
|
cpu.r[rd] = armSub(S, cpu, rd, op1, op2);
|
||||||
|
},
|
||||||
|
0x3 => {
|
||||||
|
// RSB
|
||||||
|
cpu.r[rd] = armSub(S, cpu, rd, op2, op1);
|
||||||
|
},
|
||||||
|
0x4 => {
|
||||||
|
// ADD
|
||||||
|
cpu.r[rd] = armAdd(S, cpu, rd, op1, op2);
|
||||||
|
},
|
||||||
|
0x5 => {
|
||||||
|
// ADC
|
||||||
|
cpu.r[rd] = armAdc(S, cpu, rd, op1, op2, old_carry);
|
||||||
|
},
|
||||||
|
0x6 => {
|
||||||
|
// SBC
|
||||||
|
cpu.r[rd] = armSbc(S, cpu, rd, op1, op2, old_carry);
|
||||||
|
},
|
||||||
|
0x7 => {
|
||||||
|
// RSC
|
||||||
|
cpu.r[rd] = armSbc(S, cpu, rd, op2, op1, old_carry);
|
||||||
|
},
|
||||||
0x8 => {
|
0x8 => {
|
||||||
// TST
|
// TST
|
||||||
if (rd == 0xF)
|
if (rd == 0xF)
|
||||||
return undefinedTestBehaviour(cpu);
|
return undefinedTestBehaviour(cpu);
|
||||||
|
|
||||||
result = op1 & op2;
|
const result = op1 & op2;
|
||||||
|
setTestOpFlags(S, cpu, opcode, result);
|
||||||
},
|
},
|
||||||
0x9 => {
|
0x9 => {
|
||||||
// TEQ
|
// TEQ
|
||||||
if (rd == 0xF)
|
if (rd == 0xF)
|
||||||
return undefinedTestBehaviour(cpu);
|
return undefinedTestBehaviour(cpu);
|
||||||
|
|
||||||
result = op1 ^ op2;
|
const result = op1 ^ op2;
|
||||||
|
setTestOpFlags(S, cpu, opcode, result);
|
||||||
},
|
},
|
||||||
0xA => {
|
0xA => {
|
||||||
// CMP
|
// CMP
|
||||||
if (rd == 0xF)
|
if (rd == 0xF)
|
||||||
return undefinedTestBehaviour(cpu);
|
return undefinedTestBehaviour(cpu);
|
||||||
|
|
||||||
result = op1 -% op2;
|
cmp(cpu, op1, op2);
|
||||||
},
|
},
|
||||||
0xB => {
|
0xB => {
|
||||||
// CMN
|
// CMN
|
||||||
if (rd == 0xF)
|
if (rd == 0xF)
|
||||||
return undefinedTestBehaviour(cpu);
|
return undefinedTestBehaviour(cpu);
|
||||||
|
|
||||||
overflow = @addWithOverflow(u32, op1, op2, &result);
|
cmn(cpu, op1, op2);
|
||||||
},
|
},
|
||||||
0xC => result = op1 | op2, // ORR
|
0xC => {
|
||||||
0xD => result = op2, // MOV
|
// ORR
|
||||||
0xE => result = op1 & ~op2, // BIC
|
const result = op1 | op2;
|
||||||
0xF => result = ~op2, // MVN
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write to Destination Register
|
|
||||||
switch (kind) {
|
|
||||||
0x8, 0x9, 0xA, 0xB => {}, // Test Operations
|
|
||||||
else => {
|
|
||||||
cpu.r[rd] = result;
|
cpu.r[rd] = result;
|
||||||
if (rd == 0xF) {
|
setArmLogicOpFlags(S, cpu, rd, result);
|
||||||
if (S) cpu.setCpsr(cpu.spsr.raw);
|
},
|
||||||
cpu.pipe.reload(cpu);
|
0xD => {
|
||||||
}
|
// MOV
|
||||||
|
cpu.r[rd] = op2;
|
||||||
|
setArmLogicOpFlags(S, cpu, rd, op2);
|
||||||
|
},
|
||||||
|
0xE => {
|
||||||
|
// BIC
|
||||||
|
const result = op1 & ~op2;
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
setArmLogicOpFlags(S, cpu, rd, result);
|
||||||
|
},
|
||||||
|
0xF => {
|
||||||
|
// MVN
|
||||||
|
const result = ~op2;
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
setArmLogicOpFlags(S, cpu, rd, result);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write Flags
|
if (rd == 0xF) cpu.pipe.flush();
|
||||||
switch (kind) {
|
|
||||||
0x0, 0x1, 0xC, 0xD, 0xE, 0xF => if (S and rd != 0xF) {
|
|
||||||
// Logic Operation Flags
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
// C set by Barrel Shifter, V is unaffected
|
|
||||||
|
|
||||||
},
|
|
||||||
0x2, 0x3 => if (S and rd != 0xF) {
|
|
||||||
// SUB, RSB Flags
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
|
|
||||||
if (kind == 0x2) {
|
|
||||||
// SUB specific
|
|
||||||
cpu.cpsr.c.write(op2 <= op1);
|
|
||||||
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
|
||||||
} else {
|
|
||||||
// RSB Specific
|
|
||||||
cpu.cpsr.c.write(op1 <= op2);
|
|
||||||
cpu.cpsr.v.write(((op2 ^ result) & (~op1 ^ result)) >> 31 & 1 == 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
0x4, 0x5 => if (S and rd != 0xF) {
|
|
||||||
// ADD, ADC Flags
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
cpu.cpsr.c.write(overflow);
|
|
||||||
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
|
|
||||||
},
|
|
||||||
0x6, 0x7 => if (S and rd != 0xF) {
|
|
||||||
// SBC, RSC Flags
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
|
|
||||||
if (kind == 0x6) {
|
|
||||||
// SBC specific
|
|
||||||
const subtrahend = @as(u64, op2) -% old_carry +% 1;
|
|
||||||
cpu.cpsr.c.write(subtrahend <= op1);
|
|
||||||
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
|
||||||
} else {
|
|
||||||
// RSC Specific
|
|
||||||
const subtrahend = @as(u64, op1) -% old_carry +% 1;
|
|
||||||
cpu.cpsr.c.write(subtrahend <= op2);
|
|
||||||
cpu.cpsr.v.write(((op2 ^ result) & (~op1 ^ result)) >> 31 & 1 == 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
0x8, 0x9, 0xA, 0xB => {
|
|
||||||
// Test Operation Flags
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
|
|
||||||
if (kind == 0xA) {
|
|
||||||
// CMP specific
|
|
||||||
cpu.cpsr.c.write(op2 <= op1);
|
|
||||||
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
|
||||||
} else if (kind == 0xB) {
|
|
||||||
// CMN specific
|
|
||||||
cpu.cpsr.c.write(overflow);
|
|
||||||
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
|
|
||||||
} else {
|
|
||||||
// TST, TEQ specific
|
|
||||||
// Barrel Shifter should always calc CPSR C in TST
|
|
||||||
if (!S) _ = exec(true, cpu, opcode);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sbc(left: u32, right: u32, old_carry: u1) u32 {
|
fn armSbc(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32, old_carry: u1) u32 {
|
||||||
|
var result: u32 = undefined;
|
||||||
|
if (S and rd == 0xF) {
|
||||||
|
result = sbc(false, cpu, left, right, old_carry);
|
||||||
|
cpu.setCpsr(cpu.spsr.raw);
|
||||||
|
} else {
|
||||||
|
result = sbc(S, cpu, left, right, old_carry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sbc(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32, old_carry: u1) u32 {
|
||||||
// TODO: Make your own version (thanks peach.bot)
|
// TODO: Make your own version (thanks peach.bot)
|
||||||
const subtrahend = @as(u64, right) -% old_carry +% 1;
|
const subtrahend = @as(u64, right) -% old_carry +% 1;
|
||||||
const ret = @truncate(u32, left -% subtrahend);
|
const result = @truncate(u32, left -% subtrahend);
|
||||||
|
|
||||||
return ret;
|
if (S) {
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
cpu.cpsr.c.write(subtrahend <= left);
|
||||||
|
cpu.cpsr.v.write(((left ^ result) & (~right ^ result)) >> 31 & 1 == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add(overflow: *bool, left: u32, right: u32) u32 {
|
fn armSub(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32) u32 {
|
||||||
var ret: u32 = undefined;
|
var result: u32 = undefined;
|
||||||
overflow.* = @addWithOverflow(u32, left, right, &ret);
|
if (S and rd == 0xF) {
|
||||||
return ret;
|
result = sub(false, cpu, left, right);
|
||||||
|
cpu.setCpsr(cpu.spsr.raw);
|
||||||
|
} else {
|
||||||
|
result = sub(S, cpu, left, right);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn adc(overflow: *bool, left: u32, right: u32, old_carry: u1) u32 {
|
pub fn sub(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32) u32 {
|
||||||
var ret: u32 = undefined;
|
const result = left -% right;
|
||||||
const first = @addWithOverflow(u32, left, right, &ret);
|
|
||||||
const second = @addWithOverflow(u32, ret, old_carry, &ret);
|
|
||||||
|
|
||||||
overflow.* = first or second;
|
if (S) {
|
||||||
return ret;
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
cpu.cpsr.c.write(right <= left);
|
||||||
|
cpu.cpsr.v.write(((left ^ result) & (~right ^ result)) >> 31 & 1 == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn armAdd(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32) u32 {
|
||||||
|
var result: u32 = undefined;
|
||||||
|
if (S and rd == 0xF) {
|
||||||
|
result = add(false, cpu, left, right);
|
||||||
|
cpu.setCpsr(cpu.spsr.raw);
|
||||||
|
} else {
|
||||||
|
result = add(S, cpu, left, right);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32) u32 {
|
||||||
|
var result: u32 = undefined;
|
||||||
|
const didOverflow = @addWithOverflow(u32, left, right, &result);
|
||||||
|
|
||||||
|
if (S) {
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
cpu.cpsr.c.write(didOverflow);
|
||||||
|
cpu.cpsr.v.write(((left ^ result) & (right ^ result)) >> 31 & 1 == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn armAdc(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32, old_carry: u1) u32 {
|
||||||
|
var result: u32 = undefined;
|
||||||
|
if (S and rd == 0xF) {
|
||||||
|
result = adc(false, cpu, left, right, old_carry);
|
||||||
|
cpu.setCpsr(cpu.spsr.raw);
|
||||||
|
} else {
|
||||||
|
result = adc(S, cpu, left, right, old_carry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn adc(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32, old_carry: u1) u32 {
|
||||||
|
var result: u32 = undefined;
|
||||||
|
const did = @addWithOverflow(u32, left, right, &result);
|
||||||
|
const overflow = @addWithOverflow(u32, result, old_carry, &result);
|
||||||
|
|
||||||
|
if (S) {
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
cpu.cpsr.c.write(did or overflow);
|
||||||
|
cpu.cpsr.v.write(((left ^ result) & (right ^ result)) >> 31 & 1 == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cmp(cpu: *Arm7tdmi, left: u32, right: u32) void {
|
||||||
|
const result = left -% right;
|
||||||
|
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
cpu.cpsr.c.write(right <= left);
|
||||||
|
cpu.cpsr.v.write(((left ^ result) & (~right ^ result)) >> 31 & 1 == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cmn(cpu: *Arm7tdmi, left: u32, right: u32) void {
|
||||||
|
var result: u32 = undefined;
|
||||||
|
const didOverflow = @addWithOverflow(u32, left, right, &result);
|
||||||
|
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
cpu.cpsr.c.write(didOverflow);
|
||||||
|
cpu.cpsr.v.write(((left ^ result) & (right ^ result)) >> 31 & 1 == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setArmLogicOpFlags(comptime S: bool, cpu: *Arm7tdmi, rd: u4, result: u32) void {
|
||||||
|
if (S and rd == 0xF) {
|
||||||
|
cpu.setCpsr(cpu.spsr.raw);
|
||||||
|
} else {
|
||||||
|
setLogicOpFlags(S, cpu, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setLogicOpFlags(comptime S: bool, cpu: *Arm7tdmi, result: u32) void {
|
||||||
|
if (S) {
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
// C set by Barrel Shifter, V is unaffected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setTestOpFlags(comptime S: bool, cpu: *Arm7tdmi, opcode: u32, result: u32) void {
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
// Barrel Shifter should always calc CPSR C in TST
|
||||||
|
if (!S) _ = execute(true, cpu, opcode);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn undefinedTestBehaviour(cpu: *Arm7tdmi) void {
|
fn undefinedTestBehaviour(cpu: *Arm7tdmi) void {
|
||||||
@setCold(true);
|
@setCold(true);
|
||||||
cpu.setCpsr(cpu.spsr.raw);
|
cpu.setCpsrNoFlush(cpu.spsr.raw);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
const sext = @import("../../../util.zig").sext;
|
const sext = @import("../../util.zig").sext;
|
||||||
const rotr = @import("../../../util.zig").rotr;
|
const rotr = @import("../../util.zig").rotr;
|
||||||
|
|
||||||
pub fn halfAndSignedDataTransfer(comptime P: bool, comptime U: bool, comptime I: bool, comptime W: bool, comptime L: bool) InstrFn {
|
pub fn halfAndSignedDataTransfer(comptime P: bool, comptime U: bool, comptime I: bool, comptime W: bool, comptime L: bool) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
@@ -33,8 +35,11 @@ pub fn halfAndSignedDataTransfer(comptime P: bool, comptime U: bool, comptime I:
|
|||||||
},
|
},
|
||||||
0b11 => {
|
0b11 => {
|
||||||
// LDRSH
|
// LDRSH
|
||||||
const value = bus.read(u16, address);
|
result = if (address & 1 == 1) blk: {
|
||||||
result = if (address & 1 == 1) sext(u32, u8, @truncate(u8, value >> 8)) else sext(u32, u16, value);
|
break :blk sext(u32, u8, bus.read(u8, address));
|
||||||
|
} else blk: {
|
||||||
|
break :blk sext(u32, u16, bus.read(u16, address));
|
||||||
|
};
|
||||||
},
|
},
|
||||||
0b00 => unreachable, // SWP
|
0b00 => unreachable, // SWP
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const PSR = @import("../../cpu.zig").PSR;
|
|||||||
|
|
||||||
const log = std.log.scoped(.PsrTransfer);
|
const log = std.log.scoped(.PsrTransfer);
|
||||||
|
|
||||||
const rotr = @import("../../../util.zig").rotr;
|
const rotr = @import("../../util.zig").rotr;
|
||||||
|
|
||||||
pub fn psrTransfer(comptime I: bool, comptime R: bool, comptime kind: u2) InstrFn {
|
pub fn psrTransfer(comptime I: bool, comptime R: bool, comptime kind: u2) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
const rotr = @import("../../../util.zig").rotr;
|
const rotr = @import("../../util.zig").rotr;
|
||||||
|
|
||||||
pub fn singleDataSwap(comptime B: bool) InstrFn {
|
pub fn singleDataSwap(comptime B: bool) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
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;
|
||||||
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||||
|
|
||||||
const rotr = @import("../../../util.zig").rotr;
|
const rotr = @import("../../util.zig").rotr;
|
||||||
|
|
||||||
pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool, comptime B: bool, comptime W: bool, comptime L: bool) InstrFn {
|
pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool, comptime B: bool, comptime W: bool, comptime L: bool) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
@@ -14,7 +17,7 @@ pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool,
|
|||||||
// rn is r15 and L is not set, the PC is 12 ahead
|
// rn is r15 and L is not set, the PC is 12 ahead
|
||||||
const base = cpu.r[rn] + if (!L and rn == 0xF) 4 else @as(u32, 0);
|
const base = cpu.r[rn] + if (!L and rn == 0xF) 4 else @as(u32, 0);
|
||||||
|
|
||||||
const offset = if (I) shifter.immediate(false, cpu, opcode) else opcode & 0xFFF;
|
const offset = if (I) shifter.immShift(false, cpu, opcode) else opcode & 0xFFF;
|
||||||
|
|
||||||
const modified_base = if (U) base +% offset else base -% offset;
|
const modified_base = if (U) base +% offset else base -% offset;
|
||||||
var address = if (P) modified_base else base;
|
var address = if (P) modified_base else base;
|
||||||
@@ -44,13 +47,13 @@ pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool,
|
|||||||
address = modified_base;
|
address = modified_base;
|
||||||
if (W and P or !P) {
|
if (W and P or !P) {
|
||||||
cpu.r[rn] = address;
|
cpu.r[rn] = address;
|
||||||
if (rn == 0xF) cpu.pipe.reload(cpu);
|
if (rn == 0xF) cpu.pipe.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (L) {
|
if (L) {
|
||||||
// This emulates the LDR rd == rn behaviour
|
// This emulates the LDR rd == rn behaviour
|
||||||
cpu.r[rd] = result;
|
cpu.r[rd] = result;
|
||||||
if (rd == 0xF) cpu.pipe.reload(cpu);
|
if (rd == 0xF) cpu.pipe.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ pub fn armSoftwareInterrupt() InstrFn {
|
|||||||
cpu.r[14] = ret_addr; // Resume Execution
|
cpu.r[14] = ret_addr; // Resume Execution
|
||||||
cpu.spsr.raw = cpsr; // Previous mode CPSR
|
cpu.spsr.raw = cpsr; // Previous mode CPSR
|
||||||
cpu.r[15] = 0x0000_0008;
|
cpu.r[15] = 0x0000_0008;
|
||||||
cpu.pipe.reload(cpu);
|
cpu.pipe.flush();
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,35 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
const rotr = @import("../../util.zig").rotr;
|
const rotr = @import("../util.zig").rotr;
|
||||||
|
|
||||||
pub fn exec(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
pub fn execute(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
||||||
var result: u32 = undefined;
|
var result: u32 = undefined;
|
||||||
if (opcode >> 4 & 1 == 1) {
|
if (opcode >> 4 & 1 == 1) {
|
||||||
result = register(S, cpu, opcode);
|
result = registerShift(S, cpu, opcode);
|
||||||
} else {
|
} else {
|
||||||
result = immediate(S, cpu, opcode);
|
result = immShift(S, cpu, opcode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn register(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
fn registerShift(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
||||||
const rs_idx = opcode >> 8 & 0xF;
|
const rs_idx = opcode >> 8 & 0xF;
|
||||||
const rm = cpu.r[opcode & 0xF];
|
const rm = cpu.r[opcode & 0xF];
|
||||||
const rs = @truncate(u8, cpu.r[rs_idx]);
|
const rs = @truncate(u8, cpu.r[rs_idx]);
|
||||||
|
|
||||||
return switch (@truncate(u2, opcode >> 5)) {
|
return switch (@truncate(u2, opcode >> 5)) {
|
||||||
0b00 => lsl(S, &cpu.cpsr, rm, rs),
|
0b00 => logicalLeft(S, &cpu.cpsr, rm, rs),
|
||||||
0b01 => lsr(S, &cpu.cpsr, rm, rs),
|
0b01 => logicalRight(S, &cpu.cpsr, rm, rs),
|
||||||
0b10 => asr(S, &cpu.cpsr, rm, rs),
|
0b10 => arithmeticRight(S, &cpu.cpsr, rm, rs),
|
||||||
0b11 => ror(S, &cpu.cpsr, rm, rs),
|
0b11 => rotateRight(S, &cpu.cpsr, rm, rs),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn immediate(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
pub fn immShift(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
||||||
const amount = @truncate(u8, opcode >> 7 & 0x1F);
|
const amount = @truncate(u8, opcode >> 7 & 0x1F);
|
||||||
const rm = cpu.r[opcode & 0xF];
|
const rm = cpu.r[opcode & 0xF];
|
||||||
|
|
||||||
@@ -58,17 +60,17 @@ pub fn immediate(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch (@truncate(u2, opcode >> 5)) {
|
switch (@truncate(u2, opcode >> 5)) {
|
||||||
0b00 => result = lsl(S, &cpu.cpsr, rm, amount),
|
0b00 => result = logicalLeft(S, &cpu.cpsr, rm, amount),
|
||||||
0b01 => result = lsr(S, &cpu.cpsr, rm, amount),
|
0b01 => result = logicalRight(S, &cpu.cpsr, rm, amount),
|
||||||
0b10 => result = asr(S, &cpu.cpsr, rm, amount),
|
0b10 => result = arithmeticRight(S, &cpu.cpsr, rm, amount),
|
||||||
0b11 => result = ror(S, &cpu.cpsr, rm, amount),
|
0b11 => result = rotateRight(S, &cpu.cpsr, rm, amount),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lsl(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
pub fn logicalLeft(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
||||||
const amount = @truncate(u5, total_amount);
|
const amount = @truncate(u5, total_amount);
|
||||||
const bit_count: u8 = @typeInfo(u32).Int.bits;
|
const bit_count: u8 = @typeInfo(u32).Int.bits;
|
||||||
|
|
||||||
@@ -95,7 +97,7 @@ pub fn lsl(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lsr(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u32) u32 {
|
pub fn logicalRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u32) u32 {
|
||||||
const amount = @truncate(u5, total_amount);
|
const amount = @truncate(u5, total_amount);
|
||||||
const bit_count: u8 = @typeInfo(u32).Int.bits;
|
const bit_count: u8 = @typeInfo(u32).Int.bits;
|
||||||
|
|
||||||
@@ -119,7 +121,7 @@ pub fn lsr(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u32) u32 {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn asr(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
pub fn arithmeticRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
||||||
const amount = @truncate(u5, total_amount);
|
const amount = @truncate(u5, total_amount);
|
||||||
const bit_count: u8 = @typeInfo(u32).Int.bits;
|
const bit_count: u8 = @typeInfo(u32).Int.bits;
|
||||||
|
|
||||||
@@ -136,7 +138,7 @@ pub fn asr(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ror(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
pub fn rotateRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
||||||
const result = rotr(u32, rm, total_amount);
|
const result = rotr(u32, rm, total_amount);
|
||||||
|
|
||||||
if (S and total_amount != 0) {
|
if (S and total_amount != 0) {
|
||||||
|
|||||||
@@ -4,11 +4,16 @@ const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
|||||||
|
|
||||||
const adc = @import("../arm/data_processing.zig").adc;
|
const adc = @import("../arm/data_processing.zig").adc;
|
||||||
const sbc = @import("../arm/data_processing.zig").sbc;
|
const sbc = @import("../arm/data_processing.zig").sbc;
|
||||||
|
const sub = @import("../arm/data_processing.zig").sub;
|
||||||
|
const cmp = @import("../arm/data_processing.zig").cmp;
|
||||||
|
const cmn = @import("../arm/data_processing.zig").cmn;
|
||||||
|
const setTestOpFlags = @import("../arm/data_processing.zig").setTestOpFlags;
|
||||||
|
const setLogicOpFlags = @import("../arm/data_processing.zig").setLogicOpFlags;
|
||||||
|
|
||||||
const lsl = @import("../barrel_shifter.zig").lsl;
|
const logicalLeft = @import("../barrel_shifter.zig").logicalLeft;
|
||||||
const lsr = @import("../barrel_shifter.zig").lsr;
|
const logicalRight = @import("../barrel_shifter.zig").logicalRight;
|
||||||
const asr = @import("../barrel_shifter.zig").asr;
|
const arithmeticRight = @import("../barrel_shifter.zig").arithmeticRight;
|
||||||
const ror = @import("../barrel_shifter.zig").ror;
|
const rotateRight = @import("../barrel_shifter.zig").rotateRight;
|
||||||
|
|
||||||
pub fn fmt4(comptime op: u4) InstrFn {
|
pub fn fmt4(comptime op: u4) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
@@ -17,85 +22,96 @@ pub fn fmt4(comptime op: u4) InstrFn {
|
|||||||
const rd = opcode & 0x7;
|
const rd = opcode & 0x7;
|
||||||
const carry = @boolToInt(cpu.cpsr.c.read());
|
const carry = @boolToInt(cpu.cpsr.c.read());
|
||||||
|
|
||||||
const op1 = cpu.r[rd];
|
|
||||||
const op2 = cpu.r[rs];
|
|
||||||
|
|
||||||
var result: u32 = undefined;
|
|
||||||
var overflow: bool = undefined;
|
|
||||||
switch (op) {
|
switch (op) {
|
||||||
0x0 => result = op1 & op2, // AND
|
0x0 => {
|
||||||
0x1 => result = op1 ^ op2, // EOR
|
// AND
|
||||||
0x2 => result = lsl(true, &cpu.cpsr, op1, @truncate(u8, op2)), // LSL
|
const result = cpu.r[rd] & cpu.r[rs];
|
||||||
0x3 => result = lsr(true, &cpu.cpsr, op1, @truncate(u8, op2)), // LSR
|
cpu.r[rd] = result;
|
||||||
0x4 => result = asr(true, &cpu.cpsr, op1, @truncate(u8, op2)), // ASR
|
setLogicOpFlags(true, cpu, result);
|
||||||
0x5 => result = adc(&overflow, op1, op2, carry), // ADC
|
|
||||||
0x6 => result = sbc(op1, op2, carry), // SBC
|
|
||||||
0x7 => result = ror(true, &cpu.cpsr, op1, @truncate(u8, op2)), // ROR
|
|
||||||
0x8 => result = op1 & op2, // TST
|
|
||||||
0x9 => result = 0 -% op2, // NEG
|
|
||||||
0xA => result = op1 -% op2, // CMP
|
|
||||||
0xB => overflow = @addWithOverflow(u32, op1, op2, &result), // CMN
|
|
||||||
0xC => result = op1 | op2, // ORR
|
|
||||||
0xD => result = @truncate(u32, @as(u64, op2) * @as(u64, op1)),
|
|
||||||
0xE => result = op1 & ~op2,
|
|
||||||
0xF => result = ~op2,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write to Destination Register
|
|
||||||
switch (op) {
|
|
||||||
0x8, 0xA, 0xB => {},
|
|
||||||
else => cpu.r[rd] = result,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write Flags
|
|
||||||
switch (op) {
|
|
||||||
0x0, 0x1, 0x2, 0x3, 0x4, 0x7, 0xC, 0xE, 0xF => {
|
|
||||||
// Logic Operations
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
// C set by Barrel Shifter, V is unaffected
|
|
||||||
},
|
},
|
||||||
0x8, 0xA => {
|
0x1 => {
|
||||||
// Test Flags
|
// EOR
|
||||||
// CMN (0xB) is handled with ADC
|
const result = cpu.r[rd] ^ cpu.r[rs];
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
cpu.r[rd] = result;
|
||||||
cpu.cpsr.z.write(result == 0);
|
setLogicOpFlags(true, cpu, result);
|
||||||
|
|
||||||
if (op == 0xA) {
|
|
||||||
// CMP specific
|
|
||||||
cpu.cpsr.c.write(op2 <= op1);
|
|
||||||
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
0x5, 0xB => {
|
0x2 => {
|
||||||
// ADC, CMN
|
// LSL
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
const result = logicalLeft(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
|
||||||
cpu.cpsr.z.write(result == 0);
|
cpu.r[rd] = result;
|
||||||
cpu.cpsr.c.write(overflow);
|
setLogicOpFlags(true, cpu, result);
|
||||||
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
|
},
|
||||||
|
0x3 => {
|
||||||
|
// LSR
|
||||||
|
const result = logicalRight(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
setLogicOpFlags(true, cpu, result);
|
||||||
|
},
|
||||||
|
0x4 => {
|
||||||
|
// ASR
|
||||||
|
const result = arithmeticRight(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
setLogicOpFlags(true, cpu, result);
|
||||||
|
},
|
||||||
|
0x5 => {
|
||||||
|
// ADC
|
||||||
|
cpu.r[rd] = adc(true, cpu, cpu.r[rd], cpu.r[rs], carry);
|
||||||
},
|
},
|
||||||
0x6 => {
|
0x6 => {
|
||||||
// SBC
|
// SBC
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
cpu.r[rd] = sbc(true, cpu, cpu.r[rd], cpu.r[rs], carry);
|
||||||
cpu.cpsr.z.write(result == 0);
|
},
|
||||||
|
0x7 => {
|
||||||
const subtrahend = @as(u64, op2) -% carry +% 1;
|
// ROR
|
||||||
cpu.cpsr.c.write(subtrahend <= op1);
|
const result = rotateRight(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
|
||||||
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
cpu.r[rd] = result;
|
||||||
|
setLogicOpFlags(true, cpu, result);
|
||||||
|
},
|
||||||
|
0x8 => {
|
||||||
|
// TST
|
||||||
|
const result = cpu.r[rd] & cpu.r[rs];
|
||||||
|
setLogicOpFlags(true, cpu, result);
|
||||||
},
|
},
|
||||||
0x9 => {
|
0x9 => {
|
||||||
// NEG
|
// NEG
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
cpu.r[rd] = sub(true, cpu, 0, cpu.r[rs]);
|
||||||
cpu.cpsr.z.write(result == 0);
|
},
|
||||||
cpu.cpsr.c.write(op2 <= 0);
|
0xA => {
|
||||||
cpu.cpsr.v.write(((0 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
// CMP
|
||||||
|
cmp(cpu, cpu.r[rd], cpu.r[rs]);
|
||||||
|
},
|
||||||
|
0xB => {
|
||||||
|
// CMN
|
||||||
|
cmn(cpu, cpu.r[rd], cpu.r[rs]);
|
||||||
|
},
|
||||||
|
0xC => {
|
||||||
|
// ORR
|
||||||
|
const result = cpu.r[rd] | cpu.r[rs];
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
setLogicOpFlags(true, cpu, result);
|
||||||
},
|
},
|
||||||
0xD => {
|
0xD => {
|
||||||
// Multiplication
|
// MUL
|
||||||
|
const temp = @as(u64, cpu.r[rs]) * @as(u64, cpu.r[rd]);
|
||||||
|
const result = @truncate(u32, temp);
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
cpu.cpsr.z.write(result == 0);
|
cpu.cpsr.z.write(result == 0);
|
||||||
// V is unaffected, assuming similar behaviour to ARMv4 MUL C is undefined
|
// V is unaffected, assuming similar behaviour to ARMv4 MUL C is undefined
|
||||||
},
|
},
|
||||||
|
0xE => {
|
||||||
|
// BIC
|
||||||
|
const result = cpu.r[rd] & ~cpu.r[rs];
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
setLogicOpFlags(true, cpu, result);
|
||||||
|
},
|
||||||
|
0xF => {
|
||||||
|
// MVN
|
||||||
|
const result = ~cpu.r[rs];
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
setLogicOpFlags(true, cpu, result);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ pub fn fmt14(comptime L: bool, comptime R: bool) InstrFn {
|
|||||||
if (L) {
|
if (L) {
|
||||||
const value = bus.read(u32, address);
|
const value = bus.read(u32, address);
|
||||||
cpu.r[15] = value & ~@as(u32, 1);
|
cpu.r[15] = value & ~@as(u32, 1);
|
||||||
cpu.pipe.reload(cpu);
|
cpu.pipe.flush();
|
||||||
} else {
|
} else {
|
||||||
bus.write(u32, address, cpu.r[14]);
|
bus.write(u32, address, cpu.r[14]);
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ pub fn fmt15(comptime L: bool, comptime rb: u3) InstrFn {
|
|||||||
if (opcode & 0xFF == 0) {
|
if (opcode & 0xFF == 0) {
|
||||||
if (L) {
|
if (L) {
|
||||||
cpu.r[15] = bus.read(u32, address);
|
cpu.r[15] = bus.read(u32, address);
|
||||||
cpu.pipe.reload(cpu);
|
cpu.pipe.flush();
|
||||||
} else {
|
} else {
|
||||||
bus.write(u32, address, cpu.r[15] + 2);
|
bus.write(u32, address, cpu.r[15] + 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|||||||
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
||||||
|
|
||||||
const checkCond = @import("../../cpu.zig").checkCond;
|
const checkCond = @import("../../cpu.zig").checkCond;
|
||||||
const sext = @import("../../../util.zig").sext;
|
const sext = @import("../../util.zig").sext;
|
||||||
|
|
||||||
pub fn fmt16(comptime cond: u4) InstrFn {
|
pub fn fmt16(comptime cond: u4) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
@@ -15,7 +15,7 @@ pub fn fmt16(comptime cond: u4) InstrFn {
|
|||||||
if (!checkCond(cpu.cpsr, cond)) return;
|
if (!checkCond(cpu.cpsr, cond)) return;
|
||||||
|
|
||||||
cpu.r[15] +%= sext(u32, u8, opcode & 0xFF) << 1;
|
cpu.r[15] +%= sext(u32, u8, opcode & 0xFF) << 1;
|
||||||
cpu.pipe.reload(cpu);
|
cpu.pipe.flush();
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ pub fn fmt18() InstrFn {
|
|||||||
// B but conditional
|
// B but conditional
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||||
cpu.r[15] +%= sext(u32, u11, opcode & 0x7FF) << 1;
|
cpu.r[15] +%= sext(u32, u11, opcode & 0x7FF) << 1;
|
||||||
cpu.pipe.reload(cpu);
|
cpu.pipe.flush();
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ pub fn fmt19(comptime is_low: bool) InstrFn {
|
|||||||
cpu.r[15] = cpu.r[14] +% (offset << 1);
|
cpu.r[15] = cpu.r[14] +% (offset << 1);
|
||||||
cpu.r[14] = next_opcode | 1;
|
cpu.r[14] = next_opcode | 1;
|
||||||
|
|
||||||
cpu.pipe.reload(cpu);
|
cpu.pipe.flush();
|
||||||
} else {
|
} else {
|
||||||
// Instruction 1
|
// Instruction 1
|
||||||
const lr_offset = sext(u32, u11, offset) << 12;
|
const lr_offset = sext(u32, u11, offset) << 12;
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
|
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;
|
||||||
|
const shifter = @import("../barrel_shifter.zig");
|
||||||
|
|
||||||
const add = @import("../arm/data_processing.zig").add;
|
const add = @import("../arm/data_processing.zig").add;
|
||||||
|
const sub = @import("../arm/data_processing.zig").sub;
|
||||||
|
const cmp = @import("../arm/data_processing.zig").cmp;
|
||||||
|
const setLogicOpFlags = @import("../arm/data_processing.zig").setLogicOpFlags;
|
||||||
|
|
||||||
const lsl = @import("../barrel_shifter.zig").lsl;
|
const log = std.log.scoped(.Thumb1);
|
||||||
const lsr = @import("../barrel_shifter.zig").lsr;
|
|
||||||
const asr = @import("../barrel_shifter.zig").asr;
|
|
||||||
|
|
||||||
pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
|
pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
@@ -20,7 +24,7 @@ pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
|
|||||||
if (offset == 0) {
|
if (offset == 0) {
|
||||||
break :blk cpu.r[rs];
|
break :blk cpu.r[rs];
|
||||||
} else {
|
} else {
|
||||||
break :blk lsl(true, &cpu.cpsr, cpu.r[rs], offset);
|
break :blk shifter.logicalLeft(true, &cpu.cpsr, cpu.r[rs], offset);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
0b01 => blk: {
|
0b01 => blk: {
|
||||||
@@ -29,7 +33,7 @@ pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
|
|||||||
cpu.cpsr.c.write(cpu.r[rs] >> 31 & 1 == 1);
|
cpu.cpsr.c.write(cpu.r[rs] >> 31 & 1 == 1);
|
||||||
break :blk @as(u32, 0);
|
break :blk @as(u32, 0);
|
||||||
} else {
|
} else {
|
||||||
break :blk lsr(true, &cpu.cpsr, cpu.r[rs], offset);
|
break :blk shifter.logicalRight(true, &cpu.cpsr, cpu.r[rs], offset);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
0b10 => blk: {
|
0b10 => blk: {
|
||||||
@@ -38,7 +42,7 @@ pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
|
|||||||
cpu.cpsr.c.write(cpu.r[rs] >> 31 & 1 == 1);
|
cpu.cpsr.c.write(cpu.r[rs] >> 31 & 1 == 1);
|
||||||
break :blk @bitCast(u32, @bitCast(i32, cpu.r[rs]) >> 31);
|
break :blk @bitCast(u32, @bitCast(i32, cpu.r[rs]) >> 31);
|
||||||
} else {
|
} else {
|
||||||
break :blk asr(true, &cpu.cpsr, cpu.r[rs], offset);
|
break :blk shifter.arithmeticRight(true, &cpu.cpsr, cpu.r[rs], offset);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
else => cpu.panic("[CPU/THUMB.1] 0b{b:0>2} is not a valid op", .{op}),
|
else => cpu.panic("[CPU/THUMB.1] 0b{b:0>2} is not a valid op", .{op}),
|
||||||
@@ -46,10 +50,7 @@ pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
|
|||||||
|
|
||||||
// Equivalent to an ARM MOVS
|
// Equivalent to an ARM MOVS
|
||||||
cpu.r[rd] = result;
|
cpu.r[rd] = result;
|
||||||
|
setLogicOpFlags(true, cpu, result);
|
||||||
// Write Flags
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
}
|
}
|
||||||
@@ -57,51 +58,28 @@ pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
|
|||||||
pub fn fmt5(comptime op: u2, comptime h1: u1, comptime h2: u1) InstrFn {
|
pub fn fmt5(comptime op: u2, comptime h1: u1, comptime h2: u1) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||||
const rs = @as(u4, h2) << 3 | (opcode >> 3 & 0x7);
|
const src_idx = @as(u4, h2) << 3 | (opcode >> 3 & 0x7);
|
||||||
const rd = @as(u4, h1) << 3 | (opcode & 0x7);
|
const dst_idx = @as(u4, h1) << 3 | (opcode & 0x7);
|
||||||
|
|
||||||
const op1 = cpu.r[rd];
|
const src = if (src_idx == 0xF) (cpu.r[src_idx] + 2) & 0xFFFF_FFFE else cpu.r[src_idx];
|
||||||
const op2 = cpu.r[rs];
|
const dst = if (dst_idx == 0xF) (cpu.r[dst_idx] + 2) & 0xFFFF_FFFE else cpu.r[dst_idx];
|
||||||
|
|
||||||
var result: u32 = undefined;
|
|
||||||
var overflow: bool = undefined;
|
|
||||||
switch (op) {
|
switch (op) {
|
||||||
0b00 => result = add(&overflow, op1, op2), // ADD
|
0b00 => {
|
||||||
0b01 => result = op1 -% op2, // CMP
|
// ADD
|
||||||
0b10 => result = op2, // MOV
|
const sum = add(false, cpu, dst, src);
|
||||||
0b11 => {},
|
cpu.r[dst_idx] = if (dst_idx == 0xF) sum & 0xFFFF_FFFE else sum;
|
||||||
}
|
},
|
||||||
|
0b01 => cmp(cpu, dst, src), // CMP
|
||||||
// Write to Destination Register
|
0b10 => {
|
||||||
switch (op) {
|
// MOV
|
||||||
0b01 => {}, // Test Instruction
|
cpu.r[dst_idx] = if (dst_idx == 0xF) src & 0xFFFF_FFFE else src;
|
||||||
|
},
|
||||||
0b11 => {
|
0b11 => {
|
||||||
// BX
|
// BX
|
||||||
const is_thumb = op2 & 1 == 1;
|
cpu.cpsr.t.write(src & 1 == 1);
|
||||||
cpu.r[15] = op2 & ~@as(u32, 1);
|
cpu.r[15] = src & 0xFFFF_FFFE;
|
||||||
|
|
||||||
cpu.cpsr.t.write(is_thumb);
|
|
||||||
cpu.pipe.reload(cpu);
|
|
||||||
},
|
},
|
||||||
else => {
|
|
||||||
cpu.r[rd] = result;
|
|
||||||
if (rd == 0xF) {
|
|
||||||
cpu.r[15] &= ~@as(u32, 1);
|
|
||||||
cpu.pipe.reload(cpu);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write Flags
|
|
||||||
switch (op) {
|
|
||||||
0b01 => {
|
|
||||||
// CMP
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
cpu.cpsr.c.write(op2 <= op1);
|
|
||||||
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
|
||||||
},
|
|
||||||
0b00, 0b10, 0b11 => {}, // MOV and Branch Instruction
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
@@ -112,28 +90,21 @@ pub fn fmt2(comptime I: bool, is_sub: bool, rn: u3) InstrFn {
|
|||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||||
const rs = opcode >> 3 & 0x7;
|
const rs = opcode >> 3 & 0x7;
|
||||||
const rd = @truncate(u3, opcode);
|
const rd = @truncate(u3, opcode);
|
||||||
const op1 = cpu.r[rs];
|
|
||||||
const op2: u32 = if (I) rn else cpu.r[rn];
|
|
||||||
|
|
||||||
if (is_sub) {
|
if (is_sub) {
|
||||||
// SUB
|
// SUB
|
||||||
const result = op1 -% op2;
|
cpu.r[rd] = if (I) blk: {
|
||||||
cpu.r[rd] = result;
|
break :blk sub(true, cpu, cpu.r[rs], rn);
|
||||||
|
} else blk: {
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
break :blk sub(true, cpu, cpu.r[rs], cpu.r[rn]);
|
||||||
cpu.cpsr.z.write(result == 0);
|
};
|
||||||
cpu.cpsr.c.write(op2 <= op1);
|
|
||||||
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
|
||||||
} else {
|
} else {
|
||||||
// ADD
|
// ADD
|
||||||
var overflow: bool = undefined;
|
cpu.r[rd] = if (I) blk: {
|
||||||
const result = add(&overflow, op1, op2);
|
break :blk add(true, cpu, cpu.r[rs], rn);
|
||||||
cpu.r[rd] = result;
|
} else blk: {
|
||||||
|
break :blk add(true, cpu, cpu.r[rs], cpu.r[rn]);
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
};
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
cpu.cpsr.c.write(overflow);
|
|
||||||
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
@@ -142,36 +113,17 @@ pub fn fmt2(comptime I: bool, is_sub: bool, rn: u3) InstrFn {
|
|||||||
pub fn fmt3(comptime op: u2, comptime rd: u3) InstrFn {
|
pub fn fmt3(comptime op: u2, comptime rd: u3) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||||
const op1 = cpu.r[rd];
|
const offset = @truncate(u8, opcode);
|
||||||
const op2: u32 = opcode & 0xFF; // Offset
|
|
||||||
|
|
||||||
var overflow: bool = undefined;
|
|
||||||
const result: u32 = switch (op) {
|
|
||||||
0b00 => op2, // MOV
|
|
||||||
0b01 => op1 -% op2, // CMP
|
|
||||||
0b10 => add(&overflow, op1, op2), // ADD
|
|
||||||
0b11 => op1 -% op2, // SUB
|
|
||||||
};
|
|
||||||
|
|
||||||
// Write to Register
|
|
||||||
if (op != 0b01) cpu.r[rd] = result;
|
|
||||||
|
|
||||||
// Write Flags
|
|
||||||
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
|
||||||
cpu.cpsr.z.write(result == 0);
|
|
||||||
|
|
||||||
switch (op) {
|
switch (op) {
|
||||||
0b00 => {}, // MOV | C set by Barrel Shifter, V is unaffected
|
0b00 => {
|
||||||
0b01, 0b11 => {
|
// MOV
|
||||||
// SUB, CMP
|
cpu.r[rd] = offset;
|
||||||
cpu.cpsr.c.write(op2 <= op1);
|
setLogicOpFlags(true, cpu, offset);
|
||||||
cpu.cpsr.v.write(((op1 ^ result) & (~op2 ^ result)) >> 31 & 1 == 1);
|
|
||||||
},
|
|
||||||
0b10 => {
|
|
||||||
// ADD
|
|
||||||
cpu.cpsr.c.write(overflow);
|
|
||||||
cpu.cpsr.v.write(((op1 ^ result) & (op2 ^ result)) >> 31 & 1 == 1);
|
|
||||||
},
|
},
|
||||||
|
0b01 => cmp(cpu, cpu.r[rd], offset), // CMP
|
||||||
|
0b10 => cpu.r[rd] = add(true, cpu, cpu.r[rd], offset), // ADD
|
||||||
|
0b11 => cpu.r[rd] = sub(true, cpu, cpu.r[rd], offset), // SUB
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
const rotr = @import("../../../util.zig").rotr;
|
const rotr = @import("../../util.zig").rotr;
|
||||||
const sext = @import("../../../util.zig").sext;
|
|
||||||
|
|
||||||
pub fn fmt6(comptime rd: u3) InstrFn {
|
pub fn fmt6(comptime rd: u3) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
@@ -17,6 +18,8 @@ pub fn fmt6(comptime rd: u3) InstrFn {
|
|||||||
}.inner;
|
}.inner;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sext = @import("../../util.zig").sext;
|
||||||
|
|
||||||
pub fn fmt78(comptime op: u2, comptime T: bool) InstrFn {
|
pub fn fmt78(comptime op: u2, comptime T: bool) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
|
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
|
||||||
@@ -44,8 +47,11 @@ pub fn fmt78(comptime op: u2, comptime T: bool) InstrFn {
|
|||||||
},
|
},
|
||||||
0b11 => {
|
0b11 => {
|
||||||
// LDRSH
|
// LDRSH
|
||||||
const value = bus.read(u16, address);
|
cpu.r[rd] = if (address & 1 == 1) blk: {
|
||||||
cpu.r[rd] = if (address & 1 == 1) sext(u32, u8, @truncate(u8, value >> 8)) else sext(u32, u16, value);
|
break :blk sext(u32, u8, bus.read(u8, address));
|
||||||
|
} else blk: {
|
||||||
|
break :blk sext(u32, u16, bus.read(u16, address));
|
||||||
|
};
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ pub fn fmt17() InstrFn {
|
|||||||
cpu.r[14] = ret_addr; // Resume Execution
|
cpu.r[14] = ret_addr; // Resume Execution
|
||||||
cpu.spsr.raw = cpsr; // Previous mode CPSR
|
cpu.spsr.raw = cpsr; // Previous mode CPSR
|
||||||
cpu.r[15] = 0x0000_0008;
|
cpu.r[15] = 0x0000_0008;
|
||||||
cpu.pipe.reload(cpu);
|
cpu.pipe.flush();
|
||||||
}
|
}
|
||||||
}.inner;
|
}.inner;
|
||||||
}
|
}
|
||||||
|
|||||||
198
src/core/emu.zig
198
src/core/emu.zig
@@ -1,31 +1,35 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const SDL = @import("sdl2");
|
const SDL = @import("sdl2");
|
||||||
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 RingBuffer = @import("../util.zig").RingBuffer;
|
const FilePaths = @import("util.zig").FilePaths;
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
/// 4 Cycles in 1 dot
|
// TODO: Move these to a TOML File
|
||||||
const cycles_per_dot = 4;
|
const sync_audio = true; // Enable Audio Sync
|
||||||
|
const sync_video: RunKind = .LimitedFPS; // Configure Video Sync
|
||||||
|
pub const win_scale = 3; // 1x, 2x, 3x, etc. Window Scaling
|
||||||
|
pub const cpu_logging = false; // Enable detailed CPU logging
|
||||||
|
|
||||||
/// The GBA draws 228 Horizontal which each consist 308 dots
|
// 228 Lines which consist of 308 dots (which are 4 cycles long)
|
||||||
/// (note: not all lines are visible)
|
const cycles_per_frame: u64 = 228 * (308 * 4); //280896
|
||||||
const cycles_per_frame = 228 * (308 * cycles_per_dot); //280896
|
const clock_rate: u64 = 1 << 24; // 16.78MHz
|
||||||
|
|
||||||
/// The GBA ARM7TDMI runs at 2^24 Hz
|
// TODO: Don't truncate this, be more accurate w/ timing
|
||||||
const clock_rate = 1 << 24; // 16.78MHz
|
// 59.6046447754ns (truncated to just 59ns)
|
||||||
|
const clock_period: u64 = std.time.ns_per_s / clock_rate;
|
||||||
|
const frame_period = (clock_period * cycles_per_frame);
|
||||||
|
|
||||||
/// The # of nanoseconds a frame should take
|
// 59.7275005696Hz
|
||||||
const frame_period = (std.time.ns_per_s * cycles_per_frame) / clock_rate;
|
pub const frame_rate = @intToFloat(f64, std.time.ns_per_s) /
|
||||||
|
((@intToFloat(f64, std.time.ns_per_s) / @intToFloat(f64, clock_rate)) * @intToFloat(f64, cycles_per_frame));
|
||||||
/// 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);
|
||||||
|
|
||||||
@@ -34,57 +38,18 @@ const RunKind = enum {
|
|||||||
UnlimitedFPS,
|
UnlimitedFPS,
|
||||||
Limited,
|
Limited,
|
||||||
LimitedFPS,
|
LimitedFPS,
|
||||||
|
LimitedBusy,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn run(quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: *FpsTracker) void {
|
pub fn run(quit: *Atomic(bool), fps: *FpsTracker, sched: *Scheduler, cpu: *Arm7tdmi) void {
|
||||||
const audio_sync = config.config().guest.audio_sync and !config.config().host.mute;
|
if (sync_audio) log.info("Audio sync enabled", .{});
|
||||||
if (audio_sync) log.info("Audio sync enabled", .{});
|
|
||||||
|
|
||||||
if (config.config().guest.video_sync) {
|
switch (sync_video) {
|
||||||
inner(.LimitedFPS, audio_sync, quit, scheduler, cpu, tracker);
|
.Unlimited => runUnsynchronized(quit, sched, cpu, null),
|
||||||
} else {
|
.Limited => runSynchronized(quit, sched, cpu, null),
|
||||||
inner(.UnlimitedFPS, audio_sync, quit, scheduler, cpu, tracker);
|
.UnlimitedFPS => runUnsynchronized(quit, sched, cpu, fps),
|
||||||
}
|
.LimitedFPS => runSynchronized(quit, sched, cpu, fps),
|
||||||
}
|
.LimitedBusy => runBusyLoop(quit, sched, cpu),
|
||||||
|
|
||||||
fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: ?*FpsTracker) void {
|
|
||||||
if (kind == .UnlimitedFPS or kind == .LimitedFPS) {
|
|
||||||
std.debug.assert(tracker != null);
|
|
||||||
log.info("FPS tracking enabled", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (kind) {
|
|
||||||
.Unlimited, .UnlimitedFPS => {
|
|
||||||
log.info("Emulation w/out video sync", .{});
|
|
||||||
|
|
||||||
while (!quit.load(.Monotonic)) {
|
|
||||||
runFrame(scheduler, cpu);
|
|
||||||
audioSync(audio_sync, &cpu.bus.apu.sample_queue);
|
|
||||||
|
|
||||||
if (kind == .UnlimitedFPS) tracker.?.tick();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.Limited, .LimitedFPS => {
|
|
||||||
log.info("Emulation w/ video sync", .{});
|
|
||||||
var timer = Timer.start() catch @panic("failed to initalize std.timer.Timer");
|
|
||||||
var wake_time: u64 = frame_period;
|
|
||||||
|
|
||||||
while (!quit.load(.Monotonic)) {
|
|
||||||
runFrame(scheduler, cpu);
|
|
||||||
const new_wake_time = videoSync(&timer, wake_time);
|
|
||||||
|
|
||||||
// Spin to make up the difference of OS scheduler innacuracies
|
|
||||||
// If we happen to also be syncing to audio, we choose to spin on
|
|
||||||
// the amount of time needed for audio to catch up rather than
|
|
||||||
// our expected wake-up time
|
|
||||||
|
|
||||||
audioSync(audio_sync, &cpu.bus.apu.sample_queue);
|
|
||||||
if (!audio_sync) spinLoop(&timer, wake_time);
|
|
||||||
wake_time = new_wake_time;
|
|
||||||
|
|
||||||
if (kind == .LimitedFPS) tracker.?.tick();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,30 +70,96 @@ pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn audioSync(audio_sync: bool, sample_queue: *RingBuffer(u16)) void {
|
fn syncToAudio(cpu: *const Arm7tdmi) void {
|
||||||
comptime std.debug.assert(@import("../platform.zig").sample_format == SDL.AUDIO_U16);
|
const stream = cpu.bus.apu.stream;
|
||||||
// const sample_size = 2 * @sizeOf(u16);
|
const min_sample_count = 0x800;
|
||||||
// const max_buf_size: c_int = 0x400;
|
|
||||||
|
|
||||||
_ = audio_sync;
|
// Busy Loop while we wait for the Audio system to catch up
|
||||||
_ = sample_queue;
|
while (SDL.SDL_AudioStreamAvailable(stream) > (@sizeOf(u16) * 2) * min_sample_count) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn videoSync(timer: *Timer, wake_time: u64) u64 {
|
pub fn runUnsynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*FpsTracker) void {
|
||||||
|
log.info("Emulation thread w/out video sync", .{});
|
||||||
|
|
||||||
|
if (fps) |tracker| {
|
||||||
|
log.info("FPS Tracking Enabled", .{});
|
||||||
|
|
||||||
|
while (!quit.load(.SeqCst)) {
|
||||||
|
runFrame(sched, cpu);
|
||||||
|
if (sync_audio) syncToAudio(cpu);
|
||||||
|
|
||||||
|
tracker.tick();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while (!quit.load(.SeqCst)) {
|
||||||
|
runFrame(sched, cpu);
|
||||||
|
if (sync_audio) syncToAudio(cpu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runSynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*FpsTracker) void {
|
||||||
|
log.info("Emulation thread w/ video sync", .{});
|
||||||
|
var timer = Timer.start() catch unreachable;
|
||||||
|
var wake_time: u64 = frame_period;
|
||||||
|
|
||||||
|
if (fps) |tracker| {
|
||||||
|
log.info("FPS Tracking Enabled", .{});
|
||||||
|
|
||||||
|
while (!quit.load(.SeqCst)) {
|
||||||
|
runFrame(sched, cpu);
|
||||||
|
const new_wake_time = syncToVideo(&timer, wake_time);
|
||||||
|
|
||||||
|
// Spin to make up the difference of OS scheduler innacuracies
|
||||||
|
// If we happen to also be syncing to audio, we choose to spin on
|
||||||
|
// the amount of time needed for audio to catch up rather than
|
||||||
|
// our expected wake-up time
|
||||||
|
if (sync_audio) syncToAudio(cpu) else spinLoop(&timer, wake_time);
|
||||||
|
wake_time = new_wake_time;
|
||||||
|
|
||||||
|
tracker.tick();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while (!quit.load(.SeqCst)) {
|
||||||
|
runFrame(sched, cpu);
|
||||||
|
const new_wake_time = syncToVideo(&timer, wake_time);
|
||||||
|
// see above comment
|
||||||
|
if (sync_audio) syncToAudio(cpu) else spinLoop(&timer, wake_time);
|
||||||
|
|
||||||
|
wake_time = new_wake_time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn syncToVideo(timer: *Timer, wake_time: u64) u64 {
|
||||||
// Use the OS scheduler to put the emulation thread to sleep
|
// Use the OS scheduler to put the emulation thread to sleep
|
||||||
const recalculated = sleep(timer, wake_time);
|
const maybe_recalc_wake_time = sleep(timer, wake_time);
|
||||||
|
|
||||||
// If sleep() determined we need to adjust our wake up time, do so
|
// If sleep() determined we need to adjust our wake up time, do so
|
||||||
// otherwise predict our next wake up time according to the frame period
|
// otherwise predict our next wake up time according to the frame period
|
||||||
return recalculated orelse wake_time + frame_period;
|
return if (maybe_recalc_wake_time) |recalc| recalc else wake_time + frame_period;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runBusyLoop(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi) void {
|
||||||
|
log.info("Emulation thread with video sync using busy loop", .{});
|
||||||
|
var timer = Timer.start() catch unreachable;
|
||||||
|
var wake_time: u64 = frame_period;
|
||||||
|
|
||||||
|
while (!quit.load(.SeqCst)) {
|
||||||
|
runFrame(sched, cpu);
|
||||||
|
spinLoop(&timer, wake_time);
|
||||||
|
|
||||||
|
// Update to the new wake time
|
||||||
|
wake_time += frame_period;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Better sleep impl?
|
|
||||||
fn sleep(timer: *Timer, wake_time: u64) ?u64 {
|
fn sleep(timer: *Timer, wake_time: u64) ?u64 {
|
||||||
|
// const step = std.time.ns_per_ms * 10; // 10ms
|
||||||
const timestamp = timer.read();
|
const timestamp = timer.read();
|
||||||
|
|
||||||
// ns_late is non zero if we are late.
|
// ns_late is non zero if we are late.
|
||||||
var ns_late = timestamp -| wake_time;
|
const ns_late = timestamp -| wake_time;
|
||||||
|
|
||||||
// If we're more than a frame late, skip the rest of this loop
|
// If we're more than a frame late, skip the rest of this loop
|
||||||
// Recalculate what our new wake time should be so that we can
|
// Recalculate what our new wake time should be so that we can
|
||||||
@@ -136,18 +167,15 @@ fn sleep(timer: *Timer, wake_time: u64) ?u64 {
|
|||||||
if (ns_late > frame_period) return timestamp + frame_period;
|
if (ns_late > frame_period) return timestamp + frame_period;
|
||||||
const sleep_for = frame_period - ns_late;
|
const sleep_for = frame_period - ns_late;
|
||||||
|
|
||||||
const step = 2 * std.time.ns_per_ms; // Granularity of 2ms
|
// // Employ several sleep calls in periods of 10ms
|
||||||
const times = sleep_for / step;
|
// // By doing this the behaviour should average out to be
|
||||||
var i: usize = 0;
|
// // more consistent
|
||||||
|
// const loop_count = sleep_for / step; // How many groups of 10ms
|
||||||
|
|
||||||
while (i < times) : (i += 1) {
|
// var i: usize = 0;
|
||||||
std.time.sleep(step);
|
// while (i < loop_count) : (i += 1) std.time.sleep(step);
|
||||||
|
|
||||||
// Upon wakeup, check to see if this particular sleep was longer than expected
|
std.time.sleep(sleep_for);
|
||||||
// if so we should exit early, but probably not skip a whole frame period
|
|
||||||
ns_late = timer.read() -| wake_time;
|
|
||||||
if (ns_late > frame_period) return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
514
src/core/ppu.zig
514
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,229 +10,15 @@ 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 pollBlankingDma = @import("bus/dma.zig").pollBlankingDma;
|
||||||
|
|
||||||
const getHalf = util.getHalf;
|
/// This is used to generate byuu / Talurabi's Color Correction algorithm
|
||||||
const setHalf = util.setHalf;
|
const COLOUR_LUT = genColourLut();
|
||||||
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();
|
||||||
|
|
||||||
@@ -246,55 +32,60 @@ pub const Ppu = struct {
|
|||||||
dispstat: io.DisplayStatus,
|
dispstat: io.DisplayStatus,
|
||||||
vcount: io.VCount,
|
vcount: io.VCount,
|
||||||
|
|
||||||
bld: Blend,
|
bldcnt: io.BldCnt,
|
||||||
|
bldalpha: io.BldAlpha,
|
||||||
|
bldy: io.BldY,
|
||||||
|
|
||||||
vram: Vram,
|
vram: Vram,
|
||||||
palette: Palette,
|
palette: Palette,
|
||||||
oam: Oam,
|
oam: Oam,
|
||||||
sched: *Scheduler,
|
sched: *Scheduler,
|
||||||
framebuf: FrameBuffer,
|
framebuf: FrameBuffer,
|
||||||
allocator: Allocator,
|
alloc: Allocator,
|
||||||
|
|
||||||
scanline_sprites: *[128]?Sprite,
|
scanline_sprites: [128]?Sprite,
|
||||||
scanline: Scanline,
|
scanline: Scanline,
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, sched: *Scheduler) !Self {
|
pub fn init(alloc: Allocator, sched: *Scheduler) !Self {
|
||||||
// Queue first Hblank
|
// Queue first Hblank
|
||||||
sched.push(.Draw, 240 * 4);
|
sched.push(.Draw, 240 * 4);
|
||||||
|
|
||||||
const sprites = try allocator.create([128]?Sprite);
|
const framebufs = try alloc.alloc(u8, (framebuf_pitch * height) * 2);
|
||||||
sprites.* = [_]?Sprite{null} ** 128;
|
std.mem.set(u8, framebufs, 0);
|
||||||
|
|
||||||
|
const scanline_buf = try alloc.alloc(?u16, width * 2);
|
||||||
|
std.mem.set(?u16, scanline_buf, null);
|
||||||
|
|
||||||
return Self{
|
return Self{
|
||||||
.vram = try Vram.init(allocator),
|
.vram = try Vram.init(alloc),
|
||||||
.palette = try Palette.init(allocator),
|
.palette = try Palette.init(alloc),
|
||||||
.oam = try Oam.init(allocator),
|
.oam = try Oam.init(alloc),
|
||||||
.sched = sched,
|
.sched = sched,
|
||||||
.framebuf = try FrameBuffer.init(allocator),
|
.framebuf = FrameBuffer.init(framebufs),
|
||||||
.allocator = allocator,
|
.alloc = alloc,
|
||||||
|
|
||||||
// Registers
|
// Registers
|
||||||
.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 = Scanline.init(scanline_buf),
|
||||||
.scanline_sprites = sprites,
|
.scanline_sprites = [_]?Sprite{null} ** 128,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
pub fn deinit(self: Self) void {
|
||||||
self.allocator.destroy(self.scanline_sprites);
|
self.framebuf.deinit(self.alloc);
|
||||||
self.framebuf.deinit();
|
self.scanline.deinit(self.alloc);
|
||||||
self.scanline.deinit();
|
|
||||||
self.vram.deinit();
|
self.vram.deinit();
|
||||||
self.palette.deinit();
|
self.palette.deinit();
|
||||||
self.oam.deinit();
|
self.oam.deinit();
|
||||||
self.* = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setBgOffsets(self: *Self, comptime n: u2, word: u32) void {
|
pub fn setBgOffsets(self: *Self, comptime n: u2, word: u32) void {
|
||||||
@@ -375,7 +166,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.bld.cnt, &self.scanline, x)) continue;
|
if (!shouldDrawSprite(self.bldcnt, &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);
|
||||||
@@ -404,7 +195,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.bld.cnt, &self.scanline, x, bgr555);
|
copyToSpriteBuffer(self.bldcnt, &self.scanline, x, bgr555);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,7 +216,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.bld.cnt, &self.scanline, x)) continue;
|
if (!shouldDrawSprite(self.bldcnt, &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);
|
||||||
@@ -460,7 +251,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.bld.cnt, &self.scanline, x, bgr555);
|
copyToSpriteBuffer(self.bldcnt, &self.scanline, x, bgr555);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -487,7 +278,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.bld.cnt, &self.scanline, i)) continue;
|
if (!shouldDrawBackground(n, self.bldcnt, &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;
|
||||||
@@ -506,7 +297,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.bld.cnt, &self.scanline, i, bgr555);
|
copyToBackgroundBuffer(n, self.bldcnt, &self.scanline, i, bgr555);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,7 +326,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.bld.cnt, &self.scanline, i)) continue;
|
if (!shouldDrawBackground(n, self.bldcnt, &self.scanline, i)) continue;
|
||||||
|
|
||||||
const x = hofs + i;
|
const x = hofs + i;
|
||||||
|
|
||||||
@@ -563,7 +354,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.bld.cnt, &self.scanline, i, bgr555);
|
copyToBackgroundBuffer(n, self.bldcnt, &self.scanline, i, bgr555);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -602,13 +393,13 @@ 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)], rgba888(bgr555));
|
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset Current Scanline Pixel Buffer and list of fetched sprites
|
// Reset Current Scanline Pixel Buffer and list of fetched sprites
|
||||||
// in prep for next scanline
|
// in prep for next scanline
|
||||||
self.scanline.reset();
|
self.scanline.reset();
|
||||||
std.mem.set(?Sprite, self.scanline_sprites, null);
|
std.mem.set(?Sprite, &self.scanline_sprites, null);
|
||||||
},
|
},
|
||||||
0x1 => {
|
0x1 => {
|
||||||
const fb_base = framebuf_pitch * @as(usize, scanline);
|
const fb_base = framebuf_pitch * @as(usize, scanline);
|
||||||
@@ -629,13 +420,13 @@ 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)], rgba888(bgr555));
|
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset Current Scanline Pixel Buffer and list of fetched sprites
|
// Reset Current Scanline Pixel Buffer and list of fetched sprites
|
||||||
// in prep for next scanline
|
// in prep for next scanline
|
||||||
self.scanline.reset();
|
self.scanline.reset();
|
||||||
std.mem.set(?Sprite, self.scanline_sprites, null);
|
std.mem.set(?Sprite, &self.scanline_sprites, null);
|
||||||
},
|
},
|
||||||
0x2 => {
|
0x2 => {
|
||||||
const fb_base = framebuf_pitch * @as(usize, scanline);
|
const fb_base = framebuf_pitch * @as(usize, scanline);
|
||||||
@@ -655,13 +446,13 @@ 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)], rgba888(bgr555));
|
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset Current Scanline Pixel Buffer and list of fetched sprites
|
// Reset Current Scanline Pixel Buffer and list of fetched sprites
|
||||||
// in prep for next scanline
|
// in prep for next scanline
|
||||||
self.scanline.reset();
|
self.scanline.reset();
|
||||||
std.mem.set(?Sprite, self.scanline_sprites, null);
|
std.mem.set(?Sprite, &self.scanline_sprites, null);
|
||||||
},
|
},
|
||||||
0x3 => {
|
0x3 => {
|
||||||
const vram_base = width * @sizeOf(u16) * @as(usize, scanline);
|
const vram_base = width * @sizeOf(u16) * @as(usize, scanline);
|
||||||
@@ -670,7 +461,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)], rgba888(bgr555));
|
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
0x4 => {
|
0x4 => {
|
||||||
@@ -681,7 +472,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)], rgba888(bgr555));
|
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
0x5 => {
|
0x5 => {
|
||||||
@@ -698,7 +489,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)], rgba888(bgr555));
|
std.mem.writeIntNative(u32, self.framebuf.get(.Emulator)[fb_base + i * @sizeOf(u32) ..][0..@sizeOf(u32)], COLOUR_LUT[bgr555 & 0x7FFF]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
else => std.debug.panic("[PPU] TODO: Implement BG Mode {}", .{bg_mode}),
|
else => std.debug.panic("[PPU] TODO: Implement BG Mode {}", .{bg_mode}),
|
||||||
@@ -707,11 +498,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.bld.cnt.mode.read()) {
|
return switch (self.bldcnt.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.bld.alpha) else btm,
|
0b01 => if (maybe_top) |top| alphaBlend(btm, top, self.bldalpha) else btm,
|
||||||
0b10 => blk: {
|
0b10 => blk: {
|
||||||
const evy: u16 = self.bld.y.evy.read();
|
const evy: u16 = self.bldy.evy.read();
|
||||||
|
|
||||||
const r = btm & 0x1F;
|
const r = btm & 0x1F;
|
||||||
const g = (btm >> 5) & 0x1F;
|
const g = (btm >> 5) & 0x1F;
|
||||||
@@ -724,7 +515,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.bld.y.evy.read();
|
const evy: u16 = self.bldy.evy.read();
|
||||||
|
|
||||||
const btm_r = btm & 0x1F;
|
const btm_r = btm & 0x1F;
|
||||||
const btm_g = (btm >> 5) & 0x1F;
|
const btm_g = (btm >> 5) & 0x1F;
|
||||||
@@ -772,7 +563,7 @@ pub const Ppu = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn onHdrawEnd(self: *Self, cpu: *Arm7tdmi, late: u64) void {
|
pub fn handleHDrawEnd(self: *Self, cpu: *Arm7tdmi, late: u64) void {
|
||||||
// Transitioning to a Hblank
|
// Transitioning to a Hblank
|
||||||
if (self.dispstat.hblank_irq.read()) {
|
if (self.dispstat.hblank_irq.read()) {
|
||||||
cpu.bus.io.irq.hblank.set();
|
cpu.bus.io.irq.hblank.set();
|
||||||
@@ -782,13 +573,13 @@ pub const Ppu = struct {
|
|||||||
// See if HBlank DMA is present and not enabled
|
// See if HBlank DMA is present and not enabled
|
||||||
|
|
||||||
if (!self.dispstat.vblank.read())
|
if (!self.dispstat.vblank.read())
|
||||||
pollDmaOnBlank(cpu.bus, .HBlank);
|
pollBlankingDma(cpu.bus, .HBlank);
|
||||||
|
|
||||||
self.dispstat.hblank.set();
|
self.dispstat.hblank.set();
|
||||||
self.sched.push(.HBlank, 68 * 4 -| late);
|
self.sched.push(.HBlank, 68 * 4 -| late);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn onHblankEnd(self: *Self, cpu: *Arm7tdmi, late: u64) void {
|
pub fn handleHBlankEnd(self: *Self, cpu: *Arm7tdmi, late: u64) void {
|
||||||
// The End of a Hblank (During Draw or Vblank)
|
// The End of a Hblank (During Draw or Vblank)
|
||||||
const old_scanline = self.vcount.scanline.read();
|
const old_scanline = self.vcount.scanline.read();
|
||||||
const scanline = (old_scanline + 1) % 228;
|
const scanline = (old_scanline + 1) % 228;
|
||||||
@@ -824,7 +615,7 @@ pub const Ppu = struct {
|
|||||||
self.aff_bg[1].latchRefPoints();
|
self.aff_bg[1].latchRefPoints();
|
||||||
|
|
||||||
// See if Vblank DMA is present and not enabled
|
// See if Vblank DMA is present and not enabled
|
||||||
pollDmaOnBlank(cpu.bus, .VBlank);
|
pollBlankingDma(cpu.bus, .VBlank);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scanline == 227) self.dispstat.vblank.unset();
|
if (scanline == 227) self.dispstat.vblank.unset();
|
||||||
@@ -838,21 +629,20 @@ const Palette = struct {
|
|||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
buf: []u8,
|
buf: []u8,
|
||||||
allocator: Allocator,
|
alloc: Allocator,
|
||||||
|
|
||||||
fn init(allocator: Allocator) !Self {
|
fn init(alloc: Allocator) !Self {
|
||||||
const buf = try allocator.alloc(u8, palram_size);
|
const buf = try alloc.alloc(u8, palram_size);
|
||||||
std.mem.set(u8, buf, 0);
|
std.mem.set(u8, buf, 0);
|
||||||
|
|
||||||
return Self{
|
return Self{
|
||||||
.buf = buf,
|
.buf = buf,
|
||||||
.allocator = allocator,
|
.alloc = alloc,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deinit(self: *Self) void {
|
fn deinit(self: Self) void {
|
||||||
self.allocator.free(self.buf);
|
self.alloc.free(self.buf);
|
||||||
self.* = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read(self: *const Self, comptime T: type, address: usize) T {
|
pub fn read(self: *const Self, comptime T: type, address: usize) T {
|
||||||
@@ -882,26 +672,25 @@ const Palette = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Vram = struct {
|
const Vram = struct {
|
||||||
const vram_size = 0x18000;
|
const vram_size = 0x18000;
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
buf: []u8,
|
buf: []u8,
|
||||||
allocator: Allocator,
|
alloc: Allocator,
|
||||||
|
|
||||||
fn init(allocator: Allocator) !Self {
|
fn init(alloc: Allocator) !Self {
|
||||||
const buf = try allocator.alloc(u8, vram_size);
|
const buf = try alloc.alloc(u8, vram_size);
|
||||||
std.mem.set(u8, buf, 0);
|
std.mem.set(u8, buf, 0);
|
||||||
|
|
||||||
return Self{
|
return Self{
|
||||||
.buf = buf,
|
.buf = buf,
|
||||||
.allocator = allocator,
|
.alloc = alloc,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deinit(self: *Self) void {
|
fn deinit(self: Self) void {
|
||||||
self.allocator.free(self.buf);
|
self.alloc.free(self.buf);
|
||||||
self.* = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read(self: *const Self, comptime T: type, address: usize) T {
|
pub fn read(self: *const Self, comptime T: type, address: usize) T {
|
||||||
@@ -933,7 +722,7 @@ pub const Vram = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mirror(address: usize) usize {
|
fn mirror(address: usize) usize {
|
||||||
// Mirrored in steps of 128K (64K + 32K + 32K) (abcc)
|
// Mirrored in steps of 128K (64K + 32K + 32K) (abcc)
|
||||||
const addr = address & 0x1FFFF;
|
const addr = address & 0x1FFFF;
|
||||||
|
|
||||||
@@ -948,21 +737,20 @@ const Oam = struct {
|
|||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
buf: []u8,
|
buf: []u8,
|
||||||
allocator: Allocator,
|
alloc: Allocator,
|
||||||
|
|
||||||
fn init(allocator: Allocator) !Self {
|
fn init(alloc: Allocator) !Self {
|
||||||
const buf = try allocator.alloc(u8, oam_size);
|
const buf = try alloc.alloc(u8, oam_size);
|
||||||
std.mem.set(u8, buf, 0);
|
std.mem.set(u8, buf, 0);
|
||||||
|
|
||||||
return Self{
|
return Self{
|
||||||
.buf = buf,
|
.buf = buf,
|
||||||
.allocator = allocator,
|
.alloc = alloc,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deinit(self: *Self) void {
|
fn deinit(self: Self) void {
|
||||||
self.allocator.free(self.buf);
|
self.alloc.free(self.buf);
|
||||||
self.* = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read(self: *const Self, comptime T: type, address: usize) T {
|
pub fn read(self: *const Self, comptime T: type, address: usize) T {
|
||||||
@@ -985,30 +773,6 @@ 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();
|
||||||
|
|
||||||
@@ -1028,14 +792,6 @@ 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);
|
||||||
@@ -1050,6 +806,18 @@ const Window = struct {
|
|||||||
self.in.raw = @truncate(u16, value);
|
self.in.raw = @truncate(u16, value);
|
||||||
self.out.raw = @truncate(u16, value >> 16);
|
self.out.raw = @truncate(u16, value >> 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn setInL(self: *Self, value: u8) void {
|
||||||
|
self.in.raw = (self.in.raw & 0xFF00) | value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setInH(self: *Self, value: u8) void {
|
||||||
|
self.in.raw = (self.in.raw & 0x00FF) | (@as(u16, value) << 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setOutL(self: *Self, value: u8) void {
|
||||||
|
self.out.raw = (self.out.raw & 0xFF00) | value;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Background = struct {
|
const Background = struct {
|
||||||
@@ -1069,17 +837,6 @@ 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 {
|
||||||
@@ -1316,7 +1073,7 @@ fn spriteDimensions(shape: u2, size: u2) [2]u8 {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fn rgba888(bgr555: u16) u32 {
|
fn toRgba8888(bgr555: u16) u32 {
|
||||||
const b = @as(u32, bgr555 >> 10 & 0x1F);
|
const 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);
|
||||||
@@ -1324,6 +1081,39 @@ inline fn rgba888(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();
|
||||||
@@ -1423,38 +1213,35 @@ fn copyToSpriteBuffer(bldcnt: io.BldCnt, scanline: *Scanline, x: u9, bgr555: u16
|
|||||||
const Scanline = struct {
|
const Scanline = struct {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
layers: [2][]?u16,
|
buf: [2][]?u16,
|
||||||
buf: []?u16,
|
original: []?u16,
|
||||||
|
|
||||||
allocator: Allocator,
|
fn init(buf: []?u16) Self {
|
||||||
|
std.debug.assert(buf.len == width * 2);
|
||||||
|
|
||||||
fn init(allocator: Allocator) !Self {
|
const top_slice = buf[0..][0..width];
|
||||||
const buf = try allocator.alloc(?u16, width * 2); // Top & Bottom Scanline
|
const btm_slice = buf[width..][0..width];
|
||||||
std.mem.set(?u16, buf, null);
|
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
// Top & Bototm Layers
|
.buf = [_][]?u16{ top_slice, btm_slice },
|
||||||
.layers = [_][]?u16{ buf[0..][0..width], buf[width..][0..width] },
|
.original = buf,
|
||||||
.buf = buf,
|
|
||||||
.allocator = allocator,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset(self: *Self) void {
|
fn reset(self: *Self) void {
|
||||||
std.mem.set(?u16, self.buf, null);
|
std.mem.set(?u16, self.original, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deinit(self: *Self) void {
|
fn deinit(self: Self, alloc: Allocator) void {
|
||||||
self.allocator.free(self.buf);
|
alloc.free(self.original);
|
||||||
self.* = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn top(self: *Self) []?u16 {
|
fn top(self: *Self) []?u16 {
|
||||||
return self.layers[0];
|
return self.buf[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
fn btm(self: *Self) []?u16 {
|
fn btm(self: *Self) []?u16 {
|
||||||
return self.layers[1];
|
return self.buf[1];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1462,11 +1249,9 @@ const Scanline = struct {
|
|||||||
const FrameBuffer = struct {
|
const FrameBuffer = struct {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
layers: [2][]u8,
|
buf: [2][]u8,
|
||||||
buf: []u8,
|
original: []u8,
|
||||||
current: std.atomic.Atomic(u8),
|
current: u1,
|
||||||
|
|
||||||
allocator: Allocator,
|
|
||||||
|
|
||||||
// TODO: Rename
|
// TODO: Rename
|
||||||
const Device = enum {
|
const Device = enum {
|
||||||
@@ -1474,33 +1259,28 @@ const FrameBuffer = struct {
|
|||||||
Renderer,
|
Renderer,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(allocator: Allocator) !Self {
|
pub fn init(bufs: []u8) Self {
|
||||||
const framebuf_len = framebuf_pitch * height;
|
std.debug.assert(bufs.len == framebuf_pitch * height * 2);
|
||||||
const buf = try allocator.alloc(u8, framebuf_len * 2);
|
|
||||||
std.mem.set(u8, buf, 0);
|
const front = bufs[0 .. framebuf_pitch * height];
|
||||||
|
const back = bufs[framebuf_pitch * height ..];
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
// Front and Back Framebuffers
|
.buf = [2][]u8{ front, back },
|
||||||
.layers = [_][]u8{ buf[0..][0..framebuf_len], buf[framebuf_len..][0..framebuf_len] },
|
.original = bufs,
|
||||||
.buf = buf,
|
.current = 0,
|
||||||
.current = std.atomic.Atomic(u8).init(0),
|
|
||||||
|
|
||||||
.allocator = allocator,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deinit(self: *Self) void {
|
fn deinit(self: Self, alloc: Allocator) void {
|
||||||
self.allocator.free(self.buf);
|
alloc.free(self.original);
|
||||||
self.* = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn swap(self: *Self) void {
|
pub fn swap(self: *Self) void {
|
||||||
_ = self.current.fetchXor(1, .Release); // fetchNot(.Release)
|
self.current = ~self.current;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(self: *Self, comptime dev: Device) []u8 {
|
pub fn get(self: *Self, comptime dev: Device) []u8 {
|
||||||
const current = @intCast(u1, self.current.load(.Acquire));
|
return self.buf[if (dev == .Emulator) self.current else ~self.current];
|
||||||
|
|
||||||
return self.layers[if (dev == .Emulator) current else ~current];
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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 Order = std.math.Order;
|
const Order = std.math.Order;
|
||||||
const PriorityQueue = std.PriorityQueue;
|
const PriorityQueue = std.PriorityQueue;
|
||||||
@@ -14,16 +14,15 @@ pub const Scheduler = struct {
|
|||||||
tick: u64,
|
tick: u64,
|
||||||
queue: PriorityQueue(Event, void, lessThan),
|
queue: PriorityQueue(Event, void, lessThan),
|
||||||
|
|
||||||
pub fn init(allocator: Allocator) Self {
|
pub fn init(alloc: Allocator) Self {
|
||||||
var sched = Self{ .tick = 0, .queue = PriorityQueue(Event, void, lessThan).init(allocator, {}) };
|
var sched = Self{ .tick = 0, .queue = PriorityQueue(Event, void, lessThan).init(alloc, {}) };
|
||||||
sched.queue.add(.{ .kind = .HeatDeath, .tick = std.math.maxInt(u64) }) catch unreachable;
|
sched.queue.add(.{ .kind = .HeatDeath, .tick = std.math.maxInt(u64) }) catch unreachable;
|
||||||
|
|
||||||
return sched;
|
return sched;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
pub fn deinit(self: Self) void {
|
||||||
self.queue.deinit();
|
self.queue.deinit();
|
||||||
self.* = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub inline fn now(self: *const Self) u64 {
|
pub inline fn now(self: *const Self) u64 {
|
||||||
@@ -42,32 +41,28 @@ pub const Scheduler = struct {
|
|||||||
.Draw => {
|
.Draw => {
|
||||||
// The end of a VDraw
|
// The end of a VDraw
|
||||||
cpu.bus.ppu.drawScanline();
|
cpu.bus.ppu.drawScanline();
|
||||||
cpu.bus.ppu.onHdrawEnd(cpu, late);
|
cpu.bus.ppu.handleHDrawEnd(cpu, late);
|
||||||
},
|
},
|
||||||
.TimerOverflow => |id| {
|
.TimerOverflow => |id| {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
inline 0...3 => |idx| cpu.bus.tim[idx].onTimerExpire(cpu, late),
|
0 => cpu.bus.tim[0].handleOverflow(cpu, late),
|
||||||
|
1 => cpu.bus.tim[1].handleOverflow(cpu, late),
|
||||||
|
2 => cpu.bus.tim[2].handleOverflow(cpu, late),
|
||||||
|
3 => cpu.bus.tim[3].handleOverflow(cpu, late),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.ApuChannel => |id| {
|
.ApuChannel => |id| {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
0 => cpu.bus.apu.ch1.onToneSweepEvent(late),
|
0 => cpu.bus.apu.ch1.channelTimerOverflow(late),
|
||||||
1 => cpu.bus.apu.ch2.onToneEvent(late),
|
1 => cpu.bus.apu.ch2.channelTimerOverflow(late),
|
||||||
2 => cpu.bus.apu.ch3.onWaveEvent(late),
|
2 => cpu.bus.apu.ch3.channelTimerOverflow(late),
|
||||||
3 => cpu.bus.apu.ch4.onNoiseEvent(late),
|
3 => cpu.bus.apu.ch4.channelTimerOverflow(late),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.RealTimeClock => {
|
.FrameSequencer => cpu.bus.apu.tickFrameSequencer(late),
|
||||||
const device = &cpu.bus.pak.gpio.device;
|
|
||||||
if (device.kind != .Rtc or device.ptr == null) return;
|
|
||||||
|
|
||||||
const clock = @ptrCast(*Clock, @alignCast(@alignOf(*Clock), device.ptr.?));
|
|
||||||
clock.onClockUpdate(late);
|
|
||||||
},
|
|
||||||
.FrameSequencer => cpu.bus.apu.onSequencerTick(late),
|
|
||||||
.SampleAudio => cpu.bus.apu.sampleAudio(late),
|
.SampleAudio => cpu.bus.apu.sampleAudio(late),
|
||||||
.HBlank => cpu.bus.ppu.onHblankEnd(cpu, late), // The end of a HBlank
|
.HBlank => cpu.bus.ppu.handleHBlankEnd(cpu, late), // The end of a HBlank
|
||||||
.VBlank => cpu.bus.ppu.onHdrawEnd(cpu, late), // The end of a VBlank
|
.VBlank => cpu.bus.ppu.handleHDrawEnd(cpu, late), // The end of a VBlank
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,5 +117,4 @@ pub const EventKind = union(enum) {
|
|||||||
SampleAudio,
|
SampleAudio,
|
||||||
FrameSequencer,
|
FrameSequencer,
|
||||||
ApuChannel: u2,
|
ApuChannel: u2,
|
||||||
RealTimeClock,
|
|
||||||
};
|
};
|
||||||
|
|||||||
176
src/core/util.zig
Normal file
176
src/core/util.zig
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const Log2Int = std.math.Log2Int;
|
||||||
|
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
||||||
|
|
||||||
|
// Sign-Extend value of type `T` to type `U`
|
||||||
|
pub fn sext(comptime T: type, comptime U: type, value: T) T {
|
||||||
|
// U must have less bits than T
|
||||||
|
comptime std.debug.assert(@typeInfo(U).Int.bits <= @typeInfo(T).Int.bits);
|
||||||
|
|
||||||
|
const iT = std.meta.Int(.signed, @typeInfo(T).Int.bits);
|
||||||
|
const ExtU = if (@typeInfo(U).Int.signedness == .unsigned) T else iT;
|
||||||
|
const shift = @intCast(Log2Int(T), @typeInfo(T).Int.bits - @typeInfo(U).Int.bits);
|
||||||
|
|
||||||
|
return @bitCast(T, @bitCast(iT, @as(ExtU, @truncate(U, value)) << shift) >> shift);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See https://godbolt.org/z/W3en9Eche
|
||||||
|
pub inline fn rotr(comptime T: type, x: T, r: anytype) T {
|
||||||
|
if (@typeInfo(T).Int.signedness == .signed)
|
||||||
|
@compileError("cannot rotate signed integer");
|
||||||
|
|
||||||
|
const ar = @intCast(Log2Int(T), @mod(r, @typeInfo(T).Int.bits));
|
||||||
|
return x >> ar | x << (1 +% ~ar);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const FpsTracker = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
fps: u32,
|
||||||
|
count: std.atomic.Atomic(u32),
|
||||||
|
timer: std.time.Timer,
|
||||||
|
|
||||||
|
pub fn init() Self {
|
||||||
|
return .{
|
||||||
|
.fps = 0,
|
||||||
|
.count = std.atomic.Atomic(u32).init(0),
|
||||||
|
.timer = std.time.Timer.start() catch unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(self: *Self) void {
|
||||||
|
_ = self.count.fetchAdd(1, .Monotonic);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value(self: *Self) u32 {
|
||||||
|
if (self.timer.read() >= std.time.ns_per_s) {
|
||||||
|
self.fps = self.count.swap(0, .SeqCst);
|
||||||
|
self.timer.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.fps;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn intToBytes(comptime T: type, value: anytype) [@sizeOf(T)]u8 {
|
||||||
|
comptime std.debug.assert(@typeInfo(T) == .Int);
|
||||||
|
|
||||||
|
var result: [@sizeOf(T)]u8 = undefined;
|
||||||
|
|
||||||
|
var i: Log2Int(T) = 0;
|
||||||
|
while (i < result.len) : (i += 1) result[i] = @truncate(u8, value >> i * @bitSizeOf(u8));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Title from the GBA Cartridge may be null padded to a maximum
|
||||||
|
/// length of 12 bytes.
|
||||||
|
///
|
||||||
|
/// This function returns a slice of everything just before the first
|
||||||
|
/// `\0`
|
||||||
|
pub fn asString(title: [12]u8) []const u8 {
|
||||||
|
var len = title.len;
|
||||||
|
for (title) |char, i| {
|
||||||
|
if (char == 0) {
|
||||||
|
len = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return title[0..len];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copies a Title and returns either an identical or similar
|
||||||
|
/// array consisting of ASCII that won't make any file system angry
|
||||||
|
///
|
||||||
|
/// e.g. POKEPIN R/S to POKEPIN R_S
|
||||||
|
pub fn escape(title: [12]u8) [12]u8 {
|
||||||
|
var result: [12]u8 = title;
|
||||||
|
|
||||||
|
for (result) |*char| {
|
||||||
|
if (char.* == '/' or char.* == '\\') char.* = '_';
|
||||||
|
if (char.* == 0) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const FilePaths = struct {
|
||||||
|
rom: []const u8,
|
||||||
|
bios: ?[]const u8,
|
||||||
|
save: ?[]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn readUndefined(log: anytype, comptime format: []const u8, args: anytype) u8 {
|
||||||
|
log.warn(format, args);
|
||||||
|
if (builtin.mode == .Debug) std.debug.panic("TODO: Implement I/O Register", .{});
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn writeUndefined(log: anytype, comptime format: []const u8, args: anytype) void {
|
||||||
|
log.warn(format, args);
|
||||||
|
if (builtin.mode == .Debug) std.debug.panic("TODO: Implement I/O Register", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Logger = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
buf: std.io.BufferedWriter(4096 << 2, std.fs.File.Writer),
|
||||||
|
|
||||||
|
pub fn init(file: std.fs.File) Self {
|
||||||
|
return .{
|
||||||
|
.buf = .{ .unbuffered_writer = file.writer() },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print(self: *Self, comptime format: []const u8, args: anytype) !void {
|
||||||
|
try self.buf.writer().print(format, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mgbaLog(self: *Self, arm7tdmi: *const Arm7tdmi, opcode: u32) void {
|
||||||
|
const fmt_base = "{X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} cpsr: {X:0>8} | ";
|
||||||
|
const thumb_fmt = fmt_base ++ "{X:0>4}:\n";
|
||||||
|
const arm_fmt = fmt_base ++ "{X:0>8}:\n";
|
||||||
|
|
||||||
|
if (arm7tdmi.cpsr.t.read()) {
|
||||||
|
if (opcode >> 11 == 0x1E) {
|
||||||
|
// Instruction 1 of a BL Opcode, print in ARM mode
|
||||||
|
const low = arm7tdmi.bus.dbgRead(u16, arm7tdmi.r[15]);
|
||||||
|
const bl_opcode = @as(u32, opcode) << 16 | low;
|
||||||
|
|
||||||
|
self.print(arm_fmt, Self.fmtArgs(arm7tdmi, bl_opcode)) catch @panic("failed to write to log file");
|
||||||
|
} else {
|
||||||
|
self.print(thumb_fmt, Self.fmtArgs(arm7tdmi, opcode)) catch @panic("failed to write to log file");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.print(arm_fmt, Self.fmtArgs(arm7tdmi, opcode)) catch @panic("failed to write to log file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmtArgs(arm7tdmi: *const Arm7tdmi, opcode: u32) FmtArgTuple {
|
||||||
|
return .{
|
||||||
|
arm7tdmi.r[0],
|
||||||
|
arm7tdmi.r[1],
|
||||||
|
arm7tdmi.r[2],
|
||||||
|
arm7tdmi.r[3],
|
||||||
|
arm7tdmi.r[4],
|
||||||
|
arm7tdmi.r[5],
|
||||||
|
arm7tdmi.r[6],
|
||||||
|
arm7tdmi.r[7],
|
||||||
|
arm7tdmi.r[8],
|
||||||
|
arm7tdmi.r[9],
|
||||||
|
arm7tdmi.r[10],
|
||||||
|
arm7tdmi.r[11],
|
||||||
|
arm7tdmi.r[12],
|
||||||
|
arm7tdmi.r[13],
|
||||||
|
arm7tdmi.r[14],
|
||||||
|
arm7tdmi.r[15] - if (arm7tdmi.cpsr.t.read()) 2 else @as(u32, 4),
|
||||||
|
arm7tdmi.cpsr.raw,
|
||||||
|
opcode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const FmtArgTuple = std.meta.Tuple(&.{ u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32 });
|
||||||
167
src/main.zig
167
src/main.zig
@@ -1,160 +1,105 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const known_folders = @import("known_folders");
|
const known_folders = @import("known_folders");
|
||||||
const clap = @import("clap");
|
const clap = @import("clap");
|
||||||
|
|
||||||
const config = @import("config.zig");
|
const Gui = @import("Gui.zig");
|
||||||
|
|
||||||
const Gui = @import("platform.zig").Gui;
|
|
||||||
const Bus = @import("core/Bus.zig");
|
const Bus = @import("core/Bus.zig");
|
||||||
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
|
||||||
const Scheduler = @import("core/scheduler.zig").Scheduler;
|
const Scheduler = @import("core/scheduler.zig").Scheduler;
|
||||||
const FilePaths = @import("util.zig").FilePaths;
|
const FilePaths = @import("core/util.zig").FilePaths;
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const log = std.log.scoped(.Cli);
|
const log = std.log.scoped(.CLI);
|
||||||
const width = @import("core/ppu.zig").width;
|
const width = @import("core/ppu.zig").width;
|
||||||
const height = @import("core/ppu.zig").height;
|
const height = @import("core/ppu.zig").height;
|
||||||
|
const arm7tdmi_logging = @import("core/emu.zig").cpu_logging;
|
||||||
pub const log_level = if (builtin.mode != .Debug) .info else std.log.default_level;
|
pub const log_level = if (builtin.mode != .Debug) .info else std.log.default_level;
|
||||||
|
|
||||||
|
// TODO: Reimpl Logging
|
||||||
|
|
||||||
// CLI Arguments + Help Text
|
// CLI Arguments + Help Text
|
||||||
const params = clap.parseParamsComptime(
|
const params = clap.parseParamsComptime(
|
||||||
\\-h, --help Display this help and exit.
|
\\-h, --help Display this help and exit.
|
||||||
\\-s, --skip Skip BIOS.
|
|
||||||
\\-b, --bios <str> Optional path to a GBA BIOS ROM.
|
\\-b, --bios <str> Optional path to a GBA BIOS ROM.
|
||||||
\\<str> Path to the GBA GamePak ROM.
|
\\<str> Path to the GBA GamePak ROM
|
||||||
\\
|
\\
|
||||||
);
|
);
|
||||||
|
|
||||||
pub fn main() void {
|
pub fn main() anyerror!void {
|
||||||
// Main Allocator for ZBA
|
// Main Allocator for ZBA
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
defer std.debug.assert(!gpa.deinit());
|
defer std.debug.assert(!gpa.deinit());
|
||||||
|
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
// Determine the Data Directory (stores saves)
|
// Handle CLI Input
|
||||||
const data_path = blk: {
|
const result = try clap.parse(clap.Help, ¶ms, clap.parsers.default, .{});
|
||||||
const result = known_folders.getPath(allocator, .data);
|
|
||||||
const option = result catch |e| exitln("interrupted while determining the data folder: {}", .{e});
|
|
||||||
const path = option orelse exitln("no valid data folder found", .{});
|
|
||||||
ensureDataDirsExist(path) catch |e| exitln("failed to create folders under \"{s}\": {}", .{ path, e });
|
|
||||||
|
|
||||||
break :blk path;
|
|
||||||
};
|
|
||||||
defer allocator.free(data_path);
|
|
||||||
|
|
||||||
// Determine the Config Directory
|
|
||||||
const config_path = blk: {
|
|
||||||
const result = known_folders.getPath(allocator, .roaming_configuration);
|
|
||||||
const option = result catch |e| exitln("interreupted while determining the config folder: {}", .{e});
|
|
||||||
const path = option orelse exitln("no valid config folder found", .{});
|
|
||||||
ensureConfigDirExists(path) catch |e| exitln("failed to create required folder \"{s}\": {}", .{ path, e });
|
|
||||||
|
|
||||||
break :blk path;
|
|
||||||
};
|
|
||||||
defer allocator.free(config_path);
|
|
||||||
|
|
||||||
// Parse CLI
|
|
||||||
const result = clap.parse(clap.Help, ¶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?
|
const paths = try handleArguments(allocator, &result);
|
||||||
const cfg_file_path = configFilePath(allocator, config_path) catch |e| exitln("failed to ready config file for access: {}", .{e});
|
|
||||||
defer allocator.free(cfg_file_path);
|
|
||||||
|
|
||||||
config.load(allocator, cfg_file_path) catch |e| exitln("failed to load config file: {}", .{e});
|
|
||||||
|
|
||||||
const paths = handleArguments(allocator, data_path, &result) catch |e| exitln("failed to handle cli arguments: {}", .{e});
|
|
||||||
defer if (paths.save) |path| allocator.free(path);
|
defer if (paths.save) |path| allocator.free(path);
|
||||||
|
|
||||||
const log_file = if (config.config().debug.cpu_trace) blk: {
|
|
||||||
break :blk std.fs.cwd().createFile("zba.log", .{}) catch |e| exitln("failed to create trace log file: {}", .{e});
|
|
||||||
} else null;
|
|
||||||
defer if (log_file) |file| file.close();
|
|
||||||
|
|
||||||
// TODO: Take Emulator Init Code out of main.zig
|
// TODO: Take Emulator Init Code out of main.zig
|
||||||
var scheduler = Scheduler.init(allocator);
|
var scheduler = Scheduler.init(allocator);
|
||||||
defer scheduler.deinit();
|
defer scheduler.deinit();
|
||||||
|
|
||||||
var bus: Bus = undefined;
|
var bus = try Bus.init(allocator, &scheduler, paths);
|
||||||
var cpu = Arm7tdmi.init(&scheduler, &bus, log_file);
|
|
||||||
|
|
||||||
bus.init(allocator, &scheduler, &cpu, paths) catch |e| exitln("failed to init zba bus: {}", .{e});
|
|
||||||
defer bus.deinit();
|
defer bus.deinit();
|
||||||
|
|
||||||
if (config.config().guest.skip_bios or result.args.skip or paths.bios == null) {
|
var arm7tdmi = Arm7tdmi.init(&scheduler, &bus);
|
||||||
cpu.fastBoot();
|
|
||||||
}
|
|
||||||
|
|
||||||
var gui = Gui.init(&bus.pak.title, &bus.apu, width, height) catch |e| exitln("failed to init gui: {}", .{e});
|
const log_file: ?std.fs.File = if (arm7tdmi_logging) try std.fs.cwd().createFile("zba.log", .{}) else null;
|
||||||
|
defer if (log_file) |file| file.close();
|
||||||
|
|
||||||
|
if (log_file) |file| arm7tdmi.attach(file);
|
||||||
|
bus.attach(&arm7tdmi); // TODO: Shrink Surface (only CPSR and r15?)
|
||||||
|
if (paths.bios == null) arm7tdmi.fastBoot();
|
||||||
|
|
||||||
|
var gui = Gui.init(bus.pak.title, width, height);
|
||||||
|
gui.initAudio(&bus.apu);
|
||||||
defer gui.deinit();
|
defer gui.deinit();
|
||||||
|
|
||||||
gui.run(&cpu, &scheduler) catch |e| exitln("failed to run gui thread: {}", .{e});
|
try gui.run(&arm7tdmi, &scheduler);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) !FilePaths {
|
fn getSavePath(allocator: Allocator) !?[]const u8 {
|
||||||
const rom_path = romPath(result);
|
const save_subpath = "zba" ++ [_]u8{std.fs.path.sep} ++ "save";
|
||||||
|
|
||||||
|
const maybe_data_path = try known_folders.getPath(allocator, .data);
|
||||||
|
defer if (maybe_data_path) |path| allocator.free(path);
|
||||||
|
|
||||||
|
const save_path = if (maybe_data_path) |base| try std.fs.path.join(allocator, &[_][]const u8{ base, "zba", "save" }) else null;
|
||||||
|
|
||||||
|
if (save_path) |_| {
|
||||||
|
// If we've determined what our save path should be, ensure the prereq directories
|
||||||
|
// are present so that we can successfully write to the path when necessary
|
||||||
|
const maybe_data_dir = try known_folders.open(allocator, .data, .{});
|
||||||
|
if (maybe_data_dir) |data_dir| try data_dir.makePath(save_subpath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return save_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getRomPath(result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) ![]const u8 {
|
||||||
|
return switch (result.positionals.len) {
|
||||||
|
1 => result.positionals[0],
|
||||||
|
0 => std.debug.panic("ZBA requires a positional path to a GamePak ROM.\n", .{}),
|
||||||
|
else => std.debug.panic("ZBA received too many arguments.\n", .{}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handleArguments(allocator: Allocator, result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) !FilePaths {
|
||||||
|
const rom_path = try getRomPath(result);
|
||||||
log.info("ROM path: {s}", .{rom_path});
|
log.info("ROM path: {s}", .{rom_path});
|
||||||
|
|
||||||
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.info("No BIOS provided", .{});
|
||||||
|
const save_path = try getSavePath(allocator);
|
||||||
|
if (save_path) |path| log.info("Save path: {s}", .{path});
|
||||||
|
|
||||||
const save_path = try std.fs.path.join(allocator, &[_][]const u8{ data_path, "zba", "save" });
|
return FilePaths{
|
||||||
log.info("Save path: {s}", .{save_path});
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.rom = rom_path,
|
.rom = rom_path,
|
||||||
.bios = bios_path,
|
.bios = bios_path,
|
||||||
.save = save_path,
|
.save = save_path,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn configFilePath(allocator: Allocator, config_path: []const u8) ![]const u8 {
|
|
||||||
const path = try std.fs.path.join(allocator, &[_][]const u8{ config_path, "zba", "config.toml" });
|
|
||||||
errdefer allocator.free(path);
|
|
||||||
|
|
||||||
// We try to create the file exclusively, meaning that we err out if the file already exists.
|
|
||||||
// All we care about is a file being there so we can just ignore that error in particular and
|
|
||||||
// continue down the happy pathj
|
|
||||||
std.fs.accessAbsolute(path, .{}) catch |e| {
|
|
||||||
if (e != error.FileNotFound) return e;
|
|
||||||
|
|
||||||
const config_file = std.fs.createFileAbsolute(path, .{}) catch |err| exitln("failed to create \"{s}\": {}", .{ path, err });
|
|
||||||
defer config_file.close();
|
|
||||||
|
|
||||||
try config_file.writeAll(@embedFile("../example.toml"));
|
|
||||||
};
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ensureDataDirsExist(data_path: []const u8) !void {
|
|
||||||
var dir = try std.fs.openDirAbsolute(data_path, .{});
|
|
||||||
defer dir.close();
|
|
||||||
|
|
||||||
// Will recursively create directories
|
|
||||||
try dir.makePath("zba" ++ std.fs.path.sep_str ++ "save");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ensureConfigDirExists(config_path: []const u8) !void {
|
|
||||||
var dir = try std.fs.openDirAbsolute(config_path, .{});
|
|
||||||
defer dir.close();
|
|
||||||
|
|
||||||
try dir.makePath("zba");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn romPath(result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) []const u8 {
|
|
||||||
return switch (result.positionals.len) {
|
|
||||||
1 => result.positionals[0],
|
|
||||||
0 => exitln("ZBA requires a path to a GamePak ROM", .{}),
|
|
||||||
else => exitln("ZBA received too many positional arguments.", .{}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn exitln(comptime format: []const u8, args: anytype) noreturn {
|
|
||||||
const stderr = std.io.getStdErr().writer();
|
|
||||||
stderr.print(format, args) catch {}; // Just exit already...
|
|
||||||
stderr.writeByte('\n') catch {};
|
|
||||||
std.os.exit(1);
|
|
||||||
}
|
|
||||||
|
|||||||
339
src/platform.zig
339
src/platform.zig
@@ -1,339 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const SDL = @import("sdl2");
|
|
||||||
const gl = @import("gl");
|
|
||||||
const emu = @import("core/emu.zig");
|
|
||||||
const config = @import("config.zig");
|
|
||||||
|
|
||||||
const Apu = @import("core/apu.zig").Apu;
|
|
||||||
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
|
|
||||||
const Scheduler = @import("core/scheduler.zig").Scheduler;
|
|
||||||
const FpsTracker = @import("util.zig").FpsTracker;
|
|
||||||
|
|
||||||
const gba_width = @import("core/ppu.zig").width;
|
|
||||||
const gba_height = @import("core/ppu.zig").height;
|
|
||||||
|
|
||||||
pub const sample_rate = 1 << 16;
|
|
||||||
pub const sample_format = SDL.AUDIO_U16;
|
|
||||||
|
|
||||||
const default_title = "ZBA";
|
|
||||||
|
|
||||||
pub const Gui = struct {
|
|
||||||
const Self = @This();
|
|
||||||
const SDL_GLContext = *anyopaque; // SDL.SDL_GLContext is a ?*anyopaque
|
|
||||||
const log = std.log.scoped(.Gui);
|
|
||||||
|
|
||||||
// zig fmt: off
|
|
||||||
const vertices: [32]f32 = [_]f32{
|
|
||||||
// Positions // Colours // Texture Coords
|
|
||||||
1.0, -1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, // Top Right
|
|
||||||
1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, // Bottom Right
|
|
||||||
-1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, // Bottom Left
|
|
||||||
-1.0, -1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, // Top Left
|
|
||||||
};
|
|
||||||
|
|
||||||
const indices: [6]u32 = [_]u32{
|
|
||||||
0, 1, 3, // First Triangle
|
|
||||||
1, 2, 3, // Second Triangle
|
|
||||||
};
|
|
||||||
// zig fmt: on
|
|
||||||
|
|
||||||
window: *SDL.SDL_Window,
|
|
||||||
ctx: SDL_GLContext,
|
|
||||||
title: []const u8,
|
|
||||||
audio: Audio,
|
|
||||||
|
|
||||||
program_id: gl.GLuint,
|
|
||||||
|
|
||||||
pub fn init(title: *const [12]u8, apu: *Apu, width: i32, height: i32) !Self {
|
|
||||||
if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO) < 0) panic();
|
|
||||||
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_PROFILE_MASK, SDL.SDL_GL_CONTEXT_PROFILE_CORE) < 0) panic();
|
|
||||||
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic();
|
|
||||||
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic();
|
|
||||||
|
|
||||||
const win_scale = @intCast(c_int, config.config().host.win_scale);
|
|
||||||
|
|
||||||
const window = SDL.SDL_CreateWindow(
|
|
||||||
default_title,
|
|
||||||
SDL.SDL_WINDOWPOS_CENTERED,
|
|
||||||
SDL.SDL_WINDOWPOS_CENTERED,
|
|
||||||
@as(c_int, width * win_scale),
|
|
||||||
@as(c_int, height * win_scale),
|
|
||||||
SDL.SDL_WINDOW_OPENGL | SDL.SDL_WINDOW_SHOWN,
|
|
||||||
) orelse panic();
|
|
||||||
|
|
||||||
const ctx = SDL.SDL_GL_CreateContext(window) orelse panic();
|
|
||||||
if (SDL.SDL_GL_MakeCurrent(window, ctx) < 0) panic();
|
|
||||||
|
|
||||||
try gl.load(ctx, Self.glGetProcAddress);
|
|
||||||
if (SDL.SDL_GL_SetSwapInterval(@boolToInt(config.config().host.vsync)) < 0) panic();
|
|
||||||
|
|
||||||
const program_id = try compileShaders();
|
|
||||||
|
|
||||||
return Self{
|
|
||||||
.window = window,
|
|
||||||
.title = std.mem.sliceTo(title, 0),
|
|
||||||
.ctx = ctx,
|
|
||||||
.program_id = program_id,
|
|
||||||
.audio = Audio.init(apu),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compileShaders() !gl.GLuint {
|
|
||||||
// TODO: Panic on Shader Compiler Failure + Error Message
|
|
||||||
const vert_shader = @embedFile("shader/pixelbuf.vert");
|
|
||||||
const frag_shader = @embedFile("shader/pixelbuf.frag");
|
|
||||||
|
|
||||||
const vs = gl.createShader(gl.VERTEX_SHADER);
|
|
||||||
defer gl.deleteShader(vs);
|
|
||||||
|
|
||||||
gl.shaderSource(vs, 1, &[_][*c]const u8{vert_shader}, 0);
|
|
||||||
gl.compileShader(vs);
|
|
||||||
|
|
||||||
if (!shader.didCompile(vs)) return error.VertexCompileError;
|
|
||||||
|
|
||||||
const fs = gl.createShader(gl.FRAGMENT_SHADER);
|
|
||||||
defer gl.deleteShader(fs);
|
|
||||||
|
|
||||||
gl.shaderSource(fs, 1, &[_][*c]const u8{frag_shader}, 0);
|
|
||||||
gl.compileShader(fs);
|
|
||||||
|
|
||||||
if (!shader.didCompile(fs)) return error.FragmentCompileError;
|
|
||||||
|
|
||||||
const program = gl.createProgram();
|
|
||||||
gl.attachShader(program, vs);
|
|
||||||
gl.attachShader(program, fs);
|
|
||||||
gl.linkProgram(program);
|
|
||||||
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the VAO ID since it's used in run()
|
|
||||||
fn generateBuffers() struct { c_uint, c_uint, c_uint } {
|
|
||||||
var vao_id: c_uint = undefined;
|
|
||||||
var vbo_id: c_uint = undefined;
|
|
||||||
var ebo_id: c_uint = undefined;
|
|
||||||
gl.genVertexArrays(1, &vao_id);
|
|
||||||
gl.genBuffers(1, &vbo_id);
|
|
||||||
gl.genBuffers(1, &ebo_id);
|
|
||||||
|
|
||||||
gl.bindVertexArray(vao_id);
|
|
||||||
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, vbo_id);
|
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, @sizeOf(@TypeOf(vertices)), &vertices, gl.STATIC_DRAW);
|
|
||||||
|
|
||||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo_id);
|
|
||||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, @sizeOf(@TypeOf(indices)), &indices, gl.STATIC_DRAW);
|
|
||||||
|
|
||||||
// Position
|
|
||||||
gl.vertexAttribPointer(0, 3, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, 0)); // lmao
|
|
||||||
gl.enableVertexAttribArray(0);
|
|
||||||
// Colour
|
|
||||||
gl.vertexAttribPointer(1, 3, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, (3 * @sizeOf(f32))));
|
|
||||||
gl.enableVertexAttribArray(1);
|
|
||||||
// Texture Coord
|
|
||||||
gl.vertexAttribPointer(2, 2, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, (6 * @sizeOf(f32))));
|
|
||||||
gl.enableVertexAttribArray(2);
|
|
||||||
|
|
||||||
return .{ vao_id, vbo_id, ebo_id };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generateTexture(buf: []const u8) c_uint {
|
|
||||||
var tex_id: c_uint = undefined;
|
|
||||||
gl.genTextures(1, &tex_id);
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, tex_id);
|
|
||||||
|
|
||||||
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
||||||
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
||||||
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
||||||
|
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gba_width, gba_height, 0, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, buf.ptr);
|
|
||||||
// gl.generateMipmap(gl.TEXTURE_2D); // TODO: Remove?
|
|
||||||
|
|
||||||
return tex_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run(self: *Self, cpu: *Arm7tdmi, scheduler: *Scheduler) !void {
|
|
||||||
var quit = std.atomic.Atomic(bool).init(false);
|
|
||||||
var tracker = FpsTracker.init();
|
|
||||||
|
|
||||||
var buffer_ids = Self.generateBuffers();
|
|
||||||
defer {
|
|
||||||
gl.deleteBuffers(1, &buffer_ids[2]); // EBO
|
|
||||||
gl.deleteBuffers(1, &buffer_ids[1]); // VBO
|
|
||||||
gl.deleteVertexArrays(1, &buffer_ids[0]); // VAO
|
|
||||||
}
|
|
||||||
const vao_id = buffer_ids[0];
|
|
||||||
|
|
||||||
const tex_id = Self.generateTexture(cpu.bus.ppu.framebuf.get(.Renderer));
|
|
||||||
defer gl.deleteTextures(1, &tex_id);
|
|
||||||
|
|
||||||
const thread = try std.Thread.spawn(.{}, emu.run, .{ &quit, scheduler, cpu, &tracker });
|
|
||||||
defer thread.join();
|
|
||||||
|
|
||||||
var title_buf: [0x100]u8 = undefined;
|
|
||||||
|
|
||||||
emu_loop: while (true) {
|
|
||||||
var event: SDL.SDL_Event = undefined;
|
|
||||||
while (SDL.SDL_PollEvent(&event) != 0) {
|
|
||||||
switch (event.type) {
|
|
||||||
SDL.SDL_QUIT => break :emu_loop,
|
|
||||||
SDL.SDL_KEYDOWN => {
|
|
||||||
const key_code = event.key.keysym.sym;
|
|
||||||
var keyinput = cpu.bus.io.keyinput.load(.Monotonic);
|
|
||||||
|
|
||||||
switch (key_code) {
|
|
||||||
SDL.SDLK_UP => keyinput.up.unset(),
|
|
||||||
SDL.SDLK_DOWN => keyinput.down.unset(),
|
|
||||||
SDL.SDLK_LEFT => keyinput.left.unset(),
|
|
||||||
SDL.SDLK_RIGHT => keyinput.right.unset(),
|
|
||||||
SDL.SDLK_x => keyinput.a.unset(),
|
|
||||||
SDL.SDLK_z => keyinput.b.unset(),
|
|
||||||
SDL.SDLK_a => keyinput.shoulder_l.unset(),
|
|
||||||
SDL.SDLK_s => keyinput.shoulder_r.unset(),
|
|
||||||
SDL.SDLK_RETURN => keyinput.start.unset(),
|
|
||||||
SDL.SDLK_RSHIFT => keyinput.select.unset(),
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
cpu.bus.io.keyinput.store(keyinput.raw, .Monotonic);
|
|
||||||
},
|
|
||||||
SDL.SDL_KEYUP => {
|
|
||||||
const key_code = event.key.keysym.sym;
|
|
||||||
var keyinput = cpu.bus.io.keyinput.load(.Monotonic);
|
|
||||||
|
|
||||||
switch (key_code) {
|
|
||||||
SDL.SDLK_UP => keyinput.up.set(),
|
|
||||||
SDL.SDLK_DOWN => keyinput.down.set(),
|
|
||||||
SDL.SDLK_LEFT => keyinput.left.set(),
|
|
||||||
SDL.SDLK_RIGHT => keyinput.right.set(),
|
|
||||||
SDL.SDLK_x => keyinput.a.set(),
|
|
||||||
SDL.SDLK_z => keyinput.b.set(),
|
|
||||||
SDL.SDLK_a => keyinput.shoulder_l.set(),
|
|
||||||
SDL.SDLK_s => keyinput.shoulder_r.set(),
|
|
||||||
SDL.SDLK_RETURN => keyinput.start.set(),
|
|
||||||
SDL.SDLK_RSHIFT => keyinput.select.set(),
|
|
||||||
SDL.SDLK_i => {
|
|
||||||
comptime std.debug.assert(sample_format == SDL.AUDIO_U16);
|
|
||||||
log.err("Sample Count: {}", .{cpu.bus.apu.sample_queue.len() / 2});
|
|
||||||
},
|
|
||||||
// SDL.SDLK_j => log.err("Scheduler Capacity: {} | Scheduler Event Count: {}", .{ scheduler.queue.capacity(), scheduler.queue.count() }),
|
|
||||||
SDL.SDLK_k => {},
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
cpu.bus.io.keyinput.store(keyinput.raw, .Monotonic);
|
|
||||||
},
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emulator has an internal Double Buffer
|
|
||||||
const framebuf = cpu.bus.ppu.framebuf.get(.Renderer);
|
|
||||||
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gba_width, gba_height, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, framebuf.ptr);
|
|
||||||
|
|
||||||
gl.useProgram(self.program_id);
|
|
||||||
gl.bindVertexArray(vao_id);
|
|
||||||
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, null);
|
|
||||||
SDL.SDL_GL_SwapWindow(self.window);
|
|
||||||
|
|
||||||
const dyn_title = std.fmt.bufPrintZ(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, tracker.value() }) catch unreachable;
|
|
||||||
SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
quit.store(true, .Monotonic); // Terminate Emulator Thread
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
|
||||||
self.audio.deinit();
|
|
||||||
gl.deleteProgram(self.program_id);
|
|
||||||
SDL.SDL_GL_DeleteContext(self.ctx);
|
|
||||||
SDL.SDL_DestroyWindow(self.window);
|
|
||||||
SDL.SDL_Quit();
|
|
||||||
self.* = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn glGetProcAddress(ctx: SDL.SDL_GLContext, proc: [:0]const u8) ?*anyopaque {
|
|
||||||
_ = ctx;
|
|
||||||
return SDL.SDL_GL_GetProcAddress(proc.ptr);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Audio = struct {
|
|
||||||
const Self = @This();
|
|
||||||
const log = std.log.scoped(.PlatformAudio);
|
|
||||||
|
|
||||||
device: SDL.SDL_AudioDeviceID,
|
|
||||||
|
|
||||||
fn init(apu: *Apu) Self {
|
|
||||||
var have: SDL.SDL_AudioSpec = undefined;
|
|
||||||
var want: SDL.SDL_AudioSpec = std.mem.zeroes(SDL.SDL_AudioSpec);
|
|
||||||
want.freq = sample_rate;
|
|
||||||
want.format = sample_format;
|
|
||||||
want.channels = 2;
|
|
||||||
want.samples = 0x100;
|
|
||||||
want.callback = Self.callback;
|
|
||||||
want.userdata = apu;
|
|
||||||
|
|
||||||
std.debug.assert(sample_format == SDL.AUDIO_U16);
|
|
||||||
log.info("Host Sample Rate: {}Hz, Host Format: SDL.AUDIO_U16", .{sample_rate});
|
|
||||||
|
|
||||||
const device = SDL.SDL_OpenAudioDevice(null, 0, &want, &have, 0);
|
|
||||||
if (device == 0) panic();
|
|
||||||
|
|
||||||
if (!config.config().host.mute) {
|
|
||||||
SDL.SDL_PauseAudioDevice(device, 0); // Unpause Audio
|
|
||||||
log.info("Unpaused Device", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{ .device = device };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deinit(self: *Self) void {
|
|
||||||
SDL.SDL_CloseAudioDevice(self.device);
|
|
||||||
self.* = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export fn callback(userdata: ?*anyopaque, stream: [*c]u8, len: c_int) void {
|
|
||||||
const T = *Apu;
|
|
||||||
const apu = @ptrCast(T, @alignCast(@alignOf(T), userdata));
|
|
||||||
|
|
||||||
comptime std.debug.assert(sample_format == SDL.AUDIO_U16);
|
|
||||||
const sample_buf = @ptrCast([*]u16, @alignCast(@alignOf(u16), stream))[0 .. @intCast(u32, len) / @sizeOf(u16)];
|
|
||||||
|
|
||||||
var previous: u16 = 0x8000;
|
|
||||||
for (sample_buf) |*sample| {
|
|
||||||
if (apu.sample_queue.pop()) |value| previous = value;
|
|
||||||
|
|
||||||
sample.* = previous;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const shader = struct {
|
|
||||||
const Kind = enum { vertex, fragment };
|
|
||||||
const log = std.log.scoped(.Shader);
|
|
||||||
|
|
||||||
fn didCompile(id: gl.GLuint) bool {
|
|
||||||
var success: gl.GLint = undefined;
|
|
||||||
gl.getShaderiv(id, gl.COMPILE_STATUS, &success);
|
|
||||||
|
|
||||||
if (success == 0) err(id);
|
|
||||||
|
|
||||||
return success == 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn err(id: gl.GLuint) void {
|
|
||||||
const buf_len = 512;
|
|
||||||
var error_msg: [buf_len]u8 = undefined;
|
|
||||||
|
|
||||||
gl.getShaderInfoLog(id, buf_len, 0, &error_msg);
|
|
||||||
log.err("{s}", .{std.mem.sliceTo(&error_msg, 0)});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fn panic() noreturn {
|
|
||||||
const str = @as(?[*:0]const u8, SDL.SDL_GetError()) orelse "unknown error";
|
|
||||||
@panic(std.mem.sliceTo(str, 0));
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#version 330 core
|
|
||||||
out vec4 frag_color;
|
|
||||||
|
|
||||||
in vec3 color;
|
|
||||||
in vec2 uv;
|
|
||||||
|
|
||||||
uniform sampler2D screen;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
// https://near.sh/video/color-emulation
|
|
||||||
// Thanks to Talarubi + Near for the Colour Correction
|
|
||||||
// Thanks to fleur + mattrb for the Shader Impl
|
|
||||||
|
|
||||||
vec4 color = texture(screen, uv);
|
|
||||||
color.rgb = pow(color.rgb, vec3(4.0)); // LCD Gamma
|
|
||||||
|
|
||||||
frag_color = vec4(
|
|
||||||
pow(vec3(
|
|
||||||
0 * color.b + 50 * color.g + 255 * color.r,
|
|
||||||
30 * color.b + 230 * color.g + 10 * color.r,
|
|
||||||
220 * color.b + 10 * color.g + 50 * color.r
|
|
||||||
) / 255, vec3(1.0 / 2.2)), // Out Gamma
|
|
||||||
1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#version 330 core
|
|
||||||
layout (location = 0) in vec3 pos;
|
|
||||||
layout (location = 1) in vec3 in_color;
|
|
||||||
layout (location = 2) in vec2 in_uv;
|
|
||||||
|
|
||||||
out vec3 color;
|
|
||||||
out vec2 uv;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
color = in_color;
|
|
||||||
uv = in_uv;
|
|
||||||
gl_Position = vec4(pos, 1.0);
|
|
||||||
}
|
|
||||||
384
src/util.zig
384
src/util.zig
@@ -1,384 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const builtin = @import("builtin");
|
|
||||||
const config = @import("config.zig");
|
|
||||||
|
|
||||||
const Log2Int = std.math.Log2Int;
|
|
||||||
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
|
|
||||||
|
|
||||||
// Sign-Extend value of type `T` to type `U`
|
|
||||||
pub fn sext(comptime T: type, comptime U: type, value: T) T {
|
|
||||||
// U must have less bits than T
|
|
||||||
comptime std.debug.assert(@typeInfo(U).Int.bits <= @typeInfo(T).Int.bits);
|
|
||||||
|
|
||||||
const iT = std.meta.Int(.signed, @typeInfo(T).Int.bits);
|
|
||||||
const ExtU = if (@typeInfo(U).Int.signedness == .unsigned) T else iT;
|
|
||||||
const shift_amt = @intCast(Log2Int(T), @typeInfo(T).Int.bits - @typeInfo(U).Int.bits);
|
|
||||||
|
|
||||||
return @bitCast(T, @bitCast(iT, @as(ExtU, @truncate(U, value)) << shift_amt) >> shift_amt);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// See https://godbolt.org/z/W3en9Eche
|
|
||||||
pub inline fn rotr(comptime T: type, x: T, r: anytype) T {
|
|
||||||
if (@typeInfo(T).Int.signedness == .signed)
|
|
||||||
@compileError("cannot rotate signed integer");
|
|
||||||
|
|
||||||
const ar = @intCast(Log2Int(T), @mod(r, @typeInfo(T).Int.bits));
|
|
||||||
return x >> ar | x << (1 +% ~ar);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const FpsTracker = struct {
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
fps: u32,
|
|
||||||
count: std.atomic.Atomic(u32),
|
|
||||||
timer: std.time.Timer,
|
|
||||||
|
|
||||||
pub fn init() Self {
|
|
||||||
return .{
|
|
||||||
.fps = 0,
|
|
||||||
.count = std.atomic.Atomic(u32).init(0),
|
|
||||||
.timer = std.time.Timer.start() catch unreachable,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tick(self: *Self) void {
|
|
||||||
_ = self.count.fetchAdd(1, .Monotonic);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn value(self: *Self) u32 {
|
|
||||||
if (self.timer.read() >= std.time.ns_per_s) {
|
|
||||||
self.fps = self.count.swap(0, .Monotonic);
|
|
||||||
self.timer.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.fps;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn intToBytes(comptime T: type, value: anytype) [@sizeOf(T)]u8 {
|
|
||||||
comptime std.debug.assert(@typeInfo(T) == .Int);
|
|
||||||
|
|
||||||
var result: [@sizeOf(T)]u8 = undefined;
|
|
||||||
|
|
||||||
var i: Log2Int(T) = 0;
|
|
||||||
while (i < result.len) : (i += 1) result[i] = @truncate(u8, value >> i * @bitSizeOf(u8));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a copy of a title with all Filesystem-invalid characters replaced
|
|
||||||
///
|
|
||||||
/// e.g. POKEPIN R/S to POKEPIN R_S
|
|
||||||
pub fn escape(title: [12]u8) [12]u8 {
|
|
||||||
var ret: [12]u8 = title;
|
|
||||||
|
|
||||||
//TODO: Add more replacements
|
|
||||||
std.mem.replaceScalar(u8, &ret, '/', '_');
|
|
||||||
std.mem.replaceScalar(u8, &ret, '\\', '_');
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const FilePaths = struct {
|
|
||||||
rom: []const u8,
|
|
||||||
bios: ?[]const u8,
|
|
||||||
save: ?[]const u8,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const io = struct {
|
|
||||||
pub const read = struct {
|
|
||||||
pub fn todo(comptime log: anytype, comptime format: []const u8, args: anytype) u8 {
|
|
||||||
log.debug(format, args);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn undef(comptime T: type, comptime log: anytype, comptime format: []const u8, args: anytype) ?T {
|
|
||||||
@setCold(true);
|
|
||||||
|
|
||||||
const unhandled_io = config.config().debug.unhandled_io;
|
|
||||||
|
|
||||||
log.warn(format, args);
|
|
||||||
if (builtin.mode == .Debug and !unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{});
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn err(comptime T: type, comptime log: anytype, comptime format: []const u8, args: anytype) ?T {
|
|
||||||
@setCold(true);
|
|
||||||
|
|
||||||
log.err(format, args);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const write = struct {
|
|
||||||
pub fn undef(log: anytype, comptime format: []const u8, args: anytype) void {
|
|
||||||
const unhandled_io = config.config().debug.unhandled_io;
|
|
||||||
|
|
||||||
log.warn(format, args);
|
|
||||||
if (builtin.mode == .Debug and !unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Logger = struct {
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
buf: std.io.BufferedWriter(4096 << 2, std.fs.File.Writer),
|
|
||||||
|
|
||||||
pub fn init(file: std.fs.File) Self {
|
|
||||||
return .{
|
|
||||||
.buf = .{ .unbuffered_writer = file.writer() },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn print(self: *Self, comptime format: []const u8, args: anytype) !void {
|
|
||||||
try self.buf.writer().print(format, args);
|
|
||||||
try self.buf.flush(); // FIXME: On panics, whatever is in the buffer isn't written to file
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mgbaLog(self: *Self, cpu: *const Arm7tdmi, opcode: u32) void {
|
|
||||||
const fmt_base = "{X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} cpsr: {X:0>8} | ";
|
|
||||||
const thumb_fmt = fmt_base ++ "{X:0>4}:\n";
|
|
||||||
const arm_fmt = fmt_base ++ "{X:0>8}:\n";
|
|
||||||
|
|
||||||
if (cpu.cpsr.t.read()) {
|
|
||||||
if (opcode >> 11 == 0x1E) {
|
|
||||||
// Instruction 1 of a BL Opcode, print in ARM mode
|
|
||||||
const low = cpu.bus.dbgRead(u16, cpu.r[15] - 2);
|
|
||||||
const bl_opcode = @as(u32, opcode) << 16 | low;
|
|
||||||
|
|
||||||
self.print(arm_fmt, Self.fmtArgs(cpu, bl_opcode)) catch @panic("failed to write to log file");
|
|
||||||
} else {
|
|
||||||
self.print(thumb_fmt, Self.fmtArgs(cpu, opcode)) catch @panic("failed to write to log file");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.print(arm_fmt, Self.fmtArgs(cpu, opcode)) catch @panic("failed to write to log file");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fmtArgs(cpu: *const Arm7tdmi, opcode: u32) FmtArgTuple {
|
|
||||||
return .{
|
|
||||||
cpu.r[0],
|
|
||||||
cpu.r[1],
|
|
||||||
cpu.r[2],
|
|
||||||
cpu.r[3],
|
|
||||||
cpu.r[4],
|
|
||||||
cpu.r[5],
|
|
||||||
cpu.r[6],
|
|
||||||
cpu.r[7],
|
|
||||||
cpu.r[8],
|
|
||||||
cpu.r[9],
|
|
||||||
cpu.r[10],
|
|
||||||
cpu.r[11],
|
|
||||||
cpu.r[12],
|
|
||||||
cpu.r[13],
|
|
||||||
cpu.r[14],
|
|
||||||
cpu.r[15] - if (cpu.cpsr.t.read()) 2 else @as(u32, 4),
|
|
||||||
cpu.cpsr.raw,
|
|
||||||
opcode,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const FmtArgTuple = struct { u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32 };
|
|
||||||
|
|
||||||
pub const audio = struct {
|
|
||||||
const _io = @import("core/bus/io.zig");
|
|
||||||
|
|
||||||
const ToneSweep = @import("core/apu/ToneSweep.zig");
|
|
||||||
const Tone = @import("core/apu/Tone.zig");
|
|
||||||
const Wave = @import("core/apu/Wave.zig");
|
|
||||||
const Noise = @import("core/apu/Noise.zig");
|
|
||||||
|
|
||||||
pub const length = struct {
|
|
||||||
const FrameSequencer = @import("core/apu.zig").FrameSequencer;
|
|
||||||
|
|
||||||
/// Update State of Ch1, Ch2 and Ch3 length timer
|
|
||||||
pub fn update(comptime T: type, self: *T, fs: *const FrameSequencer, nrx34: _io.Frequency) void {
|
|
||||||
comptime std.debug.assert(T == ToneSweep or T == Tone or T == Wave);
|
|
||||||
|
|
||||||
// Write to NRx4 when FS's next step is not one that clocks the length counter
|
|
||||||
if (!fs.isLengthNext()) {
|
|
||||||
// If length_enable was disabled but is now enabled and length timer is not 0 already,
|
|
||||||
// decrement the length timer
|
|
||||||
|
|
||||||
if (!self.freq.length_enable.read() and nrx34.length_enable.read() and self.len_dev.timer != 0) {
|
|
||||||
self.len_dev.timer -= 1;
|
|
||||||
|
|
||||||
// If Length Timer is now 0 and trigger is clear, disable the channel
|
|
||||||
if (self.len_dev.timer == 0 and !nrx34.trigger.read()) self.enabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const ch4 = struct {
|
|
||||||
/// update state of ch4 length timer
|
|
||||||
pub fn update(self: *Noise, fs: *const FrameSequencer, nr44: _io.NoiseControl) void {
|
|
||||||
// Write to NRx4 when FS's next step is not one that clocks the length counter
|
|
||||||
if (!fs.isLengthNext()) {
|
|
||||||
// If length_enable was disabled but is now enabled and length timer is not 0 already,
|
|
||||||
// decrement the length timer
|
|
||||||
|
|
||||||
if (!self.cnt.length_enable.read() and nr44.length_enable.read() and self.len_dev.timer != 0) {
|
|
||||||
self.len_dev.timer -= 1;
|
|
||||||
|
|
||||||
// If Length Timer is now 0 and trigger is clear, disable the channel
|
|
||||||
if (self.len_dev.timer == 0 and !nr44.trigger.read()) self.enabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Sets a quarter (8) of the bits of the u32 `left` to the value of u8 `right`
|
|
||||||
pub inline fn setQuart(left: u32, addr: u8, right: u8) u32 {
|
|
||||||
const offset = @truncate(u2, addr);
|
|
||||||
|
|
||||||
return switch (offset) {
|
|
||||||
0b00 => (left & 0xFFFF_FF00) | right,
|
|
||||||
0b01 => (left & 0xFFFF_00FF) | @as(u32, right) << 8,
|
|
||||||
0b10 => (left & 0xFF00_FFFF) | @as(u32, right) << 16,
|
|
||||||
0b11 => (left & 0x00FF_FFFF) | @as(u32, right) << 24,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculates the correct shift offset for an aligned/unaligned u8 read
|
|
||||||
///
|
|
||||||
/// TODO: Support u16 reads of u32 values?
|
|
||||||
pub inline fn getHalf(byte: u8) u4 {
|
|
||||||
return @truncate(u4, byte & 1) << 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub inline fn setHalf(comptime T: type, left: T, addr: u8, right: HalfInt(T)) T {
|
|
||||||
const offset = @truncate(u1, addr >> if (T == u32) 1 else 0);
|
|
||||||
|
|
||||||
return switch (T) {
|
|
||||||
u32 => switch (offset) {
|
|
||||||
0b0 => (left & 0xFFFF_0000) | right,
|
|
||||||
0b1 => (left & 0x0000_FFFF) | @as(u32, right) << 16,
|
|
||||||
},
|
|
||||||
u16 => switch (offset) {
|
|
||||||
0b0 => (left & 0xFF00) | right,
|
|
||||||
0b1 => (left & 0x00FF) | @as(u16, right) << 8,
|
|
||||||
},
|
|
||||||
else => @compileError("unsupported type"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The Integer type which corresponds to T with exactly half the amount of bits
|
|
||||||
fn HalfInt(comptime T: type) type {
|
|
||||||
const type_info = @typeInfo(T);
|
|
||||||
comptime std.debug.assert(type_info == .Int); // Type must be an integer
|
|
||||||
comptime std.debug.assert(type_info.Int.bits % 2 == 0); // Type must have an even amount of bits
|
|
||||||
|
|
||||||
return std.meta.Int(type_info.Int.signedness, type_info.Int.bits >> 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const Mutex = std.Thread.Mutex;
|
|
||||||
|
|
||||||
pub fn RingBuffer(comptime T: type) type {
|
|
||||||
return struct {
|
|
||||||
const Self = @This();
|
|
||||||
const Index = usize;
|
|
||||||
const max_capacity = (@as(Index, 1) << @typeInfo(Index).Int.bits - 1) - 1; // half the range of index type
|
|
||||||
|
|
||||||
const log = std.log.scoped(.RingBuffer);
|
|
||||||
|
|
||||||
read: Index,
|
|
||||||
write: Index,
|
|
||||||
|
|
||||||
buf: []T,
|
|
||||||
|
|
||||||
mutex: Mutex,
|
|
||||||
|
|
||||||
const Error = error{buffer_full};
|
|
||||||
|
|
||||||
pub fn init(buf: []T) Self {
|
|
||||||
std.mem.set(T, buf, 0);
|
|
||||||
|
|
||||||
std.debug.assert(std.math.isPowerOfTwo(buf.len)); // capacity must be a power of two
|
|
||||||
std.debug.assert(buf.len <= max_capacity);
|
|
||||||
|
|
||||||
return .{ .read = 0, .write = 0, .buf = buf, .mutex = .{} };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn push(self: *Self, left: T, right: T) Error!void {
|
|
||||||
self.mutex.lock();
|
|
||||||
defer self.mutex.unlock();
|
|
||||||
|
|
||||||
try self._push(left);
|
|
||||||
self._push(right) catch |e| {
|
|
||||||
self.write -= 1; // undo the previous write;
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pop(self: *Self) ?T {
|
|
||||||
self.mutex.lock();
|
|
||||||
defer self.mutex.unlock();
|
|
||||||
|
|
||||||
return self._pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn len(self: *Self) Index {
|
|
||||||
self.mutex.lock();
|
|
||||||
defer self.mutex.unlock();
|
|
||||||
|
|
||||||
return self._len();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _push(self: *Self, value: T) Error!void {
|
|
||||||
if (self.isFull()) return error.buffer_full;
|
|
||||||
defer self.write += 1;
|
|
||||||
|
|
||||||
self.buf[self.mask(self.write)] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _pop(self: *Self) ?T {
|
|
||||||
if (self.isEmpty()) return null;
|
|
||||||
defer self.read += 1;
|
|
||||||
|
|
||||||
return self.buf[self.mask(self.read)];
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _len(self: *const Self) Index {
|
|
||||||
return self.write - self.read;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isFull(self: *const Self) bool {
|
|
||||||
return self._len() == self.buf.len;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isEmpty(self: *const Self) bool {
|
|
||||||
return self.read == self.write;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mask(self: *const Self, idx: Index) Index {
|
|
||||||
return idx & (self.buf.len - 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test "RingBuffer" {
|
|
||||||
const Queue = RingBuffer(u8);
|
|
||||||
|
|
||||||
var buf: [4]u8 = undefined;
|
|
||||||
var queue = Queue.init(&buf);
|
|
||||||
|
|
||||||
try queue.push(1, 2);
|
|
||||||
try std.testing.expectEqual(@as(?u8, 1), queue.pop());
|
|
||||||
|
|
||||||
try queue.push(3, 4);
|
|
||||||
try std.testing.expectError(Queue.Error.buffer_full, queue.push(5, 6));
|
|
||||||
try std.testing.expectEqual(@as(?u8, 2), queue.pop());
|
|
||||||
|
|
||||||
try queue.push(7, 8);
|
|
||||||
|
|
||||||
try std.testing.expectEqual(@as(?u8, 3), queue.pop());
|
|
||||||
try std.testing.expectEqual(@as(?u8, 4), queue.pop());
|
|
||||||
try std.testing.expectEqual(@as(?u8, 7), queue.pop());
|
|
||||||
try std.testing.expectEqual(@as(?u8, 8), queue.pop());
|
|
||||||
try std.testing.expectEqual(@as(?u8, null), queue.pop());
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user