fix: implement sprite coord overflow behaviour
This commit is contained in:
parent
0c50ef1e6d
commit
cb10dfbdfd
159
src/ppu.zig
159
src/ppu.zig
|
@ -93,12 +93,19 @@ pub const Ppu = struct {
|
||||||
// Only consider enabled sprites
|
// Only consider enabled sprites
|
||||||
if (sprite.isDisabled()) continue;
|
if (sprite.isDisabled()) continue;
|
||||||
|
|
||||||
// Determine sprite bounds
|
// When fetching sprites we only care about ones that could be rendered
|
||||||
// We only care about the Y axis since that value remains constant
|
// on this scanline
|
||||||
const start = sprite.y();
|
const iy = @bitCast(i8, y);
|
||||||
const end = start + sprite.height;
|
|
||||||
|
|
||||||
if (start <= y and y < end) {
|
const start = sprite.y();
|
||||||
|
const istart = @bitCast(i8, start);
|
||||||
|
|
||||||
|
const end = start +% sprite.height;
|
||||||
|
const iend = @bitCast(i8, end);
|
||||||
|
|
||||||
|
// Sprites are expected to be able to wraparound, we perform the same check
|
||||||
|
// for unsigned and signed values so that we handle all valid sprite positions
|
||||||
|
if ((start <= y and y < end) or (istart <= iy and iy < iend)) {
|
||||||
for (self.scanline_sprites) |*maybe_sprite| {
|
for (self.scanline_sprites) |*maybe_sprite| {
|
||||||
if (maybe_sprite.* == null) {
|
if (maybe_sprite.* == null) {
|
||||||
maybe_sprite.* = sprite;
|
maybe_sprite.* = sprite;
|
||||||
|
@ -112,72 +119,90 @@ pub const Ppu = struct {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Draw all relevant sprites on a scanline
|
||||||
fn drawSprites(self: *Self, prio: u2) void {
|
fn drawSprites(self: *Self, prio: u2) void {
|
||||||
// Object VRAM 3rd and 4th (0-indexed) charblocks
|
// Object VRAM 3rd and 4th (0-indexed) charblocks
|
||||||
const char_base = 0x4000 * 4;
|
const char_base = 0x4000 * 4;
|
||||||
const scanline = self.vcount.scanline.read();
|
const y = @bitCast(i8, self.vcount.scanline.read());
|
||||||
|
|
||||||
var i: u32 = 0;
|
var i: u9 = 0;
|
||||||
while (i < width) : (i += 1) {
|
while (i < width) : (i += 1) {
|
||||||
// Exit early if a pixel is already here
|
// Exit early if a pixel is already here
|
||||||
if (self.scanline_buf[i] != null) continue;
|
if (self.scanline_buf[i] != null) continue;
|
||||||
|
|
||||||
const x = i;
|
const x = i;
|
||||||
const y = scanline;
|
|
||||||
|
|
||||||
for (self.scanline_sprites) |maybe_sprite| {
|
for (self.scanline_sprites) |maybe_sprite| {
|
||||||
if (maybe_sprite) |sprite| {
|
if (maybe_sprite) |sprite| {
|
||||||
if (sprite.priority() != prio) continue;
|
if (sprite.priority() != prio) continue;
|
||||||
|
|
||||||
|
const ix = @bitCast(i9, x);
|
||||||
|
|
||||||
const start = sprite.x();
|
const start = sprite.x();
|
||||||
const end = start + sprite.width;
|
const istart = @bitCast(i9, start);
|
||||||
|
|
||||||
if (start <= x and x < end) {
|
const end = start +% sprite.width;
|
||||||
|
const iend = @bitCast(i9, end);
|
||||||
|
|
||||||
// FIXME: We branch on this condition quite a lot
|
// Sprites are expected to wrap, by performing the same check on both
|
||||||
const is_8bpp = sprite.is_8bpp();
|
// signed and unsigned values we ensure that sprites are properly displayed
|
||||||
|
// in all valid scenarios
|
||||||
// Y and X coordinates within the context of a singular 8x8 tile
|
if ((start <= x and x < end) or (istart <= ix and ix < iend)) {
|
||||||
const tile_y: u16 = (y - sprite.y()) ^ if (sprite.v_flip()) (sprite.height - 1) else 0;
|
self.drawSpritePixel(char_base, sprite, ix, y);
|
||||||
const tile_x = (x - sprite.x()) ^ if (sprite.h_flip()) (sprite.width - 1) else 0;
|
|
||||||
|
|
||||||
const tile_id: u32 = sprite.tile_id();
|
|
||||||
const tile_row_offset: u32 = if (is_8bpp) 8 else 4;
|
|
||||||
const tile_len: u32 = if (is_8bpp) 0x40 else 0x20;
|
|
||||||
|
|
||||||
const row = tile_y % 8;
|
|
||||||
const col = tile_x % 8;
|
|
||||||
|
|
||||||
const tile_base: u32 = char_base + (0x20 * tile_id) + (tile_row_offset * row) + if (is_8bpp) col else col / 2;
|
|
||||||
|
|
||||||
var tile_offset = (tile_x / 8) * tile_len;
|
|
||||||
if (self.dispcnt.obj_mapping.read()) {
|
|
||||||
// One Dimensional
|
|
||||||
tile_offset += (tile_y / 8) * tile_len * (sprite.width / 8);
|
|
||||||
} else {
|
|
||||||
// Two Dimensional
|
|
||||||
// TODO: This doesn't work
|
|
||||||
tile_offset += (tile_y / 8) * tile_len * 0x20;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tile = self.vram.buf[tile_base + tile_offset];
|
|
||||||
|
|
||||||
const pal_id: u16 = if (!is_8bpp) blk: {
|
|
||||||
const nybble_tile = if (col & 1 == 1) tile >> 4 else tile & 0xF;
|
|
||||||
if (nybble_tile == 0) break :blk 0;
|
|
||||||
|
|
||||||
const pal_bank = @as(u8, sprite.pal_bank()) << 4;
|
|
||||||
break :blk pal_bank | nybble_tile;
|
|
||||||
} else tile;
|
|
||||||
|
|
||||||
// Sprite Palette starts at 0x0500_0200
|
|
||||||
if (pal_id != 0) self.scanline_buf[i] = self.palette.get16(0x200 + pal_id * 2);
|
|
||||||
}
|
}
|
||||||
} else break;
|
} else break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Draw a Pixel of a Sprite Tile
|
||||||
|
fn drawSpritePixel(self: *Self, char_base: u32, sprite: Sprite, x: i9, y: i8) void {
|
||||||
|
// FIXME: We branch on this condition quite a lot
|
||||||
|
const is_8bpp = sprite.is_8bpp();
|
||||||
|
|
||||||
|
// std.math.absInt is branchless
|
||||||
|
const x_diff = @bitCast(u9, std.math.absInt(x - @bitCast(i9, sprite.x())) catch unreachable);
|
||||||
|
const y_diff = @bitCast(u8, std.math.absInt(y -% @bitCast(i8, sprite.y())) catch unreachable);
|
||||||
|
|
||||||
|
// Note that we flip the tile_pos not the (tile_pos % 8) like we do for
|
||||||
|
// Background Tiles. By doing this we mirror the entire sprite instead of
|
||||||
|
// just a specific tile (see how sprite.width and sprite.height are involved)
|
||||||
|
const tile_y = y_diff ^ if (sprite.v_flip()) (sprite.height - 1) else 0;
|
||||||
|
const tile_x = x_diff ^ if (sprite.h_flip()) (sprite.width - 1) else 0;
|
||||||
|
|
||||||
|
// Like in the background Tiles are 8x8 groups of pixels in 8bpp or 4bpp formats
|
||||||
|
const tile_id = sprite.tile_id();
|
||||||
|
const tile_row_offset: u32 = if (is_8bpp) 8 else 4;
|
||||||
|
const tile_len: u32 = if (is_8bpp) 0x40 else 0x20;
|
||||||
|
|
||||||
|
const row = tile_y % 8;
|
||||||
|
const col = tile_x % 8;
|
||||||
|
|
||||||
|
// When calcualting the inital address, the first entry is always 0x20 * tile_id, even if it is 8bpp
|
||||||
|
const tile_base = char_base + (0x20 * @as(u32, tile_id)) + (tile_row_offset * row) + if (is_8bpp) col else col / 2;
|
||||||
|
|
||||||
|
// TODO: Understand more
|
||||||
|
var tile_offset = (tile_x / 8) * tile_len;
|
||||||
|
if (self.dispcnt.obj_mapping.read()) {
|
||||||
|
tile_offset += (tile_y / 8) * tile_len * (sprite.width / 8); // 1D Mapping
|
||||||
|
} else {
|
||||||
|
tile_offset += (tile_y / 8) * tile_len * 0x20; // 2D Mapping
|
||||||
|
}
|
||||||
|
|
||||||
|
const tile = self.vram.buf[tile_base + tile_offset];
|
||||||
|
|
||||||
|
const pal_id: u16 = if (!is_8bpp) blk: {
|
||||||
|
const nybble_tile = if (col & 1 == 1) tile >> 4 else tile & 0xF;
|
||||||
|
if (nybble_tile == 0) break :blk 0;
|
||||||
|
|
||||||
|
const pal_bank = @as(u8, sprite.pal_bank()) << 4;
|
||||||
|
break :blk pal_bank | nybble_tile;
|
||||||
|
} else tile;
|
||||||
|
|
||||||
|
// Sprite Palette starts at 0x0500_0200
|
||||||
|
if (pal_id != 0) self.scanline_buf[@bitCast(u9, x)] = self.palette.get16(0x200 + pal_id * 2);
|
||||||
|
}
|
||||||
|
|
||||||
fn drawBackround(self: *Self, comptime n: u3) void {
|
fn drawBackround(self: *Self, comptime n: u3) void {
|
||||||
// A Tile in a charblock is a byte, while a Screen Entry is a halfword
|
// A Tile in a charblock is a byte, while a Screen Entry is a halfword
|
||||||
const charblock_len: u32 = 0x4000;
|
const charblock_len: u32 = 0x4000;
|
||||||
|
@ -228,11 +253,11 @@ pub const Ppu = struct {
|
||||||
// If we're in 8bpp, then the tile value is an index into the palette,
|
// 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
|
// 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
|
// and then we can index the palette
|
||||||
const pal_id = if (!is_8bpp) blk: {
|
const pal_id: u16 = if (!is_8bpp) blk: {
|
||||||
const nybble_tile = if (col & 1 == 1) tile >> 4 else tile & 0xF;
|
const nybble_tile = if (col & 1 == 1) tile >> 4 else tile & 0xF;
|
||||||
if (nybble_tile == 0) break :blk 0;
|
if (nybble_tile == 0) break :blk 0;
|
||||||
|
|
||||||
const pal_bank: u16 = @as(u8, entry.palette_bank.read()) << 4;
|
const pal_bank = @as(u8, entry.pal_bank.read()) << 4;
|
||||||
break :blk pal_bank | nybble_tile;
|
break :blk pal_bank | nybble_tile;
|
||||||
} else tile;
|
} else tile;
|
||||||
|
|
||||||
|
@ -483,7 +508,7 @@ const ScreenEntry = extern union {
|
||||||
tile_id: Bitfield(u16, 0, 10),
|
tile_id: Bitfield(u16, 0, 10),
|
||||||
h_flip: Bit(u16, 10),
|
h_flip: Bit(u16, 10),
|
||||||
v_flip: Bit(u16, 11),
|
v_flip: Bit(u16, 11),
|
||||||
palette_bank: Bitfield(u16, 12, 4),
|
pal_bank: Bitfield(u16, 12, 4),
|
||||||
raw: u16,
|
raw: u16,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -494,8 +519,8 @@ const Sprite = struct {
|
||||||
attr1: Attr1,
|
attr1: Attr1,
|
||||||
attr2: Attr2,
|
attr2: Attr2,
|
||||||
|
|
||||||
width: u16,
|
width: u8,
|
||||||
height: u16,
|
height: u8,
|
||||||
|
|
||||||
fn init(attr0: Attr0, attr1: Attr1, attr2: Attr2) Self {
|
fn init(attr0: Attr0, attr1: Attr1, attr2: Attr2) Self {
|
||||||
const d = spriteDimensions(attr0.shape.read(), attr1.size.read());
|
const d = spriteDimensions(attr0.shape.read(), attr1.size.read());
|
||||||
|
@ -509,7 +534,7 @@ const Sprite = struct {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fn x(self: *const Self) u16 {
|
inline fn x(self: *const Self) u9 {
|
||||||
return self.attr1.x.read();
|
return self.attr1.x.read();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -579,28 +604,28 @@ const Attr2 = extern union {
|
||||||
pal_bank: Bitfield(u16, 12, 4),
|
pal_bank: Bitfield(u16, 12, 4),
|
||||||
};
|
};
|
||||||
|
|
||||||
fn spriteDimensions(shape: u2, size: u2) [2]u16 {
|
fn spriteDimensions(shape: u2, size: u2) [2]u8 {
|
||||||
@setRuntimeSafety(false);
|
@setRuntimeSafety(false);
|
||||||
|
|
||||||
return switch (shape) {
|
return switch (shape) {
|
||||||
0b00 => switch (size) {
|
0b00 => switch (size) {
|
||||||
// Square
|
// Square
|
||||||
0b00 => [_]u16{ 8, 8 },
|
0b00 => [_]u8{ 8, 8 },
|
||||||
0b01 => [_]u16{ 16, 16 },
|
0b01 => [_]u8{ 16, 16 },
|
||||||
0b10 => [_]u16{ 32, 32 },
|
0b10 => [_]u8{ 32, 32 },
|
||||||
0b11 => [_]u16{ 64, 64 },
|
0b11 => [_]u8{ 64, 64 },
|
||||||
},
|
},
|
||||||
0b01 => switch (size) {
|
0b01 => switch (size) {
|
||||||
0b00 => [_]u16{ 16, 8 },
|
0b00 => [_]u8{ 16, 8 },
|
||||||
0b01 => [_]u16{ 32, 8 },
|
0b01 => [_]u8{ 32, 8 },
|
||||||
0b10 => [_]u16{ 32, 16 },
|
0b10 => [_]u8{ 32, 16 },
|
||||||
0b11 => [_]u16{ 64, 32 },
|
0b11 => [_]u8{ 64, 32 },
|
||||||
},
|
},
|
||||||
0b10 => switch (size) {
|
0b10 => switch (size) {
|
||||||
0b00 => [_]u16{ 8, 16 },
|
0b00 => [_]u8{ 8, 16 },
|
||||||
0b01 => [_]u16{ 8, 32 },
|
0b01 => [_]u8{ 8, 32 },
|
||||||
0b10 => [_]u16{ 16, 32 },
|
0b10 => [_]u8{ 16, 32 },
|
||||||
0b11 => [_]u16{ 32, 64 },
|
0b11 => [_]u8{ 32, 64 },
|
||||||
},
|
},
|
||||||
else => std.debug.panic("{} is an invalid sprite shape", .{shape}),
|
else => std.debug.panic("{} is an invalid sprite shape", .{shape}),
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue