diff --git a/src/core/nds7/Bus.zig b/src/core/nds7/Bus.zig index f666843..c2df92e 100644 --- a/src/core/nds7/Bus.zig +++ b/src/core/nds7/Bus.zig @@ -110,7 +110,7 @@ fn _write(self: *@This(), comptime T: type, comptime mode: Mode, address: u32, v 0b00 => writeInt(T, self.wram[aligned_addr & 0x0000_FFFF ..][0..byte_count], value), else => self.shr_wram.write(T, .nds7, aligned_addr, value), }, - 0x0380_0000...0x0380_FFFF => writeInt(T, self.wram[aligned_addr & 0x0000_FFFF ..][0..byte_count], value), + 0x0380_0000...0x03FF_FFFF => writeInt(T, self.wram[aligned_addr & 0x0000_FFFF ..][0..byte_count], value), 0x0400_0000...0x04FF_FFFF => io.write(self, T, aligned_addr, value), 0x0600_0000...0x06FF_FFFF => self.vram.write(T, .nds7, aligned_addr, value), else => log.warn("unexpected: write(T: {}, addr: 0x{X:0>8}, value: 0x{X:0>8})", .{ T, address, value }), diff --git a/src/core/nds7/io.zig b/src/core/nds7/io.zig index b850301..bc4caf7 100644 --- a/src/core/nds7/io.zig +++ b/src/core/nds7/io.zig @@ -63,7 +63,7 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) T { 0x0400_0214 => bus.io.irq.raw, 0x0410_0000 => bus.io.shr.ipc.recv(.nds7), - else => warn("unexpected: read(T: {}, addr: 0x{X:0>8}) {} ", .{ T, address, T }), + else => 0, // warn("unexpected: read(T: {}, addr: 0x{X:0>8}) {} ", .{ T, address, T }), }, u16 => switch (address) { 0x0400_0004 => bus.io.ppu.?.nds7.dispstat.raw, @@ -76,7 +76,7 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) T { 0x0400_0130 => bus.io.shr.keyinput.load(.Monotonic), 0x0400_0180 => @truncate(bus.io.shr.ipc._nds7.sync.raw), 0x0400_0184 => @truncate(bus.io.shr.ipc._nds7.cnt.raw), - else => warn("unexpected: read(T: {}, addr: 0x{X:0>8}) {} ", .{ T, address, T }), + else => 0, // warn("unexpected: read(T: {}, addr: 0x{X:0>8}) {} ", .{ T, address, T }), }, u8 => switch (address) { // DMA Transfers @@ -89,7 +89,7 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) T { 0x0400_0241 => bus.io.shr.wramcnt.raw, 0x0400_0300 => @intFromEnum(bus.io.postflg), - else => warn("unexpected: read(T: {}, addr: 0x{X:0>8}) {} ", .{ T, address, T }), + else => 0, // warn("unexpected: read(T: {}, addr: 0x{X:0>8}) {} ", .{ T, address, T }), }, else => @compileError(T ++ " is an unsupported bus read type"), }; @@ -110,7 +110,7 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void { 0x0400_0214 => bus.io.irq.raw &= ~value, 0x0400_0188 => bus.io.shr.ipc.send(.nds7, value), - else => log.warn("unexpected: write(T: {}, addr: 0x{X:0>8}, value: 0x{X:0>8})", .{ T, address, value }), + else => {}, // log.warn("unexpected: write(T: {}, addr: 0x{X:0>8}, value: 0x{X:0>8})", .{ T, address, value }), }, u16 => switch (address) { // DMA Transfers @@ -123,7 +123,7 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void { 0x0400_0184 => bus.io.shr.ipc.setIpcFifoCnt(.nds7, value), 0x0400_0208 => bus.io.ime = value & 1 == 1, - else => log.warn("unexpected: write(T: {}, addr: 0x{X:0>8}, value: 0x{X:0>4})", .{ T, address, value }), + else => {}, // log.warn("unexpected: write(T: {}, addr: 0x{X:0>8}, value: 0x{X:0>4})", .{ T, address, value }), }, u8 => switch (address) { // DMA Transfers @@ -133,7 +133,16 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void { 0x0400_0100...0x0400_010F => log.warn("TODO: impl timer", .{}), 0x0400_0208 => bus.io.ime = value & 1 == 1, - else => log.warn("unexpected: write(T: {}, addr: 0x{X:0>8}, value: 0x{X:0>2})", .{ T, address, value }), + + 0x0400_0301 => switch ((value >> 6) & 0b11) { + 0b00 => bus.io.haltcnt = .execute, + 0b10 => bus.io.haltcnt = .halt, + else => |val| { + const tag: Haltcnt = @enumFromInt(val); + log.err("TODO: Implement {}", .{tag}); + }, + }, + else => {}, // log.warn("unexpected: write(T: {}, addr: 0x{X:0>8}, value: 0x{X:0>2})", .{ T, address, value }), }, else => @compileError(T ++ " is an unsupported bus write type"), } diff --git a/src/core/nds9/Bus.zig b/src/core/nds9/Bus.zig index 2410a92..b69ed16 100644 --- a/src/core/nds9/Bus.zig +++ b/src/core/nds9/Bus.zig @@ -18,6 +18,7 @@ const log = std.log.scoped(.nds9_bus); main: *[4 * MiB]u8, wram: *Wram, +makeshift_palram: *[2 * KiB]u8, scheduler: *Scheduler, io: io.Io, @@ -31,6 +32,7 @@ pub fn init(allocator: Allocator, scheduler: *Scheduler, ctx: SharedCtx) !@This( return .{ .main = ctx.main, .wram = ctx.wram, + .makeshift_palram = try allocator.create([2 * KiB]u8), .ppu = try Ppu.init(allocator, ctx.vram), .scheduler = scheduler, .io = io.Io.init(ctx.io), @@ -42,6 +44,8 @@ pub fn init(allocator: Allocator, scheduler: *Scheduler, ctx: SharedCtx) !@This( pub fn deinit(self: *@This(), allocator: Allocator) void { self.ppu.deinit(allocator); self.bios.deinit(allocator); + + allocator.destroy(self.makeshift_palram); } pub fn reset(_: *@This()) void { @@ -72,6 +76,7 @@ fn _read(self: *@This(), comptime T: type, comptime mode: Mode, address: u32) T 0x0200_0000...0x02FF_FFFF => readInt(T, self.main[aligned_addr & 0x003F_FFFF ..][0..byte_count]), 0x0300_0000...0x03FF_FFFF => self.wram.read(T, .nds9, aligned_addr), 0x0400_0000...0x04FF_FFFF => io.read(self, T, aligned_addr), + 0x0500_0000...0x05FF_FFFF => readInt(T, self.makeshift_palram[aligned_addr & (2 * KiB - 1) ..][0..@sizeOf(T)]), 0x0600_0000...0x06FF_FFFF => self.ppu.vram.read(T, .nds9, aligned_addr), 0xFFFF_0000...0xFFFF_FFFF => self.bios.read(T, address), else => warn("unexpected: read(T: {}, addr: 0x{X:0>8}) {} ", .{ T, address, T }), @@ -102,6 +107,7 @@ fn _write(self: *@This(), comptime T: type, comptime mode: Mode, address: u32, v 0x0200_0000...0x02FF_FFFF => writeInt(T, self.main[aligned_addr & 0x003F_FFFF ..][0..byte_count], value), 0x0300_0000...0x03FF_FFFF => self.wram.write(T, .nds9, aligned_addr, value), 0x0400_0000...0x04FF_FFFF => io.write(self, T, aligned_addr, value), + 0x0500_0000...0x05FF_FFFF => writeInt(T, self.makeshift_palram[aligned_addr & (2 * KiB - 1) ..][0..@sizeOf(T)], value), 0x0600_0000...0x06FF_FFFF => self.ppu.vram.write(T, .nds9, aligned_addr, value), 0xFFFF_0000...0xFFFF_FFFF => self.bios.write(T, address, value), else => log.warn("unexpected: write(T: {}, addr: 0x{X:0>8}, value: 0x{X:0>8})", .{ T, address, value }), diff --git a/src/core/nds9/io.zig b/src/core/nds9/io.zig index 4ff3be1..856b15b 100644 --- a/src/core/nds9/io.zig +++ b/src/core/nds9/io.zig @@ -62,6 +62,8 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) T { 0x0400_02A8, 0x0400_02AC => @truncate(bus.io.div.remainder >> shift(u64, address)), 0x0400_02B4 => @truncate(bus.io.sqrt.result), + 0x0400_1000 => bus.ppu.engines[1].dispcnt.raw, + 0x0410_0000 => bus.io.shr.ipc.recv(.nds9), 0x0400_4000, 0x0400_4008 => 0x0000_0000, // Lets software know this is NOT a DSi @@ -84,6 +86,20 @@ pub fn read(bus: *const Bus, comptime T: type, address: u32) T { 0x0400_0280 => @truncate(bus.io.div.cnt.raw), 0x0400_02B0 => @truncate(bus.io.sqrt.cnt.raw), + 0x0400_1008 => bus.ppu.engines[1].bg[0].cnt.raw, + 0x0400_100A => bus.ppu.engines[1].bg[1].cnt.raw, + 0x0400_100C => bus.ppu.engines[1].bg[2].cnt.raw, + 0x0400_100E => bus.ppu.engines[1].bg[3].cnt.raw, + + 0x0400_1010 => bus.ppu.engines[1].bg[0].hofs.raw, + 0x0400_1012 => bus.ppu.engines[1].bg[0].vofs.raw, + 0x0400_1014 => bus.ppu.engines[1].bg[1].hofs.raw, + 0x0400_1016 => bus.ppu.engines[1].bg[1].vofs.raw, + 0x0400_1018 => bus.ppu.engines[1].bg[2].hofs.raw, + 0x0400_101A => bus.ppu.engines[1].bg[2].vofs.raw, + 0x0400_101C => bus.ppu.engines[1].bg[3].hofs.raw, + 0x0400_101E => bus.ppu.engines[1].bg[3].vofs.raw, + else => warn("unexpected: read(T: {}, addr: 0x{X:0>8}) {} ", .{ T, address, T }), }, u8 => switch (address) { @@ -108,6 +124,7 @@ const subset = @import("../../util.zig").subset; pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void { switch (T) { u32 => switch (address) { + // DMA Transfers 0x0400_00B0...0x0400_00DC => log.warn("TODO: impl DMA", .{}), 0x0400_00E0...0x0400_00EC => log.warn("TODO: impl DMA fill", .{}), @@ -179,6 +196,20 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void { 0x0400_0304 => bus.ppu.io.powcnt.raw = value, + 0x0400_1008 => bus.ppu.engines[1].bg[0].cnt.raw = value, + 0x0400_100A => bus.ppu.engines[1].bg[1].cnt.raw = value, + 0x0400_100C => bus.ppu.engines[1].bg[2].cnt.raw = value, + 0x0400_100E => bus.ppu.engines[1].bg[3].cnt.raw = value, + + 0x0400_1010 => bus.ppu.engines[1].bg[0].hofs.raw = value, + 0x0400_1012 => bus.ppu.engines[1].bg[0].vofs.raw = value, + 0x0400_1014 => bus.ppu.engines[1].bg[1].hofs.raw = value, + 0x0400_1016 => bus.ppu.engines[1].bg[1].vofs.raw = value, + 0x0400_1018 => bus.ppu.engines[1].bg[2].hofs.raw = value, + 0x0400_101A => bus.ppu.engines[1].bg[2].vofs.raw = value, + 0x0400_101C => bus.ppu.engines[1].bg[3].hofs.raw = value, + 0x0400_101E => bus.ppu.engines[1].bg[3].vofs.raw = value, + else => log.warn("unexpected: write(T: {}, addr: 0x{X:0>8}, value: 0x{X:0>4})", .{ T, address, value }), }, u8 => switch (address) { @@ -400,7 +431,7 @@ pub const DispcntA = extern union { 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), + char_base: Bitfield(u32, 24, 3), screen_base: Bitfield(u32, 27, 3), bg_ext_pal_enable: Bit(u32, 30), obj_ext_pal_enable: Bit(u32, 31), @@ -476,3 +507,23 @@ pub const Dispstat = extern union { lyc: Bitfield(u16, 7, 9), raw: u16, }; + +pub const Bgcnt = extern union { + priority: Bitfield(u16, 0, 2), + char_base: Bitfield(u16, 2, 4), + mosaic_enable: Bit(u16, 6), + colour_mode: Bit(u16, 7), + screen_base: Bitfield(u16, 8, 5), + display_overflow: Bit(u16, 13), + size: Bitfield(u16, 14, 2), + raw: u16, +}; + +/// Write Only +const BackgroundOffset = extern union { + offset: Bitfield(u16, 0, 9), + raw: u16, +}; + +pub const Hofs = BackgroundOffset; +pub const Vofs = BackgroundOffset; diff --git a/src/core/ppu.zig b/src/core/ppu.zig index 183b68d..7e5873f 100644 --- a/src/core/ppu.zig +++ b/src/core/ppu.zig @@ -21,7 +21,7 @@ pub const Ppu = struct { vram: *Vram, - engines: struct { EngineA, EngineB } = .{ .{}, .{} }, + engines: struct { EngineA, EngineB }, io: Io = .{}, @@ -44,20 +44,22 @@ pub const Ppu = struct { pub fn init(allocator: Allocator, vram: *Vram) !@This() { return .{ .fb = try FrameBuffer.init(allocator), + .engines = .{ try EngineA.init(allocator), try EngineB.init(allocator) }, .vram = vram, }; } pub fn deinit(self: @This(), allocator: Allocator) void { self.fb.deinit(allocator); + inline for (self.engines) |eng| eng.deinit(allocator); } pub fn drawScanline(self: *@This(), bus: *System.Bus9) void { if (self.io.powcnt.engine2d_a.read()) - self.engines[0].drawScanline(bus, &self.fb, &self.io); + self.engines[0].drawScanline(bus, &self.fb); if (self.io.powcnt.engine2d_b.read()) - self.engines[1].drawScanline(bus, &self.fb, &self.io); + self.engines[1].drawScanline(bus, &self.fb); } /// HDraw -> HBlank diff --git a/src/core/ppu/engine.zig b/src/core/ppu/engine.zig index 3be65b6..053598c 100644 --- a/src/core/ppu/engine.zig +++ b/src/core/ppu/engine.zig @@ -11,14 +11,15 @@ const Ppu = @import("../ppu.zig").Ppu; const width = @import("../ppu.zig").screen_width; const height = @import("../ppu.zig").screen_height; +const KiB = 0x400; + 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 + const log = std.log.scoped(.engine2d); // TODO: specify between 2D-A and 2D-B // FIXME: don't commit zig crimes const Type = struct { @@ -30,25 +31,79 @@ fn Engine(comptime kind: EngineKind) type { return struct { dispcnt: Type(DispcntA, DispcntB) = .{ .raw = 0x0000_0000 }, - pub fn drawScanline(self: *@This(), bus: *Bus, fb: *FrameBuffer, io: *Ppu.Io) void { + bg: [4]bg.Text = .{ .{}, .{}, .{}, .{} }, + + // TODO: Rename + scanline: Scanline, + + pub fn init(allocator: std.mem.Allocator) !@This() { + return .{ + .scanline = try Scanline.init(allocator), + }; + } + + pub fn deinit(self: @This(), allocator: std.mem.Allocator) void { + self.scanline.deinit(allocator); + } + + pub fn drawScanline(self: *@This(), bus: *Bus, fb: *FrameBuffer) void { const disp_mode = self.dispcnt.display_mode.read(); switch (disp_mode) { 0 => { // Display Off const buf = switch (kind) { - .a => if (io.powcnt.display_swap.read()) fb.top(.back) else fb.btm(.back), - .b => if (io.powcnt.display_swap.read()) fb.btm(.back) else fb.top(.back), + .a => if (bus.ppu.io.powcnt.display_swap.read()) fb.top(.back) else fb.btm(.back), + .b => if (bus.ppu.io.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)"), + 1 => { + const bg_mode = self.dispcnt.bg_mode.read(); + const bg_enable = self.dispcnt.bg_enable.read(); + const scanline: u32 = bus.ppu.io.nds9.vcount.scanline.read(); + + switch (bg_mode) { + // BG0 BG1 BG2 BG3 + // Text/3D Text Text Text + 0 => { + // TODO: Fetch Sprites + + for (0..4) |layer| { + // TODO: Draw Sprites + + inline for (0..4) |i| { + if (layer == self.bg[i].cnt.priority.read() and (bg_enable >> i) & 1 == 1) + self.drawBackground(i, bus); + } + } + + // TODO: IN ZBA we calculate the base address of the emu framebuffer to pass in + + const buf = switch (kind) { + .a => if (bus.ppu.io.powcnt.display_swap.read()) fb.top(.back) else fb.btm(.back), + .b => if (bus.ppu.io.powcnt.display_swap.read()) fb.btm(.back) else fb.top(.back), + }; + + const scanline_buf = blk: { + const rgba_ptr: *[width * height]u32 = @ptrCast(@alignCast(buf)); + break :blk rgba_ptr[width * scanline ..][0..width]; + }; + + self.renderTextMode(scanline_buf); + }, + else => |mode| { + log.err("TODO: Implement Mode {}", .{mode}); + @panic("fatal error"); + }, + } + }, 2 => { // VRAM display if (kind == .b) return; // TODO: Master Brightness can still affect this mode - const scanline: u32 = io.nds9.vcount.scanline.read(); - const buf = if (io.powcnt.display_swap.read()) fb.top(.back) else fb.btm(.back); + const scanline: u32 = bus.ppu.io.nds9.vcount.scanline.read(); + const buf = if (bus.ppu.io.powcnt.display_swap.read()) fb.top(.back) else fb.btm(.back); const scanline_buf = blk: { const rgba_ptr: *[width * height]u32 = @ptrCast(@alignCast(buf)); @@ -69,9 +124,210 @@ fn Engine(comptime kind: EngineKind) type { }, } } + + fn drawBackground(self: *@This(), comptime layer: u2, bus: *Bus) void { + const screen_base = blk: { + const bgcnt_off: u32 = self.bg[layer].cnt.screen_base.read(); + const dispcnt_off: u32 = if (kind == .a) self.dispcnt.screen_base.read() else 0; + + break :blk (2 * KiB) * bgcnt_off + (64 * KiB) * dispcnt_off; + }; + + const char_base = blk: { + const bgcnt_off: u32 = self.bg[layer].cnt.char_base.read(); + const dispcnt_off: u32 = if (kind == .a) self.dispcnt.char_base.read() else 0; + + break :blk (16 * KiB) * bgcnt_off + (64 * KiB) * dispcnt_off; + }; + + const is_8bpp = self.bg[layer].cnt.colour_mode.read(); + const size = self.bg[layer].cnt.size.read(); + + // In 4bpp: 1 byte represents two pixels so the length is (8 x 8) / 2 + // In 8bpp: 1 byte represents one pixel so the length is 8 x 8 + const tile_len: u32 = if (is_8bpp) 0x40 else 0x20; + const tile_row_offset: u32 = if (is_8bpp) 0x8 else 0x4; + + const vofs: u32 = self.bg[layer].vofs.offset.read(); + const hofs: u32 = self.bg[layer].hofs.offset.read(); + + const y = vofs + bus.ppu.io.nds9.vcount.scanline.read(); + + for (0..width) |_i| { + const i: u32 = @intCast(_i); + const x = hofs + i; + + // TODO: Windowing + + // Grab the Screen Entry from VRAM + const entry_addr = 0x0600_0000 + screen_base + tilemapOffset(size, x, y); + const entry: bg.Screen.Entry = @bitCast(bus.read(u16, entry_addr)); + + // Calculate the Address of the Tile in the designated Charblock + // We also take this opportunity to flip tiles if necessary + const tile_id: u32 = entry.tile_id.read(); + + // Calculate row and column offsets. Understand that + // `tile_len`, `tile_row_offset` and `col` are subject to different + // values depending on whether we are in 4bpp or 8bpp mode. + const row = @as(u3, @truncate(y)) ^ if (entry.v_flip.read()) 7 else @as(u3, 0); + const col = @as(u3, @truncate(x)) ^ if (entry.h_flip.read()) 7 else @as(u3, 0); + const tile_addr = char_base + (tile_id * tile_len) + (row * tile_row_offset) + if (is_8bpp) col else col >> 1; + + const tile = bus.read(u8, 0x0600_0000 + tile_addr); + // If we're in 8bpp, then the tile value is an index into the palette, + // If we're in 4bpp, we have to account for a pal bank value in the Screen entry + // and then we can index the palette + const pal_id: u16 = if (!is_8bpp) get4bppTilePalette(entry.pal_bank.read(), col, tile) else tile; + + if (pal_id != 0) self.drawBackgroundPixel(layer, i, bus.read(u16, @as(u32, 0x0500_0000) + pal_id * 2)); + } + } + + inline fn get4bppTilePalette(pal_bank: u4, col: u3, tile: u8) u8 { + const nybble_tile = tile >> ((col & 1) << 2) & 0xF; + if (nybble_tile == 0) return 0; + + return (@as(u8, pal_bank) << 4) | nybble_tile; + } + + fn renderTextMode(self: *@This(), frame_buf: []u32) void { + for (self.scanline.top(), 0..) |maybe_top, i| { + const maybe_btm = self.scanline.btm()[i]; + _ = maybe_btm; + + const bgr555 = switch (maybe_top) { + .set => |px| px, + else => 0xAAAA, + }; + + frame_buf[i] = rgba888(bgr555); + } + + self.scanline.reset(); + } + + // TODO: Comment this + get a better understanding + fn tilemapOffset(size: u2, x: u32, y: u32) u32 { + // Current Row: (y % PIXEL_COUNT) / 8 + // Current COlumn: (x % PIXEL_COUNT) / 8 + // Length of 1 row of Screen Entries: 0x40 + // Length of 1 Screen Entry: 0x2 is the size of a screen entry + @setRuntimeSafety(false); + + return switch (size) { + 0 => (x % 256 / 8) * 2 + (y % 256 / 8) * 0x40, // 256 x 256 + 1 => blk: { + // 512 x 256 + const offset: u32 = if (x & 0x1FF > 0xFF) 0x800 else 0; + break :blk offset + (x % 256 / 8) * 2 + (y % 256 / 8) * 0x40; + }, + 2 => blk: { + // 256 x 512 + const offset: u32 = if (y & 0x1FF > 0xFF) 0x800 else 0; + break :blk offset + (x % 256 / 8) * 2 + (y % 256 / 8) * 0x40; + }, + 3 => blk: { + // 512 x 512 + const offset: u32 = if (x & 0x1FF > 0xFF) 0x800 else 0; + const offset_2: u32 = if (y & 0x1FF > 0xFF) 0x800 else 0; + break :blk offset + offset_2 + (x % 256 / 8) * 2 + (y % 512 / 8) * 0x40; + }, + }; + } + + fn drawBackgroundPixel(self: *@This(), comptime layer: u2, i: u32, bgr555: u16) void { + _ = layer; + + self.scanline.top()[i] = Scanline.Pixel.from(.Background, bgr555); + } }; } +const Scanline = struct { + const Pixel = union(enum) { + // TODO: Rename + const Layer = enum { Background, Sprite }; + + set: u16, + obj_set: u16, + unset: void, + hidden: void, + + fn from(comptime layer: Layer, bgr555: u16) Pixel { + return switch (layer) { + .Background => .{ .set = bgr555 }, + .Sprite => .{ .obj_set = bgr555 }, + }; + } + + pub fn isSet(self: @This()) bool { + return switch (self) { + .set, .obj_set => true, + .unset, .hidden => false, + }; + } + }; + + layers: [2][]Pixel, + buf: []Pixel, + + fn init(allocator: std.mem.Allocator) !@This() { + const buf = try allocator.alloc(Pixel, width * 2); // Top & Bottom Scanline + @memset(buf, .unset); + + return .{ + // Top & Bototm Layers + .layers = [_][]Pixel{ buf[0..][0..width], buf[width..][0..width] }, + .buf = buf, + }; + } + + fn reset(self: *@This()) void { + @memset(self.buf, .unset); + } + + fn deinit(self: @This(), allocator: std.mem.Allocator) void { + allocator.free(self.buf); + } + + fn top(self: *@This()) []Pixel { + return self.layers[0]; + } + + fn btm(self: *@This()) []Pixel { + return self.layers[1]; + } +}; + +const bg = struct { + const Text = struct { + const io = @import("../nds9/io.zig"); + + /// Read / Write + cnt: io.Bgcnt = .{ .raw = 0x0000 }, + /// Write Only + hofs: io.Hofs = .{ .raw = 0x0000 }, + /// Write Only + vofs: io.Vofs = .{ .raw = 0x0000 }, + }; + + const Screen = struct { + const Entry = extern union { + const Bitfield = @import("bitfield").Bitfield; + const Bit = @import("bitfield").Bit; + + tile_id: Bitfield(u16, 0, 10), + h_flip: Bit(u16, 10), + v_flip: Bit(u16, 11), + pal_bank: Bitfield(u16, 12, 4), + raw: u16, + }; + }; + + const Affine = @compileError("TODO: Implement Affine Backgrounds"); +}; + inline fn rgba888(bgr555: u16) u32 { const b: u32 = bgr555 >> 10 & 0x1F; const g: u32 = bgr555 >> 5 & 0x1F;