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