diff --git a/core/src/avm2/globals.rs b/core/src/avm2/globals.rs index 05870af18..138559e0b 100644 --- a/core/src/avm2/globals.rs +++ b/core/src/avm2/globals.rs @@ -683,7 +683,7 @@ fn load_playerglobal<'gc>( Ok(()) }; - let _ = tag_utils::decode_tags(&mut reader, tag_callback, TagCode::End); + let _ = tag_utils::decode_tags(&mut reader, tag_callback, TagCode::End, None); macro_rules! avm2_system_classes_playerglobal { ($activation:expr, $script:expr, [$(($package:expr, $class_name:expr, $field:ident)),* $(,)?]) => { $( diff --git a/core/src/backend/audio/decoders.rs b/core/src/backend/audio/decoders.rs index 2395dc371..1ba30bd3e 100644 --- a/core/src/backend/audio/decoders.rs +++ b/core/src/backend/audio/decoders.rs @@ -329,7 +329,8 @@ impl Iterator for StreamTagReader { }; let mut reader = self.swf_data.read_from(self.pos as u64); - let _ = crate::tag_utils::decode_tags(&mut reader, tag_callback, TagCode::ShowFrame); + let _ = + crate::tag_utils::decode_tags(&mut reader, tag_callback, TagCode::ShowFrame, None); self.pos = reader.get_ref().as_ptr() as usize - swf_data.as_ref().as_ptr() as usize; // If we hit a SoundStreamBlock within this frame, return it. Otherwise, the stream should end. diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index 45e09f30b..e079299f1 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -196,7 +196,7 @@ impl<'gc> MovieClip<'gc> { base: Default::default(), static_data: Gc::allocate( gc_context, - MovieClipStatic::with_data(id, swf, num_frames, None, gc_context), + MovieClipStatic::with_data(id, swf, num_frames, None, None, gc_context), ), tag_stream_pos: 0, current_frame: 0, @@ -256,6 +256,10 @@ impl<'gc> MovieClip<'gc> { movie.clone().into(), num_frames, loader_info, + Some(GcCell::allocate( + activation.context.gc_context, + PreloadProgress::default(), + )), activation.context.gc_context, ), ), @@ -334,6 +338,10 @@ impl<'gc> MovieClip<'gc> { movie.into(), total_frames, loader_info.map(|l| l.into()), + Some(GcCell::allocate( + context.gc_context, + PreloadProgress::default(), + )), context.gc_context, ), ); @@ -346,15 +354,35 @@ impl<'gc> MovieClip<'gc> { drop(mc); } - pub fn preload(self, context: &mut UpdateContext<'_, 'gc, '_>) { + /// Preload a chunk of the movie. + /// + /// A "chunk" is an implementor-chosen number of tags that are parsed + /// before this function returns. This function will only parse up to a + /// certain number of tags, and then return. If this function returns false, + /// then the preload didn't complete and further preloads should occur + /// until this returns true. + /// + /// The chunked preload assumes that preloading is happening within the + /// context of an event loop. As such, multiple chunks should be processed + /// in between yielding to the underlying event loop, either through + /// `await`, returning to the loop directly, or some other mechanism. + pub fn preload(self, context: &mut UpdateContext<'_, 'gc, '_>) -> bool { use swf::TagCode; // TODO: Re-creating static data because preload step occurs after construction. // Should be able to hoist this up somewhere, or use MaybeUninit. let mut static_data = (&*self.0.read().static_data).clone(); let data = self.0.read().static_data.swf.clone(); let mut reader = data.read_from(0); - let mut cur_frame = 1; - let mut start_pos = 0; + let (mut cur_frame, mut start_pos) = if let Some(progress) = static_data.preload_progress { + ( + progress.read().cur_preload_frame, + progress.read().last_frame_start_pos, + ) + } else { + log::warn!("Preloading a non-root movie."); + + (1, 0) + }; let tag_callback = |reader: &mut SwfStream<'_>, tag_code, tag_len| match tag_code { TagCode::CsmTextSettings => self @@ -514,7 +542,18 @@ impl<'gc> MovieClip<'gc> { .define_binary_data(context, reader), _ => Ok(()), }; - let _ = tag_utils::decode_tags(&mut reader, tag_callback, TagCode::End); + let is_finished = tag_utils::decode_tags(&mut reader, tag_callback, TagCode::End, None); + + // These variables will be persisted to be picked back up in the next + // chunk. + if let Some(progress) = static_data.preload_progress { + let mut write = progress.write(context.gc_context); + + write.next_preload_chunk = + (reader.get_ref().as_ptr() as u64).saturating_sub(data.data().as_ptr() as u64); + write.cur_preload_frame = cur_frame; + write.last_frame_start_pos = start_pos; + } // End-of-clip should be treated as ShowFrame self.0 @@ -524,6 +563,8 @@ impl<'gc> MovieClip<'gc> { self.0.write(context.gc_context).static_data = Gc::allocate(context.gc_context, static_data); + + is_finished.unwrap_or(true) } #[inline] @@ -1082,7 +1123,7 @@ impl<'gc> MovieClip<'gc> { } }; - let _ = tag_utils::decode_tags(&mut reader, tag_callback, TagCode::ShowFrame); + let _ = tag_utils::decode_tags(&mut reader, tag_callback, TagCode::ShowFrame, None); } } @@ -1165,7 +1206,7 @@ impl<'gc> MovieClip<'gc> { TagCode::SoundStreamBlock if run_sounds => self.sound_stream_block(context, reader), _ => Ok(()), }; - let _ = tag_utils::decode_tags(&mut reader, tag_callback, TagCode::ShowFrame); + let _ = tag_utils::decode_tags(&mut reader, tag_callback, TagCode::ShowFrame, None); // On AS3, we deliberately run all removals before the frame number or // tag position updates. This ensures that code that runs gotos when a @@ -1504,7 +1545,7 @@ impl<'gc> MovieClip<'gc> { ), _ => Ok(()), }; - let _ = tag_utils::decode_tags(&mut reader, tag_callback, TagCode::ShowFrame); + let _ = tag_utils::decode_tags(&mut reader, tag_callback, TagCode::ShowFrame, None); } let hit_target_frame = self.0.read().current_frame == frame; @@ -3635,6 +3676,30 @@ impl Default for Scene { } } +/// The load progress for a given SWF. +#[derive(Clone, Collect)] +#[collect(require_static)] +struct PreloadProgress { + /// The SWF offset to start the next preload chunk from. + next_preload_chunk: u64, + + /// The current frame being preloaded. + cur_preload_frame: u16, + + /// The SWF offset that the current frame started in. + last_frame_start_pos: u64, +} + +impl Default for PreloadProgress { + fn default() -> Self { + Self { + next_preload_chunk: 0, + cur_preload_frame: 1, + last_frame_start_pos: 0, + } + } +} + /// Static data shared between all instances of a movie clip. #[allow(dead_code)] #[derive(Clone, Collect)] @@ -3662,11 +3727,16 @@ struct MovieClipStatic<'gc> { /// However, it will be set for an AVM1 movie loaded from AVM2 /// via `Loader` loader_info: Option>, + + /// Preload progress for a given SWF. + /// + /// Only present for root movies in AVM1 or AVM2. + preload_progress: Option>, } impl<'gc> MovieClipStatic<'gc> { fn empty(movie: Arc, gc_context: MutationContext<'gc, '_>) -> Self { - Self::with_data(0, SwfSlice::empty(movie), 1, None, gc_context) + Self::with_data(0, SwfSlice::empty(movie), 1, None, None, gc_context) } fn with_data( @@ -3674,6 +3744,7 @@ impl<'gc> MovieClipStatic<'gc> { swf: SwfSlice, total_frames: FrameNumber, loader_info: Option>, + preload_progress: Option>, gc_context: MutationContext<'gc, '_>, ) -> Self { Self { @@ -3686,6 +3757,7 @@ impl<'gc> MovieClipStatic<'gc> { audio_stream_handle: None, exported_name: GcCell::allocate(gc_context, None), loader_info, + preload_progress, } } } diff --git a/core/src/player.rs b/core/src/player.rs index 9216bd14b..dabee6de5 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -1329,7 +1329,11 @@ impl Player { fn preload(&mut self) { self.mutate_with_update_context(|context| { let root = context.stage.root_clip(); - root.as_movie_clip().unwrap().preload(context); + let mut preload_done = false; + + while !preload_done { + preload_done = root.as_movie_clip().unwrap().preload(context); + } }); if self.swf.is_action_script_3() && self.warn_on_unsupported_content { self.ui.display_unsupported_message(); diff --git a/core/src/tag_utils.rs b/core/src/tag_utils.rs index deaa21af8..766129a42 100644 --- a/core/src/tag_utils.rs +++ b/core/src/tag_utils.rs @@ -341,15 +341,43 @@ impl SwfSlice { } } +/// Decode tags from a SWF stream reader. +/// +/// The given `tag_callback` will be called for each decoded tag. It will be +/// provided with the stream to read from, the tag code read, and the tag's +/// size. The callback is responsible for (optionally) parsing the contents of +/// the tag; otherwise, it will be skipped. +/// +/// Decoding will terminate when the following conditions occur: +/// +/// * After the given `stop_tag` is encountered and passed to the callback +/// * The decoder encounters a tag longer than the underlying SWF slice +/// * The decoder decodes more than `chunk_limit` tags (if provided), in which +/// case this function also returns `false` +/// * The SWF stream is otherwise corrupt or unreadable (indicated as an error +/// result) +/// +/// Decoding will also log tags longer than the SWF slice, error messages +/// yielded from the tag callback, and unknown tags. It will *only* return an +/// error message if the SWF tag itself could not be parsed. Otherwise, it +/// returns `true` if decoding progressed to the stop tag or EOF, or `false` if +/// the chunk limit was reached. pub fn decode_tags<'a, F>( reader: &mut SwfStream<'a>, mut tag_callback: F, stop_tag: TagCode, -) -> Result<(), Error> + mut chunk_limit: Option, +) -> Result where F: for<'b> FnMut(&'b mut SwfStream<'a>, TagCode, usize) -> DecodeResult, { loop { + if let Some(chunk_limit) = chunk_limit { + if chunk_limit < 1 { + return Ok(false); + } + } + let (tag_code, tag_len) = reader.read_tag_code_and_length()?; if tag_len > reader.get_ref().len() { log::error!("Unexpected EOF when reading tag"); @@ -376,7 +404,11 @@ where } *reader.get_mut() = end_slice; + + if let Some(ref mut chunk_limit) = chunk_limit { + *chunk_limit -= 1; + } } - Ok(()) + Ok(true) }