diff --git a/core/src/avm2.rs b/core/src/avm2.rs index 40083888b156..563ffbb036aa 100644 --- a/core/src/avm2.rs +++ b/core/src/avm2.rs @@ -94,7 +94,7 @@ pub use crate::avm2::multiname::Multiname; pub use crate::avm2::namespace::{CommonNamespaces, Namespace}; pub use crate::avm2::object::{ ArrayObject, BitmapDataObject, ClassObject, EventObject, LoaderInfoObject, Object, - SharedObjectObject, SoundChannelObject, StageObject, TObject, + SharedObjectObject, SoundChannelObject, Stage3DObject, StageObject, TObject, }; pub use crate::avm2::qname::QName; pub use crate::avm2::value::Value; diff --git a/core/src/avm2/globals/flash/display/Stage3D.as b/core/src/avm2/globals/flash/display/Stage3D.as index 122bb642bd1b..5290f8db63a0 100644 --- a/core/src/avm2/globals/flash/display/Stage3D.as +++ b/core/src/avm2/globals/flash/display/Stage3D.as @@ -15,20 +15,12 @@ package flash.display { private native function requestContext3D_internal(context3DRenderMode:String, profiles:Vector.):void; public function requestContext3D(context3DRenderMode:String = "auto", profile:String = "baseline"):void { - // Several SWFS (the examples from the Context3D documentation, and the Starling framework) - // rely on the `context3DCreate` being fired asynchronously - they initialize variables - // after the call to `requestContext3D`, and then use those variables in the event handler. - // Currently, we create a `Context3D` synchronously, so we need to delay the event dispatch - var stage3d = this; this.checkProfile(profile); - setTimeout(function() { - stage3d.requestContext3D_internal(context3DRenderMode, Vector.([profile])); - }, 0); + this.requestContext3D_internal(context3DRenderMode, Vector.([profile])); } [API("692")] public function requestContext3DMatchingProfiles(profiles:Vector.):void { - var stage3d = this; var profiles = profiles.concat(); if (profiles.length == 0) { throw new ArgumentError("Error #2008: Parameter profiles must be one of the accepted values.", 2008); @@ -36,9 +28,7 @@ package flash.display { for each (var profile in profiles) { this.checkProfile(profile); } - setTimeout(function() { - stage3d.requestContext3D_internal("auto", profiles); - }, 0); + this.requestContext3D_internal("auto", profiles); } private function checkProfile(profile:String):Boolean { diff --git a/core/src/avm2/globals/flash/display/stage.rs b/core/src/avm2/globals/flash/display/stage.rs index 5100913eed48..f47f063f8f78 100644 --- a/core/src/avm2/globals/flash/display/stage.rs +++ b/core/src/avm2/globals/flash/display/stage.rs @@ -429,11 +429,7 @@ pub fn get_stage3ds<'gc>( if let Some(stage) = this.as_display_object().and_then(|this| this.as_stage()) { let storage = VectorStorage::from_values( - stage - .stage3ds() - .iter() - .map(|obj| Value::Object(*obj)) - .collect(), + stage.stage3ds().iter().map(|s| (*s).into()).collect(), false, Some(activation.avm2().classes().stage3d.inner_class_definition()), ); diff --git a/core/src/avm2/globals/flash/display/stage_3d.rs b/core/src/avm2/globals/flash/display/stage_3d.rs index 141ae8a1a57f..a695f7a6d05a 100644 --- a/core/src/avm2/globals/flash/display/stage_3d.rs +++ b/core/src/avm2/globals/flash/display/stage_3d.rs @@ -1,5 +1,3 @@ -use crate::avm2::globals::methods::flash_events_event_dispatcher as event_dispatcher_methods; -use crate::avm2::object::{Context3DObject, EventObject}; use crate::avm2::parameters::ParametersExt; use crate::avm2::{Activation, Error, Value}; use ruffle_render::backend::Context3DProfile; @@ -53,19 +51,18 @@ pub fn request_context3d_internal<'gc>( }) .unwrap(); - if this_stage3d.context3d().is_none() { - let context = activation.context.renderer.create_context3d(profile)?; - let context3d_obj = Context3DObject::from_context(activation, context, this_stage3d); - this_stage3d.set_context3d(Some(context3d_obj), activation.gc()); + // Several SWFS (the examples from the Context3D documentation, and the + // Starling framework) rely on the `context3DCreate` being fired + // asynchronously - they initialize variables after the call to + // `requestContext3D`, and then use those variables in the event handler. - let event = EventObject::bare_default_event(activation.context, "context3DCreate"); + // Our context3D creation is synchronous, so we need to delay it. Set the + // parameters that the context3d was requested with here, then actually + // create the context3d at the end of this frame in + // `frame_lifecycle::run_all_phases_avm2` and dispatch the context3DCreate + // event. - this.call_method( - event_dispatcher_methods::DISPATCH_EVENT, - &[event.into()], - activation, - )?; - } + this_stage3d.set_requesting_context3d(activation.gc(), profile); Ok(Value::Undefined) } diff --git a/core/src/avm2/globals/flash/display3D/context_3d.rs b/core/src/avm2/globals/flash/display3D/context_3d.rs index 1eb3ae95f678..3424d3d5dec2 100644 --- a/core/src/avm2/globals/flash/display3D/context_3d.rs +++ b/core/src/avm2/globals/flash/display3D/context_3d.rs @@ -843,6 +843,6 @@ pub fn dispose<'gc>( this.as_context_3d() .unwrap() .stage3d() - .set_context3d(None, activation.gc()); + .clear_context3d(activation.gc()); Ok(Value::Undefined) } diff --git a/core/src/avm2/globals/flash/events/EventDispatcher.as b/core/src/avm2/globals/flash/events/EventDispatcher.as index de98bf1ba37e..3e95ecd91d54 100644 --- a/core/src/avm2/globals/flash/events/EventDispatcher.as +++ b/core/src/avm2/globals/flash/events/EventDispatcher.as @@ -25,7 +25,6 @@ package flash.events { public native function hasEventListener(type:String):Boolean; - [Ruffle(NativeCallable)] public function dispatchEvent(event:Event):Boolean { // Some SWFs rely on the getter for `target` being called if (event.target) { diff --git a/core/src/avm2/object/context3d_object.rs b/core/src/avm2/object/context3d_object.rs index d3e915b22097..d20f71848e2c 100644 --- a/core/src/avm2/object/context3d_object.rs +++ b/core/src/avm2/object/context3d_object.rs @@ -2,12 +2,12 @@ use crate::avm2::Error; use crate::avm2::activation::Activation; +use crate::avm2::object::TObject; use crate::avm2::object::script_object::ScriptObjectData; -use crate::avm2::object::{Object, TObject}; use crate::avm2::value::Value; use crate::avm2_stub_method; use crate::bitmap::bitmap_data::BitmapRawData; -use crate::context::RenderContext; +use crate::context::{RenderContext, UpdateContext}; use gc_arena::{Collect, Gc, GcWeak}; use naga_agal::AgalError; use ruffle_common::utils::HasPrefixField; @@ -35,21 +35,20 @@ pub struct Context3DObjectWeak<'gc>(pub GcWeak<'gc, Context3DData<'gc>>); impl<'gc> Context3DObject<'gc> { pub fn from_context( - activation: &mut Activation<'_, 'gc>, - context: Box, + context: &mut UpdateContext<'gc>, + context3d: Box, stage3d: Stage3DObject<'gc>, - ) -> Object<'gc> { - let class = activation.avm2().classes().context3d; + ) -> Self { + let class = context.avm2.classes().context3d; Context3DObject(Gc::new( - activation.gc(), + context.gc(), Context3DData { base: ScriptObjectData::new(class), - render_context: Cell::new(Some(context)), + render_context: Cell::new(Some(context3d)), stage3d, }, )) - .into() } pub fn stage3d(self) -> Stage3DObject<'gc> { diff --git a/core/src/avm2/object/stage3d_object.rs b/core/src/avm2/object/stage3d_object.rs index f7713ac676cb..a0118157ee7b 100644 --- a/core/src/avm2/object/stage3d_object.rs +++ b/core/src/avm2/object/stage3d_object.rs @@ -1,13 +1,15 @@ //! Object representation for Stage3D objects +use crate::avm2::Avm2; use crate::avm2::object::script_object::ScriptObjectData; -use crate::avm2::object::{Object, TObject}; +use crate::avm2::object::{Context3DObject, EventObject, TObject}; use crate::context::UpdateContext; use core::fmt; use gc_arena::barrier::unlock; use gc_arena::lock::Lock; use gc_arena::{Collect, Gc, GcWeak, Mutation}; use ruffle_common::utils::HasPrefixField; +use ruffle_render::backend::Context3DProfile; use std::cell::Cell; #[derive(Clone, Collect, Copy)] @@ -33,18 +35,49 @@ impl<'gc> Stage3DObject<'gc> { context.gc(), Stage3DObjectData { base: ScriptObjectData::new(class), - context3d: Lock::new(None), + context3d_status: Lock::new(Context3DStatus::None), visible: Cell::new(true), }, )) } - pub fn context3d(self) -> Option> { - self.0.context3d.get() + pub fn context3d(self) -> Option> { + match self.0.context3d_status.get() { + Context3DStatus::Ready(object) => Some(object), + _ => None, + } } - pub fn set_context3d(self, context3d: Option>, mc: &Mutation<'gc>) { - unlock!(Gc::write(mc, self.0), Stage3DObjectData, context3d).set(context3d) + pub fn set_requesting_context3d(self, mc: &Mutation<'gc>, profile: Context3DProfile) { + self.set_status(mc, Context3DStatus::Requested { profile }); + } + + pub fn clear_context3d(self, mc: &Mutation<'gc>) { + self.set_status(mc, Context3DStatus::None); + } + + pub fn update_context3d_status(self, context: &mut UpdateContext<'gc>) { + if let Context3DStatus::Requested { profile } = self.0.context3d_status.get() { + let context3d = match context.renderer.create_context3d(profile) { + Ok(context3d) => context3d, + Err(err) => { + tracing::error!("Failed to create Context3d: {}", err); + // TODO the docs say FP dispatches an "error" event here + return; + } + }; + + let context3d_obj = Context3DObject::from_context(context, context3d, self); + self.set_status(context.gc(), Context3DStatus::Ready(context3d_obj)); + + let event = EventObject::bare_default_event(context, "context3DCreate"); + + Avm2::dispatch_event(context, event, self.into()); + } + } + + fn set_status(self, mc: &Mutation<'gc>, status: Context3DStatus<'gc>) { + unlock!(Gc::write(mc, self.0), Stage3DObjectData, context3d_status).set(status); } pub fn visible(self) -> bool { @@ -63,9 +96,8 @@ pub struct Stage3DObjectData<'gc> { /// Base script object base: ScriptObjectData<'gc>, - /// The context3D object associated with this Stage3D object, - /// if it's been created with `requestContext3D` - context3d: Lock>>, + /// The state context3D object associated with this Stage3D object. + context3d_status: Lock>, visible: Cell, } @@ -74,3 +106,14 @@ impl<'gc> TObject<'gc> for Stage3DObject<'gc> { HasPrefixField::as_prefix_gc(self.0) } } + +#[derive(Clone, Collect, Copy)] +#[collect(no_drop)] +pub enum Context3DStatus<'gc> { + None, + Requested { + #[collect(require_static)] + profile: Context3DProfile, + }, + Ready(Context3DObject<'gc>), +} diff --git a/core/src/display_object/stage.rs b/core/src/display_object/stage.rs index cc190cf08854..46fe8deb6ba8 100644 --- a/core/src/display_object/stage.rs +++ b/core/src/display_object/stage.rs @@ -4,7 +4,7 @@ use crate::avm1::Object as Avm1Object; use crate::avm2::object::Stage3DObject; use crate::avm2::{ Activation as Avm2Activation, Avm2, EventObject as Avm2EventObject, LoaderInfoObject, - Object as Avm2Object, StageObject as Avm2StageObject, + Stage3DObject as Avm2Stage3DObject, StageObject as Avm2StageObject, }; use crate::backend::ui::MouseCursor; use crate::config::Letterbox; @@ -71,7 +71,7 @@ pub struct StageData<'gc> { loader_info: Lock>>, /// An array of AVM2 'Stage3D' instances - stage3ds: RefLock>>, + stage3ds: RefLock>>, /// A tracker for the current keyboard focused element focus_tracker: FocusTracker<'gc>, @@ -287,7 +287,7 @@ impl<'gc> Stage<'gc> { context.renderer.set_quality(quality); } - pub fn stage3ds(&self) -> Ref<'_, Vec>> { + pub fn stage3ds(&self) -> Ref<'_, Vec>> { self.0.stage3ds.borrow() } @@ -626,11 +626,10 @@ impl<'gc> Stage<'gc> { // layer, and gets applied when we start the frame (before // `render_viewport` is called). for stage3d in self.stage3ds().iter() { - let stage3d = stage3d.as_stage_3d().unwrap(); if stage3d.visible() && let Some(context3d) = stage3d.context3d() { - context3d.as_context_3d().unwrap().render(context); + context3d.render(context); } } @@ -793,6 +792,18 @@ impl<'gc> Stage<'gc> { pub fn focus_tracker(self) -> FocusTracker<'gc> { self.0.focus_tracker } + + /// Updates the status of all the Stage3Ds. + /// + /// If any of them have requested a context, they will create one and set + /// it on themselves. + pub fn check_requested_context3ds(self, context: &mut UpdateContext<'gc>) { + let stage3ds = self.stage3ds(); + + for stage3d in &*stage3ds { + stage3d.update_context3d_status(context); + } + } } impl<'gc> TDisplayObject<'gc> for Stage<'gc> { @@ -824,8 +835,8 @@ impl<'gc> TDisplayObject<'gc> for Stage<'gc> { Avm2StageObject::for_display_object(context.gc(), self.into(), stage_constr); // Always create 4 Stage3D instances for now, which matches the flash projector behavior - let stage3ds: Vec> = - (0..4).map(|_| Stage3DObject::new(context).into()).collect(); + let stage3ds: Vec> = + (0..4).map(|_| Stage3DObject::new(context)).collect(); let write = Gc::write(context.gc(), self.0); unlock!(write, StageData, avm2_object).set(Some(avm2_stage)); diff --git a/core/src/frame_lifecycle.rs b/core/src/frame_lifecycle.rs index ed817cce415f..91bdb52df526 100644 --- a/core/src/frame_lifecycle.rs +++ b/core/src/frame_lifecycle.rs @@ -100,6 +100,9 @@ pub fn run_all_phases_avm2(context: &mut UpdateContext<'_>) { *context.frame_phase = FramePhase::Exit; broadcast_frame_exited(context); + // The correct time to run context3DCreated events seems to be here + stage.check_requested_context3ds(context); + // We cannot easily remove dead `GcWeak` instances from the orphan list // inside `each_orphan_movie`, since the callback may modify the orphan list. // Instead, we do one cleanup at the end of the frame. diff --git a/tests/tests/swfs/avm2/context3d_creation/Test.as b/tests/tests/swfs/avm2/context3d_creation/Test.as new file mode 100644 index 000000000000..2730fd31ffd5 --- /dev/null +++ b/tests/tests/swfs/avm2/context3d_creation/Test.as @@ -0,0 +1,32 @@ +package { + import flash.display.MovieClip; + import flash.display.Stage3D; + import flash.events.Event; + + public class Test extends MovieClip { + public function Test() { + var s3d:Stage3D = stage.stage3Ds[0];; + var self:Test = this; + + trace(this.currentFrame); + + addEventListener("enterFrame", function():void { + trace("Running enterFrame, currentFrame=" + self.currentFrame); + }); + addEventListener("frameConstructed", function():void { + trace("Running frameConstructed, currentFrame=" + self.currentFrame); + }); + addEventListener("exitFrame", function():void { + trace("Running exitFrame, currentFrame=" + self.currentFrame); + }); + + s3d.addEventListener("context3DCreate", context3DCreateHandler); + s3d.requestContext3D(); + } + + public function context3DCreateHandler(e:Event):void { + trace("context3DCreate dispatched, currentFrame=" + this.currentFrame); + trace("Stack trace from within context3DCreate: " + new Error().getStackTrace()); + } + } +} diff --git a/tests/tests/swfs/avm2/context3d_creation/output.txt b/tests/tests/swfs/avm2/context3d_creation/output.txt new file mode 100644 index 000000000000..a77edfba8824 --- /dev/null +++ b/tests/tests/swfs/avm2/context3d_creation/output.txt @@ -0,0 +1,9 @@ +1 +Running frameConstructed, currentFrame=1 +Running exitFrame, currentFrame=1 +context3DCreate dispatched, currentFrame=1 +Stack trace from within context3DCreate: Error + at Test/context3DCreateHandler() +Running enterFrame, currentFrame=2 +Running frameConstructed, currentFrame=2 +Running exitFrame, currentFrame=2 diff --git a/tests/tests/swfs/avm2/context3d_creation/test.swf b/tests/tests/swfs/avm2/context3d_creation/test.swf new file mode 100644 index 000000000000..d3398a644a22 Binary files /dev/null and b/tests/tests/swfs/avm2/context3d_creation/test.swf differ diff --git a/tests/tests/swfs/avm2/context3d_creation/test.toml b/tests/tests/swfs/avm2/context3d_creation/test.toml new file mode 100644 index 000000000000..3b42017e6bf8 --- /dev/null +++ b/tests/tests/swfs/avm2/context3d_creation/test.toml @@ -0,0 +1,4 @@ +num_frames = 2 + +[player_options] +with_renderer = { optional = false, quality = "low" } diff --git a/tests/tests/swfs/avm2/stage3d_agal_upload_errors/output.ruffle.txt b/tests/tests/swfs/avm2/stage3d_agal_upload_errors/output.ruffle.txt deleted file mode 100644 index e9f01f6c8f6d..000000000000 --- a/tests/tests/swfs/avm2/stage3d_agal_upload_errors/output.ruffle.txt +++ /dev/null @@ -1,130 +0,0 @@ -valid: uploaded -sampler_conflict: Error: Error #3696: AGAL validation failed: Second use of sampler register needs to specify the exact same properties. At token 2 of fragment program. - at flash.display3D::Program3D/upload() - at Test/tryUpload() - at Test/contextCreated() - at flash.events::EventDispatcher/dispatchEventInternal() - at flash.events::EventDispatcher/dispatchEvent() - at flash.display::Stage3D/requestContext3D_internal() - at MethodInfo-3048() -bad_header: ArgumentError: Error #3612: Programs must be in little endian format. - at flash.display3D::Program3D/upload() - at Test/tryUpload() - at Test/contextCreated() - at flash.events::EventDispatcher/dispatchEventInternal() - at flash.events::EventDispatcher/dispatchEvent() - at flash.display::Stage3D/requestContext3D_internal() - at MethodInfo-3048() -truncated: ArgumentError: Error #3612: Programs must be in little endian format. - at flash.display3D::Program3D/upload() - at Test/tryUpload() - at Test/contextCreated() - at flash.events::EventDispatcher/dispatchEventInternal() - at flash.events::EventDispatcher/dispatchEvent() - at flash.display::Stage3D/requestContext3D_internal() - at MethodInfo-3048() -empty: ArgumentError: Error #3615: AGAL validation failed: Program size below minimum length for program. - at flash.display3D::Program3D/upload() - at Test/tryUpload() - at Test/contextCreated() - at flash.events::EventDispatcher/dispatchEventInternal() - at flash.events::EventDispatcher/dispatchEvent() - at flash.display::Stage3D/requestContext3D_internal() - at MethodInfo-3048() -bad_version: Error: Error #3615: AGAL validation failed: Program size below minimum length for fragment program. - at flash.display3D::Program3D/upload() - at Test/tryUpload() - at Test/contextCreated() - at flash.events::EventDispatcher/dispatchEventInternal() - at flash.events::EventDispatcher/dispatchEvent() - at flash.display::Stage3D/requestContext3D_internal() - at MethodInfo-3048() -bad_shader_type: Error: Error #3615: AGAL validation failed: Program size below minimum length for fragment program. - at flash.display3D::Program3D/upload() - at Test/tryUpload() - at Test/contextCreated() - at flash.events::EventDispatcher/dispatchEventInternal() - at flash.events::EventDispatcher/dispatchEvent() - at flash.display::Stage3D/requestContext3D_internal() - at MethodInfo-3048() -bad_opcode: Error: Error #3620: AGAL validation failed: Invalid opcode, value out of range: 255 at token 1 of fragment program. - at flash.display3D::Program3D/upload() - at Test/tryUpload() - at Test/contextCreated() - at flash.events::EventDispatcher/dispatchEventInternal() - at flash.events::EventDispatcher/dispatchEvent() - at flash.display::Stage3D/requestContext3D_internal() - at MethodInfo-3048() -source_output: Error: Error #3646: AGAL validation failed: Can not read from output register for source operand 1 at token 1 of fragment program. - at flash.display3D::Program3D/upload() - at Test/tryUpload() - at Test/contextCreated() - at flash.events::EventDispatcher/dispatchEventInternal() - at flash.events::EventDispatcher/dispatchEvent() - at flash.display::Stage3D/requestContext3D_internal() - at MethodInfo-3048() -source_sampler: Error: Error #3638: AGAL validation failed: Sampler register only allowed as second operand in texture instructions for source operand 1 at token 1 of fragment program. - at flash.display3D::Program3D/upload() - at Test/tryUpload() - at Test/contextCreated() - at flash.events::EventDispatcher/dispatchEventInternal() - at flash.events::EventDispatcher/dispatchEvent() - at flash.display::Stage3D/requestContext3D_internal() - at MethodInfo-3048() -dest_constant: Error: Error #3652: AGAL validation failed: Constant registers can not be written to for destination operand at token 1 of fragment program. - at flash.display3D::Program3D/upload() - at Test/tryUpload() - at Test/contextCreated() - at flash.events::EventDispatcher/dispatchEventInternal() - at flash.events::EventDispatcher/dispatchEvent() - at flash.display::Stage3D/requestContext3D_internal() - at MethodInfo-3048() -dest_attribute: Error: Error #3651: AGAL validation failed: Attribute registers can not be written to for destination operand at token 1 of vertex program. - at flash.display3D::Program3D/upload() - at Test/tryUpload() - at Test/contextCreated() - at flash.events::EventDispatcher/dispatchEventInternal() - at flash.events::EventDispatcher/dispatchEvent() - at flash.display::Stage3D/requestContext3D_internal() - at MethodInfo-3048() -dest_sampler: Error: Error #3649: AGAL validation failed: Sampler registers can not be written to for destination operand at token 1 of fragment program. - at flash.display3D::Program3D/upload() - at Test/tryUpload() - at Test/contextCreated() - at flash.events::EventDispatcher/dispatchEventInternal() - at flash.events::EventDispatcher/dispatchEvent() - at flash.display::Stage3D/requestContext3D_internal() - at MethodInfo-3048() -dest_fragreg: Error: Error #3749: AGAL validation failed: Depth output register index out of bounds for destination operand at token 1 of fragment program. - at flash.display3D::Program3D/upload() - at Test/tryUpload() - at Test/contextCreated() - at flash.events::EventDispatcher/dispatchEventInternal() - at flash.events::EventDispatcher/dispatchEvent() - at flash.display::Stage3D/requestContext3D_internal() - at MethodInfo-3048() -indirect_frag: Error: Error #3639: AGAL validation failed: Indirect addressing only allowed in vertex programs for source operand 1 at token 1 of fragment program. - at flash.display3D::Program3D/upload() - at Test/tryUpload() - at Test/contextCreated() - at flash.events::EventDispatcher/dispatchEventInternal() - at flash.events::EventDispatcher/dispatchEvent() - at flash.display::Stage3D/requestContext3D_internal() - at MethodInfo-3048() -indirect_non_const: Error: Error #3640: AGAL validation failed: Indirect addressing only allowed into constant registers for source operand 1 at token 1 of vertex program. - at flash.display3D::Program3D/upload() - at Test/tryUpload() - at Test/contextCreated() - at flash.events::EventDispatcher/dispatchEventInternal() - at flash.events::EventDispatcher/dispatchEvent() - at flash.display::Stage3D/requestContext3D_internal() - at MethodInfo-3048() -indirect_valid: uploaded -source_fragreg: Error: Error #3749: AGAL validation failed: Depth output register index out of bounds for source operand 1 at token 1 of fragment program. - at flash.display3D::Program3D/upload() - at Test/tryUpload() - at Test/contextCreated() - at flash.events::EventDispatcher/dispatchEventInternal() - at flash.events::EventDispatcher/dispatchEvent() - at flash.display::Stage3D/requestContext3D_internal() - at MethodInfo-3048() diff --git a/tests/tests/swfs/avm2/stage3d_agal_upload_errors/test.toml b/tests/tests/swfs/avm2/stage3d_agal_upload_errors/test.toml index 4d3e8f2c1cb0..31da7456a680 100644 --- a/tests/tests/swfs/avm2/stage3d_agal_upload_errors/test.toml +++ b/tests/tests/swfs/avm2/stage3d_agal_upload_errors/test.toml @@ -1,5 +1,4 @@ num_frames = 10 -known_failure = true [player_options] with_renderer = { optional = false, quality = "low" }