docs: create RenderTexture overhaul design document

- Comprehensive design for Issue #6 implementation
- Opt-in architecture to maintain backward compatibility
- Phased implementation plan with clear milestones
- Performance considerations and risk mitigation
- API design for clipping and future effects

Also includes Grid background color test
This commit is contained in:
John McCardle 2025-07-06 16:00:11 -04:00
parent ff7cf25806
commit 5e4224a4f8
2 changed files with 293 additions and 0 deletions

167
RENDERTEXTURE_DESIGN.md Normal file
View File

@ -0,0 +1,167 @@
# RenderTexture Overhaul Design Document
## Overview
This document outlines the design for implementing RenderTexture support across all UIDrawable classes in McRogueFace. This is Issue #6 and represents a major architectural change to the rendering system.
## Goals
1. **Automatic Clipping**: Children rendered outside parent bounds should be clipped
2. **Off-screen Rendering**: Enable post-processing effects and complex compositing
3. **Performance**: Cache static content, only re-render when changed
4. **Backward Compatibility**: Existing code should continue to work
## Current State
### Classes Already Using RenderTexture:
- **UIGrid**: Uses a 1920x1080 RenderTexture for compositing grid view
- **SceneTransition**: Uses two 1024x768 RenderTextures for transitions
- **HeadlessRenderer**: Uses RenderTexture for headless mode
### Classes Using Direct Rendering:
- **UIFrame**: Renders box and children directly
- **UICaption**: Renders text directly
- **UISprite**: Renders sprite directly
## Design Decisions
### 1. Opt-in Architecture
Not all UIDrawables need RenderTextures. We'll use an opt-in approach:
```cpp
class UIDrawable {
protected:
// RenderTexture support (opt-in)
std::unique_ptr<sf::RenderTexture> render_texture;
sf::Sprite render_sprite;
bool use_render_texture = false;
bool render_dirty = true;
// Enable RenderTexture for this drawable
void enableRenderTexture(unsigned int width, unsigned int height);
void updateRenderTexture();
};
```
### 2. When to Use RenderTexture
RenderTextures will be enabled for:
1. **UIFrame with clipping enabled** (new property: `clip_children = true`)
2. **UIDrawables with effects** (future: shaders, blend modes)
3. **Complex composites** (many children that rarely change)
### 3. Render Flow
```
Standard Flow:
render() → render directly to target
RenderTexture Flow:
render() → if dirty → clear RT → render to RT → dirty = false
→ draw RT sprite to target
```
### 4. Dirty Flag Management
Mark as dirty when:
- Properties change (position, size, color, etc.)
- Children added/removed
- Child marked as dirty (propagate up)
- Animation frame
### 5. Size Management
RenderTexture size options:
1. **Fixed Size**: Set at creation (current UIGrid approach)
2. **Dynamic Size**: Match bounds, recreate on resize
3. **Pooled Sizes**: Use standard sizes from pool
We'll use **Dynamic Size** with lazy creation.
## Implementation Plan
### Phase 1: Base Infrastructure (This PR)
1. Add RenderTexture members to UIDrawable
2. Add `enableRenderTexture()` method
3. Implement dirty flag system
4. Add `clip_children` property to UIFrame
### Phase 2: UIFrame Implementation
1. Update UIFrame::render() to use RenderTexture when clipping
2. Test with nested frames
3. Verify clipping works correctly
### Phase 3: Performance Optimization
1. Implement texture pooling
2. Add dirty flag propagation
3. Profile and optimize
### Phase 4: Extended Features
1. Blur/glow effects using RenderTexture
2. Viewport-based rendering (#8)
3. Screenshot improvements
## API Changes
### Python API:
```python
# Enable clipping on frames
frame.clip_children = True # New property
# Future: effects
frame.blur_amount = 5.0
sprite.glow_color = Color(255, 200, 100)
```
### C++ API:
```cpp
// Enable RenderTexture
frame->enableRenderTexture(width, height);
frame->setClipChildren(true);
// Mark dirty
frame->markDirty();
```
## Performance Considerations
1. **Memory**: Each RenderTexture uses GPU memory (width * height * 4 bytes)
2. **Creation Cost**: Creating RenderTextures is expensive, use pooling
3. **Clear Cost**: Clearing large RenderTextures each frame is costly
4. **Bandwidth**: Drawing to RenderTexture then to screen doubles bandwidth
## Migration Strategy
1. All existing code continues to work (direct rendering by default)
2. Gradually enable RenderTexture for specific use cases
3. Profile before/after to ensure performance gains
4. Document best practices
## Risks and Mitigation
| Risk | Mitigation |
|------|------------|
| Performance regression | Opt-in design, profile extensively |
| Memory usage increase | Texture pooling, size limits |
| Complexity increase | Clear documentation, examples |
| Integration issues | Extensive testing with SceneTransition |
## Success Criteria
1. ✓ Frames can clip children to bounds
2. ✓ No performance regression for direct rendering
3. ✓ Scene transitions continue to work
4. ✓ Memory usage is reasonable
5. ✓ API is intuitive and documented
## Future Extensions
1. **Shader Support** (#106): RenderTextures enable post-processing shaders
2. **Particle Systems** (#107): Render particles to texture for effects
3. **Caching**: Static UI elements cached in RenderTextures
4. **Resolution Independence**: RenderTextures for DPI scaling
## Conclusion
This design provides a foundation for professional rendering capabilities while maintaining backward compatibility and performance. The opt-in approach allows gradual adoption and testing.

View File

@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""Test Grid background color functionality"""
import mcrfpy
import sys
def test_grid_background():
"""Test Grid background color property"""
print("Testing Grid Background Color...")
# Create a test scene
mcrfpy.createScene("test")
ui = mcrfpy.sceneUI("test")
# Create a grid with default background
grid = mcrfpy.Grid(20, 15, grid_size=(20, 15))
grid.x = 50
grid.y = 50
grid.w = 400
grid.h = 300
ui.append(grid)
# Add some tiles to see the background better
for x in range(5, 15):
for y in range(5, 10):
point = grid.at(x, y)
point.color = mcrfpy.Color(100, 150, 100)
# Add UI to show current background color
info_frame = mcrfpy.Frame(500, 50, 200, 150,
fill_color=mcrfpy.Color(40, 40, 40),
outline_color=mcrfpy.Color(200, 200, 200),
outline=2)
ui.append(info_frame)
color_caption = mcrfpy.Caption(510, 60, "Background Color:")
color_caption.font_size = 14
color_caption.fill_color = mcrfpy.Color(255, 255, 255)
info_frame.children.append(color_caption)
color_display = mcrfpy.Caption(510, 80, "")
color_display.font_size = 12
color_display.fill_color = mcrfpy.Color(200, 200, 200)
info_frame.children.append(color_display)
# Activate the scene
mcrfpy.setScene("test")
def run_tests(dt):
"""Run background color tests"""
mcrfpy.delTimer("run_tests")
print("\nTest 1: Default background color")
default_color = grid.background_color
print(f"Default: R={default_color.r}, G={default_color.g}, B={default_color.b}, A={default_color.a}")
color_display.text = f"R:{default_color.r} G:{default_color.g} B:{default_color.b}"
def test_set_color(dt):
mcrfpy.delTimer("test_set")
print("\nTest 2: Set background to blue")
grid.background_color = mcrfpy.Color(20, 40, 100)
new_color = grid.background_color
print(f"✓ Set to: R={new_color.r}, G={new_color.g}, B={new_color.b}")
color_display.text = f"R:{new_color.r} G:{new_color.g} B:{new_color.b}"
def test_animation(dt):
mcrfpy.delTimer("test_anim")
print("\nTest 3: Manual color cycling")
# Manually change color to test property is working
colors = [
mcrfpy.Color(200, 20, 20), # Red
mcrfpy.Color(20, 200, 20), # Green
mcrfpy.Color(20, 20, 200), # Blue
]
color_index = [0] # Use list to allow modification in nested function
def cycle_red(dt):
mcrfpy.delTimer("cycle_0")
grid.background_color = colors[0]
c = grid.background_color
color_display.text = f"R:{c.r} G:{c.g} B:{c.b}"
print(f"✓ Set to Red: R={c.r}, G={c.g}, B={c.b}")
def cycle_green(dt):
mcrfpy.delTimer("cycle_1")
grid.background_color = colors[1]
c = grid.background_color
color_display.text = f"R:{c.r} G:{c.g} B:{c.b}"
print(f"✓ Set to Green: R={c.r}, G={c.g}, B={c.b}")
def cycle_blue(dt):
mcrfpy.delTimer("cycle_2")
grid.background_color = colors[2]
c = grid.background_color
color_display.text = f"R:{c.r} G:{c.g} B:{c.b}"
print(f"✓ Set to Blue: R={c.r}, G={c.g}, B={c.b}")
# Cycle through colors
mcrfpy.setTimer("cycle_0", cycle_red, 100)
mcrfpy.setTimer("cycle_1", cycle_green, 400)
mcrfpy.setTimer("cycle_2", cycle_blue, 700)
def test_complete(dt):
mcrfpy.delTimer("complete")
print("\nTest 4: Final color check")
final_color = grid.background_color
print(f"Final: R={final_color.r}, G={final_color.g}, B={final_color.b}")
print("\n✓ Grid background color tests completed!")
print("- Default background color works")
print("- Setting background color works")
print("- Color cycling works")
sys.exit(0)
# Schedule tests
mcrfpy.setTimer("test_set", test_set_color, 1000)
mcrfpy.setTimer("test_anim", test_animation, 2000)
mcrfpy.setTimer("complete", test_complete, 4500)
# Start tests
mcrfpy.setTimer("run_tests", run_tests, 100)
if __name__ == "__main__":
test_grid_background()