diff --git a/src/core/Scheduler.zig b/src/core/Scheduler.zig index 197b68b..6282e07 100644 --- a/src/core/Scheduler.zig +++ b/src/core/Scheduler.zig @@ -66,7 +66,7 @@ pub fn handle(self: *@This(), bus_ptr: ?*anyopaque, event: Event, late: u64) voi bus.ppu.onHdrawEnd(self, late); }, .hblank => bus.ppu.onHblankEnd(self, late), - .vblank => bus.ppu.onHblankEnd(self, late), + .vblank => bus.ppu.onVblankEnd(self, late), .sqrt => bus.io.sqrt.onSqrtCalc(), .div => bus.io.div.onDivCalc(), } diff --git a/src/core/emu.zig b/src/core/emu.zig index 0f321c2..c3a9b45 100644 --- a/src/core/emu.zig +++ b/src/core/emu.zig @@ -132,7 +132,7 @@ pub const SharedCtx = struct { const KiB = 0x400; pub const Io = @import("io.zig").Io; - const Vram = @import("ppu.zig").Vram; + const Vram = @import("ppu/Vram.zig"); io: *Io, main: *[4 * MiB]u8, diff --git a/src/core/nds7/Bus.zig b/src/core/nds7/Bus.zig index 859ad8d..f666843 100644 --- a/src/core/nds7/Bus.zig +++ b/src/core/nds7/Bus.zig @@ -4,7 +4,7 @@ const io = @import("io.zig"); const Scheduler = @import("../Scheduler.zig"); const SharedCtx = @import("../emu.zig").SharedCtx; const Wram = @import("../emu.zig").Wram; -const Vram = @import("../ppu.zig").Vram; +const Vram = @import("../ppu/Vram.zig"); const Bios = @import("Bios.zig"); const forceAlign = @import("../emu.zig").forceAlign; diff --git a/src/core/nds9/Cp15.zig b/src/core/nds9/Cp15.zig index 42ee24a..194ebf4 100644 --- a/src/core/nds9/Cp15.zig +++ b/src/core/nds9/Cp15.zig @@ -2,7 +2,7 @@ const std = @import("std"); const log = std.log.scoped(.cp15); -const panic_on_unimplemented: bool = true; +const panic_on_unimplemented: bool = false; control: u32 = 0x0005_2078, dtcm_size_base: u32 = 0x0300_000A, diff --git a/src/core/nds9/io.zig b/src/core/nds9/io.zig index d725a02..bd0dc63 100644 --- a/src/core/nds9/io.zig +++ b/src/core/nds9/io.zig @@ -34,10 +34,6 @@ pub const Io = struct { /// Caller must cast the `u32` to either `nds7.IntRequest` or `nds9.IntRequest` irq: IntRequest = .{ .raw = 0x0000_0000 }, - /// POWCNT1 - Graphics Power Control - /// Read / Write - powcnt: PowCnt = .{ .raw = 0x0000_0000 }, - // Read Only keyinput: AtomicKeyInput = .{}, @@ -82,7 +78,7 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) T { // Timers 0x0400_0100...0x0400_010E => warn("TODO: impl timer", .{}), - 0x0400_0004 => bus.ppu.io.dispstat.raw, + 0x0400_0004 => bus.ppu.engines[0].dispstat.raw, 0x0400_0130 => bus.io.keyinput.load(.Monotonic), 0x0400_0180 => @truncate(bus.io.shr.ipc._nds9.sync.raw), @@ -120,7 +116,7 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void { // Timers 0x0400_0100...0x0400_010C => log.warn("TODO: impl timer", .{}), - 0x0400_0000 => bus.ppu.io.dispcnt_a.raw = value, + 0x0400_0000 => bus.ppu.engines[0].dispcnt.raw = value, 0x0400_0180 => bus.io.shr.ipc.setIpcSync(.nds9, value), 0x0400_0184 => bus.io.shr.ipc.setIpcFifoCnt(.nds9, value), 0x0400_0188 => bus.io.shr.ipc.send(.nds9, value), @@ -151,7 +147,7 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void { bus.io.sqrt.schedule(bus.scheduler); }, - 0x0400_0304 => bus.io.powcnt.raw = value, + 0x0400_0304 => bus.ppu.io.powcnt.raw = value, else => log.warn("unexpected: write(T: {}, addr: 0x{X:0>8}, value: 0x{X:0>8})", .{ T, address, value }), }, @@ -177,7 +173,7 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void { bus.io.sqrt.schedule(bus.scheduler); }, - 0x0400_0304 => bus.io.powcnt.raw = value, + 0x0400_0304 => bus.ppu.io.powcnt.raw = value, else => log.warn("unexpected: write(T: {}, addr: 0x{X:0>8}, value: 0x{X:0>4})", .{ T, address, value }), }, @@ -241,7 +237,7 @@ fn warn(comptime format: []const u8, args: anytype) u0 { return 0; } -const PowCnt = extern union { +pub const PowCnt = extern union { // Enable flag for both LCDs lcd: Bit(u32, 0), engine2d_a: Bit(u32, 1), @@ -382,8 +378,7 @@ const SquareRootUnit = struct { }; pub const DispcntA = extern union { - bg_mode: Bitfield(u32, 0, 2), - + bg_mode: Bitfield(u32, 0, 3), /// toggle between 2D and 3D for BG0 bg0_dimension: Bit(u32, 3), tile_obj_mapping: Bit(u32, 4), @@ -400,7 +395,25 @@ pub const DispcntA = extern union { 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), + screen_base: Bitfield(u32, 27, 3), + bg_ext_pal_enable: Bit(u32, 30), + obj_ext_pal_enable: Bit(u32, 31), + raw: u32, +}; + +pub const DispcntB = extern union { + bg_mode: Bitfield(u32, 0, 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), + tile_obj_1d_boundary: Bitfield(u32, 20, 2), + obj_during_hblank: Bit(u32, 23), bg_ext_pal_enable: Bit(u32, 30), obj_ext_pal_enable: Bit(u32, 31), raw: u32, diff --git a/src/core/ppu.zig b/src/core/ppu.zig index 59855f6..b50c10a 100644 --- a/src/core/ppu.zig +++ b/src/core/ppu.zig @@ -4,6 +4,10 @@ const Allocator = std.mem.Allocator; const Scheduler = @import("Scheduler.zig"); const System = @import("emu.zig").System; +const Vram = @import("ppu/Vram.zig"); +const EngineA = @import("ppu/engine.zig").EngineA; +const EngineB = @import("ppu/engine.zig").EngineB; + pub const screen_width = 256; pub const screen_height = 192; const KiB = 0x400; @@ -15,18 +19,14 @@ pub const Ppu = struct { vram: *Vram, + engines: struct { EngineA, EngineB } = .{ .{}, .{} }, + io: Io = .{}, const Io = struct { - const nds9 = @import("nds9/io.zig"); + const types = @import("nds9/io.zig"); - /// Read / Write - dispcnt_a: nds9.DispcntA = .{ .raw = 0x0000_0000 }, - /// Read / Write - dispstat: nds9.Dispstat = .{ .raw = 0x0000 }, - - /// Read-Only - vcount: nds9.Vcount = .{ .raw = 0x0000 }, + powcnt: types.PowCnt = .{ .raw = 0x0000_0000 }, }; pub fn init(allocator: Allocator, vram: *Vram) !@This() { @@ -41,64 +41,69 @@ pub const Ppu = struct { } pub fn drawScanline(self: *@This(), bus: *System.Bus9) void { - const bg_mode = self.io.dispcnt_a.display_mode.read(); - const scanline = self.io.vcount.scanline.read(); + if (self.io.powcnt.engine2d_a.read()) + self.engines[0].drawScanline(bus, &self.fb, &self.io.powcnt); - 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(bus.dbgRead(u16, addr)); - } - } - }, - 0x3 => {}, - } + if (self.io.powcnt.engine2d_b.read()) + self.engines[1].drawScanline(bus, &self.fb, &self.io.powcnt); } /// HDraw -> HBlank pub fn onHdrawEnd(self: *@This(), scheduler: *Scheduler, late: u64) void { + inline for (&self.engines) |*engine| { + std.debug.assert(engine.dispstat.hblank.read() == false); + std.debug.assert(engine.dispstat.vblank.read() == false); + + // TODO: Signal HBlank IRQ + + engine.dispstat.hblank.set(); + } + 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(); scheduler.push(.{ .nds9 = .hblank }, dots_in_hblank * cycles_per_dot -| late); } + // VBlank -> HBlank (Still VBlank) + pub fn onVblankEnd(self: *@This(), scheduler: *Scheduler, late: u64) void { + inline for (&self.engines) |*engine| { + std.debug.assert(!engine.dispstat.hblank.read()); + std.debug.assert(engine.vcount.scanline.read() == 262 or engine.dispstat.vblank.read()); + + // TODO: Signal HBlank IRQ + + engine.dispstat.hblank.set(); + } + + const dots_in_hblank = 99; + scheduler.push(.{ .nds9 = .hblank }, dots_in_hblank * cycles_per_dot -| late); + } + + /// HBlank -> HDraw / VBlank pub fn onHblankEnd(self: *@This(), scheduler: *Scheduler, late: u64) void { - const scanline_count = 192 + 71; + const scanline_total = 263; // 192 visible, 71 blanking - const prev_scanline = self.io.vcount.scanline.read(); - const scanline = (prev_scanline + 1) % scanline_count; + std.debug.assert(self.engines[0].vcount.scanline.read() == self.engines[1].vcount.scanline.read()); - self.io.vcount.scanline.write(scanline); - self.io.dispstat.hblank.unset(); + inline for (&self.engines) |*engine| { + const prev_scanline = engine.vcount.scanline.read(); + const scanline = (prev_scanline + 1) % scanline_total; - const coincidence = scanline == self.io.dispstat.lyc.read(); - self.io.dispstat.coincidence.write(coincidence); + engine.vcount.scanline.write(scanline); + engine.dispstat.hblank.unset(); + + const coincidence = scanline == engine.dispstat.lyc.read(); + engine.dispstat.coincidence.write(coincidence); + } + + const scanline = self.engines[0].vcount.scanline.read(); // 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); + inline for (&self.engines) |*engine| { + std.debug.assert(engine.dispstat.vblank.read() == false); + std.debug.assert(engine.dispstat.hblank.read() == false); + } // Draw Another Scanline const dots_in_hdraw = 256; @@ -108,16 +113,22 @@ pub const Ppu = struct { if (scanline == 192) { // Transition from Hblank to Vblank self.fb.swap(); - self.io.dispstat.vblank.set(); + + inline for (&self.engines) |*engine| + engine.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)); + if (scanline == 262) { + inline for (&self.engines) |*engine| { + engine.dispstat.vblank.unset(); + std.debug.assert(engine.dispstat.vblank.read() == (scanline != 262)); + } + } - const dots_in_scanline = 256 + 99; - scheduler.push(.{ .nds9 = .hblank }, dots_in_scanline * cycles_per_dot -| late); + const dots_in_vblank = 256; + scheduler.push(.{ .nds9 = .vblank }, dots_in_vblank * cycles_per_dot -| late); } }; @@ -126,15 +137,15 @@ pub const FrameBuffer = struct { current: u1 = 0, - ptr: *[len * 4]u8, + ptr: *align(@sizeOf(u32)) [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); + const buf = try allocator.alignedAlloc(u8, @sizeOf(u32), len * 4); - return .{ .ptr = ptr }; + return .{ .ptr = buf[0 .. len * 4] }; } pub fn deinit(self: @This(), allocator: Allocator) void { @@ -162,303 +173,3 @@ pub const FrameBuffer = struct { 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 -} - -pub const Vram = struct { - const page_size = 16 * KiB; // smallest allocation is 16 KiB - const addr_space_size = 0x0100_0000; // 0x0600_0000 -> 0x06FF_FFFF (inclusive) - const table_len = addr_space_size / page_size; - const buf_len = 656 * KiB; - - const IntFittingRange = std.math.IntFittingRange; - const log = std.log.scoped(.vram); - - io: Io = .{}, - - _buf: *[buf_len]u8, - nds9_table: *const [table_len]?[*]u8, - nds7_table: *const [table_len]?[*]u8, - - const Io = struct { - const nds9 = @import("nds9/io.zig"); - const nds7 = @import("nds7/io.zig"); - pub const Vramstat = @import("nds7/io.zig").Vramstat; - - stat: nds7.Vramstat = .{ .raw = 0x00 }, - - /// Write-Only (according to melonDS these are readable lol) - cnt_a: nds9.Vramcnt.A = .{ .raw = 0x00 }, - cnt_b: nds9.Vramcnt.A = .{ .raw = 0x00 }, - cnt_c: nds9.Vramcnt.C = .{ .raw = 0x00 }, - cnt_d: nds9.Vramcnt.C = .{ .raw = 0x00 }, - cnt_e: nds9.Vramcnt.E = .{ .raw = 0x00 }, - cnt_f: nds9.Vramcnt.C = .{ .raw = 0x00 }, - cnt_g: nds9.Vramcnt.C = .{ .raw = 0x00 }, - cnt_h: nds9.Vramcnt.H = .{ .raw = 0x00 }, - cnt_i: nds9.Vramcnt.H = .{ .raw = 0x00 }, - }; - - pub fn init(self: *@This(), allocator: Allocator) !void { - const buf = try allocator.create([buf_len]u8); - errdefer allocator.destroy(buf); - @memset(buf, 0); - - const tables = try allocator.alloc(?[*]u8, 2 * table_len); - @memset(tables, null); - - self.* = .{ - .nds9_table = tables[0..table_len], - .nds7_table = tables[table_len .. 2 * table_len], - ._buf = buf, - }; - - // ROMS like redpanda.nds won't write to VRAMCNT before trying to write to VRAM - // therefore we assume some default allocation (in this casee VRAMCNT_A -> VRAMCNT_I are 0x00) - self.update(); - } - - pub fn deinit(self: @This(), allocator: Allocator) void { - allocator.destroy(self._buf); - - const ptr: [*]?[*]const u8 = @ptrCast(@constCast(self.nds9_table)); - allocator.free(ptr[0 .. 2 * table_len]); - } - - pub fn stat(self: *const @This()) Io.Vramstat { - const vram_c: u8 = @intFromBool(self.io.cnt_c.enable.read() and self.io.cnt_c.mst.read() == 2); - const vram_d: u8 = @intFromBool(self.io.cnt_d.enable.read() and self.io.cnt_d.mst.read() == 2); - - return .{ .raw = (vram_d << 1) | vram_c }; - } - - const Kind = enum { - a, - b, - c, - d, - e, - f, - g, - h, - i, - - /// In Bytes - inline fn size(self: @This()) u32 { - return switch (self) { - .a => 128 * KiB, - .b => 128 * KiB, - .c => 128 * KiB, - .d => 128 * KiB, - .e => 64 * KiB, - .f => 16 * KiB, - .g => 16 * KiB, - .h => 32 * KiB, - .i => 16 * KiB, - }; - } - }; - - // TODO: Rename - fn range(comptime kind: Kind, mst: u3, offset: u2) u32 { - const ofs: u32 = offset; - // panic messages are from GBATEK - - return switch (kind) { - .a => switch (mst) { - 0 => 0x0680_0000, - 1 => 0x0600_0000 + (0x0002_0000 * ofs), - 2 => 0x0640_0000 + (0x0002_0000 * (ofs & 0b01)), - 3 => @panic("VRAMCNT_A: Slot OFS(0-3)"), - else => std.debug.panic("Invalid MST for VRAMCNT_{s}", .{[_]u8{std.ascii.toUpper(@tagName(kind)[0])}}), - }, - .b => switch (mst) { - 0 => 0x0682_0000, - 1 => 0x0600_0000 + (0x0002_0000 * ofs), - 2 => 0x0640_0000 + (0x0002_0000 * (ofs & 0b01)), - 3 => @panic("VRAMCNT_B: Slot OFS(0-3)"), - else => std.debug.panic("Invalid MST for VRAMCNT_{s}", .{[_]u8{std.ascii.toUpper(@tagName(kind)[0])}}), - }, - .c => switch (mst) { - 0 => 0x0684_0000, - 1 => 0x0600_0000 + (0x0002_0000 * ofs), - 2 => 0x0600_0000 + (0x0002_0000 * (ofs & 0b01)), - 3 => @panic("VRAMCNT_C: Slot OFS(0-3)"), - 4 => 0x0620_0000, - else => std.debug.panic("Invalid MST for VRAMCNT_{s}", .{[_]u8{std.ascii.toUpper(@tagName(kind)[0])}}), - }, - .d => switch (mst) { - 0 => 0x0686_0000, - 1 => 0x0600_0000 + (0x0002_0000 * ofs), - 2 => 0x0600_0000 + (0x0002_0000 * (ofs & 0b01)), - 3 => @panic("VRAMCNT_D: Slot OFS(0-3)"), - 4 => 0x0660_0000, - else => std.debug.panic("Invalid MST for VRAMCNT_{s}", .{[_]u8{std.ascii.toUpper(@tagName(kind)[0])}}), - }, - .e => switch (mst) { - 0 => 0x0688_0000, - 1 => 0x0600_0000, - 2 => 0x0640_0000, - 3 => @panic("VRAMCNT_E: Slots 0-3"), - else => std.debug.panic("Invalid MST for VRAMCNT_{s}", .{[_]u8{std.ascii.toUpper(@tagName(kind)[0])}}), - }, - .f => switch (mst) { - 0 => 0x0689_0000, - 1 => 0x0600_0000 + (0x0000_4000 * (ofs & 0b01)) + (0x0001_0000 * (ofs >> 1)), - 2 => 0x0640_0000 + (0x0000_4000 * (ofs & 0b01)) + (0x0001_0000 * (ofs >> 1)), - 3 => @panic("VRAMCNT_F: Slot (OFS.0*1)+(OFS.1*4)"), - 4 => @panic("VRAMCNT_F: Slot 0-1 (OFS=0), Slot 2-3 (OFS=1)"), - 5 => @panic("VRAMCNT_F: Slot 0"), - else => std.debug.panic("Invalid MST for VRAMCNT_{s}", .{[_]u8{std.ascii.toUpper(@tagName(kind)[0])}}), - }, - .g => switch (mst) { - 0 => 0x0689_4000, - 1 => 0x0600_0000 + (0x0000_4000 * (ofs & 0b01)) + (0x0001_0000 * (ofs >> 1)), - 2 => 0x0640_0000 + (0x0000_4000 * (ofs & 0b01)) + (0x0001_0000 * (ofs >> 1)), - 3 => @panic("VRAMCNT_G: Slot (OFS.0*1)+(OFS.1*4)"), - 4 => @panic("VRAMCNT_G: Slot 0-1 (OFS=0), Slot 2-3 (OFS=1)"), - 5 => @panic("VRAMCNT_G: Slot 0"), - else => std.debug.panic("Invalid MST for VRAMCNT_{s}", .{[_]u8{std.ascii.toUpper(@tagName(kind)[0])}}), - }, - .h => switch (mst) { - 0 => 0x0689_8000, - 1 => 0x0620_0000, - 2 => @panic("VRAMCNT_H: Slot 0-3"), - else => std.debug.panic("Invalid MST for VRAMCNT_{s}", .{[_]u8{std.ascii.toUpper(@tagName(kind)[0])}}), - }, - .i => switch (mst) { - 0 => 0x068A_0000, - 1 => 0x0620_8000, - 2 => 0x0660_0000, - 3 => @panic("Slot 0"), - else => std.debug.panic("Invalid MST for VRAMCNT_{s}", .{[_]u8{std.ascii.toUpper(@tagName(kind)[0])}}), - }, - }; - } - - fn buf_offset(comptime kind: Kind) usize { - // zig fmt: off - return switch (kind) { - .a => 0, // 0x00000 - .b => (128 * KiB) * 1, // 0x20000 (+ 0x20000) - .c => (128 * KiB) * 2, // 0x40000 (+ 0x20000) - .d => (128 * KiB) * 3, // 0x60000 (+ 0x20000) - .e => (128 * KiB) * 4, // 0x80000 (+ 0x20000) - .f => (128 * KiB) * 4 + (64 * KiB), // 0x90000 (+ 0x10000) - .g => (128 * KiB) * 4 + (64 * KiB) + (16 * KiB) * 1, // 0x94000 (+ 0x04000) - .h => (128 * KiB) * 4 + (64 * KiB) + (16 * KiB) * 2, // 0x98000 (+ 0x04000) - .i => (128 * KiB) * 4 + (64 * KiB) + (16 * KiB) * 2 + (32 * KiB) // 0xA0000 (+ 0x08000) - }; - // zig fmt: on - } - - fn CntType(comptime kind: Kind) type { - const io = @import("nds9/io.zig"); - - return switch (kind) { - .a => io.Vramcnt.A, - .b => io.Vramcnt.A, - .c => io.Vramcnt.C, - .d => io.Vramcnt.C, - .e => io.Vramcnt.E, - .f => io.Vramcnt.C, - .g => io.Vramcnt.C, - .h => io.Vramcnt.H, - .i => io.Vramcnt.H, - }; - } - - fn cntValue(self: *const @This(), comptime kind: Kind) CntType(kind) { - return switch (kind) { - .a => self.io.cnt_a, - .b => self.io.cnt_b, - .c => self.io.cnt_c, - .d => self.io.cnt_d, - .e => self.io.cnt_e, - .f => self.io.cnt_f, - .g => self.io.cnt_g, - .h => self.io.cnt_h, - .i => self.io.cnt_i, - }; - } - - // TODO: We always update the entirety of VRAM when that argubably isn't necessary - pub fn update(self: *@This()) void { - const nds9_tbl = @constCast(self.nds9_table); - const nds7_tbl = @constCast(self.nds7_table); - - for (nds9_tbl, nds7_tbl, 0..) |*nds9_ptr, *nds7_ptr, i| { - const addr = 0x0600_0000 + (i * page_size); - - inline for (std.meta.fields(Kind)) |f| { - const kind = @field(Kind, f.name); - const cnt = cntValue(self, kind); - const ofs = switch (kind) { - .e, .h, .i => 0, - else => cnt.offset.read(), - }; - - const min = range(kind, cnt.mst.read(), ofs); - const max = min + kind.size(); - const offset = addr & (kind.size() - 1); - - if (min <= addr and addr < max) { - if ((kind == .c or kind == .d) and cnt.mst.read() == 2) { - // Allocate to ARM7 - nds7_ptr.* = self._buf[buf_offset(kind) + offset ..].ptr; - } else { - nds9_ptr.* = self._buf[buf_offset(kind) + offset ..].ptr; - } - } - } - } - } - - // TODO: Rename - const Device = enum { nds9, nds7 }; - - pub fn read(self: @This(), comptime T: type, comptime dev: Device, address: u32) T { - const bits = @typeInfo(IntFittingRange(0, page_size - 1)).Int.bits; - const masked_addr = address & (addr_space_size - 1); - const page = masked_addr >> bits; - const offset = masked_addr & (page_size - 1); - const table = if (dev == .nds9) self.nds9_table else self.nds7_table; - - if (table[page]) |some_ptr| { - const ptr: [*]const T = @ptrCast(@alignCast(some_ptr)); - - return ptr[offset / @sizeOf(T)]; - } - - log.err("{s}: read(T: {}, addr: 0x{X:0>8}) was in un-mapped VRAM space", .{ @tagName(dev), T, address }); - return 0x00; - } - - pub fn write(self: *@This(), comptime T: type, comptime dev: Device, address: u32, value: T) void { - const bits = @typeInfo(IntFittingRange(0, page_size - 1)).Int.bits; - const masked_addr = address & (addr_space_size - 1); - const page = masked_addr >> bits; - const offset = masked_addr & (page_size - 1); - const table = if (dev == .nds9) self.nds9_table else self.nds7_table; - - if (table[page]) |some_ptr| { - const ptr: [*]T = @ptrCast(@alignCast(some_ptr)); - ptr[offset / @sizeOf(T)] = value; - - return; - } - - log.err("{s}: write(T: {}, addr: 0x{X:0>8}, value: 0x{X:0>8}) was in un-mapped VRA< space", .{ @tagName(dev), T, address, value }); - } -}; diff --git a/src/core/ppu/Vram.zig b/src/core/ppu/Vram.zig new file mode 100644 index 0000000..6d4f43e --- /dev/null +++ b/src/core/ppu/Vram.zig @@ -0,0 +1,291 @@ +const std = @import("std"); +const KiB = 0x400; + +const Allocator = std.mem.Allocator; +const IntFittingRange = std.math.IntFittingRange; + +const page_size = 16 * KiB; // smallest allocation is 16 KiB +const addr_space_size = 0x0100_0000; // 0x0600_0000 -> 0x06FF_FFFF (inclusive) +const table_len = addr_space_size / page_size; +const buf_len = 656 * KiB; + +const log = std.log.scoped(.vram); + +io: Io = .{}, + +_buf: *[buf_len]u8, +nds9_table: *const [table_len]?[*]u8, +nds7_table: *const [table_len]?[*]u8, + +const Io = struct { + pub const Vramstat = nds7.Vramstat; + + const nds9 = @import("../nds9/io.zig"); + const nds7 = @import("../nds7/io.zig"); + + stat: nds7.Vramstat = .{ .raw = 0x00 }, + + /// Write-Only (according to melonDS these are readable lol) + cnt_a: nds9.Vramcnt.A = .{ .raw = 0x00 }, + cnt_b: nds9.Vramcnt.A = .{ .raw = 0x00 }, + cnt_c: nds9.Vramcnt.C = .{ .raw = 0x00 }, + cnt_d: nds9.Vramcnt.C = .{ .raw = 0x00 }, + cnt_e: nds9.Vramcnt.E = .{ .raw = 0x00 }, + cnt_f: nds9.Vramcnt.C = .{ .raw = 0x00 }, + cnt_g: nds9.Vramcnt.C = .{ .raw = 0x00 }, + cnt_h: nds9.Vramcnt.H = .{ .raw = 0x00 }, + cnt_i: nds9.Vramcnt.H = .{ .raw = 0x00 }, +}; + +pub fn init(self: *@This(), allocator: Allocator) !void { + const buf = try allocator.create([buf_len]u8); + errdefer allocator.destroy(buf); + @memset(buf, 0); + + const tables = try allocator.alloc(?[*]u8, 2 * table_len); + @memset(tables, null); + + self.* = .{ + .nds9_table = tables[0..table_len], + .nds7_table = tables[table_len .. 2 * table_len], + ._buf = buf, + }; + + // ROMS like redpanda.nds won't write to VRAMCNT before trying to write to VRAM + // therefore we assume some default allocation (in this casee VRAMCNT_A -> VRAMCNT_I are 0x00) + self.update(); +} + +pub fn deinit(self: @This(), allocator: Allocator) void { + allocator.destroy(self._buf); + + const ptr: [*]?[*]const u8 = @ptrCast(@constCast(self.nds9_table)); + allocator.free(ptr[0 .. 2 * table_len]); +} + +/// NDS7 VRAMSTAT +pub fn stat(self: *const @This()) Io.Vramstat { + const vram_c: u8 = @intFromBool(self.io.cnt_c.enable.read() and self.io.cnt_c.mst.read() == 2); + const vram_d: u8 = @intFromBool(self.io.cnt_d.enable.read() and self.io.cnt_d.mst.read() == 2); + + return .{ .raw = (vram_d << 1) | vram_c }; +} + +const Kind = enum { + a, + b, + c, + d, + e, + f, + g, + h, + i, + + /// In Bytes + inline fn size(self: @This()) u32 { + return switch (self) { + .a => 128 * KiB, + .b => 128 * KiB, + .c => 128 * KiB, + .d => 128 * KiB, + .e => 64 * KiB, + .f => 16 * KiB, + .g => 16 * KiB, + .h => 32 * KiB, + .i => 16 * KiB, + }; + } +}; + +// TODO: Rename +fn range(comptime kind: Kind, mst: u3, offset: u2) u32 { + const ofs: u32 = offset; + // panic messages are from GBATEK + + return switch (kind) { + .a => switch (mst) { + 0 => 0x0680_0000, + 1 => 0x0600_0000 + (0x0002_0000 * ofs), + 2 => 0x0640_0000 + (0x0002_0000 * (ofs & 0b01)), + 3 => @panic("VRAMCNT_A: Slot OFS(0-3)"), + else => std.debug.panic("Invalid MST for VRAMCNT_{s}", .{[_]u8{std.ascii.toUpper(@tagName(kind)[0])}}), + }, + .b => switch (mst) { + 0 => 0x0682_0000, + 1 => 0x0600_0000 + (0x0002_0000 * ofs), + 2 => 0x0640_0000 + (0x0002_0000 * (ofs & 0b01)), + 3 => @panic("VRAMCNT_B: Slot OFS(0-3)"), + else => std.debug.panic("Invalid MST for VRAMCNT_{s}", .{[_]u8{std.ascii.toUpper(@tagName(kind)[0])}}), + }, + .c => switch (mst) { + 0 => 0x0684_0000, + 1 => 0x0600_0000 + (0x0002_0000 * ofs), + 2 => 0x0600_0000 + (0x0002_0000 * (ofs & 0b01)), + 3 => @panic("VRAMCNT_C: Slot OFS(0-3)"), + 4 => 0x0620_0000, + else => std.debug.panic("Invalid MST for VRAMCNT_{s}", .{[_]u8{std.ascii.toUpper(@tagName(kind)[0])}}), + }, + .d => switch (mst) { + 0 => 0x0686_0000, + 1 => 0x0600_0000 + (0x0002_0000 * ofs), + 2 => 0x0600_0000 + (0x0002_0000 * (ofs & 0b01)), + 3 => @panic("VRAMCNT_D: Slot OFS(0-3)"), + 4 => 0x0660_0000, + else => std.debug.panic("Invalid MST for VRAMCNT_{s}", .{[_]u8{std.ascii.toUpper(@tagName(kind)[0])}}), + }, + .e => switch (mst) { + 0 => 0x0688_0000, + 1 => 0x0600_0000, + 2 => 0x0640_0000, + 3 => @panic("VRAMCNT_E: Slots 0-3"), + else => std.debug.panic("Invalid MST for VRAMCNT_{s}", .{[_]u8{std.ascii.toUpper(@tagName(kind)[0])}}), + }, + .f => switch (mst) { + 0 => 0x0689_0000, + 1 => 0x0600_0000 + (0x0000_4000 * (ofs & 0b01)) + (0x0001_0000 * (ofs >> 1)), + 2 => 0x0640_0000 + (0x0000_4000 * (ofs & 0b01)) + (0x0001_0000 * (ofs >> 1)), + 3 => @panic("VRAMCNT_F: Slot (OFS.0*1)+(OFS.1*4)"), + 4 => @panic("VRAMCNT_F: Slot 0-1 (OFS=0), Slot 2-3 (OFS=1)"), + 5 => @panic("VRAMCNT_F: Slot 0"), + else => std.debug.panic("Invalid MST for VRAMCNT_{s}", .{[_]u8{std.ascii.toUpper(@tagName(kind)[0])}}), + }, + .g => switch (mst) { + 0 => 0x0689_4000, + 1 => 0x0600_0000 + (0x0000_4000 * (ofs & 0b01)) + (0x0001_0000 * (ofs >> 1)), + 2 => 0x0640_0000 + (0x0000_4000 * (ofs & 0b01)) + (0x0001_0000 * (ofs >> 1)), + 3 => @panic("VRAMCNT_G: Slot (OFS.0*1)+(OFS.1*4)"), + 4 => @panic("VRAMCNT_G: Slot 0-1 (OFS=0), Slot 2-3 (OFS=1)"), + 5 => @panic("VRAMCNT_G: Slot 0"), + else => std.debug.panic("Invalid MST for VRAMCNT_{s}", .{[_]u8{std.ascii.toUpper(@tagName(kind)[0])}}), + }, + .h => switch (mst) { + 0 => 0x0689_8000, + 1 => 0x0620_0000, + 2 => @panic("VRAMCNT_H: Slot 0-3"), + else => std.debug.panic("Invalid MST for VRAMCNT_{s}", .{[_]u8{std.ascii.toUpper(@tagName(kind)[0])}}), + }, + .i => switch (mst) { + 0 => 0x068A_0000, + 1 => 0x0620_8000, + 2 => 0x0660_0000, + 3 => @panic("Slot 0"), + else => std.debug.panic("Invalid MST for VRAMCNT_{s}", .{[_]u8{std.ascii.toUpper(@tagName(kind)[0])}}), + }, + }; +} + +fn buf_offset(comptime kind: Kind) usize { + // zig fmt: off + return switch (kind) { + .a => 0, // 0x00000 + .b => (128 * KiB) * 1, // 0x20000 (+ 0x20000) + .c => (128 * KiB) * 2, // 0x40000 (+ 0x20000) + .d => (128 * KiB) * 3, // 0x60000 (+ 0x20000) + .e => (128 * KiB) * 4, // 0x80000 (+ 0x20000) + .f => (128 * KiB) * 4 + (64 * KiB), // 0x90000 (+ 0x10000) + .g => (128 * KiB) * 4 + (64 * KiB) + (16 * KiB) * 1, // 0x94000 (+ 0x04000) + .h => (128 * KiB) * 4 + (64 * KiB) + (16 * KiB) * 2, // 0x98000 (+ 0x04000) + .i => (128 * KiB) * 4 + (64 * KiB) + (16 * KiB) * 2 + (32 * KiB) // 0xA0000 (+ 0x08000) + }; + // zig fmt: on +} + +fn CntType(comptime kind: Kind) type { + const Vramcnt = @import("../nds9/io.zig").Vramcnt; + + return switch (kind) { + .a => Vramcnt.A, + .b => Vramcnt.A, + .c => Vramcnt.C, + .d => Vramcnt.C, + .e => Vramcnt.E, + .f => Vramcnt.C, + .g => Vramcnt.C, + .h => Vramcnt.H, + .i => Vramcnt.H, + }; +} + +fn cntValue(self: *const @This(), comptime kind: Kind) CntType(kind) { + return switch (kind) { + .a => self.io.cnt_a, + .b => self.io.cnt_b, + .c => self.io.cnt_c, + .d => self.io.cnt_d, + .e => self.io.cnt_e, + .f => self.io.cnt_f, + .g => self.io.cnt_g, + .h => self.io.cnt_h, + .i => self.io.cnt_i, + }; +} + +// TODO: We always update the entirety of VRAM when that argubably isn't necessary +pub fn update(self: *@This()) void { + const nds9_tbl = @constCast(self.nds9_table); + const nds7_tbl = @constCast(self.nds7_table); + + for (nds9_tbl, nds7_tbl, 0..) |*nds9_ptr, *nds7_ptr, i| { + const addr = 0x0600_0000 + (i * page_size); + + inline for (std.meta.fields(Kind)) |f| { + const kind = @field(Kind, f.name); + const cnt = cntValue(self, kind); + const ofs = switch (kind) { + .e, .h, .i => 0, + else => cnt.offset.read(), + }; + + const min = range(kind, cnt.mst.read(), ofs); + const max = min + kind.size(); + const offset = addr & (kind.size() - 1); + + if (min <= addr and addr < max) { + if ((kind == .c or kind == .d) and cnt.mst.read() == 2) { + // Allocate to ARM7 + nds7_ptr.* = self._buf[buf_offset(kind) + offset ..].ptr; + } else { + nds9_ptr.* = self._buf[buf_offset(kind) + offset ..].ptr; + } + } + } + } +} + +// TODO: Rename +const Device = enum { nds9, nds7 }; + +pub fn read(self: @This(), comptime T: type, comptime dev: Device, address: u32) T { + const bits = @typeInfo(IntFittingRange(0, page_size - 1)).Int.bits; + const masked_addr = address & (addr_space_size - 1); + const page = masked_addr >> bits; + const offset = masked_addr & (page_size - 1); + const table = if (dev == .nds9) self.nds9_table else self.nds7_table; + + if (table[page]) |some_ptr| { + const ptr: [*]const T = @ptrCast(@alignCast(some_ptr)); + + return ptr[offset / @sizeOf(T)]; + } + + log.err("{s}: read(T: {}, addr: 0x{X:0>8}) was in un-mapped VRAM space", .{ @tagName(dev), T, address }); + return 0x00; +} + +pub fn write(self: *@This(), comptime T: type, comptime dev: Device, address: u32, value: T) void { + const bits = @typeInfo(IntFittingRange(0, page_size - 1)).Int.bits; + const masked_addr = address & (addr_space_size - 1); + const page = masked_addr >> bits; + const offset = masked_addr & (page_size - 1); + const table = if (dev == .nds9) self.nds9_table else self.nds7_table; + + if (table[page]) |some_ptr| { + const ptr: [*]T = @ptrCast(@alignCast(some_ptr)); + ptr[offset / @sizeOf(T)] = value; + + return; + } + + log.err("{s}: write(T: {}, addr: 0x{X:0>8}, value: 0x{X:0>8}) was in un-mapped VRA< space", .{ @tagName(dev), T, address, value }); +} diff --git a/src/core/ppu/engine.zig b/src/core/ppu/engine.zig new file mode 100644 index 0000000..ab25cb5 --- /dev/null +++ b/src/core/ppu/engine.zig @@ -0,0 +1,91 @@ +const std = @import("std"); +const Bus = @import("../nds9/Bus.zig"); + +const FrameBuffer = @import("../ppu.zig").FrameBuffer; + +const DispcntA = @import("../nds9/io.zig").DispcntA; +const DispcntB = @import("../nds9/io.zig").DispcntB; + +const Dispstat = @import("../nds9/io.zig").Dispstat; +const Vcount = @import("../nds9/io.zig").Vcount; + +const PowCnt = @import("../nds9/io.zig").PowCnt; + +const width = @import("../ppu.zig").screen_width; +const height = @import("../ppu.zig").screen_height; + +const EngineKind = enum { a, b }; + +pub const EngineA = Engine(.a); +pub const EngineB = Engine(.b); + +fn Engine(comptime kind: EngineKind) type { + const log = std.log.scoped(.engine2d); + _ = log; // TODO: specify between 2D-A and 2D-B + + // FIXME: don't commit zig crimes + const Type = struct { + fn inner(comptime TypeA: type, comptime TypeB: type) type { + return if (kind == .a) TypeA else TypeB; + } + }.inner; + + return struct { + dispcnt: Type(DispcntA, DispcntB) = .{ .raw = 0x0000_0000 }, + dispstat: Dispstat = .{ .raw = 0x0000_0000 }, + vcount: Vcount = .{ .raw = 0x0000_0000 }, + + pub fn drawScanline(self: *@This(), bus: *Bus, fb: *FrameBuffer, powcnt: *PowCnt) void { + const disp_mode = self.dispcnt.display_mode.read(); + + switch (disp_mode) { + 0 => { // Display Off + const buf = switch (kind) { + .a => if (powcnt.display_swap.read()) fb.top(.back) else fb.btm(.back), + .b => if (powcnt.display_swap.read()) fb.btm(.back) else fb.top(.back), + }; + + @memset(buf, 0xFF); // set everything to white + }, + 1 => @panic("TODO: standard graphics display (text mode, etc)"), + 2 => { // VRAM display + if (kind == .b) return; + // TODO: Master Brightness can still affect this mode + + const scanline: u32 = self.vcount.scanline.read(); + const buf = if (powcnt.display_swap.read()) fb.top(.back) else fb.btm(.back); + + const scanline_buf = blk: { + const rgba_ptr: *[width * height]u32 = @ptrCast(@alignCast(buf)); + break :blk rgba_ptr[width * scanline ..][0..width]; + }; + + const base_addr: u32 = 0x0680_0000 + (width * @sizeOf(u16)) * @as(u32, scanline); + + for (scanline_buf, 0..) |*rgba, i| { + const addr = base_addr + @as(u32, @intCast(i)) * @sizeOf(u16); + rgba.* = rgba888(bus.dbgRead(u16, addr)); + } + }, + 3 => { + if (kind == .b) return; + + @panic("TODO: main memory display"); + }, + } + } + }; +} + +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 +}