diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index 7bc9e14..0eb08cc 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -338,9 +338,14 @@ void GameEngine::run() running = false; } } - + // Clean up before exiting the run loop cleanup(); + + // #144: Quick exit to avoid cleanup segfaults in Python/C++ destructor ordering + // This is a pragmatic workaround - proper cleanup would require careful + // attention to shared_ptr cycles and Python GC interaction + std::_Exit(0); } std::shared_ptr GameEngine::getTimer(const std::string& name) diff --git a/src/PyTexture.cpp b/src/PyTexture.cpp index 1681d37..2bea741 100644 --- a/src/PyTexture.cpp +++ b/src/PyTexture.cpp @@ -17,11 +17,37 @@ PyTexture::PyTexture(std::string filename, int sprite_w, int sprite_h) sheet_height = (size.y / sprite_height); if (size.x % sprite_width != 0 || size.y % sprite_height != 0) { - std::cout << "Warning: Texture `" << source << "` is not an even number of sprite widths or heights across." << std::endl + std::cout << "Warning: Texture `" << source << "` is not an even number of sprite widths or heights across." << std::endl << "Sprite size given was " << sprite_w << "x" << sprite_h << "px but the file has a resolution of " << sheet_width << "x" << sheet_height << "px." << std::endl; } } +// #144: Factory method to create texture from rendered content (snapshot) +std::shared_ptr PyTexture::from_rendered(sf::RenderTexture& render_tex) +{ + // Use a custom shared_ptr construction to access private default constructor + struct MakeSharedEnabler : public PyTexture { + MakeSharedEnabler() : PyTexture() {} + }; + auto ptex = std::make_shared(); + + // Copy the rendered texture data + ptex->texture = render_tex.getTexture(); + ptex->texture.setSmooth(false); // Maintain pixel art aesthetic + + // Set source to indicate this is a snapshot + ptex->source = ""; + + // Treat entire texture as single sprite + auto size = ptex->texture.getSize(); + ptex->sprite_width = size.x; + ptex->sprite_height = size.y; + ptex->sheet_width = 1; + ptex->sheet_height = 1; + + return ptex; +} + sf::Sprite PyTexture::sprite(int index, sf::Vector2f pos, sf::Vector2f s) { // Protect against division by zero if texture failed to load diff --git a/src/PyTexture.h b/src/PyTexture.h index 106e87d..b2375c8 100644 --- a/src/PyTexture.h +++ b/src/PyTexture.h @@ -15,9 +15,16 @@ private: sf::Texture texture; std::string source; int sheet_width, sheet_height; + + // Private default constructor for factory methods + PyTexture() : source(""), sprite_width(0), sprite_height(0), sheet_width(0), sheet_height(0) {} + public: int sprite_width, sprite_height; // just use them read only, OK? PyTexture(std::string filename, int sprite_w, int sprite_h); + + // #144: Factory method to create texture from rendered content (snapshot) + static std::shared_ptr from_rendered(sf::RenderTexture& render_tex); sf::Sprite sprite(int index, sf::Vector2f pos = sf::Vector2f(0, 0), sf::Vector2f s = sf::Vector2f(1.0, 1.0)); int getSpriteCount() const { return sheet_width * sheet_height; } diff --git a/src/UIArc.cpp b/src/UIArc.cpp index 120d59d..6f1c335 100644 --- a/src/UIArc.cpp +++ b/src/UIArc.cpp @@ -228,14 +228,14 @@ bool UIArc::setProperty(const std::string& name, float value) { center.x = value; position = center; vertices_dirty = true; - markDirty(); // #144 - Propagate to parent for texture caching + markCompositeDirty(); // #144 - Position change, texture still valid return true; } else if (name == "y") { center.y = value; position = center; vertices_dirty = true; - markDirty(); // #144 - Propagate to parent for texture caching + markCompositeDirty(); // #144 - Position change, texture still valid return true; } return false; @@ -253,7 +253,7 @@ bool UIArc::setProperty(const std::string& name, const sf::Color& value) { bool UIArc::setProperty(const std::string& name, const sf::Vector2f& value) { if (name == "center") { setCenter(value); - markDirty(); // #144 - Propagate to parent for texture caching + markCompositeDirty(); // #144 - Position change, texture still valid return true; } return false; diff --git a/src/UICaption.cpp b/src/UICaption.cpp index 9014ef7..ba8801e 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -471,13 +471,13 @@ bool UICaption::setProperty(const std::string& name, float value) { if (name == "x") { position.x = value; text.setPosition(position); // Keep text in sync - markDirty(); // #144 - Propagate to parent for texture caching + markCompositeDirty(); // #144 - Position change, texture still valid return true; } else if (name == "y") { position.y = value; text.setPosition(position); // Keep text in sync - markDirty(); // #144 - Propagate to parent for texture caching + markCompositeDirty(); // #144 - Position change, texture still valid return true; } else if (name == "font_size" || name == "size") { // Support both for backward compatibility diff --git a/src/UICircle.cpp b/src/UICircle.cpp index d141b95..39e6d94 100644 --- a/src/UICircle.cpp +++ b/src/UICircle.cpp @@ -180,11 +180,11 @@ bool UICircle::setProperty(const std::string& name, float value) { return true; } else if (name == "x") { position.x = value; - markDirty(); // #144 - Propagate to parent for texture caching + markCompositeDirty(); // #144 - Position change, texture still valid return true; } else if (name == "y") { position.y = value; - markDirty(); // #144 - Propagate to parent for texture caching + markCompositeDirty(); // #144 - Position change, texture still valid return true; } return false; diff --git a/src/UIDrawable.cpp b/src/UIDrawable.cpp index ce36678..441e57d 100644 --- a/src/UIDrawable.cpp +++ b/src/UIDrawable.cpp @@ -820,19 +820,39 @@ bool UIDrawable::contains_point(float x, float y) const { return global_bounds.contains(x, y); } -// #116 - Dirty flag propagation up parent chain -void UIDrawable::markDirty() { +// #144: Content dirty - texture needs rebuild +void UIDrawable::markContentDirty() { if (render_dirty) return; // Already dirty, no need to propagate render_dirty = true; + composite_dirty = true; // If content changed, composite also needs update - // Propagate to parent + // Propagate to parent - parent's composite is dirty (child content changed) auto p = parent.lock(); if (p) { - p->markDirty(); + p->markContentDirty(); // Parent also needs to rebuild to include our changes } } +// #144: Composite dirty - position changed, texture still valid +void UIDrawable::markCompositeDirty() { + // Don't set render_dirty - our cached texture is still valid + // Only mark composite_dirty so parent knows to re-blit us + + // Propagate to parent - parent needs to re-composite + auto p = parent.lock(); + if (p) { + p->composite_dirty = true; + p->render_dirty = true; // Parent needs to re-render (re-composite children) + p->markCompositeDirty(); // Continue propagating up + } +} + +// Legacy method - calls markContentDirty for backwards compatibility +void UIDrawable::markDirty() { + markContentDirty(); +} + // Python API - get parent drawable PyObject* UIDrawable::get_parent(PyObject* self, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); diff --git a/src/UIDrawable.h b/src/UIDrawable.h index 16582df..90acc8c 100644 --- a/src/UIDrawable.h +++ b/src/UIDrawable.h @@ -172,14 +172,27 @@ protected: void updateRenderTexture(); public: - // Mark this drawable as needing redraw (#116 - propagates up parent chain) - void markDirty(); + // #144: Dirty flag system - content vs composite + // content_dirty: THIS drawable's texture needs rebuild (color/text/sprite changed) + // composite_dirty: Parent needs to re-composite children (position changed) + + // Mark content as dirty - texture needs rebuild, propagates up + void markDirty(); // Legacy method - calls markContentDirty + void markContentDirty(); + + // Mark only composite as dirty - position changed, texture still valid + // Only notifies parent, doesn't set own render_dirty + void markCompositeDirty(); // Check if this drawable needs redraw bool isDirty() const { return render_dirty; } + bool isCompositeDirty() const { return composite_dirty; } - // Clear dirty flag (called after rendering) - void clearDirty() { render_dirty = false; } + // Clear dirty flags (called after rendering) + void clearDirty() { render_dirty = false; composite_dirty = false; } + +protected: + bool composite_dirty = true; // #144: Needs re-composite (child positions changed) }; typedef struct { diff --git a/src/UIFrame.cpp b/src/UIFrame.cpp index 9de4ada..9db3503 100644 --- a/src/UIFrame.cpp +++ b/src/UIFrame.cpp @@ -95,27 +95,33 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target) { // Check visibility if (!visible) return; - + // TODO: Apply opacity when SFML supports it on shapes - - // Check if we need to use RenderTexture for clipping - if (clip_children && !children->empty()) { + + // #144: Use RenderTexture for clipping OR texture caching + // clip_children: requires texture for clipping effect (only when has children) + // cache_subtree: uses texture for performance (always, even without children) + bool use_texture = (clip_children && !children->empty()) || cache_subtree; + + if (use_texture) { // Enable RenderTexture if not already enabled if (!use_render_texture) { auto size = box.getSize(); - enableRenderTexture(static_cast(size.x), - static_cast(size.y)); + if (size.x > 0 && size.y > 0) { + enableRenderTexture(static_cast(size.x), + static_cast(size.y)); + } } - + // Update RenderTexture if dirty if (use_render_texture && render_dirty) { // Clear the RenderTexture render_texture->clear(sf::Color::Transparent); - + // Draw the frame box to RenderTexture box.setPosition(0, 0); // Render at origin in texture render_texture->draw(box); - + // Sort children by z_index if needed if (children_need_sort && !children->empty()) { std::sort(children->begin(), children->end(), @@ -124,28 +130,28 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target) }); children_need_sort = false; } - + // Render children to RenderTexture at local coordinates for (auto drawable : *children) { drawable->render(sf::Vector2f(0, 0), *render_texture); } - + // Finalize the RenderTexture render_texture->display(); - + // Update sprite render_sprite.setTexture(render_texture->getTexture()); - + render_dirty = false; } - - // Draw the RenderTexture sprite + + // Draw the RenderTexture sprite (single blit!) if (use_render_texture) { render_sprite.setPosition(offset + box.getPosition()); target.draw(render_sprite); } } else { - // Standard rendering without clipping + // Standard rendering without caching box.move(offset); target.draw(box); box.move(-offset); @@ -382,6 +388,37 @@ int UIFrame::set_clip_children(PyUIFrameObject* self, PyObject* value, void* clo return 0; } +// #144 - cache_subtree property for texture caching +PyObject* UIFrame::get_cache_subtree(PyUIFrameObject* self, void* closure) +{ + return PyBool_FromLong(self->data->cache_subtree); +} + +int UIFrame::set_cache_subtree(PyUIFrameObject* self, PyObject* value, void* closure) +{ + if (!PyBool_Check(value)) { + PyErr_SetString(PyExc_TypeError, "cache_subtree must be a boolean"); + return -1; + } + + bool new_cache = PyObject_IsTrue(value); + if (new_cache != self->data->cache_subtree) { + self->data->cache_subtree = new_cache; + + // Enable or disable the render texture + if (new_cache) { + auto size = self->data->box.getSize(); + if (size.x > 0 && size.y > 0) { + self->data->enableRenderTexture(static_cast(size.x), + static_cast(size.y)); + } + } + self->data->markDirty(); // Mark as needing redraw + } + + return 0; +} + // Define the PyObjectType alias for the macros typedef PyUIFrameObject PyObjectType; @@ -413,6 +450,7 @@ PyGetSetDef UIFrame::getsetters[] = { {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIFRAME}, {"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, "Position as a Vector", (void*)PyObjectsEnum::UIFRAME}, {"clip_children", (getter)UIFrame::get_clip_children, (setter)UIFrame::set_clip_children, "Whether to clip children to frame bounds", NULL}, + {"cache_subtree", (getter)UIFrame::get_cache_subtree, (setter)UIFrame::set_cache_subtree, "#144: Cache subtree rendering to texture for performance", NULL}, UIDRAWABLE_GETSETTERS, UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIFRAME), {NULL} @@ -460,21 +498,22 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) const char* name = nullptr; float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f; int clip_children = 0; - + int cache_subtree = 0; // #144: texture caching + // Keywords list matches the new spec: positional args first, then all keyword args static const char* kwlist[] = { "pos", "size", // Positional args (as per spec) // Keyword-only args "fill_color", "outline_color", "outline", "children", "click", - "visible", "opacity", "z_index", "name", "x", "y", "w", "h", "clip_children", + "visible", "opacity", "z_index", "name", "x", "y", "w", "h", "clip_children", "cache_subtree", nullptr }; // Parse arguments with | for optional positional args - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOfOOifizffffi", const_cast(kwlist), + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOfOOifizffffii", const_cast(kwlist), &pos_obj, &size_obj, // Positional &fill_color, &outline_color, &outline, &children_arg, &click_handler, - &visible, &opacity, &z_index, &name, &x, &y, &w, &h, &clip_children)) { + &visible, &opacity, &z_index, &name, &x, &y, &w, &h, &clip_children, &cache_subtree)) { return -1; } @@ -563,6 +602,13 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) self->data->opacity = opacity; self->data->z_index = z_index; self->data->clip_children = clip_children; + self->data->cache_subtree = cache_subtree; // #144: texture caching + + // #144: Enable render texture if cache_subtree requested + if (cache_subtree && w > 0 && h > 0) { + self->data->enableRenderTexture(static_cast(w), static_cast(h)); + } + if (name) { self->data->name = std::string(name); } @@ -657,12 +703,12 @@ bool UIFrame::setProperty(const std::string& name, float value) { if (name == "x") { position.x = value; box.setPosition(position); // Keep box in sync - markDirty(); + markCompositeDirty(); // #144 - Position change, texture still valid return true; } else if (name == "y") { position.y = value; box.setPosition(position); // Keep box in sync - markDirty(); + markCompositeDirty(); // #144 - Position change, texture still valid return true; } else if (name == "w") { box.setSize(sf::Vector2f(value, box.getSize().y)); diff --git a/src/UIFrame.h b/src/UIFrame.h index 3b5e025..fb93a20 100644 --- a/src/UIFrame.h +++ b/src/UIFrame.h @@ -31,6 +31,7 @@ public: std::shared_ptr>> children; bool children_need_sort = true; // Dirty flag for z_index sorting optimization bool clip_children = false; // Whether to clip children to frame bounds + bool cache_subtree = false; // #144: Whether to cache subtree rendering to texture void render(sf::Vector2f, sf::RenderTarget&) override final; void move(sf::Vector2f); PyObjectsEnum derived_type() override final; @@ -52,6 +53,8 @@ public: static int set_pos(PyUIFrameObject* self, PyObject* value, void* closure); static PyObject* get_clip_children(PyUIFrameObject* self, void* closure); static int set_clip_children(PyUIFrameObject* self, PyObject* value, void* closure); + static PyObject* get_cache_subtree(PyUIFrameObject* self, void* closure); + static int set_cache_subtree(PyUIFrameObject* self, PyObject* value, void* closure); static PyGetSetDef getsetters[]; static PyObject* repr(PyUIFrameObject* self); static int init(PyUIFrameObject* self, PyObject* args, PyObject* kwds); @@ -109,7 +112,8 @@ namespace mcrfpydef { " y (float): Y position override. Default: 0\n" " w (float): Width override. Default: 0\n" " h (float): Height override. Default: 0\n" - " clip_children (bool): Whether to clip children to frame bounds. Default: False\n\n" + " clip_children (bool): Whether to clip children to frame bounds. Default: False\n" + " cache_subtree (bool): Cache rendering to texture for performance. Default: False\n\n" "Attributes:\n" " x, y (float): Position in pixels\n" " w, h (float): Size in pixels\n" @@ -122,7 +126,8 @@ namespace mcrfpydef { " opacity (float): Opacity value\n" " z_index (int): Rendering order\n" " name (str): Element name\n" - " clip_children (bool): Whether to clip children to frame bounds"), + " clip_children (bool): Whether to clip children to frame bounds\n" + " cache_subtree (bool): Cache subtree rendering to texture"), .tp_methods = UIFrame_methods, //.tp_members = PyUIFrame_members, .tp_getset = UIFrame::getsetters, diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index e646178..1bfbb6a 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -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") { diff --git a/src/UILine.cpp b/src/UILine.cpp index f874281..4f085b6 100644 --- a/src/UILine.cpp +++ b/src/UILine.cpp @@ -213,13 +213,13 @@ bool UILine::setProperty(const std::string& name, float value) { else if (name == "x") { float dx = value - position.x; move(dx, 0); - markDirty(); // #144 - Propagate to parent for texture caching + markCompositeDirty(); // #144 - Position change, texture still valid return true; } else if (name == "y") { float dy = value - position.y; move(0, dy); - markDirty(); // #144 - Propagate to parent for texture caching + markCompositeDirty(); // #144 - Position change, texture still valid return true; } else if (name == "start_x") { diff --git a/src/UISprite.cpp b/src/UISprite.cpp index 56bd4de..58cf3ef 100644 --- a/src/UISprite.cpp +++ b/src/UISprite.cpp @@ -2,6 +2,7 @@ #include "GameEngine.h" #include "PyVector.h" #include "PythonObjectCache.h" +#include "UIFrame.h" // #144: For snapshot= parameter // UIDrawable methods now in UIBase.h UIDrawable* UISprite::click_at(sf::Vector2f point) @@ -385,21 +386,22 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) int z_index = 0; const char* name = nullptr; float x = 0.0f, y = 0.0f; - + PyObject* snapshot = nullptr; // #144: snapshot parameter + // Keywords list matches the new spec: positional args first, then all keyword args static const char* kwlist[] = { "pos", "texture", "sprite_index", // Positional args (as per spec) // Keyword-only args "scale", "scale_x", "scale_y", "click", - "visible", "opacity", "z_index", "name", "x", "y", + "visible", "opacity", "z_index", "name", "x", "y", "snapshot", nullptr }; - + // Parse arguments with | for optional positional args - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifffOifizff", const_cast(kwlist), + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifffOifizffO", const_cast(kwlist), &pos_obj, &texture, &sprite_index, // Positional &scale, &scale_x, &scale_y, &click_handler, - &visible, &opacity, &z_index, &name, &x, &y)) { + &visible, &opacity, &z_index, &name, &x, &y, &snapshot)) { return -1; } @@ -430,9 +432,49 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) } } - // Handle texture - allow None or use default + // #144: Handle snapshot parameter - renders a UIDrawable to texture std::shared_ptr texture_ptr = nullptr; - if (texture && texture != Py_None) { + if (snapshot && snapshot != Py_None) { + // Check if snapshot is a Frame (most common case) + PyObject* frame_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"); + if (PyObject_IsInstance(snapshot, frame_type)) { + Py_DECREF(frame_type); + auto pyframe = (PyUIFrameObject*)snapshot; + if (!pyframe->data) { + PyErr_SetString(PyExc_ValueError, "Invalid Frame object for snapshot"); + return -1; + } + + // Get bounds and create render texture + auto bounds = pyframe->data->get_bounds(); + if (bounds.width <= 0 || bounds.height <= 0) { + PyErr_SetString(PyExc_ValueError, "snapshot Frame must have positive size"); + return -1; + } + + sf::RenderTexture render_tex; + if (!render_tex.create(static_cast(bounds.width), + static_cast(bounds.height))) { + PyErr_SetString(PyExc_RuntimeError, "Failed to create RenderTexture for snapshot"); + return -1; + } + + // Render the frame to the texture + render_tex.clear(sf::Color::Transparent); + pyframe->data->render(sf::Vector2f(0, 0), render_tex); + render_tex.display(); + + // Create PyTexture from the rendered content + texture_ptr = PyTexture::from_rendered(render_tex); + sprite_index = 0; // Snapshot is always sprite index 0 + } else { + Py_DECREF(frame_type); + PyErr_SetString(PyExc_TypeError, "snapshot must be a Frame instance"); + return -1; + } + } + // Handle texture - allow None or use default (only if no snapshot) + else if (texture && texture != Py_None) { if (!PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) { PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None"); return -1; @@ -443,7 +485,7 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) // Use default texture when None or not provided texture_ptr = McRFPy_API::default_texture; } - + if (!texture_ptr) { PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available"); return -1; @@ -499,13 +541,13 @@ bool UISprite::setProperty(const std::string& name, float value) { if (name == "x") { position.x = value; sprite.setPosition(position); // Keep sprite in sync - markDirty(); // #144 - Propagate to parent for texture caching + markCompositeDirty(); // #144 - Position change, texture still valid return true; } else if (name == "y") { position.y = value; sprite.setPosition(position); // Keep sprite in sync - markDirty(); // #144 - Propagate to parent for texture caching + markCompositeDirty(); // #144 - Position change, texture still valid return true; } else if (name == "scale") {