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:
parent
d44d4d26d2
commit
e5ba9b178c
2 changed files with 100 additions and 0 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// =============================================================================
|
||||
|
|
|
|||
Loading…
Reference in a new issue