Compare commits

...

2 commits

Author SHA1 Message Date
15b9cf47a7 docs(sdl2): Documentar VSync en renderer
Clarificar el uso de SDL_RENDERER_PRESENTVSYNC:
- Sincroniza con refresco del monitor (60Hz)
- Elimina screen tearing
- Reduce consumo CPU/GPU

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 20:54:03 +01:00
105ff0063d feat(context): Sistema de registro de paneles dirty
Añade el sistema de dirty panels para optimización de rendering:
- panel_areas: HashMap para registrar rectángulos de paneles
- dirty_panels: HashMap con flags de invalidación por panel
- registerPanelArea(id, rect): Registrar panel con nombre
- invalidatePanel(id): Marcar panel como dirty
- isPanelDirty(id): Consultar si necesita redraw
- markAllPanelsDirty(): Para resize/tab change
- getDirtyPanelRects(): Obtener rects dirty para renderer
- clearDirtyPanels(): Limpiar flags (automático en endFrame)

Diseño:
- La aplicación registra paneles al inicio
- La aplicación notifica cambios via invalidatePanel()
- El renderer usa getDirtyPanelRects() para limpieza selectiva
- Los flags se limpian automáticamente en endFrame()

Parte de la implementación de Dirty Panels Fase 1.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 18:46:31 +01:00
2 changed files with 148 additions and 3 deletions

View file

@ -48,10 +48,9 @@ pub const Sdl2Backend = struct {
errdefer c.SDL_DestroyWindow(window); errdefer c.SDL_DestroyWindow(window);
// Create renderer with hardware acceleration and VSync // Create renderer with hardware acceleration and VSync
// VSync syncs with monitor refresh rate (typically 60Hz) which: // VSync syncs with monitor refresh rate (typically 60Hz):
// - Eliminates screen tearing // - Eliminates screen tearing
// - Reduces CPU usage (no busy-wait needed) // - Reduces CPU/GPU usage
// - Provides consistent frame timing
// Falls back to software renderer if GPU not available // Falls back to software renderer if GPU not available
const renderer = c.SDL_CreateRenderer( const renderer = c.SDL_CreateRenderer(
window, window,

View file

@ -87,6 +87,16 @@ pub const Context = struct {
/// Whether the entire screen needs redraw /// Whether the entire screen needs redraw
full_redraw: bool, full_redraw: bool,
// =========================================================================
// Dirty Panel System (granularity per named panel)
// =========================================================================
/// Registered panel areas by name (e.g., "who_list", "doc_detail")
panel_areas: std.StringHashMapUnmanaged(Layout.Rect),
/// Dirty flags per panel (true = needs redraw)
dirty_panels: std.StringHashMapUnmanaged(bool),
/// Frame statistics /// Frame statistics
stats: FrameStats, stats: FrameStats,
@ -157,6 +167,8 @@ pub const Context = struct {
.height = height, .height = height,
.dirty_rects = .{}, .dirty_rects = .{},
.full_redraw = true, .full_redraw = true,
.panel_areas = .{},
.dirty_panels = .{},
.stats = .{}, .stats = .{},
.focus = FocusSystem.init(), .focus = FocusSystem.init(),
}; };
@ -177,6 +189,8 @@ pub const Context = struct {
.height = height, .height = height,
.dirty_rects = .{}, .dirty_rects = .{},
.full_redraw = true, .full_redraw = true,
.panel_areas = .{},
.dirty_panels = .{},
.stats = .{}, .stats = .{},
.focus = FocusSystem.init(), .focus = FocusSystem.init(),
}; };
@ -188,6 +202,8 @@ pub const Context = struct {
self.overlay_commands.deinit(self.allocator); self.overlay_commands.deinit(self.allocator);
self.id_stack.deinit(self.allocator); self.id_stack.deinit(self.allocator);
self.dirty_rects.deinit(self.allocator); self.dirty_rects.deinit(self.allocator);
self.panel_areas.deinit(self.allocator);
self.dirty_panels.deinit(self.allocator);
self.frame_arena.deinit(); self.frame_arena.deinit();
} }
@ -238,6 +254,10 @@ pub const Context = struct {
// Reset full_redraw for next frame // Reset full_redraw for next frame
self.full_redraw = false; self.full_redraw = false;
// Clear dirty panel flags AFTER rendering
// (renderer uses getDirtyPanelRects during frame)
self.clearDirtyPanels();
} }
// ========================================================================= // =========================================================================
@ -743,6 +763,132 @@ pub const Context = struct {
self.width = width; self.width = width;
self.height = height; self.height = height;
self.invalidateAll(); self.invalidateAll();
self.markAllPanelsDirty(); // Resize requires all panels to redraw
}
// =========================================================================
// Dirty Panel System (granularity per named panel)
//
// The application registers panel areas at startup:
// ctx.registerPanelArea("who_list", rect);
//
// When data changes, the application marks panels dirty:
// ctx.invalidatePanel("who_list");
//
// The renderer checks which panels are dirty:
// const dirty_rects = ctx.getDirtyPanelRects();
// renderer.clearDirtyRegions(dirty_rects);
//
// At endFrame(), dirty flags are cleared automatically.
// =========================================================================
/// Register a named panel area. Call at startup or when layout changes.
/// The ID should be descriptive (e.g., "who_list", "doc_detail").
pub fn registerPanelArea(self: *Self, id: []const u8, rect: Layout.Rect) void {
self.panel_areas.put(self.allocator, id, rect) catch {
// If we can't register, fall back to full redraw
self.full_redraw = true;
};
// New panels start dirty (need initial draw)
self.dirty_panels.put(self.allocator, id, true) catch {};
}
/// Update a panel's rect (e.g., after window resize).
/// Does NOT mark the panel dirty - call invalidatePanel separately if needed.
pub fn updatePanelArea(self: *Self, id: []const u8, rect: Layout.Rect) void {
if (self.panel_areas.getPtr(id)) |ptr| {
ptr.* = rect;
}
}
/// Mark a panel as needing redraw.
/// Call this when data changes that affects the panel's content.
pub fn invalidatePanel(self: *Self, id: []const u8) void {
if (self.dirty_panels.getPtr(id)) |ptr| {
ptr.* = true;
} else {
// Panel not registered - this is probably a bug
// For safety, do full redraw
self.full_redraw = true;
}
}
/// Check if a panel is marked dirty.
pub fn isPanelDirty(self: *Self, id: []const u8) bool {
if (self.full_redraw) return true;
return self.dirty_panels.get(id) orelse false;
}
/// Mark all registered panels as dirty.
/// Use for resize, tab change, or other global invalidation.
pub fn markAllPanelsDirty(self: *Self) void {
var iter = self.dirty_panels.iterator();
while (iter.next()) |entry| {
entry.value_ptr.* = true;
}
}
/// Get rectangles of all dirty panels (for renderer).
/// Returns slice allocated from frame arena (valid until next beginFrame).
pub fn getDirtyPanelRects(self: *Self) []const Layout.Rect {
if (self.full_redraw) {
// Return single rect covering entire screen
const full = Layout.Rect{
.x = 0,
.y = 0,
.w = self.width,
.h = self.height,
};
const result = self.frame_arena.alloc_slice(Layout.Rect, 1) orelse return &.{};
result[0] = full;
return result;
}
// Count dirty panels
var dirty_count: usize = 0;
var iter = self.dirty_panels.iterator();
while (iter.next()) |entry| {
if (entry.value_ptr.*) dirty_count += 1;
}
if (dirty_count == 0) return &.{};
// Allocate result array from frame arena
const result = self.frame_arena.alloc_slice(Layout.Rect, dirty_count) orelse return &.{};
// Fill with dirty panel rects
var i: usize = 0;
var iter2 = self.dirty_panels.iterator();
while (iter2.next()) |entry| {
if (entry.value_ptr.*) {
if (self.panel_areas.get(entry.key_ptr.*)) |rect| {
result[i] = rect;
i += 1;
}
}
}
return result[0..i];
}
/// Check if any panel is dirty (useful for skip-redraw optimization).
pub fn hasAnyDirtyPanel(self: *Self) bool {
if (self.full_redraw) return true;
var iter = self.dirty_panels.iterator();
while (iter.next()) |entry| {
if (entry.value_ptr.*) return true;
}
return false;
}
/// Clear all dirty panel flags.
/// Called automatically at endFrame(), but can be called manually if needed.
pub fn clearDirtyPanels(self: *Self) void {
var iter = self.dirty_panels.iterator();
while (iter.next()) |entry| {
entry.value_ptr.* = false;
}
} }
// ========================================================================= // =========================================================================