17 Commits

Author SHA1 Message Date
bd02f625a5 feat: upgrade to zig v0.15.1
note: emu crashes for unknown reason
2025-10-12 21:03:32 -05:00
6cacdc7180 chore: add minimum zig version 2025-09-22 18:20:37 -05:00
56ac755a73 fix(ci): disable macOS testing 2024-09-09 04:12:06 -05:00
c5ffa19e06 fix(macos): vendor macOS specific dependency in zgui 2024-09-09 03:48:42 -05:00
2d3c659b85 fix(windows): ensure that SDL is at "SDL.h" instead of "SDL2/SDL.h" 2024-09-09 03:31:55 -05:00
94894fadc6 chore(ci): update to Zig v0.13.0 2024-09-09 02:17:17 -05:00
0e02d9aaab feat: upgrade to Zig v0.13.0 2024-09-09 02:16:31 -05:00
b4830326ff chore: update SDL.zig and zgui 2024-03-22 12:53:11 -05:00
ef93bbe084 ci: resolve ci build errors for the last time i promise 2024-03-06 18:21:00 -06:00
f71aaafe41 ci: get windows and ubuntu builds working again 2024-03-06 17:52:50 -06:00
66192daf6c feat: target Zig v2024.1.0-mach 2024-02-09 01:25:13 -06:00
05b7a9014d chore(ui): don't crash on unexpected scheduler pqueue len 2023-12-20 17:58:42 -06:00
493d7aeede fix(ui): reset, bios load and rom load are properly thread safe 2023-12-20 11:38:58 -06:00
9183e6850d fix: use a mutex to pause emu thread
still not ideal imo
2023-12-15 04:10:51 -06:00
d54202bf8b chore(platform): update opengl impl fns to versions in paoda/turbo 2023-12-15 03:11:16 -06:00
d097dcc2f5 fix(gui): quit when emu thread exits first 2023-12-15 02:49:40 -06:00
203971c91a chore: zba-gdbstub as a git submodule 2023-12-15 02:35:33 -06:00
35 changed files with 1369 additions and 5806 deletions

View File

@@ -4,6 +4,7 @@ on:
push:
paths:
- "**.zig"
- "dl_sdl2.ps1"
branches:
- main
schedule:
@@ -14,13 +15,17 @@ jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
# os: [ubuntu-latest, windows-latest]
os: [ubuntu-latest, windows-latest] # TODO: Figure out Apple Silicon macOS
runs-on: ${{matrix.os}}
steps:
- uses: goto-bus-stop/setup-zig@v2
with:
version: 0.11.0
version: 0.13.0
- run: |
git config --global core.autocrlf false
- uses: actions/checkout@v3
with:
submodules: recursive
- name: prepare-linux
if: runner.os == 'Linux'
run: |
@@ -29,26 +34,19 @@ jobs:
- name: prepare-windows
if: runner.os == 'Windows'
run: |
vcpkg integrate install
vcpkg install sdl2:x64-windows
git config --global core.autocrlf false
.\dl_sdl2.ps1
- name: prepare-macos
if: runner.os == 'macOS'
run: |
brew install sdl2
- uses: actions/checkout@v3
with:
submodules: recursive
- name: build
run: zig build -Doptimize=ReleaseSafe -Dcpu=baseline
- name: prepare-executable
run: |
mv zig-out/lib/* zig-out/bin
- name: upload
uses: actions/upload-artifact@v3
with:
name: zba-${{matrix.os}}
path: zig-out/bin
path: zig-out
lint:
runs-on: ubuntu-latest
steps:
@@ -57,6 +55,5 @@ jobs:
submodules: recursive
- uses: goto-bus-stop/setup-zig@v2
with:
version: 0.11.0-dev.3395+1e7dcaa3a
- run: zig fmt src/**/*.zig
version: 0.13.0
- run: zig fmt --check {src,lib}/**/*.zig build.zig build.zig.zon

1
.gitignore vendored
View File

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

6
.gitmodules vendored
View File

@@ -1,6 +0,0 @@
[submodule "lib/SDL.zig"]
path = lib/SDL.zig
url = https://github.com/MasterQ32/SDL.zig
[submodule "lib/zgui"]
path = lib/zgui
url = https://git.musuka.dev/paoda/zgui

175
build.zig
View File

@@ -1,71 +1,154 @@
const std = @import("std");
const builtin = @import("builtin");
const Sdk = @import("lib/SDL.zig/Sdk.zig");
const zgui = @import("lib/zgui/build.zig");
// Although this function looks imperative, it does not perform the build
// directly and instead it mutates the build graph (`b`) that will be then
// executed by an external runner. The functions in `std.Build` implement a DSL
// for defining build steps and express dependencies between them, allowing the
// build runner to parallelize the build automatically (and the cache system to
// know when a step doesn't need to be re-run).
pub fn build(b: *std.Build) void {
// Minimum Zig Version
const min_ver = std.SemanticVersion.parse("0.11.0") catch return; // https://github.com/ziglang/zig/tree/0.11.0
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 allow the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
// for restricting supported target set are available.
const target = b.standardTargetOptions(.{});
// Standard optimization options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
// set a preferred release mode, allowing the user to decide how to optimize.
const optimize = b.standardOptimizeOption(.{});
// It's also possible to define more custom flags to toggle optional features
// of this build script using `b.option()`. All defined flags (including
// target and optimize options) will be listed when running `zig build --help`
// in this directory.
const exe = b.addExecutable(.{
.name = "zba",
.root_source_file = .{ .path = "src/main.zig" },
// This creates a module, which represents a collection of source files alongside
// some compilation options, such as optimization mode and linked system libraries.
// Zig modules are the preferred way of making Zig code available to consumers.
// addModule defines a module that we intend to make available for importing
// to our consumers. We must give it a name because a Zig package can expose
// multiple modules and consumers will need to be able to specify which
// module they want to access.
const exe_mod = b.addModule("zba", .{
// The root source file is the "entry point" of this module. Users of
// this module will only be able to access public declarations contained
// in this file, which means that if you have declarations that you
// intend to expose to consumers that were defined in other files part
// of this module, you will have to make sure to re-export them from
// the root file.
.root_source_file = b.path("src/main.zig"),
// Later on we'll use this module as the root module of a test executable
// which requires us to specify a target.
.target = target,
.optimize = optimize,
.link_libc = true,
});
exe.main_pkg_path = .{ .path = "." }; // Necessary so that src/main.zig can embed example.toml
exe.addModule("known_folders", b.dependency("known-folders", .{}).module("known-folders")); // https://github.com/ziglibs/known-folders
exe.addModule("datetime", b.dependency("zig-datetime", .{}).module("zig-datetime")); // https://github.com/frmdstryr/zig-datetime
exe.addModule("clap", b.dependency("zig-clap", .{}).module("clap")); // https://github.com/Hejsil/zig-clap
exe.addModule("gdbstub", b.dependency("zba-gdbstub", .{}).module("gdbstub")); // https://git.musuka.dev/paoda/zba-gdbstub
exe.addModule("zba-util", b.dependency("zba-util", .{}).module("zba-util")); // https://git.musuka.dev/paoda/zba-util
exe.addModule("tomlz", b.dependency("tomlz", .{}).module("tomlz")); // https://github.com/mattyhall/tomlz
exe.addModule("arm32", b.dependency("arm32", .{}).module("arm32")); // https://git.musuka.dev/paoda/arm32
// Here we define an executable. An executable needs to have a root module
// which needs to expose a `main` function. While we could add a main function
// to the module defined above, it's sometimes preferable to split business
// business logic and the CLI into two separate modules.
//
// If your goal is to create a Zig library for others to use, consider if
// it might benefit from also exposing a CLI tool. A parser library for a
// data serialization format could also bundle a CLI syntax checker, for example.
//
// If instead your goal is to create an executable, consider if users might
// be interested in also being able to embed the core functionality of your
// program in their own executable in order to avoid the overhead involved in
// subprocessing your CLI tool.
//
// If neither case applies to you, feel free to delete the declaration you
// don't need and to put everything under a single module.
const exe = b.addExecutable(.{ .name = "zba", .root_module = exe_mod });
// https://github.com/fabioarnold/nfd-zig
const nfd_dep = b.dependency("nfd", .{ .target = target, .optimize = optimize });
exe.linkLibrary(nfd_dep.artifact("nfd"));
exe.addModule("nfd", nfd_dep.module("nfd"));
const zgui = b.dependency("zgui", .{ .shared = false, .with_implot = true, .backend = .sdl3_opengl3 });
const sdl = b.dependency("sdl", .{ .target = target, .optimize = optimize, .preferred_linkage = .static });
const gl = @import("zigglgen").generateBindingsModule(b, .{ .api = .gl, .version = .@"3.3", .profile = .core });
// https://github.com/MasterQ32/SDL.zig
const sdk = Sdk.init(b, null);
sdk.link(exe, .dynamic);
exe.addModule("sdl2", sdk.getNativeModule());
const sdl_lib = sdl.artifact("SDL3");
const zgui_lib = zgui.artifact("imgui");
// https://git.musuka.dev/paoda/zgui
// .shared option should stay in sync with SDL.zig call above where true == .dynamic, and false == .static
const zgui_pkg = zgui.package(b, target, optimize, .{ .options = .{ .backend = .sdl2_opengl3, .shared = true } });
zgui_pkg.link(exe);
exe_mod.linkLibrary(sdl_lib);
exe_mod.linkLibrary(zgui_lib);
exe.addAnonymousModule("bitfield", .{ .source_file = .{ .path = "lib/bitfield.zig" } }); // https://github.com/FlorenceOS/
exe.addAnonymousModule("gl", .{ .source_file = .{ .path = "lib/gl.zig" } }); // https://github.com/MasterQ32/zig-opengl
exe_mod.addImport("gl", gl);
exe_mod.addImport("known_folders", b.dependency("known_folders", .{}).module("known-folders"));
exe_mod.addImport("datetime", b.dependency("datetime", .{}).module("datetime"));
exe_mod.addImport("clap", b.dependency("clap", .{}).module("clap"));
exe_mod.addImport("zba_util", b.dependency("zba_util", .{}).module("zba_util"));
exe_mod.addImport("arm32", b.dependency("arm32", .{}).module("arm32"));
exe_mod.addImport("gdbstub", b.dependency("zba_gdbstub", .{}).module("zba_gdbstub"));
exe_mod.addImport("nfd", b.dependency("nfdzig", .{}).module("nfd"));
exe_mod.addImport("zgui", zgui.module("root"));
exe_mod.addImport("bitjuggle", b.dependency("bitjuggle", .{}).module("bitjuggle"));
// This declares intent for the executable to be installed into the
// install prefix when running `zig build` (i.e. when executing the default
// step). By default the install prefix is `zig-out/` but can be overridden
// by passing `--prefix` or `-p`.
b.installArtifact(exe);
// This creates a top level step. Top level steps have a name and can be
// invoked by name when running `zig build` (e.g. `zig build run`).
// This will evaluate the `run` step rather than the default step.
// For a top level step to actually do something, it must depend on other
// steps (e.g. a Run step, as we will see in a moment).
const run_step = b.step("run", "Run the app");
// This creates a RunArtifact step in the build graph. A RunArtifact step
// invokes an executable compiled by Zig. Steps will only be executed by the
// runner if invoked directly by the user (in the case of top level steps)
// or if another step depends on it, so it's up to you to define when and
// how this Run step will be executed. In our case we want to run it when
// the user runs `zig build run`, so we create a dependency link.
const run_cmd = b.addRunArtifact(exe);
run_step.dependOn(&run_cmd.step);
// By making the run step depend on the default step, it will be run from the
// installation directory rather than directly from within the cache directory.
run_cmd.step.dependOn(b.getInstallStep());
// This allows the user to pass arguments to the application in the build
// command itself, like this: `zig build run -- arg1 arg2 etc`
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
const exe_tests = b.addTest(.{
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
// Creates an executable that will run `test` blocks from the provided module.
// Here `mod` needs to define a target, which is why earlier we made sure to
// set the releative field.
const mod_tests = b.addTest(.{
.root_module = exe_mod,
});
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&exe_tests.step);
// A run step that will run the test executable.
const run_mod_tests = b.addRunArtifact(mod_tests);
// Creates an executable that will run `test` blocks from the executable's
// root module. Note that test executables only test one module at a time,
// hence why we have to create two separate ones.
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});
// A run step that will run the second test executable.
const run_exe_tests = b.addRunArtifact(exe_tests);
// A top level step for running all tests. dependOn can be called multiple
// times and since the two run steps do not depend on one another, this will
// make the two of them run in parallel.
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_mod_tests.step);
test_step.dependOn(&run_exe_tests.step);
// Just like flags, top level steps are also listed in the `--help` menu.
//
// The Zig build system is entirely implemented in userland, which means
// that it cannot hook into private compiler APIs. All compilation work
// orchestrated by the build system will result in other Zig compiler
// subcommands being invoked with the right flags defined. You can observe
// these invocations when one fails (or you pass a flag to increase
// verbosity) to validate assumptions and diagnose problems.
//
// Lastly, the Zig build system is relatively simple and self-contained,
// and reading its source code will allow you to master it.
}

View File

@@ -1,38 +1,55 @@
.{
.name = "zba",
.name = .zba,
.version = "0.1.0",
.paths = .{
"build.zig",
"build.zig.zon",
"lib/bitfield.zig",
"lib/gl.zig",
"src",
},
.minimum_zig_version = "0.15.1",
.fingerprint = 0xcb596c7fbdb20efc,
.dependencies = .{
.nfd = .{
.url = "https://github.com/paoda/nfd-zig/archive/3333a86186a0cb9fbf57823ac416aa77d472db61.tar.gz",
.hash = "12201079ca80d9a7cfe9f2f3ffe4d5e92627a48dfdec5dd3c8cf9a1609f746a9e17f",
// .tomlz = .{
// .url = "git+https://github.com/paoda/tomlz#9a16dd53927ef2012478b6494bafb4475e44f4c9",
// .hash = "12204f922cab84980e36b5c058d354ec0ee169bda401c8e0e80a463580349b476569",
// },
.zba_util = .{ .path = "../zba-util" },
.arm32 = .{ .path = "../arm32" },
.zba_gdbstub = .{ .path = "../zba-gdbstub" },
.known_folders = .{
.url = "git+https://github.com/ziglibs/known-folders.git#ab5cf5feb936fa3b72c95d3ad0c0c67791937ba1",
.hash = "known_folders-0.0.0-Fy-PJtTTAADUOhGKM0sxzG4eMkNQxRvx9e5dfHVyaeA3",
},
.@"known-folders" = .{
.url = "https://github.com/ziglibs/known-folders/archive/fa75e1bc672952efa0cf06160bbd942b47f6d59b.tar.gz",
.hash = "122048992ca58a78318b6eba4f65c692564be5af3b30fbef50cd4abeda981b2e7fa5",
.nfdzig = .{
.url = "git+https://github.com/paoda/nfd-zig#0ad2a0c092ffba0c98613d619b82100c991f5ad6",
.hash = "nfdzig-0.1.0-11fxvN6IBgD5rvvfjrw1wPqibMsbUJ-h2ZcGR6FOEvrm",
},
.@"zig-datetime" = .{
.url = "https://github.com/frmdstryr/zig-datetime/archive/ddecb4e508e99ad6ab1314378225413959d54756.tar.gz",
.hash = "12202cbb909feb6b09164ac997307c6b1ab35cb05a846198cf41f7ec608d842c1761",
.datetime = .{
.url = "git+https://github.com/frmdstryr/zig-datetime#3a39a21e6e34dcb0ade0ff828d0914d40ba535f3",
.hash = "datetime-0.8.0-cJNXzP_YAQBxQ5hkNNP6ScnG5XsqciJmeP5RVV4xwCBA",
},
.@"zig-clap" = .{
.url = "https://github.com/Hejsil/zig-clap/archive/f49b94700e0761b7514abdca0e4f0e7f3f938a93.tar.gz",
.hash = "1220f48518ce22882e102255ed3bcdb7aeeb4891f50b2cdd3bd74b5b2e24d3149ba2",
.clap = .{
.url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.11.0.tar.gz",
.hash = "clap-0.11.0-oBajB-HnAQDPCKYzwF7rO3qDFwRcD39Q0DALlTSz5H7e",
},
.@"zba-gdbstub" = .{
.url = "https://git.musuka.dev/paoda/zba-gdbstub/archive/d3655e8f61a4943edfce40629dd2a82f9c7bf21c.tar.gz",
.hash = "122076f8e22bb55b555c9812d72d908419eda1bf07b6bb7a1f6d9ecb2bb2a49c56d0",
.sdl = .{
.url = "git+https://github.com/castholm/SDL.git#b1913e7c31ad72ecfd3ab04aeac387027754cfaf",
.hash = "sdl-0.3.0+3.2.22-7uIn9Pg3fwGG2IyIOPxxOSVe-75nUng9clt7tXGFLzMr",
},
.@"zba-util" = .{
.url = "https://git.musuka.dev/paoda/zba-util/archive/322c798e384a0d24cc84ffcfa2e4a3ca807798a0.tar.gz",
.hash = "12209ce0e729460b997706e47a53a32f1842672cd120189e612f4871731780a30ed0",
.zigglgen = .{
.url = "git+https://github.com/castholm/zigglgen.git#a1e969b3e35818785fab0373124f50463793b28a",
.hash = "zigglgen-0.4.0-bmyqLQGMLwA0EPVmSm-Nc6Olb84zBzvwMjguxwKYLf1S",
},
.tomlz = .{
.url = "https://github.com/mattyhall/tomlz/archive/47067cd7c902485f7d6e928331fd171ed47f72da.tar.gz",
.hash = "12205771687a4d42700c515ef32e4fadb8b4dbf9e3f941b0a0e6f8bde5ef6dbc27d6",
.bitjuggle = .{
.url = "git+https://github.com/leecannon/zig-bitjuggle#80111f4f8c672aaea94a8a189ae2a7c8bbaf883f",
.hash = "bitjuggle-2.0.0-SJdU76dvAAARompHEhqKDiwZ4FE4FZ8eHvPvmz5JUOS0",
},
.arm32 = .{
.url = "https://git.musuka.dev/paoda/arm32/archive/ba22b856ecb3bd6fc43530dddf6ee79b4b458b30.tar.gz",
.hash = "1220f1cc3e23804eff5c68b93a4e77d948a9cfc3492d799d39f769d8c79b7c41d83e",
.zgui = .{
.url = "git+https://github.com/zig-gamedev/zgui#7fa8081c208885b85e3fdfc043cd9d9cb9559123",
.hash = "zgui-0.6.0-dev--L6sZL7tbQAPRLYrcQAVx0V49tPHAXNxclZ-v8IP4wLr",
},
},
}

36
dl_sdl2.ps1 Normal file
View File

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

Submodule lib/SDL.zig deleted from 602aeb7f1d

View File

@@ -1,146 +0,0 @@
const std = @import("std");
fn PtrCastPreserveCV(comptime T: type, comptime PtrToT: type, comptime NewT: type) type {
return switch (PtrToT) {
*T => *NewT,
*const T => *const NewT,
*volatile T => *volatile NewT,
*const volatile T => *const volatile NewT,
else => @compileError("wtf you doing"),
};
}
fn BitType(comptime FieldType: type, comptime ValueType: type, comptime shamt: usize) type {
const self_bit: FieldType = (1 << shamt);
return extern struct {
bits: Bitfield(FieldType, shamt, 1),
pub fn set(self: anytype) void {
self.bits.field().* |= self_bit;
}
pub fn unset(self: anytype) void {
self.bits.field().* &= ~self_bit;
}
pub fn read(self: anytype) ValueType {
return @bitCast(@as(u1, @truncate(self.bits.field().* >> shamt)));
}
// Since these are mostly used with MMIO, I want to avoid
// reading the memory just to write it again, also races
pub fn write(self: anytype, val: ValueType) void {
if (@as(bool, @bitCast(val))) {
self.set();
} else {
self.unset();
}
}
};
}
// Original Bit Constructor
// pub fn Bit(comptime FieldType: type, comptime shamt: usize) type {
// return BitType(FieldType, u1, shamt);
// }
pub fn Bit(comptime FieldType: type, comptime shamt: usize) type {
return BitType(FieldType, bool, shamt);
}
fn Boolean(comptime FieldType: type, comptime shamt: usize) type {
return BitType(FieldType, bool, shamt);
}
pub fn Bitfield(comptime FieldType: type, comptime shamt: usize, comptime num_bits: usize) type {
if (shamt + num_bits > @bitSizeOf(FieldType)) {
@compileError("bitfield doesn't fit");
}
const self_mask: FieldType = ((1 << num_bits) - 1) << shamt;
const ValueType = std.meta.Int(.unsigned, num_bits);
return extern struct {
dummy: FieldType,
fn field(self: anytype) PtrCastPreserveCV(@This(), @TypeOf(self), FieldType) {
return @ptrCast(self);
}
pub fn write(self: anytype, val: ValueType) void {
self.field().* &= ~self_mask;
self.field().* |= @as(FieldType, @intCast(val)) << shamt;
}
pub fn read(self: anytype) ValueType {
const val: FieldType = self.field().*;
return @intCast((val & self_mask) >> shamt);
}
};
}
test "bit" {
const S = extern union {
low: Bit(u32, 0),
high: Bit(u32, 1),
val: u32,
};
std.testing.expect(@sizeOf(S) == 4);
std.testing.expect(@bitSizeOf(S) == 32);
var s: S = .{ .val = 1 };
std.testing.expect(s.low.read() == 1);
std.testing.expect(s.high.read() == 0);
s.low.write(0);
s.high.write(1);
std.testing.expect(s.val == 2);
}
test "boolean" {
const S = extern union {
low: Boolean(u32, 0),
high: Boolean(u32, 1),
val: u32,
};
std.testing.expect(@sizeOf(S) == 4);
std.testing.expect(@bitSizeOf(S) == 32);
var s: S = .{ .val = 2 };
std.testing.expect(s.low.read() == false);
std.testing.expect(s.high.read() == true);
s.low.write(true);
s.high.write(false);
std.testing.expect(s.val == 1);
}
test "bitfield" {
const S = extern union {
low: Bitfield(u32, 0, 16),
high: Bitfield(u32, 16, 16),
val: u32,
};
std.testing.expect(@sizeOf(S) == 4);
std.testing.expect(@bitSizeOf(S) == 32);
var s: S = .{ .val = 0x13376969 };
std.testing.expect(s.low.read() == 0x6969);
std.testing.expect(s.high.read() == 0x1337);
s.low.write(0x1337);
s.high.write(0x6969);
std.testing.expect(s.val == 0x69691337);
}

5053
lib/gl.zig

File diff suppressed because it is too large Load Diff

Submodule lib/zgui deleted from ca27a47224

View File

@@ -1,5 +1,5 @@
const std = @import("std");
const tomlz = @import("tomlz");
// const tomlz = @import("tomlz");
const Allocator = std.mem.Allocator;
@@ -59,5 +59,6 @@ pub fn load(allocator: Allocator, file_path: []const u8) !void {
const contents = try config_file.readToEndAlloc(allocator, try config_file.getEndPos());
defer allocator.free(contents);
state = try tomlz.parser.decode(Config, allocator, contents);
// FIXME(2025-09-22): re-enable
// state = try tomlz.parser.decode(Config, allocator, contents);
}

View File

@@ -13,13 +13,13 @@ const TimerTuple = @import("bus/timer.zig").TimerTuple;
const Scheduler = @import("scheduler.zig").Scheduler;
const FilePaths = @import("../util.zig").FilePaths;
const io = @import("bus/io.zig");
const _io = @import("bus/io.zig");
const Allocator = std.mem.Allocator;
const log = std.log.scoped(.Bus);
const createDmaTuple = @import("bus/dma.zig").create;
const createTimerTuple = @import("bus/timer.zig").create;
const rotr = @import("zba-util").rotr;
const rotr = @import("zba_util").rotr;
const timings: [2][0x10]u8 = [_][0x10]u8{
// BIOS, Unused, EWRAM, IWRAM, I/0, PALRAM, VRAM, OAM, ROM0, ROM0, ROM1, ROM1, ROM2, ROM2, SRAM, Unused
@@ -81,7 +81,7 @@ pub fn init(self: *Self, allocator: Allocator, sched: *Scheduler, cpu: *Arm7tdmi
.allocator = allocator,
};
self.fillReadTable(read_table);
self.fillReadTable(@ptrCast(read_table));
// Internal Display Memory behaves differently on 8-bit reads
self.fillWriteTable(u32, write_tables[0]);
@@ -135,12 +135,10 @@ pub fn replaceGamepak(self: *Self, file_path: []const u8) !void {
self.pak = try GamePak.init(self.allocator, self.cpu, file_path, save_path);
const read_ptr: *[table_len]?*const anyopaque = @constCast(self.read_table);
const write_ptrs: [2]*[table_len]?*anyopaque = .{ @constCast(self.write_tables[0]), @constCast(self.write_tables[1]) };
self.fillReadTable(read_ptr);
self.fillWriteTable(u32, write_ptrs[0]);
self.fillWriteTable(u8, write_ptrs[1]);
// SAFETY: TODO: why do we know this is safe?
self.fillReadTable(@ptrCast(@constCast(self.read_table)));
self.fillWriteTable(u32, @constCast(self.write_tables[0]));
self.fillWriteTable(u8, @constCast(self.write_tables[1]));
}
fn fillReadTable(self: *Self, table: *[table_len]?*const anyopaque) void {
@@ -169,7 +167,7 @@ fn fillReadTable(self: *Self, table: *[table_len]?*const anyopaque) void {
}
}
fn fillWriteTable(self: *Self, comptime T: type, table: *[table_len]?*const anyopaque) void {
fn fillWriteTable(self: *Self, comptime T: type, table: *[table_len]?*anyopaque) void {
comptime std.debug.assert(T == u32 or T == u16 or T == u8);
const vramMirror = @import("ppu/Vram.zig").mirror;
@@ -240,11 +238,11 @@ fn fillReadTableExternal(self: *Self, addr: u32) ?*anyopaque {
}
fn readIo(self: *const Self, comptime T: type, address: u32) T {
return io.read(self, T, address) orelse self.openBus(T, address);
return _io.read(self, T, address) orelse self.openBus(T, address);
}
fn openBus(self: *const Self, comptime T: type, address: u32) T {
@setCold(true);
@branchHint(.cold);
const r15 = self.cpu.r[15];
const word = blk: {
@@ -305,7 +303,7 @@ fn openBus(self: *const Self, comptime T: type, address: u32) T {
}
pub fn read(self: *Self, comptime T: type, unaligned_address: u32) T {
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits;
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).int.bits;
const page = unaligned_address >> bits;
const offset = unaligned_address & (page_size - 1);
@@ -328,7 +326,7 @@ pub fn read(self: *Self, comptime T: type, unaligned_address: u32) T {
}
pub fn dbgRead(self: *const Self, comptime T: type, unaligned_address: u32) T {
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits;
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).int.bits;
const page = unaligned_address >> bits;
const offset = unaligned_address & (page_size - 1);
@@ -348,7 +346,7 @@ pub fn dbgRead(self: *const Self, comptime T: type, unaligned_address: u32) T {
}
fn slowRead(self: *Self, comptime T: type, unaligned_address: u32) T {
@setCold(true);
@branchHint(.cold);
const page: u8 = @truncate(unaligned_address >> 24);
const address = forceAlign(T, unaligned_address);
@@ -419,7 +417,7 @@ fn readBackup(self: *const Self, comptime T: type, unaligned_address: u32) T {
}
pub fn write(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits;
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).int.bits;
const page = unaligned_address >> bits;
const offset = unaligned_address & (page_size - 1);
@@ -446,7 +444,7 @@ pub fn write(self: *Self, comptime T: type, unaligned_address: u32, value: T) vo
/// Mostly Identical to `Bus.write`, slowmeme is handled by `Bus.dbgSlowWrite`
pub fn dbgWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits;
const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).int.bits;
const page = unaligned_address >> bits;
const offset = unaligned_address & (page_size - 1);
@@ -469,7 +467,7 @@ pub fn dbgWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T)
}
fn slowWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
@setCold(true);
@branchHint(.cold);
const page: u8 = @truncate(unaligned_address >> 24);
const address = forceAlign(T, unaligned_address);
@@ -479,7 +477,7 @@ fn slowWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) vo
0x00 => self.bios.write(T, address, value),
0x02 => unreachable, // completely handled by fastmem
0x03 => unreachable, // completely handled by fastmem
0x04 => io.write(self, T, address, value),
0x04 => _io.write(self, T, address, value),
// Internal Display Memory
0x05 => self.ppu.palette.write(T, address, value),
@@ -494,7 +492,7 @@ fn slowWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) vo
}
fn dbgSlowWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) void {
@setCold(true);
@branchHint(.cold);
const page: u8 = @truncate(unaligned_address >> 24);
const address = forceAlign(T, unaligned_address);

View File

@@ -1,8 +1,9 @@
const std = @import("std");
const SDL = @import("sdl2");
const io = @import("bus/io.zig");
const util = @import("../util.zig");
const c = @import("../lib.zig").c;
const LinearFifo = @import("../lib/fifo.zig").LinearFifo;
const Arm7tdmi = @import("arm32").Arm7tdmi;
const Bus = @import("Bus.zig");
const Scheduler = @import("scheduler.zig").Scheduler;
@@ -11,7 +12,7 @@ const Tone = @import("apu/Tone.zig");
const Wave = @import("apu/Wave.zig");
const Noise = @import("apu/Noise.zig");
const SoundFifo = std.fifo.LinearFifo(u8, .{ .Static = 0x20 });
const SoundFifo = LinearFifo(u8, .{ .static = 0x20 });
const getHalf = util.getHalf;
const setHalf = util.setHalf;
@@ -246,7 +247,7 @@ pub const Apu = struct {
sampling_cycle: u2,
stream: *SDL.SDL_AudioStream,
stream: *c.SDL_AudioStream,
sched: *Scheduler,
fs: FrameSequencer,
@@ -271,8 +272,8 @@ pub const Apu = struct {
.bias = .{ .raw = 0x0200 },
.sampling_cycle = 0b00,
.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, 1 << 15, host_format, 2, host_rate).?,
.sched = sched,
.stream = undefined, // FIXME: bad practice
.capacitor = 0,
.fs = FrameSequencer.init(),
@@ -459,21 +460,33 @@ pub const Apu = struct {
if (self.sampling_cycle != self.bias.sampling_cycle.read()) self.replaceSDLResampler();
_ = SDL.SDL_AudioStreamPut(self.stream, &[2]u16{ ext_left, ext_right }, 2 * @sizeOf(u16));
const ret = c.SDL_PutAudioStreamData(self.stream, &[2]i16{ @bitCast(ext_left ^ 0x8000), @bitCast(ext_right ^ 0x8000) }, 2 * @sizeOf(i16));
if (!ret) @panic("TODO: Failed to put i16s into SDL Audio Queue");
}
fn replaceSDLResampler(self: *Self) void {
@setCold(true);
const sample_rate = Self.sampleRate(self.bias.sampling_cycle.read());
log.info("Sample Rate changed from {}Hz to {}Hz", .{ Self.sampleRate(self.sampling_cycle), sample_rate });
@branchHint(.cold);
_ = self;
// Sampling Cycle (Sample Rate) changed, Craete a new SDL Audio Resampler
// FIXME: Replace SDL's Audio Resampler with either a custom or more reliable one
const old_stream = self.stream;
defer SDL.SDL_FreeAudioStream(old_stream);
@panic("TODO: Implement Multiple Sample Rates...");
self.sampling_cycle = self.bias.sampling_cycle.read();
self.stream = SDL.SDL_NewAudioStream(SDL.AUDIO_U16, 2, @intCast(sample_rate), host_format, 2, host_rate).?;
// const sample_rate = Self.sampleRate(self.bias.sampling_cycle.read());
// log.info("Sample Rate changed from {}Hz to {}Hz", .{ Self.sampleRate(self.sampling_cycle), sample_rate });
// // Sampling Cycle (Sample Rate) changed, Craete a new SDL Audio Resampler
// // FIXME: Replace SDL's Audio Resampler with either a custom or more reliable one
// const old_stream = self.stream;
// defer c.SDL_DestroyAudioStream(old_stream);
// self.sampling_cycle = self.bias.sampling_cycle.read();
// var desired: c.SDL_AudioSpec = std.mem.zeroes(c.SDL_AudioSpec);
// desired.format = c.SDL_AUDIO_S16;
// desired.channels = 2;
// desired.freq = @intCast(sample_rate);
// const new_stream = c.SDL_OpenAudioDeviceStream(c.SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desired, null, null) orelse @panic("TODO: Failed to replace SDL Audio Stream");
// self.stream = new_stream;
}
fn interval(self: *const Self) u64 {
@@ -569,7 +582,7 @@ pub fn DmaSound(comptime kind: DmaSoundKind) type {
}
fn enable(self: *Self) void {
@setCold(true);
@branchHint(.cold);
self.enabled = true;
}

View File

@@ -18,7 +18,7 @@ pub fn read(self: *const Self, comptime T: type, nr30: io.WaveSelect, addr: u32)
const base = if (!nr30.bank.read()) @as(u32, 0x10) else 0; // Read from the Opposite Bank in Use
const i = base + addr - 0x0400_0090;
return std.mem.readIntSliceLittle(T, self.buf[i..][0..@sizeOf(T)]);
return std.mem.readInt(T, self.buf[i..][0..@sizeOf(T)], .little);
}
pub fn write(self: *Self, comptime T: type, nr30: io.WaveSelect, addr: u32, value: T) void {
@@ -26,7 +26,7 @@ pub fn write(self: *Self, comptime T: type, nr30: io.WaveSelect, addr: u32, valu
const base = if (!nr30.bank.read()) @as(u32, 0x10) else 0; // Write to the Opposite Bank in Use
const i = base + addr - 0x0400_0090;
std.mem.writeIntSliceLittle(T, self.buf[i..][0..@sizeOf(T)], value);
std.mem.writeInt(T, self.buf[i..][0..@sizeOf(T)], value, .little);
}
pub fn init(sched: *Scheduler) Self {

View File

@@ -3,7 +3,7 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const log = std.log.scoped(.Bios);
const rotr = @import("zba-util").rotr;
const rotr = @import("zba_util").rotr;
const forceAlign = @import("../Bus.zig").forceAlign;
/// Size of the BIOS in bytes
@@ -51,13 +51,13 @@ fn _read(self: *const Self, comptime T: type, addr: u32) T {
const buf = self.buf orelse std.debug.panic("[BIOS] ZBA tried to read {} from 0x{X:0>8} but not BIOS was present", .{ T, addr });
return switch (T) {
u32, u16, u8 => std.mem.readIntSliceLittle(T, buf[addr..][0..@sizeOf(T)]),
u32, u16, u8 => std.mem.readInt(T, buf[addr..][0..@sizeOf(T)], .little),
else => @compileError("BIOS: Unsupported read width"),
};
}
pub fn write(_: *Self, comptime T: type, addr: u32, value: T) void {
@setCold(true);
@branchHint(.cold);
log.debug("Tried to write {} 0x{X:} to 0x{X:0>8} ", .{ T, value, addr });
}

View File

@@ -11,7 +11,7 @@ pub fn read(self: *const Self, comptime T: type, address: usize) T {
const addr = address & 0x3FFFF;
return switch (T) {
u32, u16, u8 => std.mem.readIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)]),
u32, u16, u8 => std.mem.readInt(T, self.buf[addr..][0..@sizeOf(T)], .little),
else => @compileError("EWRAM: Unsupported read width"),
};
}
@@ -20,7 +20,7 @@ pub fn write(self: *const Self, comptime T: type, address: usize, value: T) void
const addr = address & 0x3FFFF;
return switch (T) {
u32, u16, u8 => std.mem.writeIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)], value),
u32, u16, u8 => std.mem.writeInt(T, self.buf[addr..][0..@sizeOf(T)], value, .little),
else => @compileError("EWRAM: Unsupported write width"),
};
}

View File

@@ -11,7 +11,7 @@ pub fn read(self: *const Self, comptime T: type, address: usize) T {
const addr = address & 0x7FFF;
return switch (T) {
u32, u16, u8 => std.mem.readIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)]),
u32, u16, u8 => std.mem.readInt(T, self.buf[addr..][0..@sizeOf(T)], .little),
else => @compileError("IWRAM: Unsupported read width"),
};
}
@@ -20,7 +20,7 @@ pub fn write(self: *const Self, comptime T: type, address: usize, value: T) void
const addr = address & 0x7FFF;
return switch (T) {
u32, u16, u8 => std.mem.writeIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)], value),
u32, u16, u8 => std.mem.writeInt(T, self.buf[addr..][0..@sizeOf(T)], value, .little),
else => @compileError("IWRAM: Unsupported write width"),
};
}

View File

@@ -121,7 +121,7 @@ pub const Eeprom = struct {
.Large => {
if (self.writer.len() == 14) {
const addr: u10 = @intCast(self.writer.finish());
const value = std.mem.readIntSliceLittle(u64, buf[@as(u13, addr) * 8 ..][0..8]);
const value = std.mem.readInt(u64, buf[@as(u13, addr) * 8 ..][0..8], .little);
self.reader.configure(value);
self.state = .RequestEnd;
@@ -131,7 +131,7 @@ pub const Eeprom = struct {
if (self.writer.len() == 6) {
// FIXME: Duplicated code from above
const addr: u6 = @intCast(self.writer.finish());
const value = std.mem.readIntSliceLittle(u64, buf[@as(u13, addr) * 8 ..][0..8]);
const value = std.mem.readInt(u64, buf[@as(u13, addr) * 8 ..][0..8], .little);
self.reader.configure(value);
self.state = .RequestEnd;
@@ -159,7 +159,7 @@ pub const Eeprom = struct {
},
.WriteTransfer => {
if (self.writer.len() == 64) {
std.mem.writeIntSliceLittle(u64, buf[self.addr * 8 ..][0..8], self.writer.finish());
std.mem.writeInt(u64, buf[self.addr * 8 ..][0..8], self.writer.finish(), .little);
self.state = .RequestEnd;
}
},

View File

@@ -13,7 +13,7 @@ const setHalf = util.setHalf;
const setQuart = util.setQuart;
const handleInterrupt = @import("../cpu_util.zig").handleInterrupt;
const rotr = @import("zba-util").rotr;
const rotr = @import("zba_util").rotr;
pub fn create() DmaTuple {
return .{ DmaController(0).init(), DmaController(1).init(), DmaController(2).init(), DmaController(3).init() };
@@ -286,17 +286,17 @@ fn DmaController(comptime id: u2) type {
if (self._word_count == 0) {
if (self.cnt.irq.read()) {
switch (id) {
0 => bus_ptr.io.irq.dma0.set(),
1 => bus_ptr.io.irq.dma1.set(),
2 => bus_ptr.io.irq.dma2.set(),
3 => bus_ptr.io.irq.dma3.set(),
0 => bus_ptr.io.irq.dma0.write(true),
1 => bus_ptr.io.irq.dma1.write(true),
2 => bus_ptr.io.irq.dma2.write(true),
3 => bus_ptr.io.irq.dma3.write(true),
}
handleInterrupt(cpu);
}
// If we're not repeating, Fire the IRQs and disable the DMA
if (!self.cnt.repeat.read()) self.cnt.enabled.unset();
if (!self.cnt.repeat.read()) self.cnt.enabled.write(false);
// We want to disable our internal enabled flag regardless of repeat
// because we only want to step A DMA that repeats during it's specific
@@ -338,7 +338,7 @@ fn DmaController(comptime id: u2) type {
// FIXME: Safe to just assume whatever DAD is set to is the FIFO Address?
// self.dad_latch = fifo_addr;
self.cnt.repeat.set();
self.cnt.repeat.write(true);
self._word_count = 4;
self.in_progress = true;
}

View File

@@ -1,5 +1,5 @@
const std = @import("std");
const Bit = @import("bitfield").Bit;
const Bit = @import("bitjuggle").Boolean;
const DateTime = @import("datetime").datetime.Datetime;
const Arm7tdmi = @import("arm32").Arm7tdmi;
@@ -408,7 +408,7 @@ pub const Clock = struct {
// TODO: Confirm that this is the right behaviour
log.debug("Force GamePak IRQ", .{});
bus_ptr.io.irq.game_pak.set();
bus_ptr.io.irq.game_pak.write(true);
handleInterrupt(self.cpu);
}

View File

@@ -5,8 +5,8 @@ const apu = @import("../apu.zig");
const ppu = @import("../ppu.zig");
const util = @import("../../util.zig");
const Bit = @import("bitfield").Bit;
const Bitfield = @import("bitfield").Bitfield;
const Bit = @import("bitjuggle").Boolean;
const Bitfield = @import("bitjuggle").Bitfield;
const Bus = @import("../Bus.zig");
const getHalf = util.getHalf;
@@ -96,7 +96,7 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) ?T {
0x0400_0128 => util.io.read.todo(log, "Read {} from SIOCNT", .{T}),
// Keypad Input
0x0400_0130 => bus.io.keyinput.load(.Monotonic),
0x0400_0130 => bus.io.keyinput.load(.monotonic),
// Serial Communication 2
0x0400_0134 => util.io.read.todo(log, "Read {} from RCNT", .{T}),
@@ -382,7 +382,7 @@ pub const KeyInput = extern union {
const AtomicKeyInput = struct {
const Self = @This();
const Ordering = std.atomic.Ordering;
const AtomicOrder = std.builtin.AtomicOrder;
inner: KeyInput,
@@ -390,18 +390,18 @@ const AtomicKeyInput = struct {
return .{ .inner = value };
}
pub inline fn load(self: *const Self, comptime ordering: Ordering) u16 {
pub inline fn load(self: *const Self, comptime ordering: AtomicOrder) u16 {
return switch (ordering) {
.AcqRel, .Release => @compileError("not supported for atomic loads"),
.acq_rel, .release => @compileError("not supported for atomic loads"),
else => @atomicLoad(u16, &self.inner.raw, ordering),
};
}
pub inline fn fetchOr(self: *Self, value: u16, comptime ordering: Ordering) void {
pub inline fn fetchOr(self: *Self, value: u16, comptime ordering: AtomicOrder) void {
_ = @atomicRmw(u16, &self.inner.raw, .Or, value, ordering);
}
pub inline fn fetchAnd(self: *Self, value: u16, comptime ordering: Ordering) void {
pub inline fn fetchAnd(self: *Self, value: u16, comptime ordering: AtomicOrder) void {
_ = @atomicRmw(u16, &self.inner.raw, .And, value, ordering);
}
};

View File

@@ -200,10 +200,10 @@ fn Timer(comptime id: u2) type {
if (self.cnt.irq.read()) {
switch (id) {
0 => io.irq.tim0.set(),
1 => io.irq.tim1.set(),
2 => io.irq.tim2.set(),
3 => io.irq.tim3.set(),
0 => io.irq.tim0.write(true),
1 => io.irq.tim1.write(true),
2 => io.irq.tim2.write(true),
3 => io.irq.tim3.write(true),
}
handleInterrupt(cpu);

View File

@@ -1,13 +1,12 @@
const std = @import("std");
const SDL = @import("sdl2");
const config = @import("../config.zig");
const c = @import("../lib.zig").c;
const Scheduler = @import("scheduler.zig").Scheduler;
const Arm7tdmi = @import("arm32").Arm7tdmi;
const Bus = @import("Bus.zig");
const Tracker = @import("../util.zig").FpsTracker;
pub const Message = enum { Pause, Resume, Quit };
const Channel = @import("../util.zig").Queue;
const stepDmaTransfer = @import("cpu_util.zig").stepDmaTransfer;
const isHalted = @import("cpu_util.zig").isHalted;
@@ -15,15 +14,29 @@ const isHalted = @import("cpu_util.zig").isHalted;
const Timer = std.time.Timer;
pub const Synchro = struct {
const AtomicBool = std.atomic.Atomic(bool);
const AtomicBool = std.atomic.Value(bool);
// UI -> Emulator
ui_busy: *AtomicBool,
paused: *AtomicBool, // FIXME: can ui_busy and paused be the same?
should_quit: *AtomicBool,
// FIXME: This Enum ends up being really LARGE!!!
pub const Message = union(enum) {
rom_path: [std.fs.max_path_bytes]u8,
bios_path: [std.fs.max_path_bytes]u8,
restart: void,
};
// Emulator -> UI
did_pause: *AtomicBool,
paused: AtomicBool = AtomicBool.init(true), // FIXME: can ui_busy and paused be the same?
should_quit: AtomicBool = AtomicBool.init(false),
ch: Channel(Message),
pub fn init(allocator: std.mem.Allocator) !@This() {
const msg_buf = try allocator.alloc(Message, 1);
return .{ .ch = Channel(Message).init(msg_buf) };
}
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
allocator.free(self.ch.inner.buf);
self.* = undefined;
}
};
/// 4 Cycles in 1 dot
@@ -52,7 +65,7 @@ const RunKind = enum {
LimitedFPS,
};
pub fn run(cpu: *Arm7tdmi, scheduler: *Scheduler, tracker: *Tracker, sync: Synchro) void {
pub fn run(cpu: *Arm7tdmi, scheduler: *Scheduler, tracker: *Tracker, sync: *Synchro) void {
const audio_sync = config.config().guest.audio_sync and !config.config().host.mute;
if (audio_sync) log.info("Audio sync enabled", .{});
@@ -63,7 +76,7 @@ pub fn run(cpu: *Arm7tdmi, scheduler: *Scheduler, tracker: *Tracker, sync: Synch
}
}
fn inner(comptime kind: RunKind, audio_sync: bool, cpu: *Arm7tdmi, scheduler: *Scheduler, tracker: ?*Tracker, sync: Synchro) void {
fn inner(comptime kind: RunKind, audio_sync: bool, cpu: *Arm7tdmi, scheduler: *Scheduler, tracker: ?*Tracker, sync: *Synchro) void {
if (kind == .UnlimitedFPS or kind == .LimitedFPS) {
std.debug.assert(tracker != null);
log.info("FPS tracking enabled", .{});
@@ -71,16 +84,15 @@ fn inner(comptime kind: RunKind, audio_sync: bool, cpu: *Arm7tdmi, scheduler: *S
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
// FIXME: audioSync accesses emulator state without any guarantees
switch (kind) {
.Unlimited, .UnlimitedFPS => {
log.info("Emulation w/out video sync", .{});
while (!sync.should_quit.load(.Monotonic)) {
if (sync.ui_busy.load(.Monotonic) or sync.paused.load(.Monotonic)) {
sync.did_pause.store(true, .Monotonic);
continue;
}
while (!sync.should_quit.load(.monotonic)) {
handleChannel(cpu, &sync.ch);
if (sync.paused.load(.monotonic)) continue;
runFrame(scheduler, cpu);
audioSync(audio_sync, bus_ptr.apu.stream, &bus_ptr.apu.is_buffer_full);
@@ -93,12 +105,9 @@ fn inner(comptime kind: RunKind, audio_sync: bool, cpu: *Arm7tdmi, scheduler: *S
var timer = Timer.start() catch @panic("failed to initalize std.timer.Timer");
var wake_time: u64 = frame_period;
while (!sync.should_quit.load(.Monotonic)) {
if (sync.ui_busy.load(.Monotonic) or sync.paused.load(.Monotonic)) {
sync.did_pause.store(true, .Release);
continue;
}
while (!sync.should_quit.load(.monotonic)) {
handleChannel(cpu, &sync.ch);
if (sync.paused.load(.monotonic)) continue;
runFrame(scheduler, cpu);
const new_wake_time = videoSync(&timer, wake_time);
@@ -118,6 +127,22 @@ fn inner(comptime kind: RunKind, audio_sync: bool, cpu: *Arm7tdmi, scheduler: *S
}
}
inline fn handleChannel(cpu: *Arm7tdmi, channel: *Channel(Synchro.Message)) void {
const message = channel.pop() orelse return;
switch (message) {
.rom_path => |path_buf| {
const path = std.mem.sliceTo(&path_buf, 0);
replaceGamepak(cpu, path) catch |e| log.err("failed to replace GamePak: {}", .{e});
},
.bios_path => |path_buf| {
const path = std.mem.sliceTo(&path_buf, 0);
replaceBios(cpu, path) catch |e| log.err("failed to replace BIOS: {}", .{e});
},
.restart => reset(cpu),
}
}
pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
const frame_end = sched.tick + cycles_per_frame;
@@ -135,26 +160,35 @@ pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
}
}
fn audioSync(audio_sync: bool, stream: *SDL.SDL_AudioStream, is_buffer_full: *bool) void {
comptime std.debug.assert(@import("../platform.zig").sample_format == SDL.AUDIO_U16);
fn audioSync(audio_sync: bool, stream: *c.SDL_AudioStream, is_buffer_full: *bool) void {
// comptime std.debug.assert(@import("../platform.zig").sample_format == SDL.AUDIO_U16);
const sample_size = 2 * @sizeOf(u16);
const max_buf_size: c_int = 0x400;
// Determine whether the APU is busy right at this moment
var still_full: bool = SDL.SDL_AudioStreamAvailable(stream) > sample_size * if (is_buffer_full.*) max_buf_size >> 1 else max_buf_size;
defer is_buffer_full.* = still_full; // Update APU Busy status right before exiting scope
_ = audio_sync;
_ = stream;
_ = is_buffer_full;
// If Busy is false, there's no need to sync here
if (!still_full) return;
_ = sample_size;
_ = max_buf_size;
// TODO: Refactor!!!!
// while (SDL.SDL_AudioStreamAvailable(stream) > sample_size * max_buf_size >> 1)
// std.atomic.spinLoopHint();
// TODO(paoda): re-enable
while (true) {
still_full = SDL.SDL_AudioStreamAvailable(stream) > sample_size * max_buf_size >> 1;
if (!audio_sync or !still_full) break;
}
// // Determine whether the APU is busy right at this moment
// var still_full: bool = SDL.SDL_AudioStreamAvailable(stream) > sample_size * if (is_buffer_full.*) max_buf_size >> 1 else max_buf_size;
// defer is_buffer_full.* = still_full; // Update APU Busy status right before exiting scope
// // If Busy is false, there's no need to sync here
// if (!still_full) return;
// // TODO: Refactor!!!!
// // while (SDL.SDL_AudioStreamAvailable(stream) > sample_size * max_buf_size >> 1)
// // std.atomic.spinLoopHint();
// while (true) {
// still_full = SDL.SDL_AudioStreamAvailable(stream) > sample_size * max_buf_size >> 1;
// if (!audio_sync or !still_full) break;
// }
}
fn videoSync(timer: *Timer, wake_time: u64) u64 {
@@ -183,7 +217,7 @@ fn sleep(timer: *Timer, wake_time: u64) ?u64 {
const times = sleep_for / step;
for (0..times) |_| {
std.time.sleep(step);
std.Thread.sleep(step);
// Upon wakeup, check to see if this particular sleep was longer than expected
// if so we should exit early, but probably not skip a whole frame period
@@ -204,6 +238,49 @@ pub const EmuThing = struct {
const Interface = @import("gdbstub").Emulator;
const Allocator = std.mem.Allocator;
pub const target =
\\<target version="1.0">
\\ <architecture>armv4t</architecture>
\\ <feature name="org.gnu.gdb.arm.core">
\\ <reg name="r0" bitsize="32" type="uint32"/>
\\ <reg name="r1" bitsize="32" type="uint32"/>
\\ <reg name="r2" bitsize="32" type="uint32"/>
\\ <reg name="r3" bitsize="32" type="uint32"/>
\\ <reg name="r4" bitsize="32" type="uint32"/>
\\ <reg name="r5" bitsize="32" type="uint32"/>
\\ <reg name="r6" bitsize="32" type="uint32"/>
\\ <reg name="r7" bitsize="32" type="uint32"/>
\\ <reg name="r8" bitsize="32" type="uint32"/>
\\ <reg name="r9" bitsize="32" type="uint32"/>
\\ <reg name="r10" bitsize="32" type="uint32"/>
\\ <reg name="r11" bitsize="32" type="uint32"/>
\\ <reg name="r12" bitsize="32" type="uint32"/>
\\ <reg name="sp" bitsize="32" type="data_ptr"/>
\\ <reg name="lr" bitsize="32"/>
\\ <reg name="pc" bitsize="32" type="code_ptr"/>
\\
\\ <reg name="cpsr" bitsize="32" regnum="25"/>
\\ </feature>
\\</target>
;
// Game Pak SRAM isn't included
// TODO: Can i be more specific here?
pub const map =
\\ <memory-map version="1.0">
\\ <memory type="rom" start="0x00000000" length="0x00004000"/>
\\ <memory type="ram" start="0x02000000" length="0x00040000"/>
\\ <memory type="ram" start="0x03000000" length="0x00008000"/>
\\ <memory type="ram" start="0x04000000" length="0x00000400"/>
\\ <memory type="ram" start="0x05000000" length="0x00000400"/>
\\ <memory type="ram" start="0x06000000" length="0x00018000"/>
\\ <memory type="ram" start="0x07000000" length="0x00000400"/>
\\ <memory type="rom" start="0x08000000" length="0x02000000"/>
\\ <memory type="rom" start="0x0A000000" length="0x02000000"/>
\\ <memory type="rom" start="0x0C000000" length="0x02000000"/>
\\ </memory-map>
;
cpu: *Arm7tdmi,
scheduler: *Scheduler,
@@ -211,8 +288,8 @@ pub const EmuThing = struct {
return .{ .cpu = cpu, .scheduler = scheduler };
}
pub fn interface(self: *Self, allocator: Allocator) Interface {
return Interface.init(allocator, self);
pub fn interface(self: *Self) Interface {
return Interface.init(self);
}
pub fn read(self: *const Self, addr: u32) u8 {
@@ -255,21 +332,21 @@ pub const EmuThing = struct {
}
};
pub fn reset(cpu: *Arm7tdmi) void {
fn reset(cpu: *Arm7tdmi) void {
// @breakpoint();
cpu.sched.reset(); // Yes this is order sensitive, see the PPU reset for why
cpu.bus.reset();
cpu.reset();
}
pub fn replaceGamepak(cpu: *Arm7tdmi, file_path: []const u8) !void {
fn replaceGamepak(cpu: *Arm7tdmi, file_path: []const u8) !void {
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
try bus_ptr.replaceGamepak(file_path);
reset(cpu);
}
pub fn replaceBios(cpu: *Arm7tdmi, file_path: []const u8) !void {
fn replaceBios(cpu: *Arm7tdmi, file_path: []const u8) !void {
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
const allocator = bus_ptr.bios.allocator;

View File

@@ -2,8 +2,8 @@ const std = @import("std");
const io = @import("bus/io.zig");
const util = @import("../util.zig");
const Bit = @import("bitfield").Bit;
const Bitfield = @import("bitfield").Bitfield;
const Bit = @import("bitjuggle").Boolean;
const Bitfield = @import("bitjuggle").Bitfield;
const dma = @import("bus/dma.zig");
const Oam = @import("ppu/Oam.zig");
@@ -1007,7 +1007,7 @@ pub const Ppu = struct {
// Transitioning to a Hblank
if (self.dispstat.hblank_irq.read()) {
bus_ptr.io.irq.hblank.set();
bus_ptr.io.irq.hblank.write(true);
handleInterrupt(cpu);
}
@@ -1015,7 +1015,7 @@ pub const Ppu = struct {
if (!self.dispstat.vblank.read())
dma.onBlanking(bus_ptr, .HBlank);
self.dispstat.hblank.set();
self.dispstat.hblank.write(true);
self.sched.push(.HBlank, 68 * 4 -| late);
}
@@ -1027,14 +1027,14 @@ pub const Ppu = struct {
const scanline = (old_scanline + 1) % 228;
self.vcount.scanline.write(scanline);
self.dispstat.hblank.unset();
self.dispstat.hblank.write(false);
// Perform Vc == VcT check
const coincidence = scanline == self.dispstat.vcount_trigger.read();
self.dispstat.coincidence.write(coincidence);
if (coincidence and self.dispstat.vcount_irq.read()) {
bus_ptr.io.irq.coincidence.set();
bus_ptr.io.irq.coincidence.write(true);
handleInterrupt(cpu);
}
@@ -1046,10 +1046,10 @@ pub const Ppu = struct {
if (scanline == 160) {
self.framebuf.swap(); // Swap FrameBuffers
self.dispstat.vblank.set();
self.dispstat.vblank.write(true);
if (self.dispstat.vblank_irq.read()) {
bus_ptr.io.irq.vblank.set();
bus_ptr.io.irq.vblank.write(true);
handleInterrupt(cpu);
}
@@ -1060,7 +1060,7 @@ pub const Ppu = struct {
dma.onBlanking(bus_ptr, .VBlank);
}
if (scanline == 227) self.dispstat.vblank.unset();
if (scanline == 227) self.dispstat.vblank.write(false);
self.sched.push(.VBlank, 240 * 4 -| late);
}
}

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ pub fn read(self: *const Self, comptime T: type, address: usize) T {
const addr = Self.mirror(address);
return switch (T) {
u32, u16, u8 => std.mem.readIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)]),
u32, u16, u8 => std.mem.readInt(T, self.buf[addr..][0..@sizeOf(T)], .little),
else => @compileError("VRAM: Unsupported read width"),
};
}
@@ -23,7 +23,7 @@ pub fn write(self: *Self, comptime T: type, dispcnt: io.DisplayControl, address:
const idx = Self.mirror(address);
switch (T) {
u32, u16 => std.mem.writeIntSliceLittle(T, self.buf[idx..][0..@sizeOf(T)], value),
u32, u16 => std.mem.writeInt(T, self.buf[idx..][0..@sizeOf(T)], value, .little),
u8 => {
// Ignore write if it falls within the boundaries of OBJ VRAM
switch (mode) {
@@ -32,7 +32,7 @@ pub fn write(self: *Self, comptime T: type, dispcnt: io.DisplayControl, address:
}
const align_idx = idx & ~@as(u32, 1); // Aligned to a halfword boundary
std.mem.writeIntSliceLittle(u16, self.buf[align_idx..][0..@sizeOf(u16)], @as(u16, value) * 0x101);
std.mem.writeInt(u16, self.buf[align_idx..][0..@sizeOf(u16)], @as(u16, value) * 0x101, .little);
},
else => @compileError("VRAM: Unsupported write width"),
}

View File

@@ -13,12 +13,13 @@ const Gui = @import("platform.zig").Gui;
const Arm7tdmi = @import("arm32").Arm7tdmi;
const Scheduler = @import("core/scheduler.zig").Scheduler;
const Bus = @import("core/Bus.zig");
const Synchro = @import("core/emu.zig").Synchro;
const RingBuffer = @import("zba-util").RingBuffer;
const RingBuffer = @import("zba_util").RingBuffer;
const Dimensions = @import("platform.zig").Dimensions;
const Allocator = std.mem.Allocator;
const GLuint = gl.GLuint;
const GLuint = gl.uint;
const gba_width = @import("core/ppu.zig").width;
const gba_height = @import("core/ppu.zig").height;
@@ -70,11 +71,13 @@ pub const State = struct {
}
};
pub fn draw(state: *State, win_dim: Dimensions, tex_id: GLuint, cpu: *Arm7tdmi) bool {
pub fn draw(state: *State, sync: *Synchro, dim: Dimensions, cpu: *const Arm7tdmi, tex_id: GLuint) bool {
const scn_scale = config.config().host.win_scale;
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
zgui.backend.newFrame(@floatFromInt(win_dim.width), @floatFromInt(win_dim.height));
zgui.backend.newFrame(dim.width, dim.height);
state.title = handleTitle(&bus_ptr.pak.title);
{
_ = zgui.beginMainMenuBar();
@@ -87,42 +90,59 @@ pub fn draw(state: *State, win_dim: Dimensions, tex_id: GLuint, cpu: *Arm7tdmi)
state.should_quit = true;
if (zgui.menuItem("Insert ROM", .{})) blk: {
const maybe_path = nfd.openFileDialog("gba", null) catch |e| {
log.err("failed to open file dialog: {}", .{e});
break :blk;
};
const file_path = tmp: {
const path_opt = nfd.openFileDialog("gba", null) catch |e| {
log.err("file dialog failed to open: {}", .{e});
break :blk;
};
const file_path = maybe_path orelse {
log.warn("did not receive a file path", .{});
break :blk;
break :tmp path_opt orelse {
log.warn("did not receive a file path", .{});
break :blk;
};
};
defer nfd.freePath(file_path);
log.info("user chose: \"{s}\"", .{file_path});
emu.replaceGamepak(cpu, file_path) catch |e| {
log.err("failed to replace GamePak: {}", .{e});
const message = tmp: {
var msg: Synchro.Message = .{ .rom_path = undefined };
@memcpy(msg.rom_path[0..file_path.len], file_path);
break :tmp msg;
};
sync.ch.push(message) catch |e| {
log.err("failed to send file path to emu thread: {}", .{e});
break :blk;
};
state.title = handleTitle(&bus_ptr.pak.title);
state.emulation = .{ .Transition = .Active };
}
if (zgui.menuItem("Load BIOS", .{})) blk: {
const maybe_path = nfd.openFileDialog("bin", null) catch |e| {
log.err("failed to open file dialog: {}", .{e});
break :blk;
};
const file_path = tmp: {
const path_opt = nfd.openFileDialog("bin", null) catch |e| {
log.err("file dialog failed to open: {}", .{e});
break :blk;
};
const file_path = maybe_path orelse {
log.warn("did not receive a file path", .{});
break :blk;
break :tmp path_opt orelse {
log.warn("did not receive a file path", .{});
break :blk;
};
};
defer nfd.freePath(file_path);
log.info("user chose: \"{s}\"", .{file_path});
emu.replaceBios(cpu, file_path) catch |e| {
log.err("failed to replace BIOS: {}", .{e});
const message = tmp: {
var msg: Synchro.Message = .{ .bios_path = undefined };
@memcpy(msg.bios_path[0..file_path.len], file_path);
break :tmp msg;
};
sync.ch.push(message) catch |e| {
log.err("failed to send file path to emu thread: {}", .{e});
break :blk;
};
}
@@ -149,7 +169,7 @@ pub fn draw(state: *State, win_dim: Dimensions, tex_id: GLuint, cpu: *Arm7tdmi)
}
if (zgui.menuItem("Restart", .{}))
emu.reset(cpu);
sync.ch.push(.restart) catch |e| log.err("failed to send restart req to emu thread: {}", .{e});
}
if (zgui.beginMenu("Stats", true)) {
@@ -175,7 +195,8 @@ pub fn draw(state: *State, win_dim: Dimensions, tex_id: GLuint, cpu: *Arm7tdmi)
_ = zgui.begin(window_title, .{ .flags = .{ .no_resize = true, .always_auto_resize = true } });
defer zgui.end();
zgui.image(@ptrFromInt(tex_id), .{ .w = w, .h = h, .uv0 = .{ 0, 1 }, .uv1 = .{ 1, 0 } });
// FIXME(paoda): go look at some documentation here
zgui.image(.{ .tex_data = null, .tex_id = @enumFromInt(tex_id) }, .{ .w = w, .h = h });
}
// TODO: Any other steps to respect the copyright of the libraries I use?
@@ -184,7 +205,7 @@ pub fn draw(state: *State, win_dim: Dimensions, tex_id: GLuint, cpu: *Arm7tdmi)
defer zgui.end();
zgui.bulletText("known-folders by ziglibs", .{});
zgui.bulletText("nfd-zig by Fabio Arnold", .{});
zgui.bulletText("nfd-zig ported by Fabio Arnold", .{});
{
zgui.indent(.{});
defer zgui.unindent(.{});
@@ -192,7 +213,7 @@ pub fn draw(state: *State, win_dim: Dimensions, tex_id: GLuint, cpu: *Arm7tdmi)
zgui.bulletText("nativefiledialog by Michael Labbe", .{});
}
zgui.bulletText("SDL.zig by Felix Queißner", .{});
zgui.bulletText("SDL ported by Carl Åstholm", .{});
{
zgui.indent(.{});
defer zgui.unindent(.{});
@@ -200,10 +221,10 @@ pub fn draw(state: *State, win_dim: Dimensions, tex_id: GLuint, cpu: *Arm7tdmi)
zgui.bulletText("SDL by Sam Lantinga", .{});
}
zgui.bulletText("tomlz by Matthew Hall", .{});
// zgui.bulletText("tomlz by Matthew Hall", .{});
zgui.bulletText("zba-gdbstub by Rekai Musuka", .{});
zgui.bulletText("zba-util by Rekai Musuka", .{});
zgui.bulletText("zgui by Michal Ziulek", .{});
zgui.bulletText("zgui ported by Michal Ziulek et al.", .{});
{
zgui.indent(.{});
defer zgui.unindent(.{});
@@ -214,8 +235,8 @@ pub fn draw(state: *State, win_dim: Dimensions, tex_id: GLuint, cpu: *Arm7tdmi)
zgui.bulletText("zig-datetime by Jairus Martin", .{});
zgui.newLine();
zgui.bulletText("bitfield.zig by Hannes Bredberg and FlorenceOS contributors", .{});
zgui.bulletText("zig-opengl by Felix Queißner", .{});
zgui.bulletText("bitfield.zig by Hannes Bredberg et al.", .{});
zgui.bulletText("zigglgen ported by Carl Åstholm", .{});
{
zgui.indent(.{});
defer zgui.unindent(.{});
@@ -252,63 +273,63 @@ pub fn draw(state: *State, win_dim: Dimensions, tex_id: GLuint, cpu: *Arm7tdmi)
_ = zgui.begin("Performance", .{ .popen = &state.win_stat.show_perf });
defer zgui.end();
const tmp = blk: {
var buf: [histogram_len]u32 = undefined;
const len = state.fps_hist.copy(&buf);
// const tmp = blk: {
// var buf: [histogram_len]u32 = undefined;
// const len = state.fps_hist.copy(&buf);
break :blk .{ buf, len };
};
const values = tmp[0];
const len = tmp[1];
// break :blk .{ buf, len };
// };
// const values = tmp[0];
// const len = tmp[1];
if (len == values.len) _ = state.fps_hist.pop();
// if (len == values.len) _ = state.fps_hist.pop();
const sorted = blk: {
var buf: @TypeOf(values) = undefined;
// const sorted = blk: {
// var buf: @TypeOf(values) = undefined;
@memcpy(buf[0..len], values[0..len]);
std.mem.sort(u32, buf[0..len], {}, std.sort.asc(u32));
// @memcpy(buf[0..len], values[0..len]);
// std.mem.sort(u32, buf[0..len], {}, std.sort.asc(u32));
break :blk buf;
};
// break :blk buf;
// };
const y_max: f64 = 2 * if (len != 0) @as(f64, @floatFromInt(sorted[len - 1])) else emu.frame_rate;
const x_max: f64 = @floatFromInt(values.len);
// const y_max: f64 = 2 * if (len != 0) @as(f64, @floatFromInt(sorted[len - 1])) else emu.frame_rate;
// const x_max: f64 = @floatFromInt(values.len);
const y_args = .{ .flags = .{ .no_grid_lines = true } };
const x_args = .{ .flags = .{ .no_grid_lines = true, .no_tick_labels = true, .no_tick_marks = true } };
// const y_args = .{ .flags = .{ .no_grid_lines = true } };
// const x_args = .{ .flags = .{ .no_grid_lines = true, .no_tick_labels = true, .no_tick_marks = true } };
if (zgui.plot.beginPlot("Emulation FPS", .{ .w = 0.0, .flags = .{ .no_title = true, .no_frame = true } })) {
defer zgui.plot.endPlot();
// if (zgui.plot.beginPlot("Emulation FPS", .{ .w = 0.0, .flags = .{ .no_title = true, .no_frame = true } })) {
// defer zgui.plot.endPlot();
zgui.plot.setupLegend(.{ .north = true, .east = true }, .{});
zgui.plot.setupAxis(.x1, x_args);
zgui.plot.setupAxis(.y1, y_args);
zgui.plot.setupAxisLimits(.y1, .{ .min = 0.0, .max = y_max, .cond = .always });
zgui.plot.setupAxisLimits(.x1, .{ .min = 0.0, .max = x_max, .cond = .always });
zgui.plot.setupFinish();
// zgui.plot.setupLegend(.{ .north = true, .east = true }, .{});
// zgui.plot.setupAxis(.x1, x_args);
// zgui.plot.setupAxis(.y1, y_args);
// zgui.plot.setupAxisLimits(.y1, .{ .min = 0.0, .max = y_max, .cond = .always });
// zgui.plot.setupAxisLimits(.x1, .{ .min = 0.0, .max = x_max, .cond = .always });
// zgui.plot.setupFinish();
zgui.plot.plotLineValues("FPS", u32, .{ .v = values[0..len] });
}
// zgui.plot.plotLineValues("FPS", u32, .{ .v = values[0..len] });
// }
const stats: struct { u32, u32, u32 } = blk: {
if (len == 0) break :blk .{ 0, 0, 0 };
// const stats: struct { u32, u32, u32 } = blk: {
// if (len == 0) break :blk .{ 0, 0, 0 };
const average: u32 = average: {
var sum: u32 = 0;
for (sorted[0..len]) |value| sum += value;
// const average: u32 = average: {
// var sum: u32 = 0;
// for (sorted[0..len]) |value| sum += value;
break :average @intCast(sum / len);
};
const median = sorted[len / 2];
const low = sorted[len / 100]; // 1% Low
// break :average @intCast(sum / len);
// };
// const median = sorted[len / 2];
// const low = sorted[len / 100]; // 1% Low
break :blk .{ average, median, low };
};
// break :blk .{ average, median, low };
// };
zgui.text("Average: {:0>3} fps", .{stats[0]});
zgui.text(" Median: {:0>3} fps", .{stats[1]});
zgui.text(" 1% Low: {:0>3} fps", .{stats[2]});
// zgui.text("Average: {:0>3} fps", .{stats[0]});
// zgui.text(" Median: {:0>3} fps", .{stats[1]});
// zgui.text(" 1% Low: {:0>3} fps", .{stats[2]});
}
if (state.win_stat.show_schedule) {
@@ -316,21 +337,25 @@ pub fn draw(state: *State, win_dim: Dimensions, tex_id: GLuint, cpu: *Arm7tdmi)
defer zgui.end();
const scheduler = cpu.sched;
_ = scheduler;
zgui.text("tick: {X:0>16}", .{scheduler.now()});
// zgui.text("tick: {X:0>16}", .{scheduler.now()});
zgui.text("tick: (TODO: FIX THIS)", .{});
zgui.separator();
const sched_ptr: *Scheduler = @ptrCast(@alignCast(cpu.sched.ptr));
const Event = std.meta.Child(@TypeOf(sched_ptr.queue.items));
var items: [20]Event = undefined;
const len = sched_ptr.queue.len;
const len = @min(sched_ptr.queue.items.len, items.len);
@memcpy(&items, sched_ptr.queue.items);
@memcpy(items[0..len], sched_ptr.queue.items[0..len]);
std.mem.sort(Event, items[0..len], {}, widgets.eventDesc(Event));
for (items[0..len]) |event| {
zgui.text("{X:0>16} | {?}", .{ event.tick, event.kind });
// zgui.text("{X:0>16} | {?}", .{ event.tick, event.kind });
_ = event;
zgui.text("TODO: Fix This", .{});
}
}
@@ -345,9 +370,9 @@ pub fn draw(state: *State, win_dim: Dimensions, tex_id: GLuint, cpu: *Arm7tdmi)
widgets.paletteGrid(.Object, cpu);
}
{
zgui.showDemoWindow(null);
}
// {
// zgui.showDemoWindow(null);
// }
return true; // request redraw
}

28
src/lib.zig Normal file
View File

@@ -0,0 +1,28 @@
pub const c = @cImport({
@cDefine("SDL_DISABLE_OLD_NAMES", {});
@cDefine("SDL_MAIN_HANDLED", {});
@cInclude("SDL3/SDL.h");
@cInclude("SDL3/SDL_main.h");
});
// https://github.com/castholm/zig-examples/blob/77a829c85b5ddbad673026d504626015db4093ac/opengl-sdl/main.zig#L200-L219
pub inline fn errify(value: anytype) error{sdl_error}!switch (@typeInfo(@TypeOf(value))) {
.bool => void,
.pointer, .optional => @TypeOf(value.?),
.int => |info| switch (info.signedness) {
.signed => @TypeOf(@max(0, value)),
.unsigned => @TypeOf(value),
},
else => @compileError("unerrifiable type: " ++ @typeName(@TypeOf(value))),
} {
return switch (@typeInfo(@TypeOf(value))) {
.bool => if (!value) error.sdl_error,
.pointer, .optional => value orelse error.sdl_error,
.int => |info| switch (info.signedness) {
.signed => if (value >= 0) @max(0, value) else error.sdl_error,
.unsigned => if (value != 0) value else error.sdl_error,
},
else => comptime unreachable,
};
}

548
src/lib/fifo.zig Normal file
View File

@@ -0,0 +1,548 @@
// FIFO of fixed size items
// Usually used for e.g. byte buffers
const std = @import("std");
const math = std.math;
const mem = std.mem;
const Allocator = mem.Allocator;
const assert = std.debug.assert;
const testing = std.testing;
pub const LinearFifoBufferType = union(enum) {
/// The buffer is internal to the fifo; it is of the specified size.
static: usize,
/// The buffer is passed as a slice to the initialiser.
slice,
/// The buffer is managed dynamically using a `mem.Allocator`.
dynamic,
};
pub fn LinearFifo(
comptime T: type,
comptime buffer_type: LinearFifoBufferType,
) type {
const autoalign = false;
const powers_of_two = switch (buffer_type) {
.static => std.math.isPowerOfTwo(buffer_type.static),
.slice => false, // Any size slice could be passed in
.dynamic => true, // This could be configurable in future
};
return struct {
allocator: if (buffer_type == .dynamic) Allocator else void,
buf: if (buffer_type == .static) [buffer_type.static]T else []T,
head: usize,
count: usize,
const Self = @This();
pub const Reader = std.io.Reader(*Self, error{}, readFn);
pub const Writer = std.io.Writer(*Self, error{OutOfMemory}, appendWrite);
// Type of Self argument for slice operations.
// If buffer is inline (Static) then we need to ensure we haven't
// returned a slice into a copy on the stack
const SliceSelfArg = if (buffer_type == .static) *Self else Self;
pub const init = switch (buffer_type) {
.static => initStatic,
.slice => initSlice,
.dynamic => initDynamic,
};
fn initStatic() Self {
comptime assert(buffer_type == .static);
return .{
.allocator = {},
.buf = undefined,
.head = 0,
.count = 0,
};
}
fn initSlice(buf: []T) Self {
comptime assert(buffer_type == .slice);
return .{
.allocator = {},
.buf = buf,
.head = 0,
.count = 0,
};
}
fn initDynamic(allocator: Allocator) Self {
comptime assert(buffer_type == .dynamic);
return .{
.allocator = allocator,
.buf = &.{},
.head = 0,
.count = 0,
};
}
pub fn deinit(self: Self) void {
if (buffer_type == .dynamic) self.allocator.free(self.buf);
}
pub fn realign(self: *Self) void {
if (self.buf.len - self.head >= self.count) {
mem.copyForwards(T, self.buf[0..self.count], self.buf[self.head..][0..self.count]);
self.head = 0;
} else {
var tmp: [4096 / 2 / @sizeOf(T)]T = undefined;
while (self.head != 0) {
const n = @min(self.head, tmp.len);
const m = self.buf.len - n;
@memcpy(tmp[0..n], self.buf[0..n]);
mem.copyForwards(T, self.buf[0..m], self.buf[n..][0..m]);
@memcpy(self.buf[m..][0..n], tmp[0..n]);
self.head -= n;
}
}
{ // set unused area to undefined
const unused = mem.sliceAsBytes(self.buf[self.count..]);
@memset(unused, undefined);
}
}
/// Reduce allocated capacity to `size`.
pub fn shrink(self: *Self, size: usize) void {
assert(size >= self.count);
if (buffer_type == .dynamic) {
self.realign();
self.buf = self.allocator.realloc(self.buf, size) catch |e| switch (e) {
error.OutOfMemory => return, // no problem, capacity is still correct then.
};
}
}
/// Ensure that the buffer can fit at least `size` items
pub fn ensureTotalCapacity(self: *Self, size: usize) !void {
if (self.buf.len >= size) return;
if (buffer_type == .dynamic) {
self.realign();
const new_size = if (powers_of_two) math.ceilPowerOfTwo(usize, size) catch return error.OutOfMemory else size;
self.buf = try self.allocator.realloc(self.buf, new_size);
} else {
return error.OutOfMemory;
}
}
/// Makes sure at least `size` items are unused
pub fn ensureUnusedCapacity(self: *Self, size: usize) error{OutOfMemory}!void {
if (self.writableLength() >= size) return;
return try self.ensureTotalCapacity(math.add(usize, self.count, size) catch return error.OutOfMemory);
}
/// Returns number of items currently in fifo
pub fn readableLength(self: Self) usize {
return self.count;
}
/// Returns a writable slice from the 'read' end of the fifo
fn readableSliceMut(self: SliceSelfArg, offset: usize) []T {
if (offset > self.count) return &[_]T{};
var start = self.head + offset;
if (start >= self.buf.len) {
start -= self.buf.len;
return self.buf[start .. start + (self.count - offset)];
} else {
const end = @min(self.head + self.count, self.buf.len);
return self.buf[start..end];
}
}
/// Returns a readable slice from `offset`
pub fn readableSlice(self: SliceSelfArg, offset: usize) []const T {
return self.readableSliceMut(offset);
}
pub fn readableSliceOfLen(self: *Self, len: usize) []const T {
assert(len <= self.count);
const buf = self.readableSlice(0);
if (buf.len >= len) {
return buf[0..len];
} else {
self.realign();
return self.readableSlice(0)[0..len];
}
}
/// Discard first `count` items in the fifo
pub fn discard(self: *Self, count: usize) void {
assert(count <= self.count);
{ // set old range to undefined. Note: may be wrapped around
const slice = self.readableSliceMut(0);
if (slice.len >= count) {
const unused = mem.sliceAsBytes(slice[0..count]);
@memset(unused, undefined);
} else {
const unused = mem.sliceAsBytes(slice[0..]);
@memset(unused, undefined);
const unused2 = mem.sliceAsBytes(self.readableSliceMut(slice.len)[0 .. count - slice.len]);
@memset(unused2, undefined);
}
}
if (autoalign and self.count == count) {
self.head = 0;
self.count = 0;
} else {
var head = self.head + count;
if (powers_of_two) {
// Note it is safe to do a wrapping subtract as
// bitwise & with all 1s is a noop
head &= self.buf.len -% 1;
} else {
head %= self.buf.len;
}
self.head = head;
self.count -= count;
}
}
/// Read the next item from the fifo
pub fn readItem(self: *Self) ?T {
if (self.count == 0) return null;
const c = self.buf[self.head];
self.discard(1);
return c;
}
/// Read data from the fifo into `dst`, returns number of items copied.
pub fn read(self: *Self, dst: []T) usize {
var dst_left = dst;
while (dst_left.len > 0) {
const slice = self.readableSlice(0);
if (slice.len == 0) break;
const n = @min(slice.len, dst_left.len);
@memcpy(dst_left[0..n], slice[0..n]);
self.discard(n);
dst_left = dst_left[n..];
}
return dst.len - dst_left.len;
}
/// Same as `read` except it returns an error union
/// The purpose of this function existing is to match `std.io.Reader` API.
fn readFn(self: *Self, dest: []u8) error{}!usize {
return self.read(dest);
}
pub fn reader(self: *Self) Reader {
return .{ .context = self };
}
/// Returns number of items available in fifo
pub fn writableLength(self: Self) usize {
return self.buf.len - self.count;
}
/// Returns the first section of writable buffer.
/// Note that this may be of length 0
pub fn writableSlice(self: SliceSelfArg, offset: usize) []T {
if (offset > self.buf.len) return &[_]T{};
const tail = self.head + offset + self.count;
if (tail < self.buf.len) {
return self.buf[tail..];
} else {
return self.buf[tail - self.buf.len ..][0 .. self.writableLength() - offset];
}
}
/// Returns a writable buffer of at least `size` items, allocating memory as needed.
/// Use `fifo.update` once you've written data to it.
pub fn writableWithSize(self: *Self, size: usize) ![]T {
try self.ensureUnusedCapacity(size);
// try to avoid realigning buffer
var slice = self.writableSlice(0);
if (slice.len < size) {
self.realign();
slice = self.writableSlice(0);
}
return slice;
}
/// Update the tail location of the buffer (usually follows use of writable/writableWithSize)
pub fn update(self: *Self, count: usize) void {
assert(self.count + count <= self.buf.len);
self.count += count;
}
/// Appends the data in `src` to the fifo.
/// You must have ensured there is enough space.
pub fn writeAssumeCapacity(self: *Self, src: []const T) void {
assert(self.writableLength() >= src.len);
var src_left = src;
while (src_left.len > 0) {
const writable_slice = self.writableSlice(0);
assert(writable_slice.len != 0);
const n = @min(writable_slice.len, src_left.len);
@memcpy(writable_slice[0..n], src_left[0..n]);
self.update(n);
src_left = src_left[n..];
}
}
/// Write a single item to the fifo
pub fn writeItem(self: *Self, item: T) !void {
try self.ensureUnusedCapacity(1);
return self.writeItemAssumeCapacity(item);
}
pub fn writeItemAssumeCapacity(self: *Self, item: T) void {
var tail = self.head + self.count;
if (powers_of_two) {
tail &= self.buf.len - 1;
} else {
tail %= self.buf.len;
}
self.buf[tail] = item;
self.update(1);
}
/// Appends the data in `src` to the fifo.
/// Allocates more memory as necessary
pub fn write(self: *Self, src: []const T) !void {
try self.ensureUnusedCapacity(src.len);
return self.writeAssumeCapacity(src);
}
/// Same as `write` except it returns the number of bytes written, which is always the same
/// as `bytes.len`. The purpose of this function existing is to match `std.io.Writer` API.
fn appendWrite(self: *Self, bytes: []const u8) error{OutOfMemory}!usize {
try self.write(bytes);
return bytes.len;
}
pub fn writer(self: *Self) Writer {
return .{ .context = self };
}
/// Make `count` items available before the current read location
fn rewind(self: *Self, count: usize) void {
assert(self.writableLength() >= count);
var head = self.head + (self.buf.len - count);
if (powers_of_two) {
head &= self.buf.len - 1;
} else {
head %= self.buf.len;
}
self.head = head;
self.count += count;
}
/// Place data back into the read stream
pub fn unget(self: *Self, src: []const T) !void {
try self.ensureUnusedCapacity(src.len);
self.rewind(src.len);
const slice = self.readableSliceMut(0);
if (src.len < slice.len) {
@memcpy(slice[0..src.len], src);
} else {
@memcpy(slice, src[0..slice.len]);
const slice2 = self.readableSliceMut(slice.len);
@memcpy(slice2[0 .. src.len - slice.len], src[slice.len..]);
}
}
/// Returns the item at `offset`.
/// Asserts offset is within bounds.
pub fn peekItem(self: Self, offset: usize) T {
assert(offset < self.count);
var index = self.head + offset;
if (powers_of_two) {
index &= self.buf.len - 1;
} else {
index %= self.buf.len;
}
return self.buf[index];
}
/// Pump data from a reader into a writer.
/// Stops when reader returns 0 bytes (EOF).
/// Buffer size must be set before calling; a buffer length of 0 is invalid.
pub fn pump(self: *Self, src_reader: anytype, dest_writer: anytype) !void {
assert(self.buf.len > 0);
while (true) {
if (self.writableLength() > 0) {
const n = try src_reader.read(self.writableSlice(0));
if (n == 0) break; // EOF
self.update(n);
}
self.discard(try dest_writer.write(self.readableSlice(0)));
}
// flush remaining data
while (self.readableLength() > 0) {
self.discard(try dest_writer.write(self.readableSlice(0)));
}
}
pub fn toOwnedSlice(self: *Self) Allocator.Error![]T {
if (self.head != 0) self.realign();
assert(self.head == 0);
assert(self.count <= self.buf.len);
const allocator = self.allocator;
if (allocator.resize(self.buf, self.count)) {
const result = self.buf[0..self.count];
self.* = Self.init(allocator);
return result;
}
const new_memory = try allocator.dupe(T, self.buf[0..self.count]);
allocator.free(self.buf);
self.* = Self.init(allocator);
return new_memory;
}
};
}
test "LinearFifo(u8, .Dynamic) discard(0) from empty buffer should not error on overflow" {
var fifo = LinearFifo(u8, .dynamic).init(testing.allocator);
defer fifo.deinit();
// If overflow is not explicitly allowed this will crash in debug / safe mode
fifo.discard(0);
}
test "LinearFifo(u8, .Dynamic)" {
var fifo = LinearFifo(u8, .dynamic).init(testing.allocator);
defer fifo.deinit();
try fifo.write("HELLO");
try testing.expectEqual(@as(usize, 5), fifo.readableLength());
try testing.expectEqualSlices(u8, "HELLO", fifo.readableSlice(0));
{
var i: usize = 0;
while (i < 5) : (i += 1) {
try fifo.write(&[_]u8{fifo.peekItem(i)});
}
try testing.expectEqual(@as(usize, 10), fifo.readableLength());
try testing.expectEqualSlices(u8, "HELLOHELLO", fifo.readableSlice(0));
}
{
try testing.expectEqual(@as(u8, 'H'), fifo.readItem().?);
try testing.expectEqual(@as(u8, 'E'), fifo.readItem().?);
try testing.expectEqual(@as(u8, 'L'), fifo.readItem().?);
try testing.expectEqual(@as(u8, 'L'), fifo.readItem().?);
try testing.expectEqual(@as(u8, 'O'), fifo.readItem().?);
}
try testing.expectEqual(@as(usize, 5), fifo.readableLength());
{ // Writes that wrap around
try testing.expectEqual(@as(usize, 11), fifo.writableLength());
try testing.expectEqual(@as(usize, 6), fifo.writableSlice(0).len);
fifo.writeAssumeCapacity("6<chars<11");
try testing.expectEqualSlices(u8, "HELLO6<char", fifo.readableSlice(0));
try testing.expectEqualSlices(u8, "s<11", fifo.readableSlice(11));
try testing.expectEqualSlices(u8, "11", fifo.readableSlice(13));
try testing.expectEqualSlices(u8, "", fifo.readableSlice(15));
fifo.discard(11);
try testing.expectEqualSlices(u8, "s<11", fifo.readableSlice(0));
fifo.discard(4);
try testing.expectEqual(@as(usize, 0), fifo.readableLength());
}
{
const buf = try fifo.writableWithSize(12);
try testing.expectEqual(@as(usize, 12), buf.len);
var i: u8 = 0;
while (i < 10) : (i += 1) {
buf[i] = i + 'a';
}
fifo.update(10);
try testing.expectEqualSlices(u8, "abcdefghij", fifo.readableSlice(0));
}
{
try fifo.unget("prependedstring");
var result: [30]u8 = undefined;
try testing.expectEqualSlices(u8, "prependedstringabcdefghij", result[0..fifo.read(&result)]);
try fifo.unget("b");
try fifo.unget("a");
try testing.expectEqualSlices(u8, "ab", result[0..fifo.read(&result)]);
}
fifo.shrink(0);
{
try fifo.writer().print("{s}, {s}!", .{ "Hello", "World" });
var result: [30]u8 = undefined;
try testing.expectEqualSlices(u8, "Hello, World!", result[0..fifo.read(&result)]);
try testing.expectEqual(@as(usize, 0), fifo.readableLength());
}
{
try fifo.writer().writeAll("This is a test");
var result: [30]u8 = undefined;
try testing.expectEqualSlices(u8, "This", (try fifo.reader().readUntilDelimiterOrEof(&result, ' ')).?);
try testing.expectEqualSlices(u8, "is", (try fifo.reader().readUntilDelimiterOrEof(&result, ' ')).?);
try testing.expectEqualSlices(u8, "a", (try fifo.reader().readUntilDelimiterOrEof(&result, ' ')).?);
try testing.expectEqualSlices(u8, "test", (try fifo.reader().readUntilDelimiterOrEof(&result, ' ')).?);
}
{
try fifo.ensureTotalCapacity(1);
var in_fbs = std.io.fixedBufferStream("pump test");
var out_buf: [50]u8 = undefined;
var out_fbs = std.io.fixedBufferStream(&out_buf);
try fifo.pump(in_fbs.reader(), out_fbs.writer());
try testing.expectEqualSlices(u8, in_fbs.buffer, out_fbs.getWritten());
}
}
test LinearFifo {
inline for ([_]type{ u1, u8, u16, u64 }) |T| {
inline for ([_]LinearFifoBufferType{ LinearFifoBufferType{ .static = 32 }, .slice, .dynamic }) |bt| {
const FifoType = LinearFifo(T, bt);
var buf: if (bt == .slice) [32]T else void = undefined;
var fifo = switch (bt) {
.static => FifoType.init(),
.slice => FifoType.init(buf[0..]),
.dynamic => FifoType.init(testing.allocator),
};
defer fifo.deinit();
try fifo.write(&[_]T{ 0, 1, 1, 0, 1 });
try testing.expectEqual(@as(usize, 5), fifo.readableLength());
{
try testing.expectEqual(@as(T, 0), fifo.readItem().?);
try testing.expectEqual(@as(T, 1), fifo.readItem().?);
try testing.expectEqual(@as(T, 1), fifo.readItem().?);
try testing.expectEqual(@as(T, 0), fifo.readItem().?);
try testing.expectEqual(@as(T, 1), fifo.readItem().?);
try testing.expectEqual(@as(usize, 0), fifo.readableLength());
}
{
try fifo.writeItem(1);
try fifo.writeItem(1);
try fifo.writeItem(1);
try testing.expectEqual(@as(usize, 3), fifo.readableLength());
}
{
var readBuf: [3]T = undefined;
const n = fifo.read(&readBuf);
try testing.expectEqual(@as(usize, 3), n); // NOTE: It should be the number of items.
}
}
}
}

View File

@@ -61,7 +61,8 @@ pub fn main() void {
defer allocator.free(config_path);
// Parse CLI
const result = clap.parse(clap.Help, &params, clap.parsers.default, .{}) catch |e| exitln("failed to parse cli: {}", .{e});
const result = clap.parse(clap.Help, &params, clap.parsers.default, .{ .allocator = allocator }) catch |e| exitln("failed to parse cli: {}", .{e});
defer result.deinit();
// TODO: Move config file to XDG Config directory?
@@ -101,8 +102,8 @@ pub fn main() void {
var bus: Bus = undefined;
var ischeduler = IScheduler.init(&scheduler);
var ibus = IBus.init(&bus);
const ischeduler = IScheduler.init(&scheduler);
const ibus = IBus.init(&bus);
var cpu = Arm7tdmi.init(ischeduler, ibus);
@@ -119,51 +120,46 @@ pub fn main() void {
var gui = Gui.init(allocator, &bus.apu, title_ptr) catch |e| exitln("failed to init gui: {}", .{e});
defer gui.deinit();
var sync: Synchro = blk: {
const AtomicBool = std.atomic.Atomic(bool);
var ui_busy = AtomicBool.init(false);
var paused = AtomicBool.init(true);
var should_quit = AtomicBool.init(false);
var did_pause = AtomicBool.init(false);
break :blk .{ .ui_busy = &ui_busy, .paused = &paused, .should_quit = &should_quit, .did_pause = &did_pause };
};
var sync = Synchro.init(allocator) catch |e| exitln("failed to allocate sync types: {}", .{e});
defer sync.deinit(allocator);
if (result.args.gdb != 0) {
const Server = @import("gdbstub").Server;
const EmuThing = @import("core/emu.zig").EmuThing;
var wrapper = EmuThing.init(&cpu, &scheduler);
var emulator = wrapper.interface(allocator);
defer emulator.deinit();
var emulator = wrapper.interface();
defer emulator.deinit(allocator);
log.info("Ready to connect", .{});
var server = Server.init(emulator) catch |e| exitln("failed to init gdb server: {}", .{e});
var server = Server.init(
emulator,
.{ .memory_map = EmuThing.map, .target = EmuThing.target },
) catch |e| exitln("failed to init gdb server: {}", .{e});
defer server.deinit(allocator);
log.info("Starting GDB Server Thread", .{});
const thread = std.Thread.spawn(.{}, Server.run, .{ &server, allocator, sync.should_quit }) catch |e| exitln("gdb server thread crashed: {}", .{e});
const thread = std.Thread.spawn(.{}, Server.run, .{ &server, allocator, &sync.should_quit }) catch |e| exitln("gdb server thread crashed: {}", .{e});
defer thread.join();
gui.run(.{
.cpu = &cpu,
.scheduler = &scheduler,
.sync = sync,
.sync = &sync,
}) catch |e| exitln("main thread panicked: {}", .{e});
} else {
var tracker = FpsTracker.init();
const thread = std.Thread.spawn(.{}, emu.run, .{ &cpu, &scheduler, &tracker, sync }) catch |e| exitln("emu thread panicked: {}", .{e});
const thread = std.Thread.spawn(.{}, emu.run, .{ &cpu, &scheduler, &tracker, &sync }) catch |e| exitln("emu thread panicked: {}", .{e});
defer thread.join();
gui.run(.{
.cpu = &cpu,
.scheduler = &scheduler,
.tracker = &tracker,
.sync = sync,
.sync = &sync,
}) catch |e| exitln("main thread panicked: {}", .{e});
}
}
@@ -201,7 +197,9 @@ fn configFilePath(allocator: Allocator, config_path: []const u8) ![]const u8 {
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"));
// FIXME(2025-09-22): re-enable
// try config_file.writeAll(@embedFile("example.toml"));
try config_file.writeAll("");
};
return path;
@@ -225,14 +223,16 @@ fn ensureConfigDirExists(config_path: []const u8) !void {
fn romPath(allocator: Allocator, result: *const clap.Result(clap.Help, &params, clap.parsers.default)) !?[]const u8 {
return switch (result.positionals.len) {
0 => null,
1 => try allocator.dupe(u8, result.positionals[0]),
1 => if (result.positionals[0]) |path| try allocator.dupe(u8, path) else null,
else => exitln("ZBA received too many positional arguments.", .{}),
};
}
fn exitln(comptime format: []const u8, args: anytype) noreturn {
const stderr = std.io.getStdErr().writer();
var buf: [1024]u8 = undefined;
var stderr = std.fs.File.stderr().writer(&buf).interface;
stderr.print(format, args) catch {}; // Just exit already...
stderr.writeByte('\n') catch {};
std.os.exit(1);
std.process.exit(1);
}

View File

@@ -1,7 +1,7 @@
const std = @import("std");
const SDL = @import("sdl2");
const gl = @import("gl");
const zgui = @import("zgui");
const c = @import("lib.zig").c;
const emu = @import("core/emu.zig");
const config = @import("config.zig");
@@ -18,24 +18,27 @@ const KeyInput = @import("core/bus/io.zig").KeyInput;
const gba_width = @import("core/ppu.zig").width;
const gba_height = @import("core/ppu.zig").height;
const GLuint = gl.GLuint;
const GLsizei = gl.GLsizei;
const SDL_GLContext = *anyopaque;
const GLsizei = gl.sizei;
const SDL_GLContext = *c.SDL_GLContextState;
const Allocator = std.mem.Allocator;
pub const Dimensions = struct { width: u32, height: u32 };
const default_dim: Dimensions = .{ .width = 1280, .height = 720 };
pub const sample_rate = 1 << 15;
pub const sample_format = SDL.AUDIO_U16;
// pub const sample_format = SDL.AUDIO_U16;
const window_title = "ZBA";
const errify = @import("lib.zig").errify;
var gl_procs: gl.ProcTable = undefined;
pub const Gui = struct {
const Self = @This();
const log = std.log.scoped(.Gui);
window: *SDL.SDL_Window,
window: *c.SDL_Window,
ctx: SDL_GLContext,
audio: Audio,
@@ -43,36 +46,42 @@ pub const Gui = struct {
allocator: Allocator,
pub fn init(allocator: Allocator, apu: *Apu, title_opt: ?*const [12]u8) !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();
c.SDL_SetMainReady();
const window = SDL.SDL_CreateWindow(
window_title,
SDL.SDL_WINDOWPOS_CENTERED,
SDL.SDL_WINDOWPOS_CENTERED,
default_dim.width,
default_dim.height,
SDL.SDL_WINDOW_OPENGL | SDL.SDL_WINDOW_SHOWN | SDL.SDL_WINDOW_RESIZABLE,
) orelse panic();
try errify(c.SDL_Init(c.SDL_INIT_VIDEO | c.SDL_INIT_AUDIO | c.SDL_INIT_EVENTS));
const ctx = SDL.SDL_GL_CreateContext(window) orelse panic();
if (SDL.SDL_GL_MakeCurrent(window, ctx) < 0) panic();
try errify(c.SDL_SetAppMetadata(window_title, "0.1.0", "moe.paoda.zba"));
try errify(c.SDL_GL_SetAttribute(c.SDL_GL_CONTEXT_MAJOR_VERSION, gl.info.version_major));
try errify(c.SDL_GL_SetAttribute(c.SDL_GL_CONTEXT_MINOR_VERSION, gl.info.version_minor));
try errify(c.SDL_GL_SetAttribute(c.SDL_GL_CONTEXT_PROFILE_MASK, c.SDL_GL_CONTEXT_PROFILE_CORE));
try errify(c.SDL_GL_SetAttribute(c.SDL_GL_CONTEXT_FLAGS, c.SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG));
gl.load(ctx, Self.glGetProcAddress) catch {};
if (SDL.SDL_GL_SetSwapInterval(@intFromBool(config.config().host.vsync)) < 0) panic();
const window: *c.SDL_Window = try errify(c.SDL_CreateWindow(window_title, default_dim.width, default_dim.height, c.SDL_WINDOW_OPENGL | c.SDL_WINDOW_RESIZABLE));
errdefer c.SDL_DestroyWindow(window);
const gl_ctx = try errify(c.SDL_GL_CreateContext(window));
errdefer errify(c.SDL_GL_DestroyContext(gl_ctx)) catch {};
try errify(c.SDL_GL_MakeCurrent(window, gl_ctx));
errdefer errify(c.SDL_GL_MakeCurrent(window, null)) catch {};
if (!gl_procs.init(c.SDL_GL_GetProcAddress)) return error.gl_init_failed;
gl.makeProcTableCurrent(&gl_procs);
errdefer gl.makeProcTableCurrent(null);
try errify(c.SDL_GL_SetSwapInterval(@intFromBool(config.config().host.vsync)));
zgui.init(allocator);
zgui.plot.init();
zgui.backend.init(window, ctx, "#version 330 core");
zgui.backend.init(window, gl_ctx);
// zgui.io.setIniFilename(null);
return Self{
.window = window,
.ctx = ctx,
.audio = Audio.init(apu),
.ctx = gl_ctx,
.audio = try Audio.init(apu),
.allocator = allocator,
.state = try imgui.State.init(allocator, title_opt),
@@ -87,15 +96,15 @@ pub const Gui = struct {
zgui.plot.deinit();
zgui.deinit();
SDL.SDL_GL_DeleteContext(self.ctx);
SDL.SDL_DestroyWindow(self.window);
SDL.SDL_Quit();
errify(c.SDL_GL_DestroyContext(self.ctx)) catch {};
c.SDL_DestroyWindow(self.window);
c.SDL_Quit();
self.* = undefined;
}
const RunOptions = struct {
sync: Synchro,
sync: *Synchro,
tracker: ?*FpsTracker = null,
cpu: *Arm7tdmi,
scheduler: *Scheduler,
@@ -108,82 +117,81 @@ pub const Gui = struct {
const bus_ptr: *Bus = @ptrCast(@alignCast(cpu.bus.ptr));
const objects = opengl_impl.createObjects();
defer gl.deleteBuffers(3, @as(*const [3]GLuint, &.{ objects.vao, objects.vbo, objects.ebo }));
const vao_id = opengl_impl.vao();
defer gl.DeleteVertexArrays(1, vao_id[0..]);
const emu_tex = opengl_impl.createScreenTexture(bus_ptr.ppu.framebuf.get(.Renderer));
const out_tex = opengl_impl.createOutputTexture();
defer gl.deleteTextures(2, &[_]GLuint{ emu_tex, out_tex });
const emu_tex = opengl_impl.screenTex(bus_ptr.ppu.framebuf.get(.Renderer));
defer gl.DeleteTextures(1, emu_tex[0..]);
const fbo_id = try opengl_impl.createFrameBuffer(out_tex);
defer gl.deleteFramebuffers(1, &fbo_id);
const out_tex = opengl_impl.outTex();
defer gl.DeleteTextures(1, out_tex[0..]);
// TODO: Support dynamically switching shaders?
const prog_id = try opengl_impl.compileShaders();
defer gl.deleteProgram(prog_id);
const fbo_id = try opengl_impl.frameBuffer(out_tex[0]);
defer gl.DeleteFramebuffers(1, fbo_id[0..]);
const prog_id = try opengl_impl.program(); // Dynamic Shaders?
defer gl.DeleteProgram(prog_id);
var win_dim: Dimensions = default_dim;
emu_loop: while (true) {
// Outside of `SDL.SDL_QUIT` below, the DearImgui UI might signal that the program
// should exit, in which case we should also handle this
if (self.state.should_quit) break :emu_loop;
if (self.state.should_quit or sync.should_quit.load(.monotonic)) break :emu_loop;
var event: SDL.SDL_Event = undefined;
while (SDL.SDL_PollEvent(&event) != 0) {
var event: c.SDL_Event = undefined;
while (c.SDL_PollEvent(&event)) {
_ = zgui.backend.processEvent(&event);
switch (event.type) {
SDL.SDL_QUIT => break :emu_loop,
SDL.SDL_KEYDOWN => {
c.SDL_EVENT_QUIT => break :emu_loop,
c.SDL_EVENT_KEY_DOWN => {
// TODO: Make use of compare_and_xor?
const key_code = event.key.keysym.sym;
var keyinput: KeyInput = .{ .raw = 0x0000 };
switch (key_code) {
SDL.SDLK_UP => keyinput.up.set(),
SDL.SDLK_DOWN => keyinput.down.set(),
SDL.SDLK_LEFT => keyinput.left.set(),
SDL.SDLK_RIGHT => keyinput.right.set(),
SDL.SDLK_x => keyinput.a.set(),
SDL.SDLK_z => keyinput.b.set(),
SDL.SDLK_a => keyinput.shoulder_l.set(),
SDL.SDLK_s => keyinput.shoulder_r.set(),
SDL.SDLK_RETURN => keyinput.start.set(),
SDL.SDLK_RSHIFT => keyinput.select.set(),
switch (event.key.scancode) {
c.SDL_SCANCODE_UP => keyinput.up.write(true),
c.SDL_SCANCODE_DOWN => keyinput.down.write(true),
c.SDL_SCANCODE_LEFT => keyinput.left.write(true),
c.SDL_SCANCODE_RIGHT => keyinput.right.write(true),
c.SDL_SCANCODE_X => keyinput.a.write(true),
c.SDL_SCANCODE_Z => keyinput.b.write(true),
c.SDL_SCANCODE_A => keyinput.shoulder_l.write(true),
c.SDL_SCANCODE_S => keyinput.shoulder_r.write(true),
c.SDL_SCANCODE_RETURN => keyinput.start.write(true),
c.SDL_SCANCODE_RSHIFT => keyinput.select.write(true),
else => {},
}
bus_ptr.io.keyinput.fetchAnd(~keyinput.raw, .Monotonic);
bus_ptr.io.keyinput.fetchAnd(~keyinput.raw, .monotonic);
},
SDL.SDL_KEYUP => {
c.SDL_EVENT_KEY_UP => {
// FIXME(paoda): merge with above?
// TODO: Make use of compare_and_xor?
const key_code = event.key.keysym.sym;
var keyinput: KeyInput = .{ .raw = 0x0000 };
switch (key_code) {
SDL.SDLK_UP => keyinput.up.set(),
SDL.SDLK_DOWN => keyinput.down.set(),
SDL.SDLK_LEFT => keyinput.left.set(),
SDL.SDLK_RIGHT => keyinput.right.set(),
SDL.SDLK_x => keyinput.a.set(),
SDL.SDLK_z => keyinput.b.set(),
SDL.SDLK_a => keyinput.shoulder_l.set(),
SDL.SDLK_s => keyinput.shoulder_r.set(),
SDL.SDLK_RETURN => keyinput.start.set(),
SDL.SDLK_RSHIFT => keyinput.select.set(),
switch (event.key.scancode) {
c.SDL_SCANCODE_UP => keyinput.up.write(true),
c.SDL_SCANCODE_DOWN => keyinput.down.write(true),
c.SDL_SCANCODE_LEFT => keyinput.left.write(true),
c.SDL_SCANCODE_RIGHT => keyinput.right.write(true),
c.SDL_SCANCODE_X => keyinput.a.write(true),
c.SDL_SCANCODE_Z => keyinput.b.write(true),
c.SDL_SCANCODE_A => keyinput.shoulder_l.write(true),
c.SDL_SCANCODE_S => keyinput.shoulder_r.write(true),
c.SDL_SCANCODE_RETURN => keyinput.start.write(true),
c.SDL_SCANCODE_RSHIFT => keyinput.select.write(true),
else => {},
}
bus_ptr.io.keyinput.fetchOr(keyinput.raw, .Monotonic);
bus_ptr.io.keyinput.fetchOr(keyinput.raw, .monotonic);
},
SDL.SDL_WINDOWEVENT => {
if (event.window.event == SDL.SDL_WINDOWEVENT_RESIZED) {
log.debug("window resized to: {}x{}", .{ event.window.data1, event.window.data2 });
c.SDL_EVENT_WINDOW_RESIZED => {
log.debug("window resized to: {}x{}", .{ event.window.data1, event.window.data2 });
win_dim.width = @intCast(event.window.data1);
win_dim.height = @intCast(event.window.data2);
}
win_dim.width = @intCast(event.window.data1);
win_dim.height = @intCast(event.window.data2);
},
else => {},
}
@@ -194,78 +202,58 @@ pub const Gui = struct {
switch (self.state.emulation) {
.Transition => |inner| switch (inner) {
.Active => {
sync.paused.store(false, .Monotonic);
if (!config.config().host.mute) SDL.SDL_PauseAudioDevice(self.audio.device, 0);
sync.paused.store(false, .monotonic);
if (!config.config().host.mute) try errify(c.SDL_PauseAudioDevice(c.SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK));
self.state.emulation = .Active;
},
.Inactive => {
// Assert that double pausing is impossible
SDL.SDL_PauseAudioDevice(self.audio.device, 1);
sync.paused.store(true, .Monotonic);
try errify(c.SDL_ResumeAudioDevice(c.SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK));
sync.paused.store(true, .monotonic);
self.state.emulation = .Inactive;
},
},
.Active => blk: {
sync.ui_busy.store(true, .Monotonic);
defer sync.ui_busy.store(false, .Monotonic);
// spin until we know the emu is paused :)
const timeout = 0x100000;
wait_loop: for (0..timeout) |i| {
const ret = sync.did_pause.compareAndSwap(true, false, .Acquire, .Monotonic);
if (ret == null) break :wait_loop;
if (i == timeout - 1) break :blk;
}
// while (sync.did_pause.compareAndSwap(true, false, .Acquire, .Acquire) != null) std.atomic.spinLoopHint();
.Active => {
// Add FPS count to the histogram
if (tracker) |t| self.state.fps_hist.push(t.value()) catch {};
// Draw GBA Screen to Texture
{
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo_id);
defer gl.bindFramebuffer(gl.FRAMEBUFFER, 0);
gl.BindFramebuffer(gl.FRAMEBUFFER, fbo_id[0]);
defer gl.BindFramebuffer(gl.FRAMEBUFFER, 0);
const buf = bus_ptr.ppu.framebuf.get(.Renderer);
gl.viewport(0, 0, gba_width, gba_height);
opengl_impl.drawScreenTexture(emu_tex, prog_id, objects, buf);
gl.Viewport(0, 0, gba_width, gba_height);
opengl_impl.drawScreen(emu_tex[0], prog_id, vao_id[0], bus_ptr.ppu.framebuf.get(.Renderer));
}
// FIXME: We only really care about locking the audio device (and therefore writing silence)
// since if nfd-zig is used the emu may be paused for way too long. Perhaps we should try and limit
// spurious calls to SDL_LockAudioDevice?
SDL.SDL_LockAudioDevice(self.audio.device);
defer SDL.SDL_UnlockAudioDevice(self.audio.device);
zgui_redraw = imgui.draw(&self.state, win_dim, out_tex, cpu);
try errify(c.SDL_LockAudioStream(self.audio.stream));
defer errify(c.SDL_UnlockAudioStream(self.audio.stream)) catch @panic("TODO: FIXME");
zgui_redraw = imgui.draw(&self.state, sync, win_dim, cpu, out_tex[0]);
},
.Inactive => zgui_redraw = imgui.draw(&self.state, win_dim, out_tex, cpu),
.Inactive => zgui_redraw = imgui.draw(&self.state, sync, win_dim, cpu, out_tex[0]),
}
if (zgui_redraw) {
// Background Colour
const size = zgui.io.getDisplaySize();
gl.viewport(0, 0, @intFromFloat(size[0]), @intFromFloat(size[1]));
gl.clearColor(0, 0, 0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.Viewport(0, 0, @intFromFloat(size[0]), @intFromFloat(size[1]));
gl.ClearColor(0, 0, 0, 1.0);
gl.Clear(gl.COLOR_BUFFER_BIT);
zgui.backend.draw();
}
SDL.SDL_GL_SwapWindow(self.window);
try errify(c.SDL_GL_SwapWindow(self.window));
}
sync.should_quit.store(true, .Monotonic);
}
fn glGetProcAddress(ctx: SDL.SDL_GLContext, proc: [:0]const u8) ?*anyopaque {
_ = ctx;
return SDL.SDL_GL_GetProcAddress(proc.ptr);
sync.should_quit.store(true, .monotonic);
}
};
@@ -273,211 +261,145 @@ const Audio = struct {
const Self = @This();
const log = std.log.scoped(.PlatformAudio);
device: SDL.SDL_AudioDeviceID,
stream: *c.SDL_AudioStream,
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;
fn init(apu: *Apu) !Self {
var desired: c.SDL_AudioSpec = std.mem.zeroes(c.SDL_AudioSpec);
desired.freq = sample_rate;
desired.format = c.SDL_AUDIO_S16LE;
desired.channels = 2;
std.debug.assert(sample_format == SDL.AUDIO_U16);
log.info("Host Sample Rate: {}Hz, Host Format: SDL.AUDIO_U16", .{sample_rate});
log.info("Host Sample Rate: {}Hz, Host Format: SDL_AUDIO_S16LE", .{sample_rate});
const device = SDL.SDL_OpenAudioDevice(null, 0, &want, &have, 0);
if (device == 0) panic();
const stream = try errify(c.SDL_OpenAudioDeviceStream(c.SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desired, null, null));
errdefer c.SDL_DestroyAudioStream(stream);
return .{ .device = device };
apu.stream = stream;
return .{ .stream = stream };
}
fn deinit(self: *Self) void {
SDL.SDL_CloseAudioDevice(self.device);
c.SDL_DestroyAudioStream(self.stream);
self.* = undefined;
}
export fn callback(userdata: ?*anyopaque, stream: [*c]u8, len: c_int) void {
const apu: *Apu = @ptrCast(@alignCast(userdata));
_ = SDL.SDL_AudioStreamGet(apu.stream, stream, len);
}
};
fn panic() noreturn {
const str = @as(?[*:0]const u8, SDL.SDL_GetError()) orelse "unknown error";
@panic(std.mem.sliceTo(str, 0));
}
const opengl_impl = struct {
// 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
};
fn drawScreen(tex_id: gl.uint, prog_id: gl.uint, vao_id: gl.uint, buf: []const u8) void {
gl.BindTexture(gl.TEXTURE_2D, tex_id);
defer gl.BindTexture(gl.TEXTURE_2D, 0);
const indices: [6]u32 = [_]u32{
0, 1, 3, // First Triangle
1, 2, 3, // Second Triangle
};
// zig fmt: on
gl.TexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gba_width, gba_height, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, buf.ptr);
const Objects = struct { vao: GLuint, vbo: GLuint, ebo: GLuint };
fn drawScreenTexture(tex_id: GLuint, prog_id: GLuint, ids: Objects, buf: []const u8) void {
gl.bindTexture(gl.TEXTURE_2D, tex_id);
defer gl.bindTexture(gl.TEXTURE_2D, 0);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gba_width, gba_height, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, buf.ptr);
// Bind VAO, EBO. VBO not bound
gl.bindVertexArray(ids.vao); // VAO
defer gl.bindVertexArray(0);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ids.ebo); // EBO
defer gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, 0);
// Bind VAO
gl.BindVertexArray(vao_id);
defer gl.BindVertexArray(0);
// Use compiled frag + vertex shader
gl.useProgram(prog_id);
defer gl.useProgram(0);
gl.UseProgram(prog_id);
defer gl.UseProgram(0);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, null);
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 3);
}
fn compileShaders() !GLuint {
const vert_shader = @embedFile("shader/pixelbuf.vert");
const frag_shader = @embedFile("shader/pixelbuf.frag");
fn program() !gl.uint {
const vert_shader: [1][*]const u8 = .{@embedFile("shader/pixelbuf.vert")};
const frag_shader: [1][*]const u8 = .{@embedFile("shader/pixelbuf.frag")};
const vs = gl.createShader(gl.VERTEX_SHADER);
defer gl.deleteShader(vs);
const vs = gl.CreateShader(gl.VERTEX_SHADER);
defer gl.DeleteShader(vs);
gl.shaderSource(vs, 1, &[_][*c]const u8{vert_shader}, 0);
gl.compileShader(vs);
gl.ShaderSource(vs, 1, vert_shader[0..], null);
gl.CompileShader(vs);
if (!shader.didCompile(vs)) return error.VertexCompileError;
const fs = gl.createShader(gl.FRAGMENT_SHADER);
defer gl.deleteShader(fs);
const fs = gl.CreateShader(gl.FRAGMENT_SHADER);
defer gl.DeleteShader(fs);
gl.shaderSource(fs, 1, &[_][*c]const u8{frag_shader}, 0);
gl.compileShader(fs);
gl.ShaderSource(fs, 1, frag_shader[0..], null);
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);
const prog = gl.CreateProgram();
gl.AttachShader(prog, vs);
gl.AttachShader(prog, fs);
gl.LinkProgram(prog);
return program;
return prog;
}
// Returns the VAO ID since it's used in run()
fn createObjects() Objects {
var vao_id: GLuint = undefined;
var vbo_id: GLuint = undefined;
var ebo_id: GLuint = undefined;
fn vao() [1]gl.uint {
var vao_id: [1]gl.uint = undefined;
gl.GenVertexArrays(1, vao_id[0..]);
gl.genVertexArrays(1, &vao_id);
gl.genBuffers(1, &vbo_id);
gl.genBuffers(1, &ebo_id);
gl.bindVertexArray(vao_id);
defer gl.bindVertexArray(0);
gl.bindBuffer(gl.ARRAY_BUFFER, vbo_id);
defer gl.bindBuffer(gl.ARRAY_BUFFER, 0);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo_id);
defer gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, 0);
gl.bufferData(gl.ARRAY_BUFFER, @sizeOf(@TypeOf(vertices)), &vertices, gl.STATIC_DRAW);
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), null); // lmao
gl.enableVertexAttribArray(0);
// Colour
gl.vertexAttribPointer(1, 3, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @ptrFromInt((3 * @sizeOf(f32))));
gl.enableVertexAttribArray(1);
// Texture Coord
gl.vertexAttribPointer(2, 2, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @ptrFromInt((6 * @sizeOf(f32))));
gl.enableVertexAttribArray(2);
return .{ .vao = vao_id, .vbo = vbo_id, .ebo = ebo_id };
return vao_id;
}
fn createScreenTexture(buf: []const u8) GLuint {
var tex_id: GLuint = undefined;
gl.genTextures(1, &tex_id);
fn screenTex(buf: []const u8) [1]gl.uint {
var tex_id: [1]gl.uint = undefined;
gl.GenTextures(1, tex_id[0..]);
gl.bindTexture(gl.TEXTURE_2D, tex_id);
defer gl.bindTexture(gl.TEXTURE_2D, 0);
gl.BindTexture(gl.TEXTURE_2D, tex_id[0]);
defer gl.BindTexture(gl.TEXTURE_2D, 0);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.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.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gba_width, gba_height, 0, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, buf.ptr);
return tex_id;
}
fn createOutputTexture() GLuint {
var tex_id: GLuint = undefined;
gl.genTextures(1, &tex_id);
fn outTex() [1]gl.uint {
var tex_id: [1]gl.uint = undefined;
gl.GenTextures(1, tex_id[0..]);
gl.bindTexture(gl.TEXTURE_2D, tex_id);
defer gl.bindTexture(gl.TEXTURE_2D, 0);
gl.BindTexture(gl.TEXTURE_2D, tex_id[0]);
defer gl.BindTexture(gl.TEXTURE_2D, 0);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gba_width, gba_height, 0, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, null);
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gba_width, gba_height, 0, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, null);
return tex_id;
}
fn createFrameBuffer(tex_id: GLuint) !GLuint {
var fbo_id: GLuint = undefined;
gl.genFramebuffers(1, &fbo_id);
fn frameBuffer(tex_id: gl.uint) ![1]gl.uint {
var fbo_id: [1]gl.uint = undefined;
gl.GenFramebuffers(1, fbo_id[0..]);
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo_id);
defer gl.bindFramebuffer(gl.FRAMEBUFFER, 0);
gl.BindFramebuffer(gl.FRAMEBUFFER, fbo_id[0]);
defer gl.BindFramebuffer(gl.FRAMEBUFFER, 0);
gl.framebufferTexture(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, tex_id, 0);
gl.FramebufferTexture(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, tex_id, 0);
gl.DrawBuffers(1, &.{gl.COLOR_ATTACHMENT0});
const draw_buffers: [1]GLuint = .{gl.COLOR_ATTACHMENT0};
gl.drawBuffers(1, &draw_buffers);
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE)
if (gl.CheckFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE)
return error.FrameBufferObejctInitFailed;
return fbo_id;
}
const shader = struct {
const Kind = enum { vertex, fragment };
const log = std.log.scoped(.Shader);
const log = std.log.scoped(.shader);
fn didCompile(id: gl.GLuint) bool {
var success: gl.GLint = undefined;
gl.getShaderiv(id, gl.COMPILE_STATUS, &success);
fn didCompile(id: gl.uint) bool {
var success: [1]gl.int = undefined;
gl.GetShaderiv(id, gl.COMPILE_STATUS, success[0..]);
if (success == 0) err(id);
if (success[0] == 0) err(id);
return success == 1;
return success[0] == 1;
}
fn err(id: gl.GLuint) void {
fn err(id: gl.uint) void {
const buf_len = 512;
var error_msg: [buf_len]u8 = undefined;
gl.getShaderInfoLog(id, buf_len, 0, &error_msg);
gl.GetShaderInfoLog(id, buf_len, null, &error_msg);
log.err("{s}", .{std.mem.sliceTo(&error_msg, 0)});
}
};

View File

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

View File

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

View File

@@ -11,24 +11,24 @@ pub const FpsTracker = struct {
const Self = @This();
fps: u32,
count: std.atomic.Atomic(u32),
count: std.atomic.Value(u32),
timer: std.time.Timer,
pub fn init() Self {
return .{
.fps = 0,
.count = std.atomic.Atomic(u32).init(0),
.count = std.atomic.Value(u32).init(0),
.timer = std.time.Timer.start() catch unreachable,
};
}
pub fn tick(self: *Self) void {
_ = self.count.fetchAdd(1, .Monotonic);
_ = 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.fps = self.count.swap(0, .monotonic);
self.timer.reset();
}
@@ -69,7 +69,7 @@ pub const io = struct {
}
pub fn undef(comptime T: type, comptime log: anytype, comptime format: []const u8, args: anytype) ?T {
@setCold(true);
@branchHint(.cold);
const unhandled_io = config.config().debug.unhandled_io;
@@ -80,7 +80,7 @@ pub const io = struct {
}
pub fn err(comptime T: type, comptime log: anytype, comptime format: []const u8, args: anytype) ?T {
@setCold(true);
@branchHint(.cold);
log.err(format, args);
return null;
@@ -245,10 +245,10 @@ pub inline fn setHalf(comptime T: type, left: T, addr: u8, right: HalfInt(T)) T
/// 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
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);
return std.meta.Int(type_info.int.signedness, type_info.int.bits >> 1);
}
/// Double Buffering Implementation
@@ -295,3 +295,31 @@ pub const FrameBuffer = struct {
return self.layers[if (dev == .Emulator) self.current else ~self.current];
}
};
const RingBuffer = @import("zba_util").RingBuffer;
// TODO: Lock Free Queue?
pub fn Queue(comptime T: type) type {
return struct {
inner: RingBuffer(T),
mtx: std.Thread.Mutex = .{},
pub fn init(buf: []T) @This() {
return .{ .inner = RingBuffer(T).init(buf) };
}
pub fn push(self: *@This(), value: T) !void {
self.mtx.lock();
defer self.mtx.unlock();
try self.inner.push(value);
}
pub fn pop(self: *@This()) ?T {
self.mtx.lock();
defer self.mtx.unlock();
return self.inner.pop();
}
};
}