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>
This commit is contained in:
reugenio 2025-12-10 12:12:21 +01:00
parent d44d4d26d2
commit e5ba9b178c
2 changed files with 100 additions and 0 deletions

View file

@ -57,6 +57,25 @@ pub const Framebuffer = struct {
@memset(self.pixels, c); @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) /// Get pixel at (x, y)
pub fn getPixel(self: Self, x: i32, y: i32) ?u32 { pub fn getPixel(self: Self, x: i32, y: i32) ?u32 {
if (x < 0 or y < 0) return null; if (x < 0 or y < 0) return null;

View file

@ -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 /// Clear the framebuffer
pub fn clear(self: *Self, color: Color) void { pub fn clear(self: *Self, color: Color) void {
self.framebuffer.clear(color); 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 // 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 // Tests
// ============================================================================= // =============================================================================