From e5ba9b178cac929ffe624159fb4b1106cb82758c Mon Sep 17 00:00:00 2001 From: reugenio Date: Wed, 10 Dec 2025 12:12:21 +0100 Subject: [PATCH] feat(render): Add partial redraw support with dirty regions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/render/framebuffer.zig | 19 +++++++++ src/render/software.zig | 81 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/src/render/framebuffer.zig b/src/render/framebuffer.zig index 87a2646..2121756 100644 --- a/src/render/framebuffer.zig +++ b/src/render/framebuffer.zig @@ -57,6 +57,25 @@ pub const Framebuffer = struct { @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; diff --git a/src/render/software.zig b/src/render/software.zig index 1b2ac9a..4c8a8a6 100644 --- a/src/render/software.zig +++ b/src/render/software.zig @@ -88,11 +88,49 @@ pub const SoftwareRenderer = struct { } } + /// Execute commands only within dirty regions (partial redraw) + /// This is an optimization that skips rendering commands outside dirty areas. + /// For full redraw, pass a single Rect covering the entire screen. + pub fn executeWithDirtyRegions( + self: *Self, + commands: []const DrawCommand, + dirty_regions: []const Rect, + clear_color: Color, + ) void { + // First, clear only the dirty regions + for (dirty_regions) |dirty| { + self.framebuffer.clearRect(dirty.x, dirty.y, dirty.w, dirty.h, clear_color); + } + + // Then render commands, but only if they intersect dirty regions + for (commands) |cmd| { + const cmd_rect = commandBounds(cmd); + if (cmd_rect) |bounds| { + // Check if command intersects any dirty region + var needs_render = false; + for (dirty_regions) |dirty| { + if (rectsIntersect(bounds, dirty)) { + needs_render = true; + break; + } + } + if (!needs_render) continue; + } + // Commands without bounds (clip, clip_end, nop) always execute + self.execute(cmd); + } + } + /// Clear the framebuffer pub fn clear(self: *Self, color: Color) void { self.framebuffer.clear(color); } + /// Clear a rectangular region (for partial redraw) + pub fn clearRect(self: *Self, x: i32, y: i32, w: u32, h: u32, color: Color) void { + self.framebuffer.clearRect(x, y, w, h, color); + } + // ========================================================================= // Private drawing functions // ========================================================================= @@ -246,6 +284,49 @@ pub const SoftwareRenderer = struct { } }; +// ============================================================================= +// Helper functions for partial redraw +// ============================================================================= + +/// Get bounding rectangle of a draw command (null for commands without bounds) +fn commandBounds(cmd: DrawCommand) ?Rect { + return switch (cmd) { + .rect => |r| Rect.init(r.x, r.y, r.w, r.h), + .text => |t| blk: { + // Estimate text bounds (width based on text length, height based on font) + // This is approximate; actual font metrics would be better + const char_width = 8; // Default font width + const char_height = 16; // Default font height + const text_width = @as(u32, @intCast(t.text.len)) * char_width; + break :blk Rect.init(t.x, t.y, text_width, char_height); + }, + .line => |l| blk: { + const min_x = @min(l.x1, l.x2); + const min_y = @min(l.y1, l.y2); + const max_x = @max(l.x1, l.x2); + const max_y = @max(l.y1, l.y2); + break :blk Rect.init( + min_x, + min_y, + @intCast(@as(u32, @intCast(max_x - min_x)) + 1), + @intCast(@as(u32, @intCast(max_y - min_y)) + 1), + ); + }, + .rect_outline => |r| Rect.init(r.x, r.y, r.w, r.h), + .clip, .clip_end, .nop => null, + }; +} + +/// Check if two rectangles intersect +fn rectsIntersect(a: Rect, b: Rect) bool { + const a_right = a.x + @as(i32, @intCast(a.w)); + const a_bottom = a.y + @as(i32, @intCast(a.h)); + const b_right = b.x + @as(i32, @intCast(b.w)); + const b_bottom = b.y + @as(i32, @intCast(b.h)); + + return a.x < b_right and a_right > b.x and a.y < b_bottom and a_bottom > b.y; +} + // ============================================================================= // Tests // =============================================================================