core: Implement chunked decoding infrastructure.

We currently do not actually enforce a decoding limit; just add the functionality necessary to do so.
This commit is contained in:
David Wendt 2021-07-20 21:38:07 -04:00 committed by kmeisthax
parent c63f915ae2
commit 4e9bb3a173
5 changed files with 123 additions and 14 deletions

View File

@ -683,7 +683,7 @@ fn load_playerglobal<'gc>(
Ok(()) 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 { macro_rules! avm2_system_classes_playerglobal {
($activation:expr, $script:expr, [$(($package:expr, $class_name:expr, $field:ident)),* $(,)?]) => { ($activation:expr, $script:expr, [$(($package:expr, $class_name:expr, $field:ident)),* $(,)?]) => {
$( $(

View File

@ -329,7 +329,8 @@ impl Iterator for StreamTagReader {
}; };
let mut reader = self.swf_data.read_from(self.pos as u64); 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; 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. // If we hit a SoundStreamBlock within this frame, return it. Otherwise, the stream should end.

View File

@ -196,7 +196,7 @@ impl<'gc> MovieClip<'gc> {
base: Default::default(), base: Default::default(),
static_data: Gc::allocate( static_data: Gc::allocate(
gc_context, 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, tag_stream_pos: 0,
current_frame: 0, current_frame: 0,
@ -256,6 +256,10 @@ impl<'gc> MovieClip<'gc> {
movie.clone().into(), movie.clone().into(),
num_frames, num_frames,
loader_info, loader_info,
Some(GcCell::allocate(
activation.context.gc_context,
PreloadProgress::default(),
)),
activation.context.gc_context, activation.context.gc_context,
), ),
), ),
@ -334,6 +338,10 @@ impl<'gc> MovieClip<'gc> {
movie.into(), movie.into(),
total_frames, total_frames,
loader_info.map(|l| l.into()), loader_info.map(|l| l.into()),
Some(GcCell::allocate(
context.gc_context,
PreloadProgress::default(),
)),
context.gc_context, context.gc_context,
), ),
); );
@ -346,15 +354,35 @@ impl<'gc> MovieClip<'gc> {
drop(mc); 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; use swf::TagCode;
// TODO: Re-creating static data because preload step occurs after construction. // TODO: Re-creating static data because preload step occurs after construction.
// Should be able to hoist this up somewhere, or use MaybeUninit. // Should be able to hoist this up somewhere, or use MaybeUninit.
let mut static_data = (&*self.0.read().static_data).clone(); let mut static_data = (&*self.0.read().static_data).clone();
let data = self.0.read().static_data.swf.clone(); let data = self.0.read().static_data.swf.clone();
let mut reader = data.read_from(0); let mut reader = data.read_from(0);
let mut cur_frame = 1; let (mut cur_frame, mut start_pos) = if let Some(progress) = static_data.preload_progress {
let mut start_pos = 0; (
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 { let tag_callback = |reader: &mut SwfStream<'_>, tag_code, tag_len| match tag_code {
TagCode::CsmTextSettings => self TagCode::CsmTextSettings => self
@ -514,7 +542,18 @@ impl<'gc> MovieClip<'gc> {
.define_binary_data(context, reader), .define_binary_data(context, reader),
_ => Ok(()), _ => 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 // End-of-clip should be treated as ShowFrame
self.0 self.0
@ -524,6 +563,8 @@ impl<'gc> MovieClip<'gc> {
self.0.write(context.gc_context).static_data = self.0.write(context.gc_context).static_data =
Gc::allocate(context.gc_context, static_data); Gc::allocate(context.gc_context, static_data);
is_finished.unwrap_or(true)
} }
#[inline] #[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), TagCode::SoundStreamBlock if run_sounds => self.sound_stream_block(context, reader),
_ => Ok(()), _ => 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 // On AS3, we deliberately run all removals before the frame number or
// tag position updates. This ensures that code that runs gotos when a // tag position updates. This ensures that code that runs gotos when a
@ -1504,7 +1545,7 @@ impl<'gc> MovieClip<'gc> {
), ),
_ => Ok(()), _ => 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; 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. /// Static data shared between all instances of a movie clip.
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Clone, Collect)] #[derive(Clone, Collect)]
@ -3662,11 +3727,16 @@ struct MovieClipStatic<'gc> {
/// However, it will be set for an AVM1 movie loaded from AVM2 /// However, it will be set for an AVM1 movie loaded from AVM2
/// via `Loader` /// via `Loader`
loader_info: Option<Avm2Object<'gc>>, loader_info: Option<Avm2Object<'gc>>,
/// Preload progress for a given SWF.
///
/// Only present for root movies in AVM1 or AVM2.
preload_progress: Option<GcCell<'gc, PreloadProgress>>,
} }
impl<'gc> MovieClipStatic<'gc> { impl<'gc> MovieClipStatic<'gc> {
fn empty(movie: Arc<SwfMovie>, gc_context: MutationContext<'gc, '_>) -> Self { fn empty(movie: Arc<SwfMovie>, 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( fn with_data(
@ -3674,6 +3744,7 @@ impl<'gc> MovieClipStatic<'gc> {
swf: SwfSlice, swf: SwfSlice,
total_frames: FrameNumber, total_frames: FrameNumber,
loader_info: Option<Avm2Object<'gc>>, loader_info: Option<Avm2Object<'gc>>,
preload_progress: Option<GcCell<'gc, PreloadProgress>>,
gc_context: MutationContext<'gc, '_>, gc_context: MutationContext<'gc, '_>,
) -> Self { ) -> Self {
Self { Self {
@ -3686,6 +3757,7 @@ impl<'gc> MovieClipStatic<'gc> {
audio_stream_handle: None, audio_stream_handle: None,
exported_name: GcCell::allocate(gc_context, None), exported_name: GcCell::allocate(gc_context, None),
loader_info, loader_info,
preload_progress,
} }
} }
} }

View File

@ -1329,7 +1329,11 @@ impl Player {
fn preload(&mut self) { fn preload(&mut self) {
self.mutate_with_update_context(|context| { self.mutate_with_update_context(|context| {
let root = context.stage.root_clip(); 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 { if self.swf.is_action_script_3() && self.warn_on_unsupported_content {
self.ui.display_unsupported_message(); self.ui.display_unsupported_message();

View File

@ -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>( pub fn decode_tags<'a, F>(
reader: &mut SwfStream<'a>, reader: &mut SwfStream<'a>,
mut tag_callback: F, mut tag_callback: F,
stop_tag: TagCode, stop_tag: TagCode,
) -> Result<(), Error> mut chunk_limit: Option<usize>,
) -> Result<bool, Error>
where where
F: for<'b> FnMut(&'b mut SwfStream<'a>, TagCode, usize) -> DecodeResult, F: for<'b> FnMut(&'b mut SwfStream<'a>, TagCode, usize) -> DecodeResult,
{ {
loop { 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()?; let (tag_code, tag_len) = reader.read_tag_code_and_length()?;
if tag_len > reader.get_ref().len() { if tag_len > reader.get_ref().len() {
log::error!("Unexpected EOF when reading tag"); log::error!("Unexpected EOF when reading tag");
@ -376,7 +404,11 @@ where
} }
*reader.get_mut() = end_slice; *reader.get_mut() = end_slice;
if let Some(ref mut chunk_limit) = chunk_limit {
*chunk_limit -= 1;
}
} }
Ok(()) Ok(true)
} }