Compare commits

...

3 commits

Author SHA1 Message Date
908815b585 fix(animation): ColorTransition epsilon para convergencia garantizada
Problema: lerpU8 truncaba incrementos <1, causando que las animaciones
nunca terminaran. Con delta_ms=16ms y duration=500ms, el factor t=0.032
hacía que diferencias de 1 (ej: 99→100) nunca convergieran.

Solución: Añadir epsilon check después del lerp. Si la diferencia en
cada canal RGBA es ≤1, forzar snap al target y devolver false.

Esto elimina los redraws infinitos en idle reportados por el usuario.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 12:04:04 +01:00
f4b1b75914 perf(telemetry): Añadir métrica executed_cmds
- FrameStats: nuevo campo executed_cmds para comandos realmente ejecutados
- SoftwareRenderer: contador que se incrementa en execute()
- Métodos resetExecutedCount() y getExecutedCount()
- CLAUDE.md: sección OPTIMIZACIONES DE RENDIMIENTO documentada

Permite comparar Total (generados) vs Exec (ejecutados) para
diagnosticar efectividad del dirty regions filtering.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 11:54:56 +01:00
4ce39c4fc8 perf: @setRuntimeSafety(false) + burst detection infrastructure
- fillRect: disable runtime safety for hot path (bounds pre-validated)
- drawGlyphBitmap: disable runtime safety for hot path
- Context: add burst detection (isSelectionBurstActive, markNavigationEvent)
- PanelFrameConfig: add burst_sensitive field
- PanelFrameResult: new struct for frame drawing results

Note: burst_sensitive mechanism available but not actively used
(causes visual flickering when suppressing panel content)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 18:27:10 +01:00
6 changed files with 102 additions and 4 deletions

View file

@ -367,6 +367,32 @@ pub const MacroPlayer = struct {
--- ---
## OPTIMIZACIONES DE RENDIMIENTO (2026-01-03)
### Optimizaciones Activas ✅
| Optimización | Archivo | Descripción |
|--------------|---------|-------------|
| **Turbo Píxeles** | `render/framebuffer.zig:117` | `@setRuntimeSafety(false)` en `fillRect` - elimina bounds checks en hot path |
| **Fast Path Texto** | `render/ttf.zig:323,375` | Escritura directa para píxeles opacos (alpha==255), evita blend innecesario |
### Optimización Probada y Revertida ❌
| Optimización | Problema | Alternativa |
|--------------|----------|-------------|
| **Burst Suppression** (auto-gestión de estrés) | Causaba paneles vacíos durante navegación rápida | El debounce en DataManager ya evita queries excesivas |
**Detalles del Burst Suppression:**
- Concepto: suprimir dibujo de paneles durante ráfagas de navegación (<100ms)
- Implementación: `if (!frame_result.should_draw) return;` en paneles
- Problema: el frame se dibujaba vacío (solo fondo), sin widgets
- Causa raíz: suprimía TODO el dibujo, no solo las queries BD
- Lección: el debounce a nivel DataManager es más elegante
**Pendiente verificar:** Mensajes continuos de redraw en idle (posible bug en animaciones).
---
## NOTAS ZIG 0.15.2 ## NOTAS ZIG 0.15.2
```zig ```zig

View file

@ -102,6 +102,10 @@ pub const Context = struct {
/// Set by application for non-dirty panels to save CPU while keeping input working. /// Set by application for non-dirty panels to save CPU while keeping input working.
suppress_commands: bool = false, suppress_commands: bool = false,
/// Burst detection: timestamp of last navigation event (selection change)
/// Used by drawPanelFrame to auto-suppress burst_sensitive panels during rapid navigation
last_navigation_time: i64 = 0,
/// Frame statistics /// Frame statistics
stats: FrameStats, stats: FrameStats,
@ -145,8 +149,10 @@ pub const Context = struct {
/// Frame statistics for performance monitoring /// Frame statistics for performance monitoring
pub const FrameStats = struct { pub const FrameStats = struct {
/// Number of commands this frame /// Number of commands generated this frame
command_count: usize = 0, command_count: usize = 0,
/// Number of commands actually executed (set by renderer)
executed_cmds: usize = 0,
/// Number of widgets drawn /// Number of widgets drawn
widget_count: usize = 0, widget_count: usize = 0,
/// Arena bytes used this frame /// Arena bytes used this frame
@ -227,6 +233,7 @@ pub const Context = struct {
// Reset frame stats // Reset frame stats
self.stats.command_count = 0; self.stats.command_count = 0;
self.stats.executed_cmds = 0;
self.stats.widget_count = 0; self.stats.widget_count = 0;
self.stats.arena_bytes = 0; self.stats.arena_bytes = 0;
self.stats.dirty_rect_count = 0; self.stats.dirty_rect_count = 0;
@ -665,10 +672,24 @@ pub const Context = struct {
draw_shadow: bool = true, draw_shadow: bool = true,
/// Draw bevel effect (default true) /// Draw bevel effect (default true)
draw_bevel: bool = true, draw_bevel: bool = true,
// === Performance ===
/// Si true, el panel se auto-suprime durante ráfagas de navegación.
/// Paneles principales (listas, fichas) deben ser false.
/// Paneles secundarios (documentos) pueden ser true para mejor rendimiento.
burst_sensitive: bool = true,
};
/// Result of drawPanelFrame for LEGO-style control flow
pub const PanelFrameResult = struct {
/// True if color transition is still animating
animating: bool,
/// False if panel was suppressed - caller should return immediately
should_draw: bool,
}; };
/// Draw a complete panel frame with focus transition and 3D effects. /// Draw a complete panel frame with focus transition and 3D effects.
/// Returns true if the transition is still animating (need more frames). /// Returns PanelFrameResult: check should_draw to know if panel content should be rendered.
/// ///
/// Supports two modes: /// Supports two modes:
/// - **Explicit**: Provide focus_bg, unfocus_bg, border_color directly /// - **Explicit**: Provide focus_bg, unfocus_bg, border_color directly
@ -678,7 +699,11 @@ pub const Context = struct {
rect: Layout.Rect, rect: Layout.Rect,
transition: *ColorTransition, transition: *ColorTransition,
config: PanelFrameConfig, config: PanelFrameConfig,
) bool { ) PanelFrameResult {
// Auto-supresión LEGO: detectar si el panel debe suprimir operaciones costosas
// El frame SIEMPRE se dibuja, pero should_draw indica si continuar con widgets/BD
const burst_suppressed = config.burst_sensitive and self.isSelectionBurstActive();
// Determine colors: Z-Design derivation or explicit // Determine colors: Z-Design derivation or explicit
const focus_bg: Style.Color = blk: { const focus_bg: Style.Color = blk: {
if (config.base_color) |base| { if (config.base_color) |base| {
@ -764,7 +789,8 @@ pub const Context = struct {
} }
} }
return animating; // should_draw = false indica que el panel debe saltar operaciones costosas (BD, widgets)
return .{ .animating = animating, .should_draw = !burst_suppressed };
} }
/// Resize the context /// Resize the context
@ -837,6 +863,23 @@ pub const Context = struct {
} }
} }
// =========================================================================
// BURST DETECTION: Para auto-supresión de paneles durante navegación rápida
// =========================================================================
/// Marca que ocurrió un evento de navegación (cambio de selección).
/// Llamar desde DataManager cuando notifica cambios de selección.
pub fn markNavigationEvent(self: *Self) void {
self.last_navigation_time = std.time.milliTimestamp();
}
/// Devuelve true si estamos en medio de una ráfaga de navegación.
/// Se considera ráfaga si pasaron menos de 100ms desde el último evento.
pub fn isSelectionBurstActive(self: *Self) bool {
const now = std.time.milliTimestamp();
return (now - self.last_navigation_time) < 100;
}
/// Get rectangles of all dirty panels (for renderer). /// Get rectangles of all dirty panels (for renderer).
/// Returns slice allocated from frame arena (valid until next beginFrame). /// Returns slice allocated from frame arena (valid until next beginFrame).
pub fn getDirtyPanelRects(self: *Self) []const Layout.Rect { pub fn getDirtyPanelRects(self: *Self) []const Layout.Rect {

View file

@ -739,6 +739,19 @@ pub const ColorTransition = struct {
.a = lerpU8(self.current.a, target.a, t), .a = lerpU8(self.current.a, target.a, t),
}; };
// EPSILON FIX: Forzar convergencia cuando estamos "suficientemente cerca"
// Sin esto, lerpU8 trunca incrementos <1 y la animación nunca termina.
// Umbral de 1 es imperceptible al ojo pero garantiza convergencia.
const dr = @abs(@as(i16, self.current.r) - @as(i16, target.r));
const dg = @abs(@as(i16, self.current.g) - @as(i16, target.g));
const db = @abs(@as(i16, self.current.b) - @as(i16, target.b));
const da = @abs(@as(i16, self.current.a) - @as(i16, target.a));
if (dr <= 1 and dg <= 1 and db <= 1 and da <= 1) {
self.current = target; // Snap al target
return false; // Animación terminada
}
return true; return true;
} }

View file

@ -114,6 +114,7 @@ pub const Framebuffer = struct {
/// Draw a filled rectangle /// Draw a filled rectangle
/// Optimized with SIMD-friendly @memset for solid colors (alpha=255) /// Optimized with SIMD-friendly @memset for solid colors (alpha=255)
pub fn fillRect(self: *Self, x: i32, y: i32, w: u32, h: u32, color: Color) void { pub fn fillRect(self: *Self, x: i32, y: i32, w: u32, h: u32, color: Color) void {
@setRuntimeSafety(false); // Hot path: bounds already validated below
const x_start = @max(0, x); const x_start = @max(0, x);
const y_start = @max(0, y); const y_start = @max(0, y);
const x_end = @min(@as(i32, @intCast(self.width)), x + @as(i32, @intCast(w))); const x_end = @min(@as(i32, @intCast(self.width)), x + @as(i32, @intCast(w)));

View file

@ -323,6 +323,9 @@ pub const SoftwareRenderer = struct {
/// Shadow cache for instant shadow blitting (optional, requires allocator) /// Shadow cache for instant shadow blitting (optional, requires allocator)
shadow_cache: ?ShadowCache = null, shadow_cache: ?ShadowCache = null,
/// Counter of actually executed commands (for telemetry)
executed_count: usize = 0,
const Self = @This(); const Self = @This();
/// Initialize the renderer (without shadow cache) /// Initialize the renderer (without shadow cache)
@ -382,6 +385,7 @@ pub const SoftwareRenderer = struct {
/// Execute a single draw command /// Execute a single draw command
pub fn execute(self: *Self, cmd: DrawCommand) void { pub fn execute(self: *Self, cmd: DrawCommand) void {
self.executed_count += 1;
switch (cmd) { switch (cmd) {
.rect => |r| self.drawRect(r), .rect => |r| self.drawRect(r),
.rounded_rect => |r| self.drawRoundedRect(r), .rounded_rect => |r| self.drawRoundedRect(r),
@ -399,6 +403,16 @@ pub const SoftwareRenderer = struct {
} }
} }
/// Reset executed command counter (call at start of frame)
pub fn resetExecutedCount(self: *Self) void {
self.executed_count = 0;
}
/// Get number of commands executed since last reset
pub fn getExecutedCount(self: Self) usize {
return self.executed_count;
}
/// Execute all commands in a list /// Execute all commands in a list
pub fn executeAll(self: *Self, commands: []const DrawCommand) void { pub fn executeAll(self: *Self, commands: []const DrawCommand) void {
for (commands) |cmd| { for (commands) |cmd| {

View file

@ -320,6 +320,7 @@ pub const TtfFont = struct {
color: Color, color: Color,
clip: Rect, clip: Rect,
) void { ) void {
@setRuntimeSafety(false); // Hot path: bounds validated in visible region calculation
_ = self; _ = self;
const width: u32 = glyph.metrics.width; const width: u32 = glyph.metrics.width;