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);
|
@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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue