Universal Dirty Flag & Texture Caching System #144

Closed
opened 2025-11-28 20:36:02 +00:00 by john · 1 comment
Owner

Overview

Expand the dirty flag system (#116) to provide optional texture caching for any UIDrawable subtree, enabling single-blit rendering of complex static UI hierarchies.

Background

  • #116 implemented dirty flags for clipped frames only (now closed)
  • #118 made Scene inherit from UIDrawable, enabling scene-level hierarchy (now closed)
  • Current limitation: non-clipped frames always render children directly every frame

Key Insight: Position vs Content Dirtiness

Position animations dirty the parent, not the child:

  • A dialog sliding into view: child texture unchanged, parent recomposites at new position ✓
  • Color/text/sprite_index animations: object texture must rebuild, propagates up ✗

This means developers can strategically enable caching on subtrees that:

  • Are static or only use position animations
  • Have stable bounding boxes (clipped frames guarantee this)
  • Contain multiple children that benefit from single-blit rendering

Proposed Features

1. Opt-in Subtree Caching

dialog = mcrfpy.Frame(pos=(100, 100), size=(400, 300))
dialog.cache_subtree = True  # New property: enable texture caching

# Children rendered to texture once, then single blit per frame
dialog.children.append(title_caption)
dialog.children.append(body_text)
dialog.children.append(ok_button)

2. Smart Cache Invalidation

  • markDirty() triggers texture rebuild only when cache_subtree=True
  • Position changes mark parent dirty (not self) - child texture remains valid
  • Content changes (color, text, sprite_index) mark self dirty
  • Bounding box changes trigger texture resize (for non-clipped frames)

3. Clipped vs Non-Clipped Behavior

  • Clipped frames (clip_children=True): Texture size = frame size (stable)
  • Non-clipped frames: Texture size = AABB of frame + all descendants (may vary)
  • Recommend clipping for cache stability

4. Memory Management

  • Texture pooling/reuse for similar-sized caches
  • Maximum texture size limits with fallback to direct render
  • Debug overlay showing texture memory usage

5. Performance Metrics

  • Track texture rebuild count per frame
  • Track cache hit rate
  • Expose via getMetrics() Python API
  • F3 overlay shows caching statistics

Immediate Prerequisites

Bug fix needed first: Several UIDrawable subclasses don't call markDirty() in their setProperty() methods:

  • UISprite - missing markDirty() calls
  • UICaption - missing markDirty() calls
  • UIGrid - needs verification
  • UILine, UICircle, UIArc - need verification

This must be fixed before the caching system can work correctly.

Definition of Done

  • Benchmarks show measurable improvement for static UI
  • Position animations don't trigger texture rebuilds
  • Content animations correctly invalidate caches
  • Memory usage is bounded and predictable
  • F3 overlay shows caching statistics
  • Documentation explains when to use caching
  • Builds on: #116 (dirty flags), #118 (scene hierarchy)
  • Requires: #104 (benchmarking) for validation
  • See also: #136 (ImGui debug interfaces) for inspector support
## Overview Expand the dirty flag system (#116) to provide optional texture caching for any UIDrawable subtree, enabling single-blit rendering of complex static UI hierarchies. ## Background - #116 implemented dirty flags for clipped frames only (now closed) - #118 made Scene inherit from UIDrawable, enabling scene-level hierarchy (now closed) - Current limitation: non-clipped frames always render children directly every frame ## Key Insight: Position vs Content Dirtiness **Position animations dirty the parent, not the child:** - A dialog sliding into view: child texture unchanged, parent recomposites at new position ✓ - Color/text/sprite_index animations: object texture must rebuild, propagates up ✗ This means developers can strategically enable caching on subtrees that: - Are static or only use position animations - Have stable bounding boxes (clipped frames guarantee this) - Contain multiple children that benefit from single-blit rendering ## Proposed Features ### 1. Opt-in Subtree Caching ```python dialog = mcrfpy.Frame(pos=(100, 100), size=(400, 300)) dialog.cache_subtree = True # New property: enable texture caching # Children rendered to texture once, then single blit per frame dialog.children.append(title_caption) dialog.children.append(body_text) dialog.children.append(ok_button) ``` ### 2. Smart Cache Invalidation - `markDirty()` triggers texture rebuild only when `cache_subtree=True` - Position changes mark parent dirty (not self) - child texture remains valid - Content changes (color, text, sprite_index) mark self dirty - Bounding box changes trigger texture resize (for non-clipped frames) ### 3. Clipped vs Non-Clipped Behavior - **Clipped frames** (`clip_children=True`): Texture size = frame size (stable) - **Non-clipped frames**: Texture size = AABB of frame + all descendants (may vary) - Recommend clipping for cache stability ### 4. Memory Management - Texture pooling/reuse for similar-sized caches - Maximum texture size limits with fallback to direct render - Debug overlay showing texture memory usage ### 5. Performance Metrics - Track texture rebuild count per frame - Track cache hit rate - Expose via `getMetrics()` Python API - F3 overlay shows caching statistics ## Immediate Prerequisites **Bug fix needed first:** Several UIDrawable subclasses don't call `markDirty()` in their `setProperty()` methods: - UISprite - missing markDirty() calls - UICaption - missing markDirty() calls - UIGrid - needs verification - UILine, UICircle, UIArc - need verification This must be fixed before the caching system can work correctly. ## Definition of Done - [ ] Benchmarks show measurable improvement for static UI - [ ] Position animations don't trigger texture rebuilds - [ ] Content animations correctly invalidate caches - [ ] Memory usage is bounded and predictable - [ ] F3 overlay shows caching statistics - [ ] Documentation explains when to use caching ## Related Issues - Builds on: #116 (dirty flags), #118 (scene hierarchy) - Requires: #104 (benchmarking) for validation - See also: #136 (ImGui debug interfaces) for inspector support
Author
Owner

Implementation Progress

Completed

  1. cache_subtree property on Frame - Enables opt-in texture caching for any frame and its children

    • Constructor kwarg: Frame(pos=(x,y), size=(w,h), cache_subtree=True)
    • Runtime property: frame.cache_subtree = True
  2. Benchmarks show measurable improvement

    • deep_nesting (15-level hierarchy): 0.287ms → 0.081ms with caching (3.5x faster)
    • Frame count increased from 10,496 to 36,218 (3.5x more frames)
    • Best suited for complex nested hierarchies that don't change frequently
  3. markDirty() already implemented - All UIDrawable subclasses call markDirty() on property changes (prerequisite was done earlier)

  4. Content changes invalidate caches - Any property change triggers cache rebuild via markDirty() propagation

Remaining Work

  1. Position vs Content distinction - Currently ALL changes trigger rebuild. Optimization: position changes should only mark parent dirty, not rebuild child texture. This requires more sophisticated dirty flag handling.

  2. Memory management - No texture pooling or max size limits yet. Currently unbounded.

  3. F3 overlay statistics - Not implemented. Would need cache hit/miss counters.

  4. Documentation - Need to document when caching is beneficial:

    • Good for: static UI panels, nested hierarchies, dialog boxes
    • ⚠️ Not beneficial for: frequently animating content, simple elements (overhead > savings)

Benchmark Results

deep_nesting:        0.29ms avg (10496 frames)
deep_nesting_cached: 0.08ms avg (36218 frames)  ← 3.5x improvement

static_scene:        1.76ms avg (1080 frames)
static_scene_cached: 1.92ms avg (1017 frames)   ← No improvement (Grid dominates)

The static_scene test shows that caching simple frames doesn't help when a Grid (1200 cells + 20 entities) dominates render time.

## Implementation Progress ### Completed ✅ 1. **`cache_subtree` property on Frame** - Enables opt-in texture caching for any frame and its children - Constructor kwarg: `Frame(pos=(x,y), size=(w,h), cache_subtree=True)` - Runtime property: `frame.cache_subtree = True` 2. **Benchmarks show measurable improvement** - `deep_nesting` (15-level hierarchy): 0.287ms → 0.081ms with caching (**3.5x faster**) - Frame count increased from 10,496 to 36,218 (**3.5x more frames**) - Best suited for complex nested hierarchies that don't change frequently 3. **markDirty() already implemented** - All UIDrawable subclasses call `markDirty()` on property changes (prerequisite was done earlier) 4. **Content changes invalidate caches** - Any property change triggers cache rebuild via `markDirty()` propagation ### Remaining Work 1. **Position vs Content distinction** - Currently ALL changes trigger rebuild. Optimization: position changes should only mark parent dirty, not rebuild child texture. This requires more sophisticated dirty flag handling. 2. **Memory management** - No texture pooling or max size limits yet. Currently unbounded. 3. **F3 overlay statistics** - Not implemented. Would need cache hit/miss counters. 4. **Documentation** - Need to document when caching is beneficial: - ✅ Good for: static UI panels, nested hierarchies, dialog boxes - ⚠️ Not beneficial for: frequently animating content, simple elements (overhead > savings) ### Benchmark Results ``` deep_nesting: 0.29ms avg (10496 frames) deep_nesting_cached: 0.08ms avg (36218 frames) ← 3.5x improvement static_scene: 1.76ms avg (1080 frames) static_scene_cached: 1.92ms avg (1017 frames) ← No improvement (Grid dominates) ``` The static_scene test shows that caching simple frames doesn't help when a Grid (1200 cells + 20 entities) dominates render time.
john closed this issue 2025-11-29 00:30:31 +00:00
Sign in to join this conversation.
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: john/McRogueFace#144
No description provided.