From 97105afd2485464952318afc593c2c73fcf636b7 Mon Sep 17 00:00:00 2001 From: Eetu Rantanen Date: Fri, 17 Apr 2026 01:12:38 +0300 Subject: [PATCH] Add display.isValidObject - Add display.isValidObject( object ) to init.lua - Set _isRemoved on removed proxies; cascade to group and snapshot descendants - Clear _isRemoved on re-insert so same-frame re-parent rescues work - Make double-remove a no-op instead of an error - Set _isInvalid on newImage / newImageRect proxies when the bitmap loads as zero bytes --- librtt/Display/Rtt_LuaLibDisplay.cpp | 20 +++- librtt/Rtt_LuaProxyVTable.cpp | 136 +++++++++++++++++++++++++-- platform/resources/init.lua | 7 ++ 3 files changed, 152 insertions(+), 11 deletions(-) diff --git a/librtt/Display/Rtt_LuaLibDisplay.cpp b/librtt/Display/Rtt_LuaLibDisplay.cpp index e188edac6..b6d1892e9 100644 --- a/librtt/Display/Rtt_LuaLibDisplay.cpp +++ b/librtt/Display/Rtt_LuaLibDisplay.cpp @@ -1143,7 +1143,9 @@ DisplayLibrary::newImage( lua_State *L ) Runtime& runtime = library->GetDisplay().GetRuntime(); BitmapPaint *paint = BitmapPaint::NewBitmap( runtime, imageName, baseDir, flags ); - if ( paint && paint->GetBitmap() && paint->GetBitmap()->NumBytes() == 0 ) + bool isInvalid = paint && paint->GetBitmap() && paint->GetBitmap()->NumBytes() == 0; + + if ( isInvalid ) { CoronaLuaWarning(L, "file '%s' does not contain a valid image", imageName); } @@ -1151,6 +1153,12 @@ DisplayLibrary::newImage( lua_State *L ) if ( paint ) { result = NULL != PushImage( L, p, paint, display, parent, replacement ); + if ( result && isInvalid ) + { + lua_pushstring( L, "_isInvalid" ); + lua_pushboolean( L, 1 ); + lua_rawset( L, -3 ); + } } } else if ( lua_isuserdata( L, nextArg ) ) @@ -1257,13 +1265,21 @@ DisplayLibrary::newImageRect( lua_State *L ) Runtime& runtime = library->GetDisplay().GetRuntime(); BitmapPaint *paint = BitmapPaint::NewBitmap( runtime, imageName, baseDir, flags ); - if ( paint && paint->GetBitmap() && paint->GetBitmap()->NumBytes() == 0 ) + bool isInvalid = paint && paint->GetBitmap() && paint->GetBitmap()->NumBytes() == 0; + + if ( isInvalid ) { CoronaLuaWarning(L, "file '%s' does not contain a valid image", imageName); } if ( Rtt_VERIFY( paint ) ) { result = NULL != PushImage( L, NULL, paint, display, parent, w, h, replacement ); + if ( result && isInvalid ) + { + lua_pushstring( L, "_isInvalid" ); + lua_pushboolean( L, 1 ); + lua_rawset( L, -3 ); + } } } else diff --git a/librtt/Rtt_LuaProxyVTable.cpp b/librtt/Rtt_LuaProxyVTable.cpp index 88aea44dc..01dac93b2 100644 --- a/librtt/Rtt_LuaProxyVTable.cpp +++ b/librtt/Rtt_LuaProxyVTable.cpp @@ -3846,6 +3846,83 @@ LuaGroupObjectProxyVTable::Constant() return kVTable; } +// Marks a single object as removed via its Lua proxy table +static void +MarkObjectAsRemoved( lua_State *L, DisplayObject* object ) +{ + LuaProxy* proxy = object->GetProxy(); + if ( proxy ) + { + proxy->PushTable( L ); + lua_pushstring( L, "_isRemoved" ); + lua_pushboolean( L, 1 ); + lua_rawset( L, -3 ); + lua_pop( L, 1 ); + } +} + +// Forward declaration for MarkSnapshotInternalsAsRemoved <-> MarkDescendantsAsRemoved recursion. +static void MarkDescendantsAsRemoved( lua_State *L, GroupObject* group ); + +// SnapshotObject extends RectObject, not GroupObject, so snapshot.group and +// snapshot.canvas are not walked by the normal child cascade. Mark them and their +// contents explicitly. +static void +MarkSnapshotInternalsAsRemoved( lua_State *L, SnapshotObject* snap ) +{ + GroupObject& snapshotGroup = snap->GetGroup(); + MarkObjectAsRemoved( L, &snapshotGroup ); + MarkDescendantsAsRemoved( L, &snapshotGroup ); + + GroupObject& snapshotCanvas = snap->GetCanvas(); + MarkObjectAsRemoved( L, &snapshotCanvas ); + MarkDescendantsAsRemoved( L, &snapshotCanvas ); +} + +// Recursively marks all descendants of a group as removed +static void +MarkDescendantsAsRemoved( lua_State *L, GroupObject* group ) +{ + for ( S32 i = group->NumChildren(); --i >= 0; ) + { + DisplayObject& child = group->ChildAt( i ); + MarkObjectAsRemoved( L, &child ); + + GroupObject* subGroup = child.AsGroupObject(); + if ( subGroup ) + { + MarkDescendantsAsRemoved( L, subGroup ); + } + else if ( &child.ProxyVTable() == &LuaSnapshotObjectProxyVTable::Constant() ) + { + MarkSnapshotInternalsAsRemoved( L, static_cast< SnapshotObject* >( &child ) ); + } + } +} + +// Recursively clears _isRemoved flag on an object and all descendants +static void +ClearRemovedFlag( lua_State *L, DisplayObject* object ) +{ + LuaProxy* proxy = object->GetProxy(); + if ( proxy ) + { + proxy->PushTable( L ); + lua_pushstring( L, "_isRemoved" ); + lua_pushnil( L ); + lua_rawset( L, -3 ); + lua_pop( L, 1 ); + } + GroupObject* group = object->AsGroupObject(); + if ( group ) + { + for ( S32 i = group->NumChildren(); --i >= 0; ) + { + ClearRemovedFlag( L, &group->ChildAt( i ) ); + } + } +} + int LuaGroupObjectProxyVTable::Insert( lua_State *L, GroupObject *parent ) { @@ -3897,13 +3974,31 @@ LuaGroupObjectProxyVTable::Insert( lua_State *L, GroupObject *parent ) if ( oldParent != parent ) { StageObject* canvas = parent->GetStage(); - if ( canvas && oldParent == canvas->GetDisplay().Orphanage() ) + if ( canvas ) { - lua_pushvalue( L, childIndex ); // push table representing child - child->GetProxy()->AcquireTableRef( L ); // reacquire a ref for table - lua_pop( L, 1 ); + if ( oldParent == canvas->GetDisplay().Orphanage() ) + { + lua_pushvalue( L, childIndex ); // push table representing child + child->GetProxy()->AcquireTableRef( L ); // reacquire a ref for table + lua_pop( L, 1 ); + + child->WillMoveOnscreen(); + } - child->WillMoveOnscreen(); + // Clear _isRemoved on re-insertion; flag may be set directly or via + // MarkDescendantsAsRemoved on an ancestor. + LuaProxy* proxy = child->GetProxy(); + if ( proxy ) + { + proxy->PushTable( L ); + lua_getfield( L, -1, "_isRemoved" ); + bool wasMarkedRemoved = lua_toboolean( L, -1 ); + lua_pop( L, 2 ); + if ( wasMarkedRemoved ) + { + ClearRemovedFlag( L, child ); + } + } } } } @@ -3943,8 +4038,15 @@ LuaDisplayObjectProxyVTable::PushAndRemove( lua_State *L, GroupObject* parent, S StageObject *stage = parent->GetStage(); if ( stage ) { - Rtt_ASSERT( LuaContext::GetRuntime( L )->GetDisplay().HitTestOrphanage() != parent - && LuaContext::GetRuntime( L )->GetDisplay().Orphanage() != parent ); + Display& display = LuaContext::GetRuntime( L )->GetDisplay(); + if ( display.HitTestOrphanage() == parent || display.Orphanage() == parent ) + { + // Parent is already the orphanage: the object is mid-removal. + // Treat as a no-op so double-remove (direct, or via stale + // reference after a parent group was removed) stays safe. + lua_pushnil( L ); + return; + } SUMMED_TIMING( par1, "Object: PushAndRemove (release)" ); @@ -3972,14 +4074,30 @@ LuaDisplayObjectProxyVTable::PushAndRemove( lua_State *L, GroupObject* parent, S LuaProxy* proxy = child->GetProxy(); proxy->PushTable( L ); + // Mark the object as removed for immediate Lua-side detection + lua_pushstring( L, "_isRemoved" ); + lua_pushboolean( L, 1 ); + lua_rawset( L, -3 ); + + // If the object is a group, recursively mark all descendants. + // Snapshots are not GroupObjects but expose internal snapshot.group / + // snapshot.canvas via their proxy, so cascade those separately. + GroupObject* childGroup = child->AsGroupObject(); + if ( childGroup ) + { + MarkDescendantsAsRemoved( L, childGroup ); + } + else if ( &child->ProxyVTable() == &LuaSnapshotObjectProxyVTable::Constant() ) + { + MarkSnapshotInternalsAsRemoved( L, static_cast< SnapshotObject* >( child ) ); + } + // Rtt_TRACE( ( "release table ref(%x)\n", lua_topointer( L, -1 ) ) ); // Anytime we add to the Orphanage, it means the DisplayObject is no // longer on the display. Therefore, we should luaL_unref the // DisplayObject's table. If it's later re-inserted, then we simply // luaL_ref the incoming table. - Display& display = LuaContext::GetRuntime( L )->GetDisplay(); - // NOTE: Snapshot renamed to HitTest orphanage to clarify usage // TODO: Remove snapshot orphanage --- or verify that we still need it? diff --git a/platform/resources/init.lua b/platform/resources/init.lua index 53d606863..695156e95 100755 --- a/platform/resources/init.lua +++ b/platform/resources/init.lua @@ -575,6 +575,13 @@ display.remove = function( object ) end end +-- check if object is a valid, usable display object. +-- removeSelf confirms it's a display object. _isRemoved is set by the engine at the moment of removal, bypassing the one-frame delay where +-- an object's properties are still valid. _isInvalid is set by display.newImage / newImageRect when the file is not an image or is corrupted. +display.isValidObject = function( object ) + return "table" == type( object ) and "function" == type( object.removeSelf ) and not (object._isRemoved or object._isInvalid) +end + -- display function to create retina-compatible text for double-pixel devices function display.newRetinaText( ... ) print( "WARNING: display.newRetinaText() has been deprecated. display.newText() is now retina-aware." )