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:
89
src/core/cartridge.zig
Normal file
89
src/core/cartridge.zig
Normal file
@@ -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);
|
||||
}
|
||||
};
|
109
src/core/emu.zig
Normal file
109
src/core/emu.zig
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
189
src/core/io.zig
Normal file
189
src/core/io.zig
Normal file
@@ -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 };
|
||||
};
|
21
src/core/nds7.zig
Normal file
21
src/core/nds7.zig
Normal file
@@ -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();
|
||||
}
|
||||
};
|
100
src/core/nds7/Bus.zig
Normal file
100
src/core/nds7/Bus.zig
Normal file
@@ -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;
|
||||
}
|
59
src/core/nds7/Scheduler.zig
Normal file
59
src/core/nds7/Scheduler.zig
Normal file
@@ -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);
|
||||
}
|
||||
};
|
71
src/core/nds7/io.zig
Normal file
71
src/core/nds7/io.zig
Normal file
@@ -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;
|
||||
}
|
21
src/core/nds9.zig
Normal file
21
src/core/nds9.zig
Normal file
@@ -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();
|
||||
}
|
||||
};
|
111
src/core/nds9/Bus.zig
Normal file
111
src/core/nds9/Bus.zig
Normal file
@@ -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;
|
||||
}
|
69
src/core/nds9/Scheduler.zig
Normal file
69
src/core/nds9/Scheduler.zig
Normal file
@@ -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);
|
||||
}
|
||||
};
|
217
src/core/nds9/io.zig
Normal file
217
src/core/nds9/io.zig
Normal file
@@ -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);
|
||||
}
|
||||
};
|
178
src/core/ppu.zig
Normal file
178
src/core/ppu.zig
Normal file
@@ -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 },
|
||||
};
|
Reference in New Issue
Block a user