Framebuffer: - Add clearRect() for clearing specific rectangular regions SoftwareRenderer: - Add executeWithDirtyRegions() for optimized partial rendering - Add clearRect() convenience method - Add commandBounds() to extract bounding box from draw commands - Add rectsIntersect() helper for intersection testing This enables applications to: 1. Clear only dirty regions instead of full screen 2. Skip rendering commands outside dirty areas 3. Significantly reduce CPU when only small areas change Usage: Pass dirty_regions from Context.getDirtyRects() to executeWithDirtyRegions() instead of using clear()+executeAll(). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
253 lines
7.9 KiB
Zig
253 lines
7.9 KiB
Zig
//! Framebuffer - Pixel buffer for software rendering
|
|
//!
|
|
//! A simple 2D array of RGBA pixels.
|
|
//! The software rasterizer writes to this, then it's blitted to the screen.
|
|
|
|
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
const Style = @import("../core/style.zig");
|
|
const Color = Style.Color;
|
|
|
|
/// A 2D pixel buffer
|
|
pub const Framebuffer = struct {
|
|
allocator: Allocator,
|
|
pixels: []u32,
|
|
width: u32,
|
|
height: u32,
|
|
|
|
const Self = @This();
|
|
|
|
/// Create a new framebuffer
|
|
pub fn init(allocator: Allocator, width: u32, height: u32) !Self {
|
|
const size = @as(usize, width) * @as(usize, height);
|
|
const pixels = try allocator.alloc(u32, size);
|
|
@memset(pixels, 0);
|
|
|
|
return .{
|
|
.allocator = allocator,
|
|
.pixels = pixels,
|
|
.width = width,
|
|
.height = height,
|
|
};
|
|
}
|
|
|
|
/// Free the framebuffer
|
|
pub fn deinit(self: Self) void {
|
|
self.allocator.free(self.pixels);
|
|
}
|
|
|
|
/// Resize the framebuffer
|
|
pub fn resize(self: *Self, width: u32, height: u32) !void {
|
|
if (width == self.width and height == self.height) return;
|
|
|
|
const size = @as(usize, width) * @as(usize, height);
|
|
const new_pixels = try self.allocator.alloc(u32, size);
|
|
@memset(new_pixels, 0);
|
|
|
|
self.allocator.free(self.pixels);
|
|
self.pixels = new_pixels;
|
|
self.width = width;
|
|
self.height = height;
|
|
}
|
|
|
|
/// Clear the entire buffer to a color
|
|
pub fn clear(self: *Self, color: Color) void {
|
|
const c = color.toABGR();
|
|
@memset(self.pixels, c);
|
|
}
|
|
|
|
/// Clear a rectangular region to a color (for partial redraw)
|
|
pub fn clearRect(self: *Self, x: i32, y: i32, w: u32, h: u32, color: Color) void {
|
|
const x_start: u32 = if (x < 0) 0 else @intCast(@min(x, @as(i32, @intCast(self.width))));
|
|
const y_start: u32 = if (y < 0) 0 else @intCast(@min(y, @as(i32, @intCast(self.height))));
|
|
const x_end = @min(x_start + w, self.width);
|
|
const y_end = @min(y_start + h, self.height);
|
|
|
|
if (x_start >= x_end or y_start >= y_end) return;
|
|
|
|
const c = color.toABGR();
|
|
const row_width = x_end - x_start;
|
|
|
|
var py = y_start;
|
|
while (py < y_end) : (py += 1) {
|
|
const row_start = py * self.width + x_start;
|
|
@memset(self.pixels[row_start..][0..row_width], c);
|
|
}
|
|
}
|
|
|
|
/// Get pixel at (x, y)
|
|
pub fn getPixel(self: Self, x: i32, y: i32) ?u32 {
|
|
if (x < 0 or y < 0) return null;
|
|
const ux = @as(u32, @intCast(x));
|
|
const uy = @as(u32, @intCast(y));
|
|
if (ux >= self.width or uy >= self.height) return null;
|
|
|
|
const idx = uy * self.width + ux;
|
|
return self.pixels[idx];
|
|
}
|
|
|
|
/// Set pixel at (x, y)
|
|
pub fn setPixel(self: *Self, x: i32, y: i32, color: Color) void {
|
|
if (x < 0 or y < 0) return;
|
|
const ux = @as(u32, @intCast(x));
|
|
const uy = @as(u32, @intCast(y));
|
|
if (ux >= self.width or uy >= self.height) return;
|
|
|
|
const idx = uy * self.width + ux;
|
|
|
|
if (color.a == 255) {
|
|
self.pixels[idx] = color.toABGR();
|
|
} else if (color.a > 0) {
|
|
// Blend with existing pixel
|
|
const existing = self.pixels[idx];
|
|
const bg = Color{
|
|
.r = @truncate(existing),
|
|
.g = @truncate(existing >> 8),
|
|
.b = @truncate(existing >> 16),
|
|
.a = @truncate(existing >> 24),
|
|
};
|
|
self.pixels[idx] = color.blend(bg).toABGR();
|
|
}
|
|
}
|
|
|
|
/// Draw a filled rectangle
|
|
pub fn fillRect(self: *Self, x: i32, y: i32, w: u32, h: u32, color: Color) void {
|
|
const x_start = @max(0, x);
|
|
const y_start = @max(0, y);
|
|
const x_end = @min(@as(i32, @intCast(self.width)), x + @as(i32, @intCast(w)));
|
|
const y_end = @min(@as(i32, @intCast(self.height)), y + @as(i32, @intCast(h)));
|
|
|
|
if (x_start >= x_end or y_start >= y_end) return;
|
|
|
|
const c = color.toABGR();
|
|
|
|
var py = y_start;
|
|
while (py < y_end) : (py += 1) {
|
|
const row_start = @as(u32, @intCast(py)) * self.width;
|
|
var px = x_start;
|
|
while (px < x_end) : (px += 1) {
|
|
const idx = row_start + @as(u32, @intCast(px));
|
|
if (color.a == 255) {
|
|
self.pixels[idx] = c;
|
|
} else if (color.a > 0) {
|
|
const existing = self.pixels[idx];
|
|
const bg = Color{
|
|
.r = @truncate(existing),
|
|
.g = @truncate(existing >> 8),
|
|
.b = @truncate(existing >> 16),
|
|
.a = @truncate(existing >> 24),
|
|
};
|
|
self.pixels[idx] = color.blend(bg).toABGR();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Draw a rectangle outline
|
|
pub fn drawRect(self: *Self, x: i32, y: i32, w: u32, h: u32, color: Color) void {
|
|
if (w == 0 or h == 0) return;
|
|
|
|
// Top and bottom
|
|
self.fillRect(x, y, w, 1, color);
|
|
self.fillRect(x, y + @as(i32, @intCast(h)) - 1, w, 1, color);
|
|
|
|
// Left and right
|
|
self.fillRect(x, y + 1, 1, h -| 2, color);
|
|
self.fillRect(x + @as(i32, @intCast(w)) - 1, y + 1, 1, h -| 2, color);
|
|
}
|
|
|
|
/// Draw a horizontal line
|
|
pub fn drawHLine(self: *Self, x: i32, y: i32, w: u32, color: Color) void {
|
|
self.fillRect(x, y, w, 1, color);
|
|
}
|
|
|
|
/// Draw a vertical line
|
|
pub fn drawVLine(self: *Self, x: i32, y: i32, h: u32, color: Color) void {
|
|
self.fillRect(x, y, 1, h, color);
|
|
}
|
|
|
|
/// Draw a line (Bresenham's algorithm)
|
|
pub fn drawLine(self: *Self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) void {
|
|
var x = x0;
|
|
var y = y0;
|
|
|
|
const dx = @abs(x1 - x0);
|
|
const dy = @abs(y1 - y0);
|
|
const sx: i32 = if (x0 < x1) 1 else -1;
|
|
const sy: i32 = if (y0 < y1) 1 else -1;
|
|
var err = @as(i32, @intCast(dx)) - @as(i32, @intCast(dy));
|
|
|
|
while (true) {
|
|
self.setPixel(x, y, color);
|
|
|
|
if (x == x1 and y == y1) break;
|
|
|
|
const e2 = err * 2;
|
|
if (e2 > -@as(i32, @intCast(dy))) {
|
|
err -= @as(i32, @intCast(dy));
|
|
x += sx;
|
|
}
|
|
if (e2 < @as(i32, @intCast(dx))) {
|
|
err += @as(i32, @intCast(dx));
|
|
y += sy;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Get raw pixel data (for blitting to SDL texture)
|
|
pub fn getData(self: Self) []const u32 {
|
|
return self.pixels;
|
|
}
|
|
|
|
/// Get pitch in bytes
|
|
pub fn getPitch(self: Self) u32 {
|
|
return self.width * 4;
|
|
}
|
|
};
|
|
|
|
// =============================================================================
|
|
// Tests
|
|
// =============================================================================
|
|
|
|
test "Framebuffer basic" {
|
|
var fb = try Framebuffer.init(std.testing.allocator, 100, 100);
|
|
defer fb.deinit();
|
|
|
|
fb.clear(Color.black);
|
|
fb.setPixel(50, 50, Color.white);
|
|
|
|
const pixel = fb.getPixel(50, 50);
|
|
try std.testing.expect(pixel != null);
|
|
}
|
|
|
|
test "Framebuffer fillRect" {
|
|
var fb = try Framebuffer.init(std.testing.allocator, 100, 100);
|
|
defer fb.deinit();
|
|
|
|
fb.clear(Color.black);
|
|
fb.fillRect(10, 10, 20, 20, Color.red);
|
|
|
|
// Check inside
|
|
const inside = fb.getPixel(15, 15);
|
|
try std.testing.expect(inside != null);
|
|
try std.testing.expectEqual(Color.red.toABGR(), inside.?);
|
|
|
|
// Check outside
|
|
const outside = fb.getPixel(5, 5);
|
|
try std.testing.expect(outside != null);
|
|
try std.testing.expectEqual(Color.black.toABGR(), outside.?);
|
|
}
|
|
|
|
test "Framebuffer out of bounds" {
|
|
var fb = try Framebuffer.init(std.testing.allocator, 100, 100);
|
|
defer fb.deinit();
|
|
|
|
// These should not crash
|
|
fb.setPixel(-1, 50, Color.white);
|
|
fb.setPixel(50, -1, Color.white);
|
|
fb.setPixel(100, 50, Color.white);
|
|
fb.setPixel(50, 100, Color.white);
|
|
|
|
try std.testing.expect(fb.getPixel(-1, 50) == null);
|
|
}
|