feat: Implement texture caching system with dirty flag optimization (closes #144)

- Add cache_subtree property on Frame for opt-in RenderTexture caching
- Add PyTexture::from_rendered() factory for runtime texture creation
- Add snapshot= parameter to Sprite for creating sprites from Frame content
- Implement content_dirty vs composite_dirty distinction:
  - markContentDirty(): content changed, invalidate self and ancestors
  - markCompositeDirty(): position changed, ancestors need recomposite only
- Update all UIDrawable position setters to use markCompositeDirty()
- Add quick exit workaround for cleanup segfaults

Benchmark: deep_nesting_cached is 3.7x faster (0.09ms vs 0.35ms)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
John McCardle 2025-11-28 19:30:24 -05:00
commit 68f8349fe8
13 changed files with 220 additions and 56 deletions

View file

@ -2725,14 +2725,14 @@ bool UIGrid::setProperty(const std::string& name, float value) {
position.x = value;
box.setPosition(position);
output.setPosition(position);
markDirty(); // #144 - Propagate to parent for texture caching
markCompositeDirty(); // #144 - Position change, texture still valid
return true;
}
else if (name == "y") {
position.y = value;
box.setPosition(position);
output.setPosition(position);
markDirty(); // #144 - Propagate to parent for texture caching
markCompositeDirty(); // #144 - Position change, texture still valid
return true;
}
else if (name == "w" || name == "width") {
@ -2795,7 +2795,7 @@ bool UIGrid::setProperty(const std::string& name, const sf::Vector2f& value) {
position = value;
box.setPosition(position);
output.setPosition(position);
markDirty(); // #144 - Propagate to parent for texture caching
markCompositeDirty(); // #144 - Position change, texture still valid
return true;
}
else if (name == "size") {