feat: initial commit
some basic ROMs already boot (ARM946E-S only), working on Armwrestler and Rockwrestler next so I can ensure CPU compatability for but ARMv5TE and ARMv4T
This commit is contained in:
commit
e32d6534c6
|
@ -0,0 +1,5 @@
|
|||
zig-cache/
|
||||
zig-out/
|
||||
bin/
|
||||
doc/
|
||||
imgui.ini
|
|
@ -0,0 +1,9 @@
|
|||
[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
|
||||
[submodule "lib/arm32"]
|
||||
path = lib/arm32
|
||||
url = https://git.musuka.dev/paoda/arm32.git
|
|
@ -0,0 +1,90 @@
|
|||
const std = @import("std");
|
||||
|
||||
const Sdk = @import("lib/SDL.zig/Sdk.zig");
|
||||
const zgui = @import("lib/zgui/build.zig");
|
||||
const arm32 = @import("lib/arm32/build.zig");
|
||||
|
||||
// Although this function looks imperative, note that its job is to
|
||||
// declaratively construct a build graph that will be executed by an external
|
||||
// runner.
|
||||
pub fn build(b: *std.Build) void {
|
||||
// Standard target options allows the person running `zig build` to choose
|
||||
// what target to build for. Here we do not override the defaults, which
|
||||
// means any target is allowed, and the default is native. Other options
|
||||
// for restricting supported target set are available.
|
||||
const target = b.standardTargetOptions(.{});
|
||||
|
||||
// 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(.{});
|
||||
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "turbo",
|
||||
// In this case the main source file is merely a path, however, in more
|
||||
// complicated build scripts, this could be a generated file.
|
||||
.root_source_file = .{ .path = "src/main.zig" },
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
exe.addModule("arm32", arm32.module(b));
|
||||
exe.addModule("zig-clap", b.dependency("zig-clap", .{}).module("clap"));
|
||||
|
||||
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
|
||||
|
||||
// https://github.com/MasterQ32/SDL.zig
|
||||
const sdk = Sdk.init(b, null);
|
||||
sdk.link(exe, .dynamic);
|
||||
exe.addModule("sdl2", sdk.getNativeModule());
|
||||
|
||||
// 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);
|
||||
|
||||
// This declares intent for the executable to be installed into the
|
||||
// standard location when the user invokes the "install" step (the default
|
||||
// step when running `zig build`).
|
||||
b.installArtifact(exe);
|
||||
|
||||
// This *creates* a Run step in the build graph, to be executed when another
|
||||
// step is evaluated that depends on it. The next line below will establish
|
||||
// such a dependency.
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
|
||||
// By making the run step depend on the install step, it will be run from the
|
||||
// installation directory rather than directly from within the cache directory.
|
||||
// This is not necessary, however, if the application depends on other installed
|
||||
// files, this ensures they will be present and in the expected location.
|
||||
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);
|
||||
}
|
||||
|
||||
// This creates a build step. It will be visible in the `zig build --help` menu,
|
||||
// and can be selected like this: `zig build run`
|
||||
// This will evaluate the `run` step rather than the default, which is "install".
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
|
||||
// Creates a step for unit testing. This only builds the test executable
|
||||
// but does not run it.
|
||||
const unit_tests = b.addTest(.{
|
||||
.root_source_file = .{ .path = "src/main.zig" },
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const run_unit_tests = b.addRunArtifact(unit_tests);
|
||||
|
||||
// Similar to creating the run step earlier, this exposes a `test` step to
|
||||
// the `zig build --help` menu, providing a way for the user to request
|
||||
// running the unit tests.
|
||||
const test_step = b.step("test", "Run unit tests");
|
||||
test_step.dependOn(&run_unit_tests.step);
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
.{
|
||||
.name = "turbo",
|
||||
.version = "0.1.0",
|
||||
.dependencies = .{
|
||||
.@"zig-clap" = .{
|
||||
.url = "https://github.com/Hejsil/zig-clap/archive/bdb5853b678d68f342ec65b04a6785af522ca6c9.tar.gz",
|
||||
.hash = "12202af04ec78191f2018458a7be29f54e0d9118f7688e7a226857acf754d68b8473",
|
||||
},
|
||||
.@"zba-util" = .{
|
||||
// Necessary to use paoda/arm32 as a git submodule
|
||||
.url = "https://git.musuka.dev/paoda/zba-util/archive/322c798e384a0d24cc84ffcfa2e4a3ca807798a0.tar.gz",
|
||||
.hash = "12209ce0e729460b997706e47a53a32f1842672cd120189e612f4871731780a30ed0",
|
||||
},
|
||||
},
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 4d565b54227b862c1540719e0e21a36d649e87d5
|
|
@ -0,0 +1 @@
|
|||
Subproject commit ada2a08516d55a61bddd96f1c29e1547d0466049
|
|
@ -0,0 +1,146 @@
|
|||
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);
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
|||
Subproject commit 25fa193b1f324d9183b3808bc6582ee7d0e3b13f
|
|
@ -0,0 +1,89 @@
|
|||
const std = @import("std");
|
||||
|
||||
/// For use with withe tiniest ROM
|
||||
pub const Header = extern struct {
|
||||
title: [12]u8,
|
||||
game_code: [4]u8,
|
||||
maker_code: [2]u8,
|
||||
unit_code: u8,
|
||||
encryption_seed_select: u8,
|
||||
device_capacity: u8,
|
||||
_: [7]u8 = [_]u8{0} ** 7,
|
||||
__: u8 = 0,
|
||||
nds_region: u8,
|
||||
version: u8,
|
||||
auto_start: u8,
|
||||
|
||||
arm9_rom_offset: u32,
|
||||
arm9_entry_address: u32,
|
||||
arm9_ram_address: u32,
|
||||
arm9_size: u32,
|
||||
|
||||
arm7_rom_offset: u32,
|
||||
arm7_entry_address: u32,
|
||||
arm7_ram_address: u32,
|
||||
arm7_size: u32,
|
||||
|
||||
/// File Name Table Offset
|
||||
fnt_offset: u32,
|
||||
/// File Name Table Size
|
||||
fnt_size: u32,
|
||||
|
||||
/// File Allocation Table Offset
|
||||
fat_offset: u32,
|
||||
// File Allocation Table Size
|
||||
fat_size: u32,
|
||||
|
||||
/// File ARM9 Overlay Offset
|
||||
farm9_overlay_offset: u32,
|
||||
// File ARM9 Overlay Size
|
||||
farm9_overlay_size: u32,
|
||||
/// File ARM9 Overlay Offset
|
||||
farm7_overlay_offset: u32,
|
||||
// File ARM9 Overlay Size
|
||||
farm7_overlay_size: u32,
|
||||
|
||||
/// Port 40001A4h setting for normal commands (usually 00586000h)
|
||||
gamecard_control_setting_normal: u32, // TODO: rename these fields
|
||||
/// Port 40001A4h setting for KEY1 commands (usually 001808F8h)
|
||||
gamecard_control_setting_key1: u32,
|
||||
|
||||
/// Icon / Title Offset
|
||||
icon_title_offset: u32,
|
||||
|
||||
/// Secure Area Checksum
|
||||
secure_checksum: u16,
|
||||
/// Secure Area Delay
|
||||
secure_delay: u16,
|
||||
|
||||
// TODO: Document
|
||||
arm9_auto_load_list: u32,
|
||||
arm7_auto_load_list: u32,
|
||||
secure_disable: u64 align(1),
|
||||
|
||||
total_used: u32,
|
||||
header_size: u32,
|
||||
___: u32 = 0, // TODO: may not be zero?
|
||||
____: u64 align(1) = 0,
|
||||
|
||||
rom_nand_end: u16,
|
||||
rw_nand_start: u16,
|
||||
_____: [0x18]u8 = [_]u8{0} ** 0x18,
|
||||
______: [0x10]u8 = [_]u8{0} ** 0x10,
|
||||
|
||||
logo: [0x9C]u8,
|
||||
logo_checksum: u16,
|
||||
|
||||
/// Header Checksum
|
||||
checksum: u16,
|
||||
|
||||
// note, we're missing some debug_ prefixed fields here
|
||||
// but we want the header struct to be 0x160 bytes so that
|
||||
// the smallest NDS rom's header can be read without any speicifc
|
||||
// workarounds
|
||||
// TODO: Determine if we ever will need those debug fields, and if so: Implement them
|
||||
|
||||
comptime {
|
||||
std.debug.assert(@sizeOf(@This()) == 0x160);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,109 @@
|
|||
const std = @import("std");
|
||||
const nds9 = @import("nds9.zig");
|
||||
const nds7 = @import("nds7.zig");
|
||||
|
||||
const Header = @import("cartridge.zig").Header;
|
||||
const Arm946es = nds9.Arm946es;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
/// Load a NDS Cartridge
|
||||
///
|
||||
/// intended to be used immediately after Emulator initialization
|
||||
pub fn load(allocator: Allocator, nds7_group: nds7.Group, nds9_group: nds9.Group, rom_file: std.fs.File) ![12]u8 {
|
||||
const log = std.log.scoped(.load_rom);
|
||||
|
||||
const rom_buf = try rom_file.readToEndAlloc(allocator, try rom_file.getEndPos());
|
||||
defer allocator.free(rom_buf);
|
||||
|
||||
var stream = std.io.fixedBufferStream(rom_buf);
|
||||
const header = try stream.reader().readStruct(Header);
|
||||
|
||||
log.info("Title: \"{s}\"", .{std.mem.sliceTo(&header.title, 0)});
|
||||
log.info("Game Code: \"{s}\"", .{std.mem.sliceTo(&header.game_code, 0)});
|
||||
log.info("Maker Code: \"{s}\"", .{std.mem.sliceTo(&header.maker_code, 0)});
|
||||
|
||||
// Dealing with the ARM946E-S
|
||||
{
|
||||
const arm946es = nds9_group.cpu;
|
||||
|
||||
log.debug("ARM9 ROM Offset: 0x{X:0>8}", .{header.arm9_rom_offset});
|
||||
log.debug("ARM9 Entry Address: 0x{X:0>8}", .{header.arm9_entry_address});
|
||||
log.debug("ARM9 RAM Address: 0x{X:0>8}", .{header.arm9_ram_address});
|
||||
log.debug("ARM9 Size: 0x{X:0>8}", .{header.arm9_size});
|
||||
|
||||
// Copy ARM9 Code into Main Memory
|
||||
for (rom_buf[header.arm9_rom_offset..][0..header.arm9_size], 0..) |value, i| {
|
||||
const address = header.arm9_ram_address + @as(u32, @intCast(i));
|
||||
nds9_group.bus.dbgWrite(u8, address, value);
|
||||
}
|
||||
|
||||
arm946es.r[15] = header.arm9_entry_address;
|
||||
}
|
||||
|
||||
// Dealing with the ARM7TDMI
|
||||
{
|
||||
const arm7tdmi = nds7_group.cpu;
|
||||
|
||||
log.debug("ARM7 ROM Offset: 0x{X:0>8}", .{header.arm7_rom_offset});
|
||||
log.debug("ARM7 Entry Address: 0x{X:0>8}", .{header.arm7_entry_address});
|
||||
log.debug("ARM7 RAM Address: 0x{X:0>8}", .{header.arm7_ram_address});
|
||||
log.debug("ARM7 Size: 0x{X:0>8}", .{header.arm7_size});
|
||||
|
||||
// Copy ARM7 Code into Main Memory
|
||||
for (rom_buf[header.arm7_rom_offset..][0..header.arm7_size], 0..) |value, i| {
|
||||
const address = header.arm7_ram_address + @as(u32, @intCast(i));
|
||||
nds7_group.bus.dbgWrite(u8, address, value);
|
||||
}
|
||||
|
||||
arm7tdmi.r[15] = header.arm7_entry_address;
|
||||
}
|
||||
|
||||
return header.title;
|
||||
}
|
||||
|
||||
const bus_clock = 33513982; // 33.513982 Hz
|
||||
const dot_clock = 5585664; // 5.585664 Hz
|
||||
const arm7_clock = bus_clock;
|
||||
const arm9_clock = bus_clock * 2;
|
||||
|
||||
pub fn runFrame(nds7_group: nds7.Group, nds9_group: nds9.Group) void {
|
||||
// TODO: might be more efficient to run them both in the same loop?
|
||||
{
|
||||
const scheduler = nds7_group.scheduler;
|
||||
|
||||
const cycles_per_dot = arm7_clock / dot_clock + 1;
|
||||
comptime std.debug.assert(cycles_per_dot == 6);
|
||||
|
||||
const cycles_per_frame = 355 * 263 * cycles_per_dot;
|
||||
const frame_end = scheduler.tick + cycles_per_frame;
|
||||
|
||||
const cpu = nds7_group.cpu;
|
||||
const bus = nds7_group.bus;
|
||||
|
||||
while (scheduler.tick < frame_end) {
|
||||
cpu.step();
|
||||
|
||||
if (scheduler.tick >= scheduler.next()) scheduler.handle(bus);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const scheduler = nds9_group.scheduler;
|
||||
|
||||
const cycles_per_dot = arm9_clock / dot_clock + 1;
|
||||
comptime std.debug.assert(cycles_per_dot == 12);
|
||||
|
||||
const cycles_per_frame = 355 * 263 * cycles_per_dot;
|
||||
const frame_end = scheduler.tick + cycles_per_frame;
|
||||
|
||||
const cpu = nds9_group.cpu;
|
||||
const bus = nds9_group.bus;
|
||||
|
||||
while (scheduler.tick < frame_end) {
|
||||
cpu.step();
|
||||
|
||||
if (scheduler.tick >= scheduler.next()) scheduler.handle(bus);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
const std = @import("std");
|
||||
|
||||
const Bitfield = @import("bitfield").Bitfield;
|
||||
const Bit = @import("bitfield").Bit;
|
||||
|
||||
const log = std.log.scoped(.shared_io);
|
||||
|
||||
pub const Io = struct {
|
||||
/// Interrupt Master Enable
|
||||
/// Read/Write
|
||||
ime: bool = false,
|
||||
|
||||
/// Interrupt Enable
|
||||
/// Read/Write
|
||||
///
|
||||
/// Caller must cast the `u32` to either `nds7.IntEnable` or `nds9.IntEnable`
|
||||
ie: u32 = 0x0000_0000,
|
||||
|
||||
/// IF - Interrupt Request
|
||||
/// Read/Write
|
||||
///
|
||||
/// Caller must cast the `u32` to either `nds7.IntRequest` or `nds9.IntRequest`
|
||||
irq: u32 = 0x0000_0000,
|
||||
|
||||
/// IPC Synchronize
|
||||
/// Read/Write
|
||||
ipc_sync: IpcSync = .{ .raw = 0x0000_0000 },
|
||||
|
||||
/// IPC Fifo Control
|
||||
/// Read/Write
|
||||
ipc_fifo_cnt: IpcFifoCnt = .{ .raw = 0x0000_0000 },
|
||||
|
||||
/// IPC Send FIFO
|
||||
/// Write-Only
|
||||
ipc_fifo_send: u32 = 0x0000_0000,
|
||||
|
||||
/// IPC Receive FIFO
|
||||
/// Read-Only
|
||||
ipc_fifo_recv: u32 = 0x0000_0000,
|
||||
|
||||
/// Post Boot Flag
|
||||
/// Read/Write
|
||||
///
|
||||
/// Caller must cast the `u8` to either `nds7.PostFlg` or `nds9.PostFlg`
|
||||
post_flg: u8 = @intFromEnum(nds7.PostFlag.in_progress),
|
||||
|
||||
// TODO: DS Cartridge I/O Ports
|
||||
};
|
||||
|
||||
fn warn(comptime format: []const u8, args: anytype) u0 {
|
||||
log.warn(format, args);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// TODO: Please Rename
|
||||
// TODO: Figure out a way to apply masks while calling valueAtAddressOffset
|
||||
// TODO: These aren't optimized well. Can we improve that?
|
||||
pub inline fn valueAtAddressOffset(comptime T: type, address: u32, value: T) u8 {
|
||||
const L2I = std.math.Log2Int(T);
|
||||
|
||||
return @truncate(switch (T) {
|
||||
u16 => value >> @as(L2I, @truncate((address & 1) << 3)),
|
||||
u32 => value >> @as(L2I, @truncate((address & 3) << 3)),
|
||||
else => @compileError("unsupported for " ++ @typeName(T) ++ "values"),
|
||||
});
|
||||
}
|
||||
|
||||
fn WriteOption(comptime T: type) type {
|
||||
return struct { mask: ?T = null };
|
||||
}
|
||||
|
||||
// TODO: also please rename
|
||||
// TODO: Figure out a way to apply masks while calling writeToAddressOffset
|
||||
// TODO: These aren't optimized well. Can we improve that?
|
||||
pub inline fn writeToAddressOffset(
|
||||
register: anytype,
|
||||
address: u32,
|
||||
value: anytype,
|
||||
// mask: WriteOption(@typeInfo(@TypeOf(register)).Pointer.child),
|
||||
) void {
|
||||
const Ptr = @TypeOf(register);
|
||||
const ChildT = @typeInfo(Ptr).Pointer.child;
|
||||
const ValueT = @TypeOf(value);
|
||||
|
||||
const left = register.*;
|
||||
|
||||
register.* = switch (ChildT) {
|
||||
u32 => switch (ValueT) {
|
||||
u16 => blk: {
|
||||
// TODO: This probably gets deleted
|
||||
const offset: u1 = @truncate(address >> 1);
|
||||
|
||||
break :blk switch (offset) {
|
||||
0b0 => (left & 0xFFFF_0000) | value,
|
||||
0b1 => (left & 0x0000_FFFF) | @as(u32, value) << 16,
|
||||
};
|
||||
},
|
||||
u8 => blk: {
|
||||
// TODO: Remove branching
|
||||
const offset: u2 = @truncate(address);
|
||||
|
||||
break :blk switch (offset) {
|
||||
0b00 => (left & 0xFFFF_FF00) | value,
|
||||
0b01 => (left & 0xFFFF_00FF) | @as(u32, value) << 8,
|
||||
0b10 => (left & 0xFF00_FFFF) | @as(u32, value) << 16,
|
||||
0b11 => (left & 0x00FF_FFFF) | @as(u32, value) << 24,
|
||||
};
|
||||
},
|
||||
else => @compileError("for " ++ @typeName(Ptr) ++ ", T must be u16 or u8"),
|
||||
},
|
||||
u16 => blk: {
|
||||
if (ValueT != u8) @compileError("for " ++ @typeName(Ptr) ++ ", T must be u8");
|
||||
|
||||
const shamt = @as(u4, @truncate(address & 1)) << 3;
|
||||
const mask: u16 = 0xFF00 >> shamt;
|
||||
const value_shifted = @as(u16, value) << shamt;
|
||||
|
||||
break :blk (left & mask) | value_shifted;
|
||||
},
|
||||
else => @compileError("unsupported for " ++ @typeName(Ptr) ++ " values"),
|
||||
};
|
||||
}
|
||||
|
||||
const IpcSync = extern union {
|
||||
/// Data input to IPCSYNC Bit 8->11 of remote CPU
|
||||
/// Read-Only
|
||||
data_input: Bitfield(u32, 0, 4),
|
||||
|
||||
/// Data output to IPCSYNC Bit 0->3 of remote CPU
|
||||
/// Read/Write
|
||||
data_output: Bitfield(u32, 8, 4),
|
||||
|
||||
/// Send IRQ to remote CPU
|
||||
/// Write-Only
|
||||
send_irq: Bit(u32, 13),
|
||||
|
||||
/// Enable IRQ from remote CPU
|
||||
/// Read/Write
|
||||
recv_irq: Bit(u32, 14),
|
||||
|
||||
raw: u32,
|
||||
};
|
||||
|
||||
const IpcFifoCnt = extern union {
|
||||
/// Read-Only
|
||||
send_fifo_empty: Bit(u32, 0),
|
||||
/// Read-Only
|
||||
send_fifo_full: Bit(u32, 1),
|
||||
/// Read/Write
|
||||
send_fifo_irq_enable: Bit(u32, 2),
|
||||
/// Write-Only
|
||||
send_fifo_clear: Bit(u32, 3),
|
||||
|
||||
/// Read-Only
|
||||
recv_fifo_empty: Bit(u32, 8),
|
||||
/// Read-Only
|
||||
recv_fifo_full: Bit(u32, 9),
|
||||
|
||||
/// IRQ for when the Receive FIFO is **not empty**
|
||||
/// Read/Write
|
||||
recv_fifo_irq_enable: Bit(u32, 10),
|
||||
|
||||
/// Error, recv FIFO empty or send FIFO full
|
||||
/// Read/Write
|
||||
fifo_error: Bit(u32, 14),
|
||||
/// Read/Write
|
||||
enable_fifos: Bit(u32, 15),
|
||||
|
||||
raw: u32,
|
||||
};
|
||||
|
||||
pub const nds7 = struct {
|
||||
pub const IntEnable = extern union {
|
||||
raw: u32,
|
||||
};
|
||||
|
||||
pub const IntRequest = IntEnable;
|
||||
pub const PostFlag = enum(u8) { in_progress = 0, completed };
|
||||
};
|
||||
|
||||
pub const nds9 = struct {
|
||||
pub const IntEnable = extern union {
|
||||
raw: u32,
|
||||
};
|
||||
|
||||
pub const IntRequest = IntEnable;
|
||||
|
||||
pub const PostFlag = enum(u8) { in_progress = 0, completed };
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub const Bus = @import("nds7/Bus.zig");
|
||||
pub const io = @import("nds7/io.zig");
|
||||
pub const Scheduler = @import("nds7/Scheduler.zig");
|
||||
pub const Arm7tdmi = @import("arm32").Arm7tdmi;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
// TODO: Rename (maybe Devices?)
|
||||
pub const Group = struct {
|
||||
cpu: *Arm7tdmi,
|
||||
bus: *Bus,
|
||||
scheduler: *Scheduler,
|
||||
|
||||
/// Responsible for deallocated the ARM7 CPU, Bus and Scheduler
|
||||
pub fn deinit(self: @This(), allocator: Allocator) void {
|
||||
self.bus.deinit(allocator);
|
||||
self.scheduler.deinit();
|
||||
}
|
||||
};
|
|
@ -0,0 +1,100 @@
|
|||
const std = @import("std");
|
||||
|
||||
const io = @import("io.zig");
|
||||
|
||||
const Scheduler = @import("Scheduler.zig");
|
||||
const SharedIo = @import("../io.zig").Io;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Mode = enum { normal, debug };
|
||||
const MiB = 0x100000;
|
||||
const KiB = 0x400;
|
||||
|
||||
const log = std.log.scoped(.nds7_bus);
|
||||
|
||||
scheduler: *Scheduler,
|
||||
main: *[4 * MiB]u8,
|
||||
wram: *[64 * KiB]u8,
|
||||
io: io.Io,
|
||||
|
||||
pub fn init(allocator: Allocator, scheduler: *Scheduler, shared_io: *SharedIo) !@This() {
|
||||
const main_mem = try allocator.create([4 * MiB]u8);
|
||||
errdefer allocator.destroy(main_mem);
|
||||
@memset(main_mem, 0);
|
||||
|
||||
const wram = try allocator.create([64 * KiB]u8);
|
||||
errdefer allocator.destroy(wram);
|
||||
@memset(wram, 0);
|
||||
|
||||
return .{
|
||||
.main = main_mem,
|
||||
.wram = wram,
|
||||
.scheduler = scheduler,
|
||||
.io = io.Io.init(shared_io),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *@This(), allocator: Allocator) void {
|
||||
allocator.destroy(self.main);
|
||||
allocator.destroy(self.wram);
|
||||
}
|
||||
|
||||
pub fn reset(_: *@This()) void {}
|
||||
|
||||
pub fn read(self: *@This(), comptime T: type, address: u32) T {
|
||||
return self._read(T, .normal, address);
|
||||
}
|
||||
|
||||
pub fn dbgRead(self: *@This(), comptime T: type, address: u32) T {
|
||||
return self._read(T, .debug, address);
|
||||
}
|
||||
|
||||
fn _read(self: *@This(), comptime T: type, comptime mode: Mode, address: u32) T {
|
||||
const byte_count = @divExact(@typeInfo(T).Int.bits, 8);
|
||||
const readInt = std.mem.readIntLittle;
|
||||
|
||||
switch (mode) {
|
||||
// .debug => log.debug("read {} from 0x{X:0>8}", .{ T, address }),
|
||||
.debug => {},
|
||||
else => self.scheduler.tick += 1,
|
||||
}
|
||||
|
||||
return switch (address) {
|
||||
0x0200_0000...0x02FF_FFFF => readInt(T, self.main[address & 0x003F_FFFF ..][0..byte_count]),
|
||||
0x0380_0000...0x0380_FFFF => readInt(T, self.wram[address & 0x0000_FFFF ..][0..byte_count]),
|
||||
0x0400_0000...0x04FF_FFFF => io.read(self, T, address),
|
||||
else => warn("unexpected read: 0x{x:0>8} -> {}", .{ address, T }),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write(self: *@This(), comptime T: type, address: u32, value: T) void {
|
||||
return self._write(T, .normal, address, value);
|
||||
}
|
||||
|
||||
pub fn dbgWrite(self: *@This(), comptime T: type, address: u32, value: T) void {
|
||||
return self._write(T, .debug, address, value);
|
||||
}
|
||||
|
||||
fn _write(self: *@This(), comptime T: type, comptime mode: Mode, address: u32, value: T) void {
|
||||
const byte_count = @divExact(@typeInfo(T).Int.bits, 8);
|
||||
const writeInt = std.mem.writeIntLittle;
|
||||
|
||||
switch (mode) {
|
||||
// .debug => log.debug("wrote 0x{X:}{} to 0x{X:0>8}", .{ value, T, address }),
|
||||
.debug => {},
|
||||
else => self.scheduler.tick += 1,
|
||||
}
|
||||
|
||||
switch (address) {
|
||||
0x0200_0000...0x02FF_FFFF => writeInt(T, self.main[address & 0x003F_FFFF ..][0..byte_count], value),
|
||||
0x0380_0000...0x0380_FFFF => writeInt(T, self.wram[address & 0x0000_FFFF ..][0..byte_count], value),
|
||||
0x0400_0000...0x04FF_FFFF => io.write(self, T, address, value),
|
||||
else => log.warn("unexpected write: 0x{X:}{} -> 0x{X:0>8}", .{ value, T, address }),
|
||||
}
|
||||
}
|
||||
|
||||
fn warn(comptime format: []const u8, args: anytype) u0 {
|
||||
log.warn(format, args);
|
||||
return 0;
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
const std = @import("std");
|
||||
|
||||
const Bus = @import("Bus.zig");
|
||||
|
||||
const PriorityQueue = std.PriorityQueue(Event, void, Event.lessThan);
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
tick: u64 = 0,
|
||||
queue: PriorityQueue,
|
||||
|
||||
pub fn init(allocator: Allocator) !@This() {
|
||||
var queue = PriorityQueue.init(allocator, {});
|
||||
try queue.add(.{ .tick = std.math.maxInt(u64), .kind = .heat_death });
|
||||
|
||||
return .{ .queue = queue };
|
||||
}
|
||||
|
||||
pub fn push(self: *@This(), kind: Event.Kind, offset: u64) void {
|
||||
self.queue.add(.{ .kind = kind, .tick = self.tick + offset }) catch unreachable;
|
||||
}
|
||||
|
||||
pub fn deinit(self: @This()) void {
|
||||
self.queue.deinit();
|
||||
}
|
||||
|
||||
pub fn now(self: @This()) u64 {
|
||||
return self.tick;
|
||||
}
|
||||
|
||||
pub fn next(self: @This()) u64 {
|
||||
@setRuntimeSafety(false);
|
||||
return self.queue.items[0].tick;
|
||||
}
|
||||
|
||||
pub fn reset(self: *@This()) void {
|
||||
self.tick = 0;
|
||||
}
|
||||
|
||||
pub fn handle(self: *@This(), bus: *Bus) void {
|
||||
_ = bus;
|
||||
const event = self.queue.remove();
|
||||
const late = self.tick - event.tick;
|
||||
_ = late;
|
||||
|
||||
switch (event.kind) {
|
||||
.heat_death => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
pub const Event = struct {
|
||||
tick: u64,
|
||||
kind: Kind,
|
||||
|
||||
pub const Kind = enum { heat_death };
|
||||
|
||||
fn lessThan(_: void, left: @This(), right: @This()) std.math.Order {
|
||||
return std.math.order(left.tick, right.tick);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,71 @@
|
|||
const std = @import("std");
|
||||
|
||||
const Bitfield = @import("bitfield").Bitfield;
|
||||
const Bit = @import("bitfield").Bit;
|
||||
|
||||
const Bus = @import("Bus.zig");
|
||||
const SharedIo = @import("../io.zig").Io;
|
||||
const writeToAddressOffset = @import("../io.zig").writeToAddressOffset;
|
||||
const valueAtAddressOffset = @import("../io.zig").valueAtAddressOffset;
|
||||
|
||||
const log = std.log.scoped(.nds7_io);
|
||||
|
||||
pub const Io = struct {
|
||||
shared: *SharedIo,
|
||||
|
||||
pub fn init(io: *SharedIo) @This() {
|
||||
return .{ .shared = io };
|
||||
}
|
||||
};
|
||||
|
||||
pub fn read(bus: *const Bus, comptime T: type, address: u32) T {
|
||||
return switch (T) {
|
||||
// zig fmt: off
|
||||
u32 =>
|
||||
@as(T, read(bus, u8, address + 3)) << 24
|
||||
| @as(T, read(bus, u8, address + 2)) << 16
|
||||
| @as(T, read(bus, u8, address + 1)) << 8
|
||||
| read(bus, u8, address + 0) << 0,
|
||||
// zig fmt: on
|
||||
u16 => @as(T, read(bus, u8, address + 1)) << 8 | read(bus, u8, address),
|
||||
u8 => switch (address) {
|
||||
0x0400_0180...0x0400_0183 => valueAtAddressOffset(u32, address, bus.io.shared.ipc_sync.raw),
|
||||
0x0400_0184...0x0400_0187 => valueAtAddressOffset(u32, address, bus.io.shared.ipc_fifo_cnt.raw),
|
||||
|
||||
0x0400_0208...0x0400_020B => valueAtAddressOffset(u32, address, @intFromBool(bus.io.shared.ime)),
|
||||
|
||||
else => warn("unexpected read: 0x{X:0>8}", .{address}),
|
||||
},
|
||||
else => @compileError(T ++ " is an unsupported bus read type"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
|
||||
switch (T) {
|
||||
u32 => {
|
||||
write(bus, u8, address + 3, @as(u8, @truncate(value >> 24)));
|
||||
write(bus, u8, address + 2, @as(u8, @truncate(value >> 16)));
|
||||
write(bus, u8, address + 1, @as(u8, @truncate(value >> 8)));
|
||||
write(bus, u8, address + 0, @as(u8, @truncate(value >> 0)));
|
||||
},
|
||||
u16 => {
|
||||
write(bus, u8, address + 1, @as(u8, @truncate(value >> 8)));
|
||||
write(bus, u8, address + 0, @as(u8, @truncate(value >> 0)));
|
||||
},
|
||||
u8 => switch (address) {
|
||||
0x0400_0180...0x0400_0183 => writeToAddressOffset(&bus.io.shared.ipc_sync.raw, address, value),
|
||||
0x0400_0184...0x0400_0187 => writeToAddressOffset(&bus.io.shared.ipc_fifo_cnt.raw, address, value),
|
||||
|
||||
0x0400_0208 => bus.io.shared.ime = value & 1 == 1,
|
||||
0x0400_0209...0x0400_020B => {}, // unused bytes from IME
|
||||
|
||||
else => log.warn("unexpected write: 0x{X:}u8 -> 0x{X:0>8}", .{ value, address }),
|
||||
},
|
||||
else => @compileError(T ++ " is an unsupported bus write type"),
|
||||
}
|
||||
}
|
||||
|
||||
fn warn(comptime format: []const u8, args: anytype) u0 {
|
||||
log.warn(format, args);
|
||||
return 0;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub const Bus = @import("nds9/Bus.zig");
|
||||
pub const io = @import("nds9/io.zig");
|
||||
pub const Scheduler = @import("nds9/Scheduler.zig");
|
||||
pub const Arm946es = @import("arm32").Arm946es;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
// TODO: Rename
|
||||
pub const Group = struct {
|
||||
cpu: *Arm946es,
|
||||
bus: *Bus,
|
||||
scheduler: *Scheduler,
|
||||
|
||||
/// Responsible for deallocating the ARM9 CPU, Bus and Scheduler
|
||||
pub fn deinit(self: @This(), allocator: Allocator) void {
|
||||
self.bus.deinit(allocator);
|
||||
self.scheduler.deinit();
|
||||
}
|
||||
};
|
|
@ -0,0 +1,111 @@
|
|||
const std = @import("std");
|
||||
|
||||
const Ppu = @import("../ppu.zig").Ppu;
|
||||
const Scheduler = @import("Scheduler.zig");
|
||||
const SharedIo = @import("../io.zig").Io;
|
||||
const io = @import("io.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Mode = enum { normal, debug };
|
||||
const MiB = 0x100000;
|
||||
const KiB = 0x400;
|
||||
|
||||
const log = std.log.scoped(.nds9_bus);
|
||||
|
||||
main: *[4 * MiB]u8,
|
||||
vram1: *[512 * KiB]u8, // TODO: Rename
|
||||
io: io.Io,
|
||||
ppu: Ppu,
|
||||
|
||||
scheduler: *Scheduler,
|
||||
|
||||
pub fn init(allocator: Allocator, scheduler: *Scheduler, shared_io: *SharedIo) !@This() {
|
||||
const main_mem = try allocator.create([4 * MiB]u8);
|
||||
errdefer allocator.destroy(main_mem);
|
||||
@memset(main_mem, 0);
|
||||
|
||||
const vram1_mem = try allocator.create([512 * KiB]u8);
|
||||
errdefer allocator.destroy(vram1_mem);
|
||||
@memset(vram1_mem, 0);
|
||||
|
||||
const dots_per_cycle = 3; // ARM946E-S runs twice as fast as the ARM7TDMI
|
||||
scheduler.push(.draw, 256 * dots_per_cycle);
|
||||
|
||||
return .{
|
||||
.main = main_mem,
|
||||
.vram1 = vram1_mem,
|
||||
.ppu = try Ppu.init(allocator),
|
||||
.scheduler = scheduler,
|
||||
.io = io.Io.init(shared_io),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *@This(), allocator: Allocator) void {
|
||||
self.ppu.deinit(allocator);
|
||||
|
||||
allocator.destroy(self.main);
|
||||
allocator.destroy(self.vram1);
|
||||
}
|
||||
|
||||
pub fn reset(self: *@This()) void {
|
||||
@memset(self.main, 0);
|
||||
@memset(self.vram1, 0);
|
||||
}
|
||||
|
||||
pub fn read(self: *@This(), comptime T: type, address: u32) T {
|
||||
return self._read(T, .normal, address);
|
||||
}
|
||||
|
||||
pub fn dbgRead(self: *@This(), comptime T: type, address: u32) T {
|
||||
return self._read(T, .debug, address);
|
||||
}
|
||||
|
||||
fn _read(self: *@This(), comptime T: type, comptime mode: Mode, address: u32) T {
|
||||
const byte_count = @divExact(@typeInfo(T).Int.bits, 8);
|
||||
const readInt = std.mem.readIntLittle;
|
||||
|
||||
switch (mode) {
|
||||
// .debug => log.debug("read {} from 0x{X:0>8}", .{ T, address }),
|
||||
.debug => {},
|
||||
else => self.scheduler.tick += 1,
|
||||
}
|
||||
|
||||
return switch (address) {
|
||||
0x0200_0000...0x02FF_FFFF => readInt(T, self.main[address & 0x003F_FFFF ..][0..byte_count]),
|
||||
0x0400_0000...0x04FF_FFFF => io.read(self, T, address),
|
||||
0x0600_0000...0x06FF_FFFF => readInt(T, self.vram1[address & 0x0007_FFFF ..][0..byte_count]),
|
||||
else => warn("unexpected read: 0x{x:0>8} -> {}", .{ address, T }),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write(self: *@This(), comptime T: type, address: u32, value: T) void {
|
||||
return self._write(T, .normal, address, value);
|
||||
}
|
||||
|
||||
pub fn dbgWrite(self: *@This(), comptime T: type, address: u32, value: T) void {
|
||||
return self._write(T, .debug, address, value);
|
||||
}
|
||||
|
||||
fn _write(self: *@This(), comptime T: type, comptime mode: Mode, address: u32, value: T) void {
|
||||
const byte_count = @divExact(@typeInfo(T).Int.bits, 8);
|
||||
const writeInt = std.mem.writeIntLittle;
|
||||
|
||||
switch (mode) {
|
||||
// .debug => log.debug("wrote 0x{X:}{} to 0x{X:0>8}", .{ value, T, address }),
|
||||
.debug => {},
|
||||
else => self.scheduler.tick += 1,
|
||||
}
|
||||
|
||||
switch (address) {
|
||||
0x0200_0000...0x02FF_FFFF => writeInt(T, self.main[address & 0x003F_FFFF ..][0..byte_count], value),
|
||||
0x0400_0000...0x04FF_FFFF => io.write(self, T, address, value),
|
||||
0x0600_0000...0x06FF_FFFF => writeInt(T, self.vram1[address & 0x0007_FFFF ..][0..byte_count], value),
|
||||
else => log.warn("unexpected write: 0x{X:}{} -> 0x{X:0>8}", .{ value, T, address }),
|
||||
}
|
||||
}
|
||||
|
||||
fn warn(comptime format: []const u8, args: anytype) u0 {
|
||||
log.warn(format, args);
|
||||
return 0;
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
const std = @import("std");
|
||||
|
||||
const Bus = @import("Bus.zig");
|
||||
|
||||
const PriorityQueue = std.PriorityQueue(Event, void, Event.lessThan);
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
tick: u64 = 0,
|
||||
queue: PriorityQueue,
|
||||
|
||||
pub fn init(allocator: Allocator) !@This() {
|
||||
var queue = PriorityQueue.init(allocator, {});
|
||||
try queue.add(.{ .tick = std.math.maxInt(u64), .kind = .heat_death });
|
||||
|
||||
return .{ .queue = queue };
|
||||
}
|
||||
|
||||
pub fn push(self: *@This(), kind: Event.Kind, offset: u64) void {
|
||||
self.queue.add(.{ .kind = kind, .tick = self.tick + offset }) catch unreachable;
|
||||
}
|
||||
|
||||
pub fn deinit(self: @This()) void {
|
||||
self.queue.deinit();
|
||||
}
|
||||
|
||||
pub fn now(self: @This()) u64 {
|
||||
return self.tick;
|
||||
}
|
||||
|
||||
pub fn next(self: @This()) u64 {
|
||||
@setRuntimeSafety(false);
|
||||
|
||||
return self.queue.items[0].tick;
|
||||
}
|
||||
|
||||
pub fn reset(self: *@This()) void {
|
||||
self.tick = 0;
|
||||
}
|
||||
|
||||
pub fn handle(self: *@This(), bus: *Bus) void {
|
||||
const event = self.queue.remove();
|
||||
const late = self.tick - event.tick;
|
||||
|
||||
switch (event.kind) {
|
||||
.heat_death => unreachable,
|
||||
.draw => {
|
||||
bus.ppu.drawScanline(bus);
|
||||
bus.ppu.onHdrawEnd(self, late);
|
||||
},
|
||||
.hblank => bus.ppu.onHblankEnd(self, late),
|
||||
.vblank => bus.ppu.onHblankEnd(self, late),
|
||||
}
|
||||
}
|
||||
|
||||
pub const Event = struct {
|
||||
tick: u64,
|
||||
kind: Kind,
|
||||
|
||||
pub const Kind = enum {
|
||||
heat_death,
|
||||
draw,
|
||||
hblank,
|
||||
vblank,
|
||||
};
|
||||
|
||||
fn lessThan(_: void, left: @This(), right: @This()) std.math.Order {
|
||||
return std.math.order(left.tick, right.tick);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,217 @@
|
|||
const std = @import("std");
|
||||
|
||||
const Bitfield = @import("bitfield").Bitfield;
|
||||
const Bit = @import("bitfield").Bit;
|
||||
|
||||
const Bus = @import("Bus.zig");
|
||||
const SharedIo = @import("../io.zig").Io;
|
||||
const writeToAddressOffset = @import("../io.zig").writeToAddressOffset;
|
||||
const valueAtAddressOffset = @import("../io.zig").valueAtAddressOffset;
|
||||
|
||||
const log = std.log.scoped(.nds9_io);
|
||||
|
||||
pub const Io = struct {
|
||||
shared: *SharedIo,
|
||||
|
||||
/// POWCNT1 - Graphics Power Control
|
||||
/// Read / Write
|
||||
powcnt: PowCnt = .{ .raw = 0x0000_0000 },
|
||||
|
||||
// Read Only
|
||||
keyinput: AtomicKeyInput = .{},
|
||||
|
||||
pub fn init(io: *SharedIo) @This() {
|
||||
return .{ .shared = io };
|
||||
}
|
||||
};
|
||||
|
||||
pub fn read(bus: *const Bus, comptime T: type, address: u32) T {
|
||||
return switch (T) {
|
||||
// zig fmt: off
|
||||
u32 =>
|
||||
@as(T, read(bus, u8, address + 3)) << 24
|
||||
| @as(T, read(bus, u8, address + 2)) << 16
|
||||
| @as(T, read(bus, u8, address + 1)) << 8
|
||||
| read(bus, u8, address + 0) << 0,
|
||||
// zig fmt: on
|
||||
u16 => @as(T, read(bus, u8, address + 1)) << 8 | read(bus, u8, address),
|
||||
u8 => switch (address) {
|
||||
0x0400_0000...0x0400_0003 => valueAtAddressOffset(u32, address, bus.ppu.io.dispcnt_a.raw),
|
||||
0x0400_0004...0x0400_0005 => valueAtAddressOffset(u16, address, bus.ppu.io.dispstat.raw),
|
||||
|
||||
0x0400_0130...0x0400_0131 => valueAtAddressOffset(u16, address, bus.io.keyinput.load(.Monotonic)),
|
||||
0x0400_0180...0x0400_0183 => valueAtAddressOffset(u32, address, bus.io.shared.ipc_sync.raw),
|
||||
0x0400_0184...0x0400_0187 => valueAtAddressOffset(u32, address, bus.io.shared.ipc_fifo_cnt.raw),
|
||||
|
||||
0x0400_0208...0x0400_020B => valueAtAddressOffset(u32, address, @intFromBool(bus.io.shared.ime)),
|
||||
|
||||
0x0400_0304...0x0400_0307 => valueAtAddressOffset(u32, address, bus.io.powcnt.raw),
|
||||
else => warn("unexpected read: 0x{X:0>8}", .{address}),
|
||||
},
|
||||
else => @compileError(T ++ " is an unsupported bus read type"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
|
||||
switch (T) {
|
||||
u32 => {
|
||||
write(bus, u8, address + 3, @as(u8, @truncate(value >> 24)));
|
||||
write(bus, u8, address + 2, @as(u8, @truncate(value >> 16)));
|
||||
write(bus, u8, address + 1, @as(u8, @truncate(value >> 8)));
|
||||
write(bus, u8, address + 0, @as(u8, @truncate(value >> 0)));
|
||||
},
|
||||
u16 => {
|
||||
write(bus, u8, address + 1, @as(u8, @truncate(value >> 8)));
|
||||
write(bus, u8, address + 0, @as(u8, @truncate(value >> 0)));
|
||||
},
|
||||
u8 => switch (address) {
|
||||
0x0400_0000...0x0400_0003 => writeToAddressOffset(&bus.ppu.io.dispcnt_a.raw, address, value),
|
||||
|
||||
0x0400_0180...0x0400_0183 => writeToAddressOffset(&bus.io.shared.ipc_sync.raw, address, value),
|
||||
0x0400_0184...0x0400_0187 => writeToAddressOffset(&bus.io.shared.ipc_fifo_cnt.raw, address, value),
|
||||
|
||||
0x0400_0208 => bus.io.shared.ime = value & 1 == 1,
|
||||
0x0400_0209...0x0400_020B => {}, // unused bytes from IME
|
||||
|
||||
0x0400_0240 => bus.ppu.io.vramcnt_a.raw = value,
|
||||
0x0400_0241 => bus.ppu.io.vramcnt_b.raw = value,
|
||||
0x0400_0242 => bus.ppu.io.vramcnt_c.raw = value,
|
||||
0x0400_0243 => bus.ppu.io.vramcnt_d.raw = value,
|
||||
|
||||
0x0400_0304...0x0400_0307 => writeToAddressOffset(&bus.io.powcnt.raw, address, value),
|
||||
else => log.warn("unexpected write: 0x{X:}u8 -> 0x{X:0>8}", .{ value, address }),
|
||||
},
|
||||
else => @compileError(T ++ " is an unsupported bus write type"),
|
||||
}
|
||||
}
|
||||
|
||||
fn warn(comptime format: []const u8, args: anytype) u0 {
|
||||
log.warn(format, args);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const PowCnt = extern union {
|
||||
// Enable flag for both LCDs
|
||||
lcd: Bit(u32, 0),
|
||||
gfx_2da: Bit(u32, 1),
|
||||
render_3d: Bit(u32, 2),
|
||||
geometry_3d: Bit(u32, 3),
|
||||
gfx_2db: Bit(u32, 9),
|
||||
display_swap: Bit(u32, 15),
|
||||
raw: u32,
|
||||
};
|
||||
|
||||
pub const DispcntA = extern union {
|
||||
bg_mode: Bitfield(u32, 0, 2),
|
||||
|
||||
/// toggle between 2D and 3D for BG0
|
||||
bg0_dimension: Bit(u32, 3),
|
||||
tile_obj_mapping: Bit(u32, 4),
|
||||
bitmap_obj_2d_dimension: Bit(u32, 5),
|
||||
bitmap_obj_mapping: Bit(u32, 6),
|
||||
forced_blank: Bit(u32, 7),
|
||||
bg_enable: Bitfield(u32, 8, 4),
|
||||
obj_enable: Bit(u32, 12),
|
||||
win_enable: Bitfield(u32, 13, 2),
|
||||
obj_win_enable: Bit(u32, 15),
|
||||
display_mode: Bitfield(u32, 16, 2),
|
||||
vram_block: Bitfield(u32, 18, 2),
|
||||
tile_obj_1d_boundary: Bitfield(u32, 20, 2),
|
||||
bitmap_obj_1d_boundary: Bit(u32, 22),
|
||||
obj_during_hblank: Bit(u32, 23),
|
||||
character_base: Bitfield(u32, 24, 3),
|
||||
screen_base: Bitfield(u32, 27, 2),
|
||||
bg_ext_pal_enable: Bit(u32, 30),
|
||||
obj_ext_pal_enable: Bit(u32, 31),
|
||||
raw: u32,
|
||||
};
|
||||
|
||||
pub const Vramcnt = struct {
|
||||
/// Can be used by VRAM-A and VRAM-B
|
||||
pub const A = extern union {
|
||||
mst: Bitfield(u8, 0, 2),
|
||||
offset: Bitfield(u8, 3, 2),
|
||||
enable: Bit(u8, 7),
|
||||
raw: u8,
|
||||
};
|
||||
|
||||
/// Can be used by VRAM-C, VRAM-D, VRAM-F, VRAM-G
|
||||
pub const C = extern union {
|
||||
mst: Bitfield(u8, 0, 3),
|
||||
offset: Bitfield(u8, 3, 2),
|
||||
enable: Bit(u8, 7),
|
||||
raw: u8,
|
||||
};
|
||||
|
||||
/// Can be used by VRAM-E
|
||||
pub const E = extern union {
|
||||
mst: Bitfield(u8, 0, 3),
|
||||
enable: Bit(u8, 7),
|
||||
raw: u8,
|
||||
};
|
||||
|
||||
/// can be used by VRAM-H and VRAM-I
|
||||
pub const H = extern union {
|
||||
mst: Bitfield(u8, 0, 2),
|
||||
enable: Bit(u8, 7),
|
||||
raw: u8,
|
||||
};
|
||||
};
|
||||
|
||||
// Compared to the GBA:
|
||||
// - LY/LYC values are now 9-bits
|
||||
pub const Vcount = extern union {
|
||||
scanline: Bitfield(u16, 0, 9),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
pub const Dispstat = extern union {
|
||||
vblank: Bit(u16, 0),
|
||||
hblank: Bit(u16, 1),
|
||||
coincidence: Bit(u16, 2),
|
||||
vblank_irq: Bit(u16, 3),
|
||||
hblank_irq: Bit(u16, 4),
|
||||
vcount_irq: Bit(u16, 5),
|
||||
|
||||
/// FIXME: confirm that I'm reading DISPSTAT.7 correctly into LYC
|
||||
lyc: Bitfield(u16, 7, 9),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
/// Read Only
|
||||
/// 0 = Pressed, 1 = Released
|
||||
pub const KeyInput = extern union {
|
||||
a: Bit(u16, 0),
|
||||
b: Bit(u16, 1),
|
||||
select: Bit(u16, 2),
|
||||
start: Bit(u16, 3),
|
||||
right: Bit(u16, 4),
|
||||
left: Bit(u16, 5),
|
||||
up: Bit(u16, 6),
|
||||
down: Bit(u16, 7),
|
||||
shoulder_r: Bit(u16, 8),
|
||||
shoulder_l: Bit(u16, 9),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
const AtomicKeyInput = struct {
|
||||
const Self = @This();
|
||||
const Ordering = std.atomic.Ordering;
|
||||
|
||||
inner: KeyInput = .{ .raw = 0x03FF },
|
||||
|
||||
pub inline fn load(self: *const Self, comptime ordering: Ordering) u16 {
|
||||
return switch (ordering) {
|
||||
.AcqRel, .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 {
|
||||
_ = @atomicRmw(u16, &self.inner.raw, .Or, value, ordering);
|
||||
}
|
||||
|
||||
pub inline fn fetchAnd(self: *Self, value: u16, comptime ordering: Ordering) void {
|
||||
_ = @atomicRmw(u16, &self.inner.raw, .And, value, ordering);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,178 @@
|
|||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const nds9 = @import("nds9.zig");
|
||||
|
||||
pub const screen_width = 256;
|
||||
pub const screen_height = 192;
|
||||
|
||||
const cycles_per_dot = 6;
|
||||
|
||||
pub const Ppu = struct {
|
||||
fb: FrameBuffer,
|
||||
|
||||
io: io = .{},
|
||||
|
||||
pub fn init(allocator: Allocator) !@This() {
|
||||
return .{
|
||||
.fb = try FrameBuffer.init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: @This(), allocator: Allocator) void {
|
||||
self.fb.deinit(allocator);
|
||||
}
|
||||
|
||||
pub fn drawScanline(self: *@This(), nds9_bus: *nds9.Bus) void {
|
||||
const bg_mode = self.io.dispcnt_a.display_mode.read();
|
||||
const scanline = self.io.vcount.scanline.read();
|
||||
|
||||
switch (bg_mode) {
|
||||
0x0 => {},
|
||||
0x1 => {},
|
||||
0x2 => {
|
||||
// Draw Top Screen
|
||||
{
|
||||
const buf = self.fb.top(.back);
|
||||
|
||||
const ptr: *[screen_width * screen_height]u32 = @ptrCast(@alignCast(buf.ptr));
|
||||
const scanline_ptr = ptr[screen_width * @as(u32, scanline) ..][0..screen_width];
|
||||
|
||||
const base_addr: u32 = 0x0680_0000 + (screen_width * @sizeOf(u16)) * @as(u32, scanline);
|
||||
|
||||
// FIXME: I don't think it's okay to be accessing the ARM9 Bus instead of just working with
|
||||
// memory directly. However, I do understand that VRAM-A, VRAM-B might change things
|
||||
|
||||
for (scanline_ptr, 0..) |*rgba, i| {
|
||||
const addr = base_addr + @as(u32, @intCast(i)) * @sizeOf(u16);
|
||||
rgba.* = rgba888(nds9_bus.dbgRead(u16, addr));
|
||||
}
|
||||
}
|
||||
},
|
||||
0x3 => {},
|
||||
}
|
||||
}
|
||||
|
||||
/// HDraw -> HBlank
|
||||
pub fn onHdrawEnd(self: *@This(), nds9_scheduler: *nds9.Scheduler, late: u64) void {
|
||||
const dots_in_hblank = 99;
|
||||
std.debug.assert(self.io.dispstat.hblank.read() == false);
|
||||
std.debug.assert(self.io.dispstat.vblank.read() == false);
|
||||
|
||||
// TODO: Signal HBlank IRQ
|
||||
|
||||
self.io.dispstat.hblank.set();
|
||||
nds9_scheduler.push(.hblank, dots_in_hblank * cycles_per_dot -| late);
|
||||
}
|
||||
|
||||
pub fn onHblankEnd(self: *@This(), nds9_scheduler: *nds9.Scheduler, late: u64) void {
|
||||
const scanline_count = 192 + 71;
|
||||
|
||||
const prev_scanline = self.io.vcount.scanline.read();
|
||||
const scanline = (prev_scanline + 1) % scanline_count;
|
||||
|
||||
self.io.vcount.scanline.write(scanline);
|
||||
self.io.dispstat.hblank.unset();
|
||||
|
||||
const coincidence = scanline == self.io.dispstat.lyc.read();
|
||||
self.io.dispstat.coincidence.write(coincidence);
|
||||
|
||||
// TODO: LYC == LY IRQ
|
||||
|
||||
if (scanline < 192) {
|
||||
std.debug.assert(self.io.dispstat.vblank.read() == false);
|
||||
std.debug.assert(self.io.dispstat.hblank.read() == false);
|
||||
|
||||
// Draw Another Scanline
|
||||
const dots_in_hdraw = 256;
|
||||
return nds9_scheduler.push(.draw, dots_in_hdraw * cycles_per_dot -| late);
|
||||
}
|
||||
|
||||
if (scanline == 192) {
|
||||
// Transition from Hblank to Vblank
|
||||
self.fb.swap();
|
||||
self.io.dispstat.vblank.set();
|
||||
|
||||
// TODO: Signal VBlank IRQ
|
||||
}
|
||||
|
||||
if (scanline == 262) self.io.dispstat.vblank.unset();
|
||||
std.debug.assert(self.io.dispstat.vblank.read() == (scanline != 262));
|
||||
|
||||
const dots_in_scanline = 256 + 99;
|
||||
nds9_scheduler.push(.hblank, dots_in_scanline * cycles_per_dot -| late);
|
||||
}
|
||||
};
|
||||
|
||||
pub const FrameBuffer = struct {
|
||||
const len = (screen_width * @sizeOf(u32)) * screen_height;
|
||||
|
||||
current: u1 = 0,
|
||||
|
||||
ptr: *[len * 4]u8,
|
||||
|
||||
const Position = enum { top, bottom };
|
||||
const Layer = enum { front, back };
|
||||
|
||||
pub fn init(allocator: Allocator) !@This() {
|
||||
const ptr = try allocator.create([len * 4]u8);
|
||||
|
||||
return .{ .ptr = ptr };
|
||||
}
|
||||
|
||||
pub fn deinit(self: @This(), allocator: Allocator) void {
|
||||
allocator.destroy(self.ptr);
|
||||
}
|
||||
|
||||
fn get(self: @This(), comptime position: Position, comptime layer: Layer) *[len]u8 {
|
||||
const toggle: usize = if (layer == .front) self.current else ~self.current;
|
||||
|
||||
return switch (position) {
|
||||
.top => self.ptr[len * toggle ..][0..len],
|
||||
.bottom => self.ptr[(len << 1) + len * toggle ..][0..len],
|
||||
};
|
||||
}
|
||||
|
||||
pub fn swap(self: *@This()) void {
|
||||
self.current = ~self.current;
|
||||
}
|
||||
|
||||
pub fn top(self: @This(), comptime layer: Layer) *[len]u8 {
|
||||
return self.get(.top, layer);
|
||||
}
|
||||
|
||||
pub fn btm(self: @This(), comptime layer: Layer) *[len]u8 {
|
||||
return self.get(.bottom, layer);
|
||||
}
|
||||
};
|
||||
|
||||
inline fn rgba888(bgr555: u16) u32 {
|
||||
const b: u32 = bgr555 >> 10 & 0x1F;
|
||||
const g: u32 = bgr555 >> 5 & 0x1F;
|
||||
const r: u32 = bgr555 & 0x1F;
|
||||
|
||||
// zig fmt: off
|
||||
return (r << 3 | r >> 2) << 24
|
||||
| (g << 3 | g >> 2) << 16
|
||||
| (b << 3 | b >> 2) << 8
|
||||
| 0xFF;
|
||||
// zig fmt: on
|
||||
}
|
||||
|
||||
const io = struct {
|
||||
const nds9_io = @import("nds9/io.zig"); // TODO: rename
|
||||
|
||||
/// Read / Write
|
||||
dispcnt_a: nds9_io.DispcntA = .{ .raw = 0x0000_0000 },
|
||||
/// Read / Write
|
||||
dispstat: nds9_io.Dispstat = .{ .raw = 0x0000 },
|
||||
|
||||
/// Read-Only
|
||||
vcount: nds9_io.Vcount = .{ .raw = 0x0000 },
|
||||
|
||||
/// Write-Only
|
||||
vramcnt_a: nds9_io.Vramcnt.A = .{ .raw = 0x00 },
|
||||
vramcnt_b: nds9_io.Vramcnt.A = .{ .raw = 0x00 },
|
||||
vramcnt_c: nds9_io.Vramcnt.C = .{ .raw = 0x00 },
|
||||
vramcnt_d: nds9_io.Vramcnt.C = .{ .raw = 0x00 },
|
||||
};
|
|
@ -0,0 +1,77 @@
|
|||
const std = @import("std");
|
||||
const clap = @import("zig-clap");
|
||||
|
||||
const nds9 = @import("core/nds9.zig");
|
||||
const nds7 = @import("core/nds7.zig");
|
||||
const emu = @import("core/emu.zig");
|
||||
|
||||
const IBus = @import("arm32").Bus;
|
||||
const IScheduler = @import("arm32").Scheduler;
|
||||
const Ui = @import("platform.zig").Ui;
|
||||
const SharedIo = @import("core/io.zig").Io;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ClapResult = clap.Result(clap.Help, &cli_params, clap.parsers.default);
|
||||
|
||||
const cli_params = clap.parseParamsComptime(
|
||||
\\-h, --help Display this help and exit.
|
||||
\\<str> Path to the NDS ROM
|
||||
\\
|
||||
);
|
||||
|
||||
pub fn main() !void {
|
||||
const log = std.log.scoped(.main);
|
||||
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer std.debug.assert(gpa.deinit() == .ok);
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
const result = try clap.parse(clap.Help, &cli_params, clap.parsers.default, .{});
|
||||
defer result.deinit();
|
||||
|
||||
const rom_path = try handlePositional(result);
|
||||
log.debug("loading rom from: {s}", .{rom_path});
|
||||
|
||||
const rom_file = try std.fs.cwd().openFile(rom_path, .{});
|
||||
defer rom_file.close();
|
||||
|
||||
// FIXME: Perf win to allocating on the stack instead?
|
||||
const shared_io = try allocator.create(SharedIo);
|
||||
defer allocator.destroy(shared_io);
|
||||
shared_io.* = .{};
|
||||
|
||||
const nds9_group: nds9.Group = blk: {
|
||||
var scheduler = try nds9.Scheduler.init(allocator);
|
||||
var bus = try nds9.Bus.init(allocator, &scheduler, shared_io);
|
||||
var arm946es = nds9.Arm946es.init(IScheduler.init(&scheduler), IBus.init(&bus));
|
||||
|
||||
break :blk .{ .cpu = &arm946es, .bus = &bus, .scheduler = &scheduler };
|
||||
};
|
||||
defer nds9_group.deinit(allocator);
|
||||
|
||||
const nds7_group: nds7.Group = blk: {
|
||||
var scheduler = try nds7.Scheduler.init(allocator);
|
||||
var bus = try nds7.Bus.init(allocator, &scheduler, shared_io);
|
||||
var arm7tdmi = nds7.Arm7tdmi.init(IScheduler.init(&scheduler), IBus.init(&bus));
|
||||
|
||||
break :blk .{ .cpu = &arm7tdmi, .bus = &bus, .scheduler = &scheduler };
|
||||
};
|
||||
defer nds7_group.deinit(allocator);
|
||||
|
||||
const rom_title = try emu.load(allocator, nds7_group, nds9_group, rom_file);
|
||||
|
||||
var ui = try Ui.init(allocator);
|
||||
defer ui.deinit(allocator);
|
||||
|
||||
ui.setTitle(rom_title);
|
||||
try ui.run(nds7_group, nds9_group);
|
||||
}
|
||||
|
||||
fn handlePositional(result: ClapResult) ![]const u8 {
|
||||
return switch (result.positionals.len) {
|
||||
0 => error.too_few_positional_arguments,
|
||||
1 => result.positionals[0],
|
||||
else => return error.too_many_positional_arguments,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,385 @@
|
|||
const std = @import("std");
|
||||
const SDL = @import("sdl2");
|
||||
const gl = @import("gl");
|
||||
const zgui = @import("zgui");
|
||||
|
||||
const nds9 = @import("core/nds9.zig");
|
||||
const nds7 = @import("core/nds7.zig");
|
||||
|
||||
const ppu = @import("core/ppu.zig");
|
||||
const imgui = @import("ui/imgui.zig");
|
||||
const emu = @import("core/emu.zig");
|
||||
|
||||
const KeyInput = @import("core/nds9/io.zig").KeyInput;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const GLuint = gl.GLuint;
|
||||
const GLsizei = gl.GLsizei;
|
||||
const SDL_GLContext = *anyopaque;
|
||||
|
||||
const nds_width = ppu.screen_width;
|
||||
const nds_height = ppu.screen_height;
|
||||
|
||||
pub const Dimensions = struct { width: u32, height: u32 };
|
||||
|
||||
const window_title = "Turbo (Name Pending)";
|
||||
|
||||
pub const Ui = struct {
|
||||
const Self = @This();
|
||||
|
||||
window: *SDL.SDL_Window,
|
||||
ctx: SDL_GLContext,
|
||||
|
||||
state: imgui.State,
|
||||
|
||||
pub fn init(allocator: Allocator) !Self {
|
||||
var state = imgui.State{};
|
||||
|
||||
if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO) < 0) panic();
|
||||
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_PROFILE_MASK, SDL.SDL_GL_CONTEXT_PROFILE_CORE) < 0) panic();
|
||||
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic();
|
||||
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic();
|
||||
|
||||
const window = SDL.SDL_CreateWindow(
|
||||
window_title,
|
||||
SDL.SDL_WINDOWPOS_CENTERED,
|
||||
SDL.SDL_WINDOWPOS_CENTERED,
|
||||
@intCast(state.dim.width),
|
||||
@intCast(state.dim.height),
|
||||
SDL.SDL_WINDOW_OPENGL | SDL.SDL_WINDOW_SHOWN | SDL.SDL_WINDOW_RESIZABLE,
|
||||
) orelse panic();
|
||||
|
||||
const ctx = SDL.SDL_GL_CreateContext(window) orelse panic();
|
||||
if (SDL.SDL_GL_MakeCurrent(window, ctx) < 0) panic();
|
||||
|
||||
gl.load(ctx, Self.glGetProcAddress) catch {};
|
||||
if (SDL.SDL_GL_SetSwapInterval(0) < 0) panic();
|
||||
|
||||
zgui.init(allocator);
|
||||
zgui.plot.init();
|
||||
zgui.backend.init(window, ctx, "#version 330 core");
|
||||
|
||||
// zgui.io.setIniFilename(null);
|
||||
|
||||
return Self{
|
||||
.window = window,
|
||||
.ctx = ctx,
|
||||
.state = state,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Self, allocator: Allocator) void {
|
||||
_ = allocator;
|
||||
// self.state.deinit(self.allocator);
|
||||
|
||||
zgui.backend.deinit();
|
||||
zgui.plot.deinit();
|
||||
zgui.deinit();
|
||||
|
||||
SDL.SDL_GL_DeleteContext(self.ctx);
|
||||
SDL.SDL_DestroyWindow(self.window);
|
||||
SDL.SDL_Quit();
|
||||
}
|
||||
|
||||
fn glGetProcAddress(ctx: SDL.SDL_GLContext, proc: [:0]const u8) ?*anyopaque {
|
||||
_ = ctx;
|
||||
return SDL.SDL_GL_GetProcAddress(proc.ptr);
|
||||
}
|
||||
|
||||
pub fn run(self: *Self, nds7_group: nds7.Group, nds9_group: nds9.Group) !void {
|
||||
// TODO: Sort this out please
|
||||
|
||||
const objects = opengl_impl.createObjects();
|
||||
defer gl.deleteBuffers(3, &[_]GLuint{ objects.vao, objects.vbo, objects.ebo });
|
||||
|
||||
const top_tex = opengl_impl.createScreenTexture(nds9_group.bus.ppu.fb.top(.front));
|
||||
const btm_tex = opengl_impl.createScreenTexture(nds9_group.bus.ppu.fb.btm(.front));
|
||||
const top_out_tex = opengl_impl.createOutputTexture();
|
||||
const btm_out_tex = opengl_impl.createOutputTexture();
|
||||
defer gl.deleteTextures(4, &[_]GLuint{ top_tex, top_out_tex, btm_tex, btm_out_tex });
|
||||
|
||||
const top_fbo = try opengl_impl.createFrameBuffer(top_out_tex);
|
||||
const btm_fbo = try opengl_impl.createFrameBuffer(btm_out_tex);
|
||||
defer gl.deleteFramebuffers(2, &[_]GLuint{ top_fbo, btm_fbo });
|
||||
|
||||
const prog_id = try opengl_impl.compileShaders();
|
||||
defer gl.deleteProgram(prog_id);
|
||||
|
||||
var event: SDL.SDL_Event = undefined;
|
||||
|
||||
emu_loop: while (true) {
|
||||
emu.runFrame(nds7_group, nds9_group);
|
||||
|
||||
while (SDL.SDL_PollEvent(&event) != 0) {
|
||||
_ = zgui.backend.processEvent(&event);
|
||||
|
||||
switch (event.type) {
|
||||
SDL.SDL_QUIT => break :emu_loop,
|
||||
SDL.SDL_WINDOWEVENT => {
|
||||
if (event.window.event == SDL.SDL_WINDOWEVENT_RESIZED) {
|
||||
std.log.debug("window resized to: {}x{}", .{ event.window.data1, event.window.data2 });
|
||||
|
||||
self.state.dim.width = @intCast(event.window.data1);
|
||||
self.state.dim.height = @intCast(event.window.data2);
|
||||
}
|
||||
},
|
||||
SDL.SDL_KEYDOWN => {
|
||||
// TODO: Make use of compare_and_xor?
|
||||
const key_code = event.key.keysym.sym;
|
||||
var keyinput: KeyInput = .{ .raw = 0x0000 };
|
||||
|
||||
switch (key_code) {
|
||||
SDL.SDLK_UP => keyinput.up.set(),
|
||||
SDL.SDLK_DOWN => keyinput.down.set(),
|
||||
SDL.SDLK_LEFT => keyinput.left.set(),
|
||||
SDL.SDLK_RIGHT => keyinput.right.set(),
|
||||
SDL.SDLK_x => keyinput.a.set(),
|
||||
SDL.SDLK_z => keyinput.b.set(),
|
||||
SDL.SDLK_a => keyinput.shoulder_l.set(),
|
||||
SDL.SDLK_s => keyinput.shoulder_r.set(),
|
||||
SDL.SDLK_RETURN => keyinput.start.set(),
|
||||
SDL.SDLK_RSHIFT => keyinput.select.set(),
|
||||
else => {},
|
||||
}
|
||||
|
||||
nds9_group.bus.io.keyinput.fetchAnd(~keyinput.raw, .Monotonic);
|
||||
},
|
||||
SDL.SDL_KEYUP => {
|
||||
// TODO: Make use of compare_and_xor?
|
||||
const key_code = event.key.keysym.sym;
|
||||
var keyinput: KeyInput = .{ .raw = 0x0000 };
|
||||
|
||||
switch (key_code) {
|
||||
SDL.SDLK_UP => keyinput.up.set(),
|
||||
SDL.SDLK_DOWN => keyinput.down.set(),
|
||||
SDL.SDLK_LEFT => keyinput.left.set(),
|
||||
SDL.SDLK_RIGHT => keyinput.right.set(),
|
||||
SDL.SDLK_x => keyinput.a.set(),
|
||||
SDL.SDLK_z => keyinput.b.set(),
|
||||
SDL.SDLK_a => keyinput.shoulder_l.set(),
|
||||
SDL.SDLK_s => keyinput.shoulder_r.set(),
|
||||
SDL.SDLK_RETURN => keyinput.start.set(),
|
||||
SDL.SDLK_RSHIFT => keyinput.select.set(),
|
||||
else => {},
|
||||
}
|
||||
|
||||
nds9_group.bus.io.keyinput.fetchOr(keyinput.raw, .Monotonic);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, top_fbo);
|
||||
defer gl.bindFramebuffer(gl.FRAMEBUFFER, 0);
|
||||
|
||||
gl.viewport(0, 0, nds_width, nds_height);
|
||||
opengl_impl.drawScreenTexture(top_tex, prog_id, objects, nds9_group.bus.ppu.fb.top(.front));
|
||||
}
|
||||
|
||||
{
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, btm_fbo);
|
||||
defer gl.bindFramebuffer(gl.FRAMEBUFFER, 0);
|
||||
|
||||
gl.viewport(0, 0, nds_width, nds_height);
|
||||
opengl_impl.drawScreenTexture(btm_tex, prog_id, objects, nds9_group.bus.ppu.fb.btm(.front));
|
||||
}
|
||||
|
||||
const zgui_redraw = imgui.draw(&self.state, top_out_tex, btm_out_tex, nds9_group.cpu);
|
||||
|
||||
if (zgui_redraw) {
|
||||
// Background Colour
|
||||
const size = zgui.io.getDisplaySize();
|
||||
gl.viewport(0, 0, @intFromFloat(size[0]), @intFromFloat(size[1]));
|
||||
gl.clearColor(0, 0, 0, 1.0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
zgui.backend.draw();
|
||||
}
|
||||
|
||||
SDL.SDL_GL_SwapWindow(self.window);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setTitle(self: *@This(), title: [12]u8) void {
|
||||
self.state.title = title ++ [_:0]u8{};
|
||||
}
|
||||
};
|
||||
|
||||
fn panic() noreturn {
|
||||
const str: [*: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
|
||||
};
|
||||
|
||||
const indices: [6]u32 = [_]u32{
|
||||
0, 1, 3, // First Triangle
|
||||
1, 2, 3, // Second Triangle
|
||||
};
|
||||
// zig fmt: on
|
||||
|
||||
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, nds_width, nds_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);
|
||||
|
||||
// Use compiled frag + vertex shader
|
||||
gl.useProgram(prog_id);
|
||||
defer gl.useProgram(0);
|
||||
|
||||
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, null);
|
||||
}
|
||||
|
||||
fn compileShaders() !GLuint {
|
||||
const vert_shader = @embedFile("shader/pixelbuf.vert");
|
||||
const frag_shader = @embedFile("shader/pixelbuf.frag");
|
||||
|
||||
const vs = gl.createShader(gl.VERTEX_SHADER);
|
||||
defer gl.deleteShader(vs);
|
||||
|
||||
gl.shaderSource(vs, 1, &[_][*c]const u8{vert_shader}, 0);
|
||||
gl.compileShader(vs);
|
||||
|
||||
if (!shader.didCompile(vs)) return error.VertexCompileError;
|
||||
|
||||
const fs = gl.createShader(gl.FRAGMENT_SHADER);
|
||||
defer gl.deleteShader(fs);
|
||||
|
||||
gl.shaderSource(fs, 1, &[_][*c]const u8{frag_shader}, 0);
|
||||
gl.compileShader(fs);
|
||||
|
||||
if (!shader.didCompile(fs)) return error.FragmentCompileError;
|
||||
|
||||
const program = gl.createProgram();
|
||||
gl.attachShader(program, vs);
|
||||
gl.attachShader(program, fs);
|
||||
gl.linkProgram(program);
|
||||
|
||||
return program;
|
||||
}
|
||||
|
||||
// Returns the VAO ID since it's used in run()
|
||||
fn createObjects() Objects {
|
||||
var vao_id: GLuint = undefined;
|
||||
var vbo_id: GLuint = undefined;
|
||||
var ebo_id: GLuint = undefined;
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
fn createScreenTexture(buf: []const u8) GLuint {
|
||||
var tex_id: GLuint = undefined;
|
||||
gl.genTextures(1, &tex_id);
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex_id);
|
||||
defer gl.bindTexture(gl.TEXTURE_2D, 0);
|
||||
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, nds_width, nds_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);
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex_id);
|
||||
defer gl.bindTexture(gl.TEXTURE_2D, 0);
|
||||
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, nds_width, nds_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);
|
||||
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo_id);
|
||||
defer gl.bindFramebuffer(gl.FRAMEBUFFER, 0);
|
||||
|
||||
gl.framebufferTexture(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, tex_id, 0);
|
||||
|
||||
const draw_buffers: [1]GLuint = .{gl.COLOR_ATTACHMENT0};
|
||||
gl.drawBuffers(1, &draw_buffers);
|
||||
|
||||
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);
|
||||
|
||||
fn didCompile(id: gl.GLuint) bool {
|
||||
var success: gl.GLint = undefined;
|
||||
gl.getShaderiv(id, gl.COMPILE_STATUS, &success);
|
||||
|
||||
if (success == 0) err(id);
|
||||
|
||||
return success == 1;
|
||||
}
|
||||
|
||||
fn err(id: gl.GLuint) void {
|
||||
const buf_len = 512;
|
||||
var error_msg: [buf_len]u8 = undefined;
|
||||
|
||||
gl.getShaderInfoLog(id, buf_len, 0, &error_msg);
|
||||
log.err("{s}", .{std.mem.sliceTo(&error_msg, 0)});
|
||||
}
|
||||
};
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
#version 330 core
|
||||
out vec4 frag_color;
|
||||
|
||||
in vec3 color;
|
||||
in vec2 uv;
|
||||
|
||||
uniform sampler2D screen;
|
||||
|
||||
void main() {
|
||||
frag_color = vec4(texture(screen, uv).rgb, 1.0);
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
#version 330 core
|
||||
layout (location = 0) in vec3 pos;
|
||||
layout (location = 1) in vec3 in_color;
|
||||
layout (location = 2) in vec2 in_uv;
|
||||
|
||||
out vec3 color;
|
||||
out vec2 uv;
|
||||
|
||||
void main() {
|
||||
color = in_color;
|
||||
uv = in_uv;
|
||||
gl_Position = vec4(pos, 1.0);
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
const std = @import("std");
|
||||
const zgui = @import("zgui");
|
||||
|
||||
const platform = @import("../platform.zig");
|
||||
|
||||
const Arm946es = @import("../core/nds9.zig").Arm946es;
|
||||
const Dimensions = platform.Dimensions;
|
||||
|
||||
const nds_height = @import("../core/ppu.zig").screen_height;
|
||||
const nds_width = @import("../core/ppu.zig").screen_width;
|
||||
|
||||
const GLuint = c_uint;
|
||||
|
||||
pub const State = struct {
|
||||
const default_rom_title: [12:0]u8 = "No Title\x00\x00\x00\x00".*;
|
||||
|
||||
title: [12:0]u8 = default_rom_title,
|
||||
dim: Dimensions = .{ .width = 1280, .height = 720 },
|
||||
};
|
||||
|
||||
pub fn draw(state: *const State, top_tex: GLuint, btm_tex: GLuint, arm946es: *Arm946es) bool {
|
||||
_ = arm946es;
|
||||
zgui.backend.newFrame(@floatFromInt(state.dim.width), @floatFromInt(state.dim.height));
|
||||
|
||||
{
|
||||
const w: f32 = @floatFromInt((nds_width * 3) / 2);
|
||||
const h: f32 = @floatFromInt((nds_height * 3) / 2);
|
||||
|
||||
const provided = std.mem.sliceTo(&state.title, 0);
|
||||
const window_title = if (provided.len == 0) &State.default_rom_title else provided;
|
||||
|
||||
_ = zgui.begin(window_title, .{ .flags = .{ .no_resize = true, .always_auto_resize = true } });
|
||||
defer zgui.end();
|
||||
|
||||
zgui.image(@ptrFromInt(top_tex), .{ .w = w, .h = h, .uv0 = .{ 0, 1 }, .uv1 = .{ 1, 0 } });
|
||||
zgui.image(@ptrFromInt(btm_tex), .{ .w = w, .h = h, .uv0 = .{ 0, 1 }, .uv1 = .{ 1, 0 } });
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
Loading…
Reference in New Issue