zcatgui/src/render/framebuffer.zig
reugenio e5ba9b178c feat(render): Add partial redraw support with dirty regions
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>
2025-12-10 12:12:21 +01:00

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);
}