chore: reimplement alpha blending

This commit is contained in:
Rekai Nyangadzayi Musuka 2022-11-17 12:16:35 -04:00
parent 4ceed382ed
commit acdb270793
1 changed files with 232 additions and 112 deletions

View File

@ -398,7 +398,7 @@ pub const Ppu = struct {
// Sprite Palette starts at 0x0500_0200 // Sprite Palette starts at 0x0500_0200
if (pal_id != 0) { if (pal_id != 0) {
const bgr555 = self.palette.read(u16, 0x200 + pal_id * 2); const bgr555 = self.palette.read(u16, 0x200 + pal_id * 2);
copyToSpriteBuffer(self.bld.cnt, &self.scanline, x, bgr555); drawSpritePixel(self.bld.cnt, &self.scanline, x, bgr555);
} }
} }
} }
@ -448,7 +448,7 @@ pub const Ppu = struct {
// Sprite Palette starts at 0x0500_0200 // Sprite Palette starts at 0x0500_0200
if (pal_id != 0) { if (pal_id != 0) {
const bgr555 = self.palette.read(u16, 0x200 + pal_id * 2); const bgr555 = self.palette.read(u16, 0x200 + pal_id * 2);
copyToSpriteBuffer(self.bld.cnt, &self.scanline, x, bgr555); drawSpritePixel(self.bld.cnt, &self.scanline, x, bgr555);
} }
} }
} }
@ -493,10 +493,7 @@ pub const Ppu = struct {
const tile_addr = char_base + (tile_id * 0x40) + (row * 0x8) + col; const tile_addr = char_base + (tile_id * 0x40) + (row * 0x8) + col;
const pal_id: u16 = self.vram.buf[tile_addr]; const pal_id: u16 = self.vram.buf[tile_addr];
if (pal_id != 0) { if (pal_id != 0) self.drawBackgroundPixel(n, i, self.palette.read(u16, pal_id * 2));
const bgr555 = self.palette.read(u16, pal_id * 2);
self.copyToBackgroundBuffer(n, win_bounds, i, bgr555);
}
} }
// Update BGxX and BGxY // Update BGxX and BGxY
@ -551,10 +548,7 @@ pub const Ppu = struct {
// and then we can index the palette // and then we can index the palette
const pal_id: u16 = if (!is_8bpp) get4bppTilePalette(entry.pal_bank.read(), col, tile) else tile; const pal_id: u16 = if (!is_8bpp) get4bppTilePalette(entry.pal_bank.read(), col, tile) else tile;
if (pal_id != 0) { if (pal_id != 0) self.drawBackgroundPixel(n, i, self.palette.read(u16, pal_id * 2));
const bgr555 = self.palette.read(u16, pal_id * 2);
self.copyToBackgroundBuffer(n, win_bounds, i, bgr555);
}
} }
} }
@ -669,8 +663,7 @@ pub const Ppu = struct {
// FIXME: @ptrCast between slices changing the length isn't implemented yet // FIXME: @ptrCast between slices changing the length isn't implemented yet
const framebuf = @ptrCast([*]u32, @alignCast(@alignOf(u32), self.framebuf.get(.Emulator))); const framebuf = @ptrCast([*]u32, @alignCast(@alignOf(u32), self.framebuf.get(.Emulator)));
for (self.scanline.top()) |maybe_px, i| { for (self.scanline.top()) |maybe_top, i| {
const maybe_top = maybe_px;
const maybe_btm = self.scanline.btm()[i]; const maybe_btm = self.scanline.btm()[i];
const bgr555 = self.getBgr555(maybe_top, maybe_btm); const bgr555 = self.getBgr555(maybe_top, maybe_btm);
@ -683,12 +676,25 @@ pub const Ppu = struct {
std.mem.set(?Sprite, self.scanline_sprites, null); std.mem.set(?Sprite, self.scanline_sprites, null);
} }
fn getBgr555(self: *Self, maybe_top: ?u16, maybe_btm: ?u16) u16 { fn getBgr555(self: *Self, maybe_top: Scanline.Pixel, maybe_btm: Scanline.Pixel) u16 {
if (maybe_btm) |btm| {
return switch (self.bld.cnt.mode.read()) { return switch (self.bld.cnt.mode.read()) {
0b00 => if (maybe_top) |top| top else btm, 0b00 => switch (maybe_top) {
0b01 => if (maybe_top) |top| alphaBlend(btm, top, self.bld.alpha) else btm, .set => |top| top,
0b10 => blk: { else => self.palette.backdrop(),
},
0b01 => switch (maybe_top) {
.set => |top| switch (maybe_btm) {
.set => |btm| alphaBlend(top, btm, self.bld.alpha), // ALPHA_BLEND
else => top,
},
else => switch (maybe_btm) {
.set => |btm| btm,
else => self.palette.backdrop(),
},
},
0b10 => switch (maybe_btm) {
.set => |btm| blk: {
// BLD_WHITE
const evy: u16 = self.bld.y.evy.read(); const evy: u16 = self.bld.y.evy.read();
const r = btm & 0x1F; const r = btm & 0x1F;
@ -701,51 +707,87 @@ pub const Ppu = struct {
break :blk (bld_b << 10) | (bld_g << 5) | bld_r; break :blk (bld_b << 10) | (bld_g << 5) | bld_r;
}, },
0b11 => blk: { else => switch (maybe_top) {
.set => |top| top,
else => self.palette.backdrop(),
},
},
0b11 => switch (maybe_btm) {
.set => |btm| blk: {
// BLD_BLACK
const evy: u16 = self.bld.y.evy.read(); const evy: u16 = self.bld.y.evy.read();
const btm_r = btm & 0x1F; const r = btm & 0x1F;
const btm_g = (btm >> 5) & 0x1F; const g = (btm >> 5) & 0x1F;
const btm_b = (btm >> 10) & 0x1F; const b = (btm >> 10) & 0x1F;
const bld_r = btm_r - ((btm_r * evy) >> 4); const bld_r = r - ((r * evy) >> 4);
const bld_g = btm_g - ((btm_g * evy) >> 4); const bld_g = g - ((g * evy) >> 4);
const bld_b = btm_b - ((btm_b * evy) >> 4); const bld_b = b - ((b * evy) >> 4);
break :blk (bld_b << 10) | (bld_g << 5) | bld_r; break :blk (bld_b << 10) | (bld_g << 5) | bld_r;
}, },
else => switch (maybe_top) {
.set => |top| top,
else => self.palette.backdrop(),
},
},
}; };
} }
if (maybe_top) |top| return top; fn drawBackgroundPixel(self: *Self, comptime layer: u2, i: usize, bgr555: u16) void {
return self.palette.backdrop(); // When writing to the scanline buffer, we want to be aware of a top and bottom layer. Some preconditions were
} // already determined by shouldDrawBackground, so we should be aware of what we can assume to be true or false
fn copyToBackgroundBuffer(self: *Self, comptime n: u2, bounds: ?WindowBounds, i: usize, bgr555: u16) void { switch (self.bld.cnt.mode.read()) {
if (self.bld.cnt.mode.read() != 0b00) { 0b00 => {}, // pass through
// Standard Alpha Blending 0b01 => {
const a_layers = self.bld.cnt.layer_a.read(); // We are to alpha blend here so we should pay attention to which layer ths pixel should be written to
const is_blend_enabled = (a_layers >> n) & 1 == 1; // FIXME: We redo work here that we've already figured out. Is this worth refactorning?
// If Alpha Blending is enabled and we've found an eligible layer for // If the current layer is makred as Layer A, write to top buffer
// Pixel A, store the pixel in the bottom pixel buffer const top_layer = self.bld.cnt.layer_a.read();
const is_top_layer = (top_layer >> layer) & 1 == 1;
const win_part = if (bounds) |win| blk: { if (is_top_layer) {
// Window Enabled self.scanline.top()[i] = Scanline.Pixel.from(bgr555);
break :blk switch (win) {
.win0 => self.win.in.w0_bld.read(),
.win1 => self.win.in.w1_bld.read(),
.out => self.win.out.out_bld.read(),
};
} else true;
if (win_part and is_blend_enabled) {
self.scanline.btm()[i] = bgr555;
return; return;
} }
// If the current layer is marked as Layer B, we want to continue if there's an available space on that buffer
const btm_layer = self.bld.cnt.layer_b.read();
const is_btm_layer = (btm_layer >> layer) & 1 == 1;
if (is_btm_layer) {
self.scanline.btm()[i] = Scanline.Pixel.from(bgr555);
return;
} }
self.scanline.top()[i] = bgr555; // The code we're about to fall-through to assumes that alpha blending takes place. In order to withold all invariants
// we need to discard anything that might be in the bottom buffer.
self.scanline.btm()[i] = .hidden;
},
0b10, 0b11 => {
// BLD_WHITE, BLD_BLACK
// Weare to blend with White or black here. By convention we store regular ol' pixels in the top layer, which means that if we want to
// treat some pixels (in this case the ones relegated to blending) we need to keep them separate as we can't apply the blending to the top layer.
// While in these modes, (and since this is a scanline renderer), the bottom layer will be completely unused. While it's a bit unintuitive, since we'll
// be moving layer A pixels there, we will repurpose the bottom layer as the "to blend", layer
// If the current layer is makred as Layer A, write to top buffer
const top_layer = self.bld.cnt.layer_a.read();
const is_top_layer = (top_layer >> layer) & 1 == 1;
if (is_top_layer) {
self.scanline.btm()[i] = Scanline.Pixel.from(bgr555); // this is intentional
return;
}
},
}
// If we aren't blending here at all, just add the pixel to the top layer
self.scanline.top()[i] = Scanline.Pixel.from(bgr555);
} }
const WindowBounds = enum { win0, win1, out }; const WindowBounds = enum { win0, win1, out };
@ -763,48 +805,76 @@ pub const Ppu = struct {
return .out; return .out;
} }
fn shouldDrawBackground(self: *Self, comptime n: u2, bounds: ?WindowBounds, i: usize) bool { fn shouldDrawBackground(self: *Self, comptime layer: u2, bounds: ?WindowBounds, i: usize) bool {
// If a pixel has been drawn on the top layer, it's because: switch (self.bld.cnt.mode.read()) {
// 1. The pixel is to be blended with a pixel on the bottom layer 0b00 => if (self.scanline.top()[i] == .set) return false, // pass through
// 2. The pixel is not to be blended at all 0b01 => blk: {
// Also, if we find a pixel on the top layer we don't need to bother with this I think? // BLD_ALPHA
if (self.scanline.top()[i] != null) return false;
if (bounds) |win| { // If the current layer is marked as Layer B, we want to continue if there's an available space on that buffer
switch (win) { const btm_layer = self.bld.cnt.layer_b.read();
.win0 => if ((self.win.in.w0_bg.read() >> n) & 1 == 0) return false, const is_btm_layer = (btm_layer >> layer) & 1 == 1;
.win1 => if ((self.win.in.w1_bg.read() >> n) & 1 == 0) return false,
.out => if ((self.win.out.out_bg.read() >> n) & 1 == 0) return false, if (is_btm_layer) {
if (self.scanline.btm()[i] == .set) return false;
// In some previous iteration we have determined that an opaque pixel was drawn at this position
// therefore there's no reason to draw anything here
if (self.scanline.btm()[i] == .hidden) return false;
// We have a pixel and we know it to be a part of hte bottom layer.
// when getBgr555 sees that thre's a pixel in the top and bottom layer it chooses to blend the two
// Meaning that if we want to prevent Alpha Blending from happening (like for example if a window is preventing it)
// we need to make that happen now.
// We can do this by not drawing the bottom pixel, since with alpha blending disabled it wouldn't be visible anyways
// if (bounds) |win| {
// switch (win) {
// .win0 => if (!self.win.in.w0_bld.read()) return false,
// .win1 => if (!self.win.in.w1_bld.read()) return false,
// .out => if (!self.win.out.out_bld.read()) return false,
// }
// }
break :blk;
}
if (self.scanline.top()[i] == .set) return false;
},
0b10, 0b11 => {
// BLD_WHITE and BLD_BLACK
// we want to treat the bottom layer the same as the top (despite it being repurposed)
// so we should apply the same logic to the bottom layer
if (self.scanline.top()[i] == .set) return false;
if (self.scanline.btm()[i] == .set) return false;
},
}
// At this point we will have exited early if we determined that we'd be overwriting a pixel
// with a higher priority. We can now move own to determining whether the pixel is visible or not
// The first thing that may or may not affect visibility is windowing. We should check to see if ths pixel is in bounds
// of of the background Window if it is enabled
// TODO: Do Window Bounds checking here instead of outside this function?
if (bounds) |window| {
// If this parameter is non-null, we know that:
// 1. Win0, Win1 or WinObj are enabled
// 2. This specific pixel exists within the range of a window
// Here, we check to see if the Window for this background is enabled. If not, we won't render the pixel
// FIXME: We perform needless computations on Window Bounds by checking for enable here after we've already computed this information
switch (window) {
.win0 => if ((self.win.in.w0_bg.read() >> layer) & 1 == 0) return false,
.win1 => if ((self.win.in.w1_bg.read() >> layer) & 1 == 0) return false,
.out => if ((self.win.out.out_bg.read() >> layer) & 1 == 0) return false,
} }
} }
if (self.scanline.btm()[i] != null) { // Otherwise, return true
// The pixel found in the bottom layer is:
// 1. From a higher priority background
// 2. From a background that is marked for blending (Pixel A)
// If Alpha Blending isn't enabled, then we've already found a higher prio
// pixel, we can return early
if (self.bld.cnt.mode.read() != 0b01) return false;
const b_layers = self.bld.cnt.layer_b.read();
const win_part = if (bounds) |win| blk: {
// Window Enabled
break :blk switch (win) {
.win0 => self.win.in.w0_bld.read(),
.win1 => self.win.in.w1_bld.read(),
.out => self.win.out.out_bld.read(),
};
} else true;
// If the Background is not marked for blending, we've already found
// a higher priority pixel, move on.
const is_blend_enabled = win_part and ((b_layers >> n) & 1 == 1);
if (!is_blend_enabled) return false;
}
return true; return true;
} }
@ -1287,56 +1357,106 @@ fn alphaBlend(top: u16, btm: u16, bldalpha: io.BldAlpha) u16 {
} }
fn shouldDrawSprite(bldcnt: io.BldCnt, scanline: *Scanline, x: u9) bool { fn shouldDrawSprite(bldcnt: io.BldCnt, scanline: *Scanline, x: u9) bool {
if (scanline.top()[x] != null) return false; if (scanline.top()[x] == .set) return false;
if (scanline.btm()[x] != null) { switch (bldcnt.mode.read()) {
if (bldcnt.mode.read() != 0b01) return false; 0b00 => if (scanline.top()[x] == .set) return false, // pass through
0b01 => {
// BLD_ALPHA
const b_layers = bldcnt.layer_b.read(); // We want to check if we're concerned aout the bottom layer first
const is_blend_enabled = (b_layers >> 4) & 1 == 1; // because if so, the top layer already having a pixel is OK
if (!is_blend_enabled) return false; const btm_layers = bldcnt.layer_b.read();
const is_btm_layer = (btm_layers >> 4) & 1 == 1;
if (is_btm_layer and scanline.btm()[x] == .set) return false;
if (scanline.top()[x] == .set) return false;
},
0b10, 0b11 => {
if (scanline.top()[x] == .set) return false;
if (scanline.btm()[x] == .set) return false;
},
} }
return true; return true;
} }
fn copyToSpriteBuffer(bldcnt: io.BldCnt, scanline: *Scanline, x: u9, bgr555: u16) void { fn drawSpritePixel(bldcnt: io.BldCnt, scanline: *Scanline, x: u9, bgr555: u16) void {
if (bldcnt.mode.read() != 0b00) { switch (bldcnt.mode.read()) {
// Alpha Blending 0b00 => {}, // pass through
const a_layers = bldcnt.layer_a.read(); 0b01 => {
const is_blend_enabled = (a_layers >> 4) & 1 == 1; // BLD_ALPHA
const top_layers = bldcnt.layer_a.read();
const is_top_layer = (top_layers >> 4) & 1 == 1;
if (is_blend_enabled) { if (is_top_layer) {
scanline.btm()[x] = bgr555; scanline.top()[x] = Scanline.Pixel.from(bgr555);
return; return;
} }
const btm_layers = bldcnt.layer_b.read();
const is_btm_layer = (btm_layers >> 4) & 1 == 1;
if (is_btm_layer) {
scanline.btm()[x] = Scanline.Pixel.from(bgr555);
return;
} }
scanline.top()[x] = bgr555; // We're rendering a normal pixel that isn't alpha blended
// we can mark the pixel on the bottom layer as hidden
scanline.btm()[x] = .hidden;
},
0b10, 0b11 => {
// This is explained in drawBackgroundPixel, we're reusing the bottom layer to draw layer A pixels we will want to
// later blend with WHITE or BLACK
const top_layers = bldcnt.layer_a.read();
const is_top_layer = (top_layers >> 4) & 1 == 1;
if (is_top_layer) {
scanline.btm()[x] = Scanline.Pixel.from(bgr555); // This is intentional
return;
}
},
}
scanline.top()[x] = Scanline.Pixel.from(bgr555);
} }
const Scanline = struct { const Scanline = struct {
const Self = @This(); const Self = @This();
layers: [2][]?u16, const Pixel = union(enum) {
buf: []?u16, set: u16,
unset: void,
hidden: void,
fn from(bgr555: u16) Pixel {
return .{ .set = bgr555 };
}
};
layers: [2][]Pixel,
buf: []Pixel,
allocator: Allocator, allocator: Allocator,
fn init(allocator: Allocator) !Self { fn init(allocator: Allocator) !Self {
const buf = try allocator.alloc(?u16, width * 2); // Top & Bottom Scanline const buf = try allocator.alloc(Pixel, width * 2); // Top & Bottom Scanline
std.mem.set(?u16, buf, null); std.mem.set(Pixel, buf, .unset);
return .{ return .{
// Top & Bototm Layers // Top & Bototm Layers
.layers = [_][]?u16{ buf[0..][0..width], buf[width..][0..width] }, .layers = [_][]Pixel{ buf[0..][0..width], buf[width..][0..width] },
.buf = buf, .buf = buf,
.allocator = allocator, .allocator = allocator,
}; };
} }
fn reset(self: *Self) void { fn reset(self: *Self) void {
std.mem.set(?u16, self.buf, null); std.mem.set(Pixel, self.buf, .unset);
} }
fn deinit(self: *Self) void { fn deinit(self: *Self) void {
@ -1344,11 +1464,11 @@ const Scanline = struct {
self.* = undefined; self.* = undefined;
} }
fn top(self: *Self) []?u16 { fn top(self: *Self) []Pixel {
return self.layers[0]; return self.layers[0];
} }
fn btm(self: *Self) []?u16 { fn btm(self: *Self) []Pixel {
return self.layers[1]; return self.layers[1];
} }
}; };