core: Implement lazy decoding of bitmaps

We hit a pathological case in House
(https://github.com/ruffle-rs/ruffle/issues/15154),
where eagerly decoding bitmaps during preloading results in
over 10GB of ram being used.

With this PR, we store the compressed bitmap, and only decode it
each time we instantiate it. In order to support bitmap fills,
we store the decoded width/height and a lazily-initialized GPU handle
in `Character::Bitmap`
This commit is contained in:
Aaron Hill 2024-02-07 08:25:10 -05:00
parent 05fc77e8cd
commit 900a8407d6
17 changed files with 343 additions and 134 deletions

View File

@ -1498,20 +1498,20 @@ fn load_bitmap<'gc>(
.library_for_movie(movie) .library_for_movie(movie)
.and_then(|l| l.character_by_export_name(name)); .and_then(|l| l.character_by_export_name(name));
let Some(Character::Bitmap(bitmap)) = character else { let Some((_id, Character::Bitmap { compressed, .. })) = character else {
return Ok(Value::Undefined); return Ok(Value::Undefined);
}; };
let bitmap = compressed.decode().unwrap();
let transparency = true; let transparency = true;
let bitmap_data = BitmapData::new_with_pixels( let bitmap_data = BitmapData::new_with_pixels(
bitmap.width().into(), bitmap.width(),
bitmap.height().into(), bitmap.height(),
transparency, transparency,
bitmap bitmap
.bitmap_data(activation.context.renderer) .as_colors()
.read() .map(crate::bitmap::bitmap_data::Color::from)
.pixels() .collect(),
.to_vec(),
); );
Ok(new_bitmap_data( Ok(new_bitmap_data(
activation.context.gc_context, activation.context.gc_context,

View File

@ -826,7 +826,7 @@ fn attach_movie<'gc>(
.context .context
.library .library
.library_for_movie(movie_clip.movie()) .library_for_movie(movie_clip.movie())
.ok_or("Movie is missing!") .ok_or("Movie is missing!".into())
.and_then(|l| l.instantiate_by_export_name(export_name, activation.context.gc_context)) .and_then(|l| l.instantiate_by_export_name(export_name, activation.context.gc_context))
{ {
// Set name and attach to parent. // Set name and attach to parent.

View File

@ -79,7 +79,7 @@ fn attach_sound<'gc>(
.owner() .owner()
.unwrap_or_else(|| activation.base_clip().avm1_root()) .unwrap_or_else(|| activation.base_clip().avm1_root())
.movie(); .movie();
if let Some(Character::Sound(sound)) = activation if let Some((_, Character::Sound(sound))) = activation
.context .context
.library .library
.library_for_movie_mut(movie) .library_for_movie_mut(movie)
@ -439,7 +439,7 @@ fn stop<'gc>(
.owner() .owner()
.unwrap_or_else(|| activation.base_clip().avm1_root()) .unwrap_or_else(|| activation.base_clip().avm1_root())
.movie(); .movie();
if let Some(Character::Sound(sound)) = activation if let Some((_, Character::Sound(sound))) = activation
.context .context
.library .library
.library_for_movie_mut(movie) .library_for_movie_mut(movie)

View File

@ -5,6 +5,7 @@ use crate::avm2::Activation;
use crate::avm2::AvmString; use crate::avm2::AvmString;
use crate::avm2::Multiname; use crate::avm2::Multiname;
use crate::avm2::Value; use crate::avm2::Value;
use std::borrow::Cow;
use std::fmt::Debug; use std::fmt::Debug;
use std::mem::size_of; use std::mem::size_of;
@ -661,6 +662,12 @@ impl<'gc, 'a> From<&'a str> for Error<'gc> {
} }
} }
impl<'gc, 'a> From<Cow<'a, str>> for Error<'gc> {
fn from(val: Cow<'a, str>) -> Error<'gc> {
Error::RustError(val.into())
}
}
impl<'gc> From<String> for Error<'gc> { impl<'gc> From<String> for Error<'gc> {
fn from(val: String) -> Error<'gc> { fn from(val: String) -> Error<'gc> {
Error::RustError(val.into()) Error::RustError(val.into())

View File

@ -44,14 +44,18 @@ pub fn bitmap_allocator<'gc>(
.avm2_class_registry() .avm2_class_registry()
.class_symbol(class) .class_symbol(class)
{ {
if let Some(Character::Bitmap(bitmap)) = activation if let Some(Character::Bitmap {
compressed,
avm2_bitmapdata_class: _,
handle: _,
}) = activation
.context .context
.library .library
.library_for_movie_mut(movie) .library_for_movie_mut(movie)
.character_by_id(symbol) .character_by_id(symbol)
.cloned() .cloned()
{ {
let new_bitmap_data = fill_bitmap_data_from_symbol(activation, &bitmap); let new_bitmap_data = fill_bitmap_data_from_symbol(activation, &compressed);
let bitmap_data_obj = BitmapDataObject::from_bitmap_data_internal( let bitmap_data_obj = BitmapDataObject::from_bitmap_data_internal(
activation, activation,
BitmapDataWrapper::dummy(activation.context.gc_context), BitmapDataWrapper::dummy(activation.context.gc_context),
@ -66,10 +70,9 @@ pub fn bitmap_allocator<'gc>(
new_bitmap_data, new_bitmap_data,
false, false,
&activation.caller_movie_or_root(), &activation.caller_movie_or_root(),
) );
.into();
let obj = initialize_for_allocator(activation, child, orig_class)?; let obj = initialize_for_allocator(activation, child.into(), orig_class)?;
obj.set_public_property("bitmapData", bitmap_data_obj.into(), activation)?; obj.set_public_property("bitmapData", bitmap_data_obj.into(), activation)?;
return Ok(obj); return Ok(obj);
} }

View File

@ -16,8 +16,7 @@ use crate::bitmap::bitmap_data::{
}; };
use crate::bitmap::bitmap_data::{BitmapDataDrawError, IBitmapDrawable}; use crate::bitmap::bitmap_data::{BitmapDataDrawError, IBitmapDrawable};
use crate::bitmap::{is_size_valid, operations}; use crate::bitmap::{is_size_valid, operations};
use crate::character::Character; use crate::character::{Character, CompressedBitmap};
use crate::display_object::Bitmap;
use crate::display_object::TDisplayObject; use crate::display_object::TDisplayObject;
use crate::ecma_conversions::round_to_even; use crate::ecma_conversions::round_to_even;
use crate::swf::BlendMode; use crate::swf::BlendMode;
@ -67,18 +66,19 @@ fn get_rectangle_x_y_width_height<'gc>(
/// class named by `name`. /// class named by `name`.
pub fn fill_bitmap_data_from_symbol<'gc>( pub fn fill_bitmap_data_from_symbol<'gc>(
activation: &mut Activation<'_, 'gc>, activation: &mut Activation<'_, 'gc>,
bd: &Bitmap<'gc>, bd: &CompressedBitmap,
) -> BitmapDataWrapper<'gc> { ) -> BitmapDataWrapper<'gc> {
let bitmap = bd.decode().expect("Failed to decode BitmapData");
let new_bitmap_data = GcCell::new( let new_bitmap_data = GcCell::new(
activation.context.gc_context, activation.context.gc_context,
BitmapData::new_with_pixels( BitmapData::new_with_pixels(
Bitmap::width(*bd).into(), bitmap.width(),
Bitmap::height(*bd).into(), bitmap.height(),
true, true,
bd.bitmap_data(activation.context.renderer) bitmap
.read() .as_colors()
.pixels() .map(crate::bitmap::bitmap_data::Color::from)
.to_vec(), .collect(),
), ),
); );
BitmapDataWrapper::new(new_bitmap_data) BitmapDataWrapper::new(new_bitmap_data)
@ -111,9 +111,14 @@ pub fn init<'gc>(
.cloned() .cloned()
}); });
let new_bitmap_data = if let Some(Character::Bitmap(bitmap)) = character { let new_bitmap_data = if let Some(Character::Bitmap {
compressed,
avm2_bitmapdata_class: _,
handle: _,
}) = character
{
// Instantiating BitmapData from an Animate-style bitmap asset // Instantiating BitmapData from an Animate-style bitmap asset
fill_bitmap_data_from_symbol(activation, &bitmap) fill_bitmap_data_from_symbol(activation, &compressed)
} else { } else {
if character.is_some() { if character.is_some() {
//TODO: Determine if mismatched symbols will still work as a //TODO: Determine if mismatched symbols will still work as a

View File

@ -1,10 +1,14 @@
use std::cell::RefCell;
use crate::backend::audio::SoundHandle; use crate::backend::audio::SoundHandle;
use crate::binary_data::BinaryData; use crate::binary_data::BinaryData;
use crate::display_object::{ use crate::display_object::{
Avm1Button, Avm2Button, Bitmap, EditText, Graphic, MorphShape, MovieClip, Text, Video, Avm1Button, Avm2Button, BitmapClass, EditText, Graphic, MorphShape, MovieClip, Text, Video,
}; };
use crate::font::Font; use crate::font::Font;
use gc_arena::Collect; use gc_arena::{Collect, GcCell};
use ruffle_render::bitmap::{BitmapHandle, BitmapSize};
use swf::DefineBitsLossless;
#[derive(Clone, Collect, Debug)] #[derive(Clone, Collect, Debug)]
#[collect(no_drop)] #[collect(no_drop)]
@ -12,7 +16,16 @@ pub enum Character<'gc> {
EditText(EditText<'gc>), EditText(EditText<'gc>),
Graphic(Graphic<'gc>), Graphic(Graphic<'gc>),
MovieClip(MovieClip<'gc>), MovieClip(MovieClip<'gc>),
Bitmap(Bitmap<'gc>), Bitmap {
#[collect(require_static)]
compressed: CompressedBitmap,
/// A lazily constructed GPU handle, used when performing fills with this bitmap
#[collect(require_static)]
handle: RefCell<Option<BitmapHandle>>,
/// The bitmap class set by `SymbolClass` - this is used when we instantaite
/// a `Bitmap` displayobject.
avm2_bitmapdata_class: GcCell<'gc, BitmapClass<'gc>>,
},
Avm1Button(Avm1Button<'gc>), Avm1Button(Avm1Button<'gc>),
Avm2Button(Avm2Button<'gc>), Avm2Button(Avm2Button<'gc>),
Font(Font<'gc>), Font(Font<'gc>),
@ -22,3 +35,46 @@ pub enum Character<'gc> {
Video(Video<'gc>), Video(Video<'gc>),
BinaryData(BinaryData), BinaryData(BinaryData),
} }
/// Holds a bitmap from an SWF tag, plus the decoded width/height.
/// We avoid decompressing the image until it's actually needed - some pathological SWFS
/// like 'House' have thousands of highly-compressed (mostly empty) bitmaps, which can
/// take over 10GB of ram if we decompress them all during preloading.
#[derive(Clone, Debug)]
pub enum CompressedBitmap {
Jpeg {
data: Vec<u8>,
alpha: Option<Vec<u8>>,
width: u16,
height: u16,
},
Lossless(DefineBitsLossless<'static>),
}
impl CompressedBitmap {
pub fn size(&self) -> BitmapSize {
match self {
CompressedBitmap::Jpeg { width, height, .. } => BitmapSize {
width: *width,
height: *height,
},
CompressedBitmap::Lossless(define_bits_lossless) => BitmapSize {
width: define_bits_lossless.width,
height: define_bits_lossless.height,
},
}
}
pub fn decode(&self) -> Result<ruffle_render::bitmap::Bitmap, ruffle_render::error::Error> {
match self {
CompressedBitmap::Jpeg {
data,
alpha,
width: _,
height: _,
} => ruffle_render::utils::decode_define_bits_jpeg(data, alpha.as_deref()),
CompressedBitmap::Lossless(define_bits_lossless) => {
ruffle_render::utils::decode_define_bits_lossless(define_bits_lossless)
}
}
}
}

View File

@ -261,7 +261,7 @@ pub fn open_character_button(ui: &mut Ui, character: &Character) {
Character::EditText(_) => "EditText", Character::EditText(_) => "EditText",
Character::Graphic(_) => "Graphic", Character::Graphic(_) => "Graphic",
Character::MovieClip(_) => "MovieClip", Character::MovieClip(_) => "MovieClip",
Character::Bitmap(_) => "Bitmap", Character::Bitmap { .. } => "Bitmap",
Character::Avm1Button(_) => "Avm1Button", Character::Avm1Button(_) => "Avm1Button",
Character::Avm2Button(_) => "Avm2Button", Character::Avm2Button(_) => "Avm2Button",
Character::Font(_) => "Font", Character::Font(_) => "Font",

View File

@ -46,7 +46,7 @@ pub use crate::display_object::container::{
}; };
pub use avm1_button::{Avm1Button, ButtonState, ButtonTracking}; pub use avm1_button::{Avm1Button, ButtonState, ButtonTracking};
pub use avm2_button::Avm2Button; pub use avm2_button::Avm2Button;
pub use bitmap::Bitmap; pub use bitmap::{Bitmap, BitmapClass};
pub use edit_text::{AutoSizeMode, EditText, TextSelection}; pub use edit_text::{AutoSizeMode, EditText, TextSelection};
pub use graphic::Graphic; pub use graphic::Graphic;
pub use interactive::{Avm2MousePick, InteractiveObject, TInteractiveObject}; pub use interactive::{Avm2MousePick, InteractiveObject, TInteractiveObject};

View File

@ -39,7 +39,7 @@ impl<'gc> BitmapWeak<'gc> {
/// ///
/// Bitmaps may be associated with either a `Bitmap` or a `BitmapData` /// Bitmaps may be associated with either a `Bitmap` or a `BitmapData`
/// subclass. Its superclass determines how the Bitmap will be constructed. /// subclass. Its superclass determines how the Bitmap will be constructed.
#[derive(Clone, Collect, Copy)] #[derive(Clone, Collect, Copy, Debug)]
#[collect(no_drop)] #[collect(no_drop)]
pub enum BitmapClass<'gc> { pub enum BitmapClass<'gc> {
/// This Bitmap uses the stock Flash Player classes for itself. /// This Bitmap uses the stock Flash Player classes for itself.
@ -61,6 +61,23 @@ pub enum BitmapClass<'gc> {
BitmapData(Avm2ClassObject<'gc>), BitmapData(Avm2ClassObject<'gc>),
} }
impl<'gc> BitmapClass<'gc> {
pub fn from_class_object(
class: Avm2ClassObject<'gc>,
context: &mut UpdateContext<'_, 'gc>,
) -> Option<Self> {
if class.has_class_in_chain(context.avm2.classes().bitmap.inner_class_definition()) {
Some(BitmapClass::Bitmap(class))
} else if class
.has_class_in_chain(context.avm2.classes().bitmapdata.inner_class_definition())
{
Some(BitmapClass::BitmapData(class))
} else {
None
}
}
}
/// A Bitmap display object is a raw bitamp on the stage. /// A Bitmap display object is a raw bitamp on the stage.
/// This can only be instanitated on the display list in SWFv9 AVM2 files. /// This can only be instanitated on the display list in SWFv9 AVM2 files.
/// In AVM1, this is only a library symbol that is referenced by `Graphic`. /// In AVM1, this is only a library symbol that is referenced by `Graphic`.
@ -157,7 +174,7 @@ impl<'gc> Bitmap<'gc> {
/// Create a `Bitmap` with static bitmap data only. /// Create a `Bitmap` with static bitmap data only.
pub fn new( pub fn new(
context: &mut UpdateContext<'_, 'gc>, mc: &Mutation<'gc>,
id: CharacterId, id: CharacterId,
bitmap: ruffle_render::bitmap::Bitmap, bitmap: ruffle_render::bitmap::Bitmap,
movie: Arc<SwfMovie>, movie: Arc<SwfMovie>,
@ -179,9 +196,9 @@ impl<'gc> Bitmap<'gc> {
let smoothing = true; let smoothing = true;
Ok(Self::new_with_bitmap_data( Ok(Self::new_with_bitmap_data(
context.gc_context, mc,
id, id,
BitmapDataWrapper::new(GcCell::new(context.gc_context, bitmap_data)), BitmapDataWrapper::new(GcCell::new(mc, bitmap_data)),
smoothing, smoothing,
&movie, &movie,
)) ))
@ -259,24 +276,8 @@ impl<'gc> Bitmap<'gc> {
} }
} }
pub fn set_avm2_bitmapdata_class( pub fn set_avm2_bitmapdata_class(self, mc: &Mutation<'gc>, class: BitmapClass<'gc>) {
self, self.0.write(mc).avm2_bitmap_class = class;
context: &mut UpdateContext<'_, 'gc>,
class: Avm2ClassObject<'gc>,
) {
let bitmap_class = if class
.has_class_in_chain(context.avm2.classes().bitmap.inner_class_definition())
{
BitmapClass::Bitmap(class)
} else if class
.has_class_in_chain(context.avm2.classes().bitmapdata.inner_class_definition())
{
BitmapClass::BitmapData(class)
} else {
return tracing::error!("Associated class {:?} for symbol {} must extend flash.display.Bitmap or BitmapData, does neither", class.inner_class_definition().read().name(), self.id());
};
self.0.write(context.gc_context).avm2_bitmap_class = bitmap_class;
} }
pub fn smoothing(self) -> bool { pub fn smoothing(self) -> bool {

View File

@ -15,7 +15,7 @@ use bitflags::bitflags;
use crate::avm1::Avm1; use crate::avm1::Avm1;
use crate::avm1::{Activation as Avm1Activation, ActivationIdentifier}; use crate::avm1::{Activation as Avm1Activation, ActivationIdentifier};
use crate::binary_data::BinaryData; use crate::binary_data::BinaryData;
use crate::character::Character; use crate::character::{Character, CompressedBitmap};
use crate::context::{ActionType, RenderContext, UpdateContext}; use crate::context::{ActionType, RenderContext, UpdateContext};
use crate::context_stub; use crate::context_stub;
use crate::display_object::container::{ use crate::display_object::container::{
@ -25,8 +25,8 @@ use crate::display_object::interactive::{
InteractiveObject, InteractiveObjectBase, TInteractiveObject, InteractiveObject, InteractiveObjectBase, TInteractiveObject,
}; };
use crate::display_object::{ use crate::display_object::{
Avm1Button, Avm2Button, Bitmap, DisplayObjectBase, DisplayObjectPtr, EditText, Graphic, Avm1Button, Avm2Button, DisplayObjectBase, DisplayObjectPtr, EditText, Graphic, MorphShape,
MorphShape, TDisplayObject, Text, Video, TDisplayObject, Text, Video,
}; };
use crate::drawing::Drawing; use crate::drawing::Drawing;
use crate::events::{ButtonKeyCode, ClipEvent, ClipEventResult}; use crate::events::{ButtonKeyCode, ClipEvent, ClipEventResult};
@ -42,14 +42,16 @@ use crate::vminterface::{AvmObject, Instantiator};
use core::fmt; use core::fmt;
use gc_arena::{Collect, Gc, GcCell, GcWeakCell, Mutation}; use gc_arena::{Collect, Gc, GcCell, GcWeakCell, Mutation};
use smallvec::SmallVec; use smallvec::SmallVec;
use std::cell::{Ref, RefMut}; use std::borrow::Cow;
use std::cell::{Ref, RefCell, RefMut};
use std::cmp::max; use std::cmp::max;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use swf::extensions::ReadSwfExt; use swf::extensions::ReadSwfExt;
use swf::{ClipEventFlag, FrameLabelData, TagCode}; use swf::{ClipEventFlag, DefineBitsLossless, FrameLabelData, TagCode};
use super::interactive::Avm2MousePick; use super::interactive::Avm2MousePick;
use super::BitmapClass;
type FrameNumber = u16; type FrameNumber = u16;
@ -935,11 +937,29 @@ impl<'gc> MovieClip<'gc> {
Some(Character::BinaryData(_)) => {} Some(Character::BinaryData(_)) => {}
Some(Character::Font(_)) => {} Some(Character::Font(_)) => {}
Some(Character::Sound(_)) => {} Some(Character::Sound(_)) => {}
Some(Character::Bitmap(bitmap)) => { Some(Character::Bitmap { .. }) => {
bitmap.set_avm2_bitmapdata_class( if let Some(bitmap_class) = BitmapClass::from_class_object(
&mut activation.context,
class_object, class_object,
); &mut activation.context,
) {
// We need to re-fetch the library and character to satisfy the borrow checker
let library = activation
.context
.library
.library_for_movie_mut(movie.clone());
let Some(Character::Bitmap {
avm2_bitmapdata_class,
..
}) = library.character_by_id(id)
else {
unreachable!();
};
*avm2_bitmapdata_class.write(activation.context.gc_context) =
bitmap_class;
} else {
tracing::error!("Associated class {:?} for symbol {} must extend flash.display.Bitmap or BitmapData, does neither", class_object.inner_class_definition().read().name(), self.id());
}
} }
_ => { _ => {
tracing::warn!( tracing::warn!(
@ -3553,12 +3573,24 @@ impl<'gc, 'a> MovieClipData<'gc> {
version: u8, version: u8,
) -> Result<(), Error> { ) -> Result<(), Error> {
let define_bits_lossless = reader.read_define_bits_lossless(version)?; let define_bits_lossless = reader.read_define_bits_lossless(version)?;
let bitmap = ruffle_render::utils::decode_define_bits_lossless(&define_bits_lossless)?;
let bitmap = Bitmap::new(context, define_bits_lossless.id, bitmap, self.movie())?;
context context
.library .library
.library_for_movie_mut(self.movie()) .library_for_movie_mut(self.movie())
.register_character(define_bits_lossless.id, Character::Bitmap(bitmap)); .register_character(
define_bits_lossless.id,
Character::Bitmap {
compressed: CompressedBitmap::Lossless(DefineBitsLossless {
id: define_bits_lossless.id,
format: define_bits_lossless.format,
width: define_bits_lossless.width,
height: define_bits_lossless.height,
version: define_bits_lossless.version,
data: Cow::Owned(define_bits_lossless.data.into_owned()),
}),
handle: RefCell::new(None),
avm2_bitmapdata_class: GcCell::new(context.gc_context, BitmapClass::NoSubclass),
},
);
Ok(()) Ok(())
} }
@ -3688,13 +3720,25 @@ impl<'gc, 'a> MovieClipData<'gc> {
.library .library
.library_for_movie_mut(self.movie()) .library_for_movie_mut(self.movie())
.jpeg_tables(); .jpeg_tables();
let jpeg_data = ruffle_render::utils::glue_tables_to_jpeg(jpeg_data, jpeg_tables); let jpeg_data =
let bitmap = ruffle_render::utils::decode_define_bits_jpeg(&jpeg_data, None)?; ruffle_render::utils::glue_tables_to_jpeg(jpeg_data, jpeg_tables).into_owned();
let bitmap = Bitmap::new(context, id, bitmap, self.movie())?; let (width, height) = ruffle_render::utils::decode_define_bits_jpeg_dimensions(&jpeg_data)?;
context context
.library .library
.library_for_movie_mut(self.movie()) .library_for_movie_mut(self.movie())
.register_character(id, Character::Bitmap(bitmap)); .register_character(
id,
Character::Bitmap {
compressed: CompressedBitmap::Jpeg {
data: jpeg_data,
alpha: None,
width,
height,
},
handle: RefCell::new(None),
avm2_bitmapdata_class: GcCell::new(context.gc_context, BitmapClass::NoSubclass),
},
);
Ok(()) Ok(())
} }
@ -3706,12 +3750,23 @@ impl<'gc, 'a> MovieClipData<'gc> {
) -> Result<(), Error> { ) -> Result<(), Error> {
let id = reader.read_u16()?; let id = reader.read_u16()?;
let jpeg_data = reader.read_slice_to_end(); let jpeg_data = reader.read_slice_to_end();
let bitmap = ruffle_render::utils::decode_define_bits_jpeg(jpeg_data, None)?; let (width, height) = ruffle_render::utils::decode_define_bits_jpeg_dimensions(jpeg_data)?;
let bitmap = Bitmap::new(context, id, bitmap, self.movie())?;
context context
.library .library
.library_for_movie_mut(self.movie()) .library_for_movie_mut(self.movie())
.register_character(id, Character::Bitmap(bitmap)); .register_character(
id,
Character::Bitmap {
compressed: CompressedBitmap::Jpeg {
data: jpeg_data.to_vec(),
alpha: None,
width,
height,
},
handle: RefCell::new(None),
avm2_bitmapdata_class: GcCell::new(context.gc_context, BitmapClass::NoSubclass),
},
);
Ok(()) Ok(())
} }
@ -3729,12 +3784,23 @@ impl<'gc, 'a> MovieClipData<'gc> {
} }
let jpeg_data = reader.read_slice(jpeg_len)?; let jpeg_data = reader.read_slice(jpeg_len)?;
let alpha_data = reader.read_slice_to_end(); let alpha_data = reader.read_slice_to_end();
let bitmap = ruffle_render::utils::decode_define_bits_jpeg(jpeg_data, Some(alpha_data))?; let (width, height) = ruffle_render::utils::decode_define_bits_jpeg_dimensions(jpeg_data)?;
let bitmap = Bitmap::new(context, id, bitmap, self.movie())?;
context context
.library .library
.library_for_movie_mut(self.movie()) .library_for_movie_mut(self.movie())
.register_character(id, Character::Bitmap(bitmap)); .register_character(
id,
Character::Bitmap {
compressed: CompressedBitmap::Jpeg {
data: jpeg_data.to_owned(),
alpha: Some(alpha_data.to_owned()),
width,
height,
},
handle: RefCell::new(None),
avm2_bitmapdata_class: GcCell::new(context.gc_context, BitmapClass::NoSubclass),
},
);
Ok(()) Ok(())
} }

View File

@ -124,6 +124,7 @@ impl<'gc> Avm2ClassRegistry<'gc> {
#[derive(Collect)] #[derive(Collect)]
#[collect(no_drop)] #[collect(no_drop)]
pub struct MovieLibrary<'gc> { pub struct MovieLibrary<'gc> {
swf: Arc<SwfMovie>,
characters: HashMap<CharacterId, Character<'gc>>, characters: HashMap<CharacterId, Character<'gc>>,
export_characters: Avm1PropertyMap<'gc, CharacterId>, export_characters: Avm1PropertyMap<'gc, CharacterId>,
jpeg_tables: Option<Vec<u8>>, jpeg_tables: Option<Vec<u8>>,
@ -132,8 +133,9 @@ pub struct MovieLibrary<'gc> {
} }
impl<'gc> MovieLibrary<'gc> { impl<'gc> MovieLibrary<'gc> {
pub fn new() -> Self { pub fn new(swf: Arc<SwfMovie>) -> Self {
Self { Self {
swf,
characters: HashMap::new(), characters: HashMap::new(),
export_characters: Avm1PropertyMap::new(), export_characters: Avm1PropertyMap::new(),
jpeg_tables: None, jpeg_tables: None,
@ -179,9 +181,12 @@ impl<'gc> MovieLibrary<'gc> {
self.characters.get(&id) self.characters.get(&id)
} }
pub fn character_by_export_name(&self, name: AvmString<'gc>) -> Option<&Character<'gc>> { pub fn character_by_export_name(
&self,
name: AvmString<'gc>,
) -> Option<(CharacterId, &Character<'gc>)> {
if let Some(id) = self.export_characters.get(name, false) { if let Some(id) = self.export_characters.get(name, false) {
return self.characters.get(id); return Some((*id, self.characters.get(id).unwrap()));
} }
None None
} }
@ -191,13 +196,13 @@ impl<'gc> MovieLibrary<'gc> {
pub fn instantiate_by_id( pub fn instantiate_by_id(
&self, &self,
id: CharacterId, id: CharacterId,
gc_context: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Result<DisplayObject<'gc>, &'static str> { ) -> Result<DisplayObject<'gc>, Cow<'static, str>> {
if let Some(character) = self.characters.get(&id) { if let Some(character) = self.characters.get(&id) {
self.instantiate_display_object(character, gc_context) self.instantiate_display_object(id, character, mc)
} else { } else {
tracing::error!("Tried to instantiate non-registered character ID {}", id); tracing::error!("Tried to instantiate non-registered character ID {}", id);
Err("Character id doesn't exist") Err("Character id doesn't exist".into())
} }
} }
@ -206,16 +211,16 @@ impl<'gc> MovieLibrary<'gc> {
pub fn instantiate_by_export_name( pub fn instantiate_by_export_name(
&self, &self,
export_name: AvmString<'gc>, export_name: AvmString<'gc>,
gc_context: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Result<DisplayObject<'gc>, &'static str> { ) -> Result<DisplayObject<'gc>, Cow<'static, str>> {
if let Some(character) = self.character_by_export_name(export_name) { if let Some((id, character)) = self.character_by_export_name(export_name) {
self.instantiate_display_object(character, gc_context) self.instantiate_display_object(id, character, mc)
} else { } else {
tracing::error!( tracing::error!(
"Tried to instantiate non-registered character {}", "Tried to instantiate non-registered character {}",
export_name export_name
); );
Err("Character id doesn't exist") Err("Character id doesn't exist".into())
} }
} }
@ -223,28 +228,31 @@ impl<'gc> MovieLibrary<'gc> {
/// The object must then be post-instantiated before being used. /// The object must then be post-instantiated before being used.
fn instantiate_display_object( fn instantiate_display_object(
&self, &self,
id: CharacterId,
character: &Character<'gc>, character: &Character<'gc>,
gc_context: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Result<DisplayObject<'gc>, &'static str> { ) -> Result<DisplayObject<'gc>, Cow<'static, str>> {
match character { match character {
Character::Bitmap(bitmap) => Ok(bitmap.instantiate(gc_context)), Character::Bitmap {
Character::EditText(edit_text) => Ok(edit_text.instantiate(gc_context)), compressed,
Character::Graphic(graphic) => Ok(graphic.instantiate(gc_context)), avm2_bitmapdata_class,
Character::MorphShape(morph_shape) => Ok(morph_shape.instantiate(gc_context)), handle: _,
Character::MovieClip(movie_clip) => Ok(movie_clip.instantiate(gc_context)), } => {
Character::Avm1Button(button) => Ok(button.instantiate(gc_context)), let bitmap = compressed.decode().unwrap();
Character::Avm2Button(button) => Ok(button.instantiate(gc_context)), let bitmap = Bitmap::new(mc, id, bitmap, self.swf.clone())
Character::Text(text) => Ok(text.instantiate(gc_context)), .map_err(|e| Cow::Owned(format!("Failed to instantiate bitmap: {:?}", e)))?;
Character::Video(video) => Ok(video.instantiate(gc_context)), bitmap.set_avm2_bitmapdata_class(mc, *avm2_bitmapdata_class.read());
_ => Err("Not a DisplayObject"), Ok(bitmap.instantiate(mc))
} }
} Character::EditText(edit_text) => Ok(edit_text.instantiate(mc)),
Character::Graphic(graphic) => Ok(graphic.instantiate(mc)),
pub fn get_bitmap(&self, id: CharacterId) -> Option<Bitmap<'gc>> { Character::MorphShape(morph_shape) => Ok(morph_shape.instantiate(mc)),
if let Some(&Character::Bitmap(bitmap)) = self.characters.get(&id) { Character::MovieClip(movie_clip) => Ok(movie_clip.instantiate(mc)),
Some(bitmap) Character::Avm1Button(button) => Ok(button.instantiate(mc)),
} else { Character::Avm2Button(button) => Ok(button.instantiate(mc)),
None Character::Text(text) => Ok(text.instantiate(mc)),
Character::Video(video) => Ok(video.instantiate(mc)),
_ => Err("Not a DisplayObject".into()),
} }
} }
@ -344,26 +352,43 @@ pub struct MovieLibrarySource<'a, 'gc> {
impl<'a, 'gc> ruffle_render::bitmap::BitmapSource for MovieLibrarySource<'a, 'gc> { impl<'a, 'gc> ruffle_render::bitmap::BitmapSource for MovieLibrarySource<'a, 'gc> {
fn bitmap_size(&self, id: u16) -> Option<ruffle_render::bitmap::BitmapSize> { fn bitmap_size(&self, id: u16) -> Option<ruffle_render::bitmap::BitmapSize> {
self.library if let Some(Character::Bitmap { compressed, .. }) = self.library.characters.get(&id) {
.get_bitmap(id) Some(compressed.size())
.map(|bitmap| ruffle_render::bitmap::BitmapSize { } else {
width: bitmap.width(), None
height: bitmap.height(), }
})
} }
fn bitmap_handle(&self, id: u16, backend: &mut dyn RenderBackend) -> Option<BitmapHandle> { fn bitmap_handle(&self, id: u16, backend: &mut dyn RenderBackend) -> Option<BitmapHandle> {
self.library.get_bitmap(id).map(|bitmap| { let Some(Character::Bitmap {
bitmap compressed,
.bitmap_data_wrapper() handle,
.bitmap_handle(self.gc_context, backend) avm2_bitmapdata_class: _,
}) }) = self.library.characters.get(&id)
} else {
} return None;
};
impl Default for MovieLibrary<'_> { let mut handle = handle.borrow_mut();
fn default() -> Self { if let Some(handle) = &*handle {
Self::new() return Some(handle.clone());
}
let decoded = match compressed.decode() {
Ok(decoded) => decoded,
Err(e) => {
tracing::error!("Failed to decode bitmap character {id:?}: {e:?}");
return None;
}
};
let new_handle = match backend.register_bitmap(decoded) {
Ok(handle) => handle,
Err(e) => {
tracing::error!("Failed to register bitmap character {id:?}: {e:?}");
return None;
}
};
// FIXME - do we ever want to release this handle, to avoid taking up GPU memory?
*handle = Some(new_handle.clone());
Some(new_handle)
} }
} }
@ -431,8 +456,8 @@ impl<'gc> Library<'gc> {
// NOTE(Clippy): Cannot use or_default() here as PtrWeakKeyHashMap does not have such a method on its Entry API // NOTE(Clippy): Cannot use or_default() here as PtrWeakKeyHashMap does not have such a method on its Entry API
#[allow(clippy::unwrap_or_default)] #[allow(clippy::unwrap_or_default)]
self.movie_libraries self.movie_libraries
.entry(movie) .entry(movie.clone())
.or_insert_with(MovieLibrary::new) .or_insert_with(|| MovieLibrary::new(movie))
} }
pub fn known_movies(&self) -> Vec<Arc<SwfMovie>> { pub fn known_movies(&self) -> Vec<Arc<SwfMovie>> {

View File

@ -42,6 +42,16 @@ pub fn decode_define_bits_jpeg(data: &[u8], alpha_data: Option<&[u8]>) -> Result
} }
} }
pub fn decode_define_bits_jpeg_dimensions(data: &[u8]) -> Result<(u16, u16), Error> {
let format = determine_jpeg_tag_format(data);
match format {
JpegTagFormat::Jpeg => decode_jpeg_dimensions(data),
JpegTagFormat::Png => decode_png_dimensions(data),
JpegTagFormat::Gif => decode_gif_dimensions(data),
JpegTagFormat::Unknown => Err(Error::UnknownType),
}
}
/// Glues the JPEG encoding tables from a JPEGTables SWF tag to the JPEG data /// Glues the JPEG encoding tables from a JPEGTables SWF tag to the JPEG data
/// in a DefineBits tag, producing complete JPEG data suitable for a decoder. /// in a DefineBits tag, producing complete JPEG data suitable for a decoder.
pub fn glue_tables_to_jpeg<'a>( pub fn glue_tables_to_jpeg<'a>(
@ -153,6 +163,18 @@ fn validate_size(width: u16, height: u16) -> Result<(), Error> {
Ok(()) Ok(())
} }
fn decode_jpeg_dimensions(jpeg_data: &[u8]) -> Result<(u16, u16), Error> {
let jpeg_data = remove_invalid_jpeg_data(jpeg_data);
let mut decoder = jpeg_decoder::Decoder::new(&jpeg_data[..]);
decoder.read_info()?;
let metadata = decoder
.info()
.expect("info() should always return Some if read_info returned Ok");
validate_size(metadata.width, metadata.height)?;
Ok((metadata.width, metadata.height))
}
/// Decodes a JPEG with optional alpha data. /// Decodes a JPEG with optional alpha data.
/// The decoded bitmap will have pre-multiplied alpha. /// The decoded bitmap will have pre-multiplied alpha.
fn decode_jpeg(jpeg_data: &[u8], alpha_data: Option<&[u8]>) -> Result<Bitmap, Error> { fn decode_jpeg(jpeg_data: &[u8], alpha_data: Option<&[u8]>) -> Result<Bitmap, Error> {
@ -235,7 +257,7 @@ fn decode_jpeg(jpeg_data: &[u8], alpha_data: Option<&[u8]>) -> Result<Bitmap, Er
/// palletized. /// palletized.
pub fn decode_define_bits_lossless(swf_tag: &swf::DefineBitsLossless) -> Result<Bitmap, Error> { pub fn decode_define_bits_lossless(swf_tag: &swf::DefineBitsLossless) -> Result<Bitmap, Error> {
// Decompress the image data (DEFLATE compression). // Decompress the image data (DEFLATE compression).
let mut decoded_data = decompress_zlib(swf_tag.data)?; let mut decoded_data = decompress_zlib(&swf_tag.data)?;
let has_alpha = swf_tag.version == 2; let has_alpha = swf_tag.version == 2;
@ -329,6 +351,20 @@ pub fn decode_define_bits_lossless(swf_tag: &swf::DefineBitsLossless) -> Result<
)) ))
} }
fn decode_png_dimensions(data: &[u8]) -> Result<(u16, u16), Error> {
use png::Transformations;
let mut decoder = png::Decoder::new(data);
// Normalize output to 8-bit grayscale or RGB.
// Ideally we'd want to normalize to 8-bit RGB only, but seems like the `png` crate provides no such a feature.
decoder.set_transformations(Transformations::normalize_to_color8());
let reader = decoder.read_info()?;
Ok((
reader.info().width.try_into().expect("Invalid PNG width"),
reader.info().height.try_into().expect("Invalid PNG height"),
))
}
/// Decodes the bitmap data in DefineBitsLossless tag into RGBA. /// Decodes the bitmap data in DefineBitsLossless tag into RGBA.
/// DefineBitsLossless is Zlib encoded pixel data (similar to PNG), possibly /// DefineBitsLossless is Zlib encoded pixel data (similar to PNG), possibly
/// palletized. /// palletized.
@ -378,6 +414,13 @@ fn decode_png(data: &[u8]) -> Result<Bitmap, Error> {
Ok(Bitmap::new(info.width, info.height, format, data)) Ok(Bitmap::new(info.width, info.height, format, data))
} }
fn decode_gif_dimensions(data: &[u8]) -> Result<(u16, u16), Error> {
let mut decode_options = gif::DecodeOptions::new();
decode_options.set_color_output(gif::ColorOutput::RGBA);
let reader = decode_options.read_info(data)?;
Ok((reader.width(), reader.height()))
}
/// Decodes the bitmap data in DefineBitsLossless tag into RGBA. /// Decodes the bitmap data in DefineBitsLossless tag into RGBA.
/// DefineBitsLossless is Zlib encoded pixel data (similar to PNG), possibly /// DefineBitsLossless is Zlib encoded pixel data (similar to PNG), possibly
/// palletized. /// palletized.

View File

@ -8,6 +8,7 @@ use crate::{
use bitstream_io::BitRead; use bitstream_io::BitRead;
use byteorder::{LittleEndian, ReadBytesExt}; use byteorder::{LittleEndian, ReadBytesExt};
use simple_asn1::ASN1Block; use simple_asn1::ASN1Block;
use std::borrow::Cow;
use std::io::{self, Read}; use std::io::{self, Read};
/// Parse a decompressed SWF. /// Parse a decompressed SWF.
@ -2496,7 +2497,7 @@ impl<'a> Reader<'a> {
format, format,
width, width,
height, height,
data, data: Cow::Borrowed(data),
}) })
} }

View File

@ -9,6 +9,7 @@ use crate::string::{SwfStr, WINDOWS_1252};
use crate::tag_code::TagCode; use crate::tag_code::TagCode;
use crate::types::*; use crate::types::*;
use crate::write::write_swf; use crate::write::write_swf;
use std::borrow::Cow;
use std::fs::File; use std::fs::File;
use std::vec::Vec; use std::vec::Vec;
@ -165,9 +166,9 @@ pub fn tag_tests() -> Vec<TagTestData> {
format: BitmapFormat::Rgb32, format: BitmapFormat::Rgb32,
width: 8, width: 8,
height: 8, height: 8,
data: &[ data: Cow::Borrowed(&[
120, 218, 251, 207, 192, 240, 255, 255, 8, 198, 0, 4, 128, 127, 129, 120, 218, 251, 207, 192, 240, 255, 255, 8, 198, 0, 4, 128, 127, 129,
], ]),
}), }),
read_tag_bytes_from_file( read_tag_bytes_from_file(
"tests/swfs/DefineBitsLossless.swf", "tests/swfs/DefineBitsLossless.swf",
@ -182,9 +183,9 @@ pub fn tag_tests() -> Vec<TagTestData> {
format: BitmapFormat::Rgb32, format: BitmapFormat::Rgb32,
width: 8, width: 8,
height: 8, height: 8,
data: &[ data: Cow::Borrowed(&[
120, 218, 107, 96, 96, 168, 107, 24, 193, 24, 0, 227, 81, 63, 129, 120, 218, 107, 96, 96, 168, 107, 24, 193, 24, 0, 227, 81, 63, 129,
], ]),
}), }),
read_tag_bytes_from_file( read_tag_bytes_from_file(
"tests/swfs/DefineBitsLossless2.swf", "tests/swfs/DefineBitsLossless2.swf",

View File

@ -7,6 +7,7 @@
use crate::string::SwfStr; use crate::string::SwfStr;
use bitflags::bitflags; use bitflags::bitflags;
use enum_map::Enum; use enum_map::Enum;
use std::borrow::Cow;
use std::fmt::{self, Display, Formatter}; use std::fmt::{self, Display, Formatter};
use std::num::NonZeroU8; use std::num::NonZeroU8;
use std::str::FromStr; use std::str::FromStr;
@ -1696,7 +1697,7 @@ pub struct DefineBitsLossless<'a> {
pub format: BitmapFormat, pub format: BitmapFormat,
pub width: u16, pub width: u16,
pub height: u16, pub height: u16,
pub data: &'a [u8], pub data: Cow<'a, [u8]>,
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]

View File

@ -562,7 +562,7 @@ impl<W: Write> Writer<W> {
if let BitmapFormat::ColorMap8 { num_colors } = tag.format { if let BitmapFormat::ColorMap8 { num_colors } = tag.format {
self.write_u8(num_colors)?; self.write_u8(num_colors)?;
} }
self.output.write_all(tag.data)?; self.output.write_all(&tag.data)?;
} }
Tag::DefineButton(ref button) => self.write_define_button(button)?, Tag::DefineButton(ref button) => self.write_define_button(button)?,