From 755425ebfaa8f9bca381411309a157fe80fbbfd6 Mon Sep 17 00:00:00 2001 From: CUB3D Date: Mon, 19 Dec 2022 23:48:50 +0000 Subject: [PATCH] avm1: Delay clip removals when a child has an unload listener When removing a clip, first check if it has an unload event listener somewhere it's hierarchy. If it does, enqueue the removal to happen on the next frame, by moving it to a negative depth. --- core/src/avm1/runtime.rs | 44 ++++++++++++ core/src/display_object.rs | 6 ++ core/src/display_object/container.rs | 64 +++++++++++++++++- core/src/display_object/movie_clip.rs | 16 ++++- core/src/player.rs | 4 +- tests/tests/swfs/avm1/unload/output.txt | 20 ++++++ tests/tests/swfs/avm1/unload/test.fla | Bin 0 -> 5896 bytes tests/tests/swfs/avm1/unload/test.swf | Bin 0 -> 1026 bytes tests/tests/swfs/avm1/unload/test.toml | 1 + .../swfs/avm1/unload_nested_child/input.json | 15 ++++ .../swfs/avm1/unload_nested_child/output.txt | 5 ++ .../swfs/avm1/unload_nested_child/test.fla | Bin 0 -> 6979 bytes .../swfs/avm1/unload_nested_child/test.swf | Bin 0 -> 949 bytes .../swfs/avm1/unload_nested_child/test.toml | 1 + 14 files changed, 171 insertions(+), 5 deletions(-) create mode 100644 tests/tests/swfs/avm1/unload/output.txt create mode 100644 tests/tests/swfs/avm1/unload/test.fla create mode 100644 tests/tests/swfs/avm1/unload/test.swf create mode 100644 tests/tests/swfs/avm1/unload/test.toml create mode 100644 tests/tests/swfs/avm1/unload_nested_child/input.json create mode 100644 tests/tests/swfs/avm1/unload_nested_child/output.txt create mode 100644 tests/tests/swfs/avm1/unload_nested_child/test.fla create mode 100644 tests/tests/swfs/avm1/unload_nested_child/test.swf create mode 100644 tests/tests/swfs/avm1/unload_nested_child/test.toml diff --git a/core/src/avm1/runtime.rs b/core/src/avm1/runtime.rs index 7f44a88f4..8ec8cffa5 100644 --- a/core/src/avm1/runtime.rs +++ b/core/src/avm1/runtime.rs @@ -392,9 +392,53 @@ impl<'gc> Avm1<'gc> { self.registers.get_mut(id) } + /// Find all display objects with negative depth recurisvely + /// + /// If an object is pending removal due to being removed by a removeObject tag on the previous frame, + /// while it had an unload event listener attached, avm1 requires that the object is kept around for one extra frame. + /// + /// This will be called at the start of each frame, to gather the objects for removal + fn find_display_objects_pending_removal( + obj: DisplayObject<'gc>, + out: &mut Vec>, + ) { + if let Some(parent) = obj.as_container() { + for child in parent.iter_render_list() { + if child.pending_removal() { + out.push(child); + } + + Self::find_display_objects_pending_removal(child, out); + } + } + } + + /// Remove all display objects pending removal + /// See [`find_display_objects_pending_removal`] for details + fn remove_pending(context: &mut UpdateContext<'_, 'gc>) { + // Storage for objects to remove + // Have to do this in two passes to avoid borrow-mut while already borrowed + let mut out = Vec::new(); + + // Find objects to remove + Self::find_display_objects_pending_removal(context.stage.root_clip(), &mut out); + + for child in out { + // Get the parent of this object + let parent = child.parent().unwrap(); + let parent_container = parent.as_container().unwrap(); + + // Remove it + parent_container.remove_child_directly(context, child); + } + } + // Run a single frame. #[instrument(level = "debug", skip_all)] pub fn run_frame(context: &mut UpdateContext<'_, 'gc>) { + // Remove pending objects + Self::remove_pending(context); + // In AVM1, we only ever execute the update phase, and all the work that // would ordinarily be phased is instead run all at once in whatever order // the SWF requests it. diff --git a/core/src/display_object.rs b/core/src/display_object.rs index b1db53d9e..4c3637824 100644 --- a/core/src/display_object.rs +++ b/core/src/display_object.rs @@ -1096,10 +1096,16 @@ pub trait TDisplayObject<'gc>: fn removed(&self) -> bool { self.base().removed() } + fn set_removed(&self, gc_context: MutationContext<'gc, '_>, value: bool) { self.base_mut(gc_context).set_removed(value) } + /// Is this object waiting to be removed on the start of the next frame + fn pending_removal(&self) -> bool { + self.depth() < 0 + } + /// Whether this display object is visible. /// Invisible objects are not rendered, but otherwise continue to exist normally. /// Returned by the `_visible`/`visible` ActionScript properties. diff --git a/core/src/display_object/container.rs b/core/src/display_object/container.rs index bfd288584..74827780b 100644 --- a/core/src/display_object/container.rs +++ b/core/src/display_object/container.rs @@ -6,7 +6,7 @@ use crate::display_object::avm1_button::Avm1Button; use crate::display_object::loader_display::LoaderDisplay; use crate::display_object::movie_clip::MovieClip; use crate::display_object::stage::Stage; -use crate::display_object::{Depth, DisplayObject, TDisplayObject}; +use crate::display_object::{Depth, DisplayObject, TDisplayObject, TInteractiveObject}; use crate::string::WStr; use gc_arena::{Collect, MutationContext}; use ruffle_macros::enum_trait_object; @@ -299,12 +299,29 @@ pub trait TDisplayObjectContainer<'gc>: } /// Remove (and unloads) a child display object from this container's render and depth lists. + /// + /// Will also handle AVM1 delayed clip removal, when a unload listener is present fn remove_child(&mut self, context: &mut UpdateContext<'_, 'gc>, child: DisplayObject<'gc>) { + // We should always be the parent of this child debug_assert!(DisplayObject::ptr_eq( child.parent().unwrap(), (*self).into() )); + // Check if this child should have delayed removal + if ChildContainer::should_delay_removal(child) { + ChildContainer::queue_removal(child, context); + } else { + self.remove_child_directly(context, child); + } + } + + /// Remove (and unloads) a child display object from this container's render and depth lists. + fn remove_child_directly( + &self, + context: &mut UpdateContext<'_, 'gc>, + child: DisplayObject<'gc>, + ) { dispatch_removed_event(child, context); let mut write = self.raw_container_mut(context.gc_context); @@ -774,9 +791,52 @@ impl<'gc> ChildContainer<'gc> { } /// Yield children in the order they are rendered. - fn iter_render_list<'a>(&'a self) -> impl 'a + Iterator> { + pub fn iter_render_list<'a>(&'a self) -> impl 'a + Iterator> { self.render_list.iter().copied() } + + /// Should the removal of this clip be delayed to the start of the next frame + /// + /// Checks recursively for unload handlers + pub fn should_delay_removal(child: DisplayObject<'gc>) -> bool { + // Do we have an unload event handler + if let Some(mc) = child.as_movie_clip() { + if mc.has_unload_handler() { + return true; + } + } + + // Otherwise, check children if we have them + if let Some(c) = child.as_container() { + for child in c.iter_render_list() { + if Self::should_delay_removal(child) { + return true; + } + } + } + + false + } + + /// Enqueue the given child and all sub-children for delayed removal at the start of the next frame + /// + /// This just moves the children to a negative depth + /// Will also fire unload events, as they should occur when the removal is queued, not when it actually occurs + fn queue_removal(child: DisplayObject<'gc>, context: &mut UpdateContext<'_, 'gc>) { + if let Some(c) = child.as_container() { + for child in c.iter_render_list() { + Self::queue_removal(child, context); + } + } + + let cur_depth = child.depth(); + child.set_depth(context.gc_context, -cur_depth); + + if let Some(mc) = child.as_movie_clip() { + // Clip events should still fire + mc.event_dispatch(context, crate::events::ClipEvent::Unload); + } + } } pub struct RenderIter<'gc> { diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index 2e32cf0e9..c18691ac0 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -897,6 +897,15 @@ impl<'gc> MovieClip<'gc> { self.0.write(context.gc_context).stop(context) } + /// Does this clip have a unload handler + pub fn has_unload_handler(&self) -> bool { + self.0 + .read() + .clip_event_handlers + .iter() + .any(|handler| handler.events.contains(ClipEventFlag::UNLOAD)) + } + /// Queues up a goto to the specified frame. /// `frame` should be 1-based. /// @@ -2561,7 +2570,12 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { let mut mc = self.0.write(context.gc_context); mc.stop_audio_stream(context); } - self.event_dispatch(context, ClipEvent::Unload); + + // If this clip is currently pending removal, then it unload event will have already been dispatched + if !self.pending_removal() { + self.event_dispatch(context, ClipEvent::Unload); + } + self.set_removed(context.gc_context, true); } diff --git a/core/src/player.rs b/core/src/player.rs index 8f293ee3a..e1c8396b1 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -1573,8 +1573,8 @@ impl Player { pub fn run_actions(context: &mut UpdateContext<'_, '_>) { // Note that actions can queue further actions, so a while loop is necessary here. while let Some(action) = context.action_queue.pop_action() { - // We don't run frame actions if the clip was removed after it queued the action. - if !action.is_unload && action.clip.removed() { + // We don't run frame actions if the clip was removed (or scheduled to be removed) after it queued the action. + if !action.is_unload && (action.clip.removed() || action.clip.pending_removal()) { continue; } diff --git a/tests/tests/swfs/avm1/unload/output.txt b/tests/tests/swfs/avm1/unload/output.txt new file mode 100644 index 000000000..db1ff0d6d --- /dev/null +++ b/tests/tests/swfs/avm1/unload/output.txt @@ -0,0 +1,20 @@ +Frame 1: +clip = _level0.clip +clip2 = _level0.clip2 +clip3 = _level0.clip3 +clip4 = _level0.clip4 +End frame 1 +Unload clip4 +unload clipEvent +Frame 2 +clip = _level0.clip +clip2 = undefined +clip3 = undefined +clip4 = undefined +End frame 2 +Frame 3 +clip = undefined +clip2 = undefined +clip3 = undefined +clip4 = undefined +End frame 3 diff --git a/tests/tests/swfs/avm1/unload/test.fla b/tests/tests/swfs/avm1/unload/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..7b55ab99ec1801d9aa7f89e04b089573dde9f794 GIT binary patch literal 5896 zcmb_gWmr^Q*B+E^Mp7E-Mi>!E0qGoCLb`@-7#bv{JEdU&K|rJ#Bm|{PLK=sV1_1$q zZ+Lxud|uz@`+j|Uu6><3`>cDdbM14lb?tSlDc(jU1^_StfFolYb->>SatWX$FA07O z*5gt8U#+l^R?1H_A9Km8$o$*v7AEGuob3iAbf=&K01wCj00N|$w5qbSqnW$Cg@YTn zm%ZJNuD;`n46(mM(U0)WZ%pbf?UYWo3{MnC7?|`l-|lEv`0<{Qm(+_|i!i^QygD7~ zA;gfvY@{p(drR=HXFImLQpa5`Tzsa%iYB(egaj zG7Ico z7PYVD*6(EKKaS9?_%D&;L^4HkE)J77421t!$D^;~ei6bb3!33g&+|=t&C}^l^6{D@ zr+@qootS^SQZM5No$JXJI-Dd1Nn-`|0qRutjtMUhIypH)yfNY@y3BcVK>lQWAH$9J zLF@Uv{$>$z2LD)~JlPAyoH*&u;Kcz=?) zR?So0OG$*np=SGI}Vrk$V2{Wd-vSA;$RJCp(3yMxmE?+3OLsns*X?{+A1-= zyP+Gn>?&($BG+f%%xTmJXY+8TiPtl?S=@4l$V-KKH|&G0ta!gHKKLOIQzC3xO}WHZ zEn&416)+pGa}Clo6}qU&Zqv;#OxT`MB4!GYj#iQZWaosyRaM(LE?E)S>bia;j0*X> zh!VmdtR$h03^nWw5GhL?^OLXvSRWPQjpjJ3!UrnR?`%1Q3i?b;EAFJQ{9X=C&VVc5 z574zhfs6Cy;^iYrvO?d0OKm$ZrV3|1R;x#H`|0RzLQA(-RO;=7RE%eQI-!_{z4mBc z&-kUoQLP4c{fG<4C+A`i;@@lQUY(xCE8UhkSW)A*fO6QQ=vT7U6c^;v4oHc|MhsUl z^!E^vL}Y(7rdod?AR2rMmspzC(|SCe{gm;JpR4FPlZ$p=>bi!pC~8zVJ)M@l+lUMRad0my4Tx;Y*5J*kp*fp02T~HxpVCtplF@olhXn7xl>Ifvdr0cZ0 z@1M{cG&{;|-Mam*9nP8cW!gts=&tz|%#ghH7!|>IAw&v3dl!#IY$w zK4h|OcUeWZ<@UK$RL`h@}FoiQ2Z4-F2%B>w8;sd&XW6 z(FzKony~>%Kk)2sGV^R#&(?qYIoZ_;qjr=tg!=Ow`rP4Dl6UGIsrA&;_MnP2+fx(=igsGtz5f(a&_Mh-sycb>>B|p2m{j<>hs$lXwHoV-o0RVDj zg(UfVp|iJeGch-DGx=G^1|3#-3ENNED!(f9DOEjz2D%R-3FIP%N}+JUWpKjTacYAHf9+WJ(c+s_z_ye5+DZ898w&Dv4Og@F3y z;W30Ow{7*N&x`w0H^&h;gs|(JJaLUodrbT~G|*xx{1%TscD`@-YhRTfHIxj6!x8Z8 zCdR&*6Al#(zgo4XmW~uAxeVIm*RY^=fT7f*i7ysqWno+BpOjxyMETM04W`E)7&gH3 zlN`A$G8u`&oN9D6Nhh)6SO`IPV~Ay@v|#yBTIun{ccc12a_ z`h`)1txT^Bi-4`i#N$< zq!)^+V(pqm zxWzCbQDT{qIx|Y$FPWJj9eArg zer$x1@6r3Uo?5n=eh^0Sp0pjcK*Dtgs|ii}BzLh+pvek;QhbK7*gwY1P-6^R)K}Q` zDmIRvHHQU8W-%3H*^|wNLdRAujrgpGdEmxAQ~`Vz%P_9?LU$_Zedk&_K8R{j+B5ag zkCiyw;gFXBL%I*Ox~#LPE7S#{hz$SOo@i70z8`)GUhQkG^gt$-p#Xx;JJG^S%c1I& zYhg`G7S8&Lr~L*-kD@L3s!upi%!|(X2#m;gEIs_&+>fZzQ|ZSWvI)_~@0(xc@^7$8 zkA45vbsb1_Z{CMV5Y3&kVsxl34@i$W#g2X-3|y`TR?ky?xqqYy$f2A#LWB{2L%(m@%W5e zXU9Zcgd+NJxV5EYQHRUoU##I}CSHUNQK=K=E=vgV+S1eyXVSzmJf)lrLk-TGBe<)` z?VxwJekhZMHUd|EJCc>=NvcaqS;8?{K3W!s$?@YQSs60y(P#WFPFOvYHO`XTY9Mrz6F{i|#Gw!LFU{ zjQzfQXnrX#JH@c22+%v1D%D8ocDg0NO&r9R30xy_`R6vu#B@6!B@Rn*(R6%YwNmu4 z!6!N4rdU1#gy`#&ZN!k$=0vMO>d24EraH!T>)>h6fjB?*cT5rn!%#>kkO}to$4e0O zwn3>YO}+31*D-X~Vb7CuRHmRlz&vuuzXcj<4u#_cm+s}m=7rF|qOvNtYQ!@v+&aKV zRcVoM%fBQZxx6>298(4Wktt0GO}g$hs4DiozpL~368;*zl8dUQdjFS#u&Jun);1L3=dWq@Dw{~& z(=^yIr>QsK+`bpNPht^W{32FAkaD&vn(G^1MCd5U;{H^#&8>GR>lydoTrmiwO7{`5 zxjfPst=DMQ2*Iso9>1R#j#P8ms!wz7R>-?hO=KJXl8A%3gEj(xJ+wNyC z?9oS7(UOEIbR{}T%f_}3f+Blo&6y{;&MYNK8>EZ6eDhVgX`PR z=rrR*W8zS{B0t67i^@LIVz$ne-K$iK%$qK)|0sZv;NHS)?D`(4Cq6L~Jh+igwu!5= zcW=RjsJ_J_>}{RMSM$UdbT3R&HZ+wRK8I7MPdEx@NKtxF;iSq7Q}sE7#9Yh$kRbgw zUU<0qoYxzQ^EEOq!eC9EExdBn*!N3*I4=~C65Ob|v6OS-IwiMd0*4(RR5ITx z+;{Mew{Hj8t#Osy`#FV@m}u4GpneLwzMvCITrxU`=CK+ug2Q~?I*i!;ke4}LWkW}f zRqIPQp2~*9W*td`6=PdIaNr;i!|VfvsJB6xQULw|OtN!bQ6~h9>mnr!9p+V`iuZ!y zhbMAp`8@yPcb7$4?IG(TeDXfGuEGgLwk0on{L{X}VhO@k6mXQ^5Tkz0Gwl-dohnVk z_g7Gw-nUhX>LlW}J%L+{85Q)PDF?3E= zGh=FdeC=%!S0|ni?>obRj+J)K=i8(nTcWweT1#? z2sG>PD!j52msKt3SBNUc$BGKwTqib4M)}wp?^=bW_FR`aJoA2r zpl~?X_Z=#CD)(IlLgEYePjAe$>&!(=4#tCg0(=haYL>jkRyjJyx@5Hko`+139hjzg zK=Bkdc{}X-GD7&Pn>=@N%dSC-Lu?7p^VWLseffgKpfMz8?|M8hN~TZ(i}6QhA#-G$ zK`R7yHB&#}>sk73+RgLsF2_YnWD^drE}=4H!zr@?$LAQUTyFEPOG>J^^^*E!r$!H2 zPUKHTzj*hgugX$ulmp6QE9Q%jOb{nF;S(KDOMhksNwT}{Cu6~5FG`6u`>4b(!kU`~s*4UV z+6V>$kd$Te&?Ax_a=V8dpBP)?s8qN06Y)UErqDDTq4{OzJK|am#9h4IC zaDnXxcsc7KRk))4hKfFh0w%srmhNRk8lk*xGG@=|? z9L^pbG6;8Fp7%>XJIa~5?Ru~K0H$aF0J7=$OMubvwl{UOqvN|d5v6Dh}LFD1_Kw0 zj&vCrlHaYkDznp(FSUIH>#{f);cVtmxVzSlO)) zS#-df+3|>;KYc#NW1dk$aaQMDnZ<>f$!o)zg11BiqASuH(H8A0P!HY~-gOp4y(f>B z+{hwDAu~WEHkal#KPnPlq6og`bk$jzn2xd4>&@eGKDlRz*xL5VS#7C)M##>2rz0bj zuxUDHc{4q;j;f(iIh#fzi+*4Zu7+UI_e8ZV5NmxeYXjn@_3uA(f zYDYlw^)%yr-6m6e0^j(=99M!YNRQsd_yu1hSnG=41h}P&9^AhoXJBYjsa+j=Q0fnBPzE@$QmqyTJI#gkMrzYwO!S z3gqg|ywUmWZIB(Ux08j5larl|nTeZ?qXUlzGK`72qp1bAm!;kR8{G75k`^_d`RyCs z@Mhjbce8LsDuT!$U{zf`E-4L=nj*?A;v4=WpZ=39iNqiW;m^xoIr4v@ZbJPWHz;Q0 z{F`X`GuW@D|4-TS-?$qHa{eJ={)+hv{HJ2@8%%)A=)YqAT|fBK#h*RVZx>F;?f8cd z>9>Rb-mROO^LwZhA>ZL2D$sv!z@IhfH$Di7|0N0kqS#GE`t=6CzSp1J{p}$dN#%cF v@}Ca=$68qU{;#h6EX`Ga; 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 Km0LNX0j zvNGP~4v$ndrF?4~cS1IaWTw^K`>(hok+3ShRKGW_Xsh-5nmUd+RR>|4c3-a!I(r@! z+caQ3-=HX8n(RrdrCoD_khliDrdMzxXIZ8u7YfCCQ7<>;QnR7Ua#=1(YDq2Cl)9?Q zQZY*OWayNpGo$BFH=NMSx1b#hoziaj*0zPG;tXn4&r9`ssnjeL~WRkhK4Rbr+vBqpR2zm*r*Y>*Y|yHq--FHApZa$b-nl6+G2zWVLpy`B z>xm!n?Y@dDzU?y>f=dTs(---o=rn)!d5Q~nG4Kq;|`F3q8ENiaSNcr za2Ap?mh@%6CM26dQ(bNn#$;BJ+DES#`_hZ0oiK`z!@A3Ha06o`c z#Dq))e3YC=n@OPJa_W1Su5lxB&2d)@6J7k_v2bA6mWPgbil=>v!)5`xJXb-1%n;WY XDHby*T2IWK5ac0~ilF}j`SunV7IMuS literal 0 HcmV?d00001 diff --git a/tests/tests/swfs/avm1/unload_nested_child/test.toml b/tests/tests/swfs/avm1/unload_nested_child/test.toml new file mode 100644 index 000000000..c64cb4c7d --- /dev/null +++ b/tests/tests/swfs/avm1/unload_nested_child/test.toml @@ -0,0 +1 @@ +num_frames = 3