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:
parent
05fc77e8cd
commit
900a8407d6
|
@ -1498,20 +1498,20 @@ fn load_bitmap<'gc>(
|
|||
.library_for_movie(movie)
|
||||
.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);
|
||||
};
|
||||
let bitmap = compressed.decode().unwrap();
|
||||
|
||||
let transparency = true;
|
||||
let bitmap_data = BitmapData::new_with_pixels(
|
||||
bitmap.width().into(),
|
||||
bitmap.height().into(),
|
||||
bitmap.width(),
|
||||
bitmap.height(),
|
||||
transparency,
|
||||
bitmap
|
||||
.bitmap_data(activation.context.renderer)
|
||||
.read()
|
||||
.pixels()
|
||||
.to_vec(),
|
||||
.as_colors()
|
||||
.map(crate::bitmap::bitmap_data::Color::from)
|
||||
.collect(),
|
||||
);
|
||||
Ok(new_bitmap_data(
|
||||
activation.context.gc_context,
|
||||
|
|
|
@ -826,7 +826,7 @@ fn attach_movie<'gc>(
|
|||
.context
|
||||
.library
|
||||
.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))
|
||||
{
|
||||
// Set name and attach to parent.
|
||||
|
|
|
@ -79,7 +79,7 @@ fn attach_sound<'gc>(
|
|||
.owner()
|
||||
.unwrap_or_else(|| activation.base_clip().avm1_root())
|
||||
.movie();
|
||||
if let Some(Character::Sound(sound)) = activation
|
||||
if let Some((_, Character::Sound(sound))) = activation
|
||||
.context
|
||||
.library
|
||||
.library_for_movie_mut(movie)
|
||||
|
@ -439,7 +439,7 @@ fn stop<'gc>(
|
|||
.owner()
|
||||
.unwrap_or_else(|| activation.base_clip().avm1_root())
|
||||
.movie();
|
||||
if let Some(Character::Sound(sound)) = activation
|
||||
if let Some((_, Character::Sound(sound))) = activation
|
||||
.context
|
||||
.library
|
||||
.library_for_movie_mut(movie)
|
||||
|
|
|
@ -5,6 +5,7 @@ use crate::avm2::Activation;
|
|||
use crate::avm2::AvmString;
|
||||
use crate::avm2::Multiname;
|
||||
use crate::avm2::Value;
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Debug;
|
||||
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> {
|
||||
fn from(val: String) -> Error<'gc> {
|
||||
Error::RustError(val.into())
|
||||
|
|
|
@ -44,14 +44,18 @@ pub fn bitmap_allocator<'gc>(
|
|||
.avm2_class_registry()
|
||||
.class_symbol(class)
|
||||
{
|
||||
if let Some(Character::Bitmap(bitmap)) = activation
|
||||
if let Some(Character::Bitmap {
|
||||
compressed,
|
||||
avm2_bitmapdata_class: _,
|
||||
handle: _,
|
||||
}) = activation
|
||||
.context
|
||||
.library
|
||||
.library_for_movie_mut(movie)
|
||||
.character_by_id(symbol)
|
||||
.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(
|
||||
activation,
|
||||
BitmapDataWrapper::dummy(activation.context.gc_context),
|
||||
|
@ -66,10 +70,9 @@ pub fn bitmap_allocator<'gc>(
|
|||
new_bitmap_data,
|
||||
false,
|
||||
&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)?;
|
||||
return Ok(obj);
|
||||
}
|
||||
|
|
|
@ -16,8 +16,7 @@ use crate::bitmap::bitmap_data::{
|
|||
};
|
||||
use crate::bitmap::bitmap_data::{BitmapDataDrawError, IBitmapDrawable};
|
||||
use crate::bitmap::{is_size_valid, operations};
|
||||
use crate::character::Character;
|
||||
use crate::display_object::Bitmap;
|
||||
use crate::character::{Character, CompressedBitmap};
|
||||
use crate::display_object::TDisplayObject;
|
||||
use crate::ecma_conversions::round_to_even;
|
||||
use crate::swf::BlendMode;
|
||||
|
@ -67,18 +66,19 @@ fn get_rectangle_x_y_width_height<'gc>(
|
|||
/// class named by `name`.
|
||||
pub fn fill_bitmap_data_from_symbol<'gc>(
|
||||
activation: &mut Activation<'_, 'gc>,
|
||||
bd: &Bitmap<'gc>,
|
||||
bd: &CompressedBitmap,
|
||||
) -> BitmapDataWrapper<'gc> {
|
||||
let bitmap = bd.decode().expect("Failed to decode BitmapData");
|
||||
let new_bitmap_data = GcCell::new(
|
||||
activation.context.gc_context,
|
||||
BitmapData::new_with_pixels(
|
||||
Bitmap::width(*bd).into(),
|
||||
Bitmap::height(*bd).into(),
|
||||
bitmap.width(),
|
||||
bitmap.height(),
|
||||
true,
|
||||
bd.bitmap_data(activation.context.renderer)
|
||||
.read()
|
||||
.pixels()
|
||||
.to_vec(),
|
||||
bitmap
|
||||
.as_colors()
|
||||
.map(crate::bitmap::bitmap_data::Color::from)
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
BitmapDataWrapper::new(new_bitmap_data)
|
||||
|
@ -111,9 +111,14 @@ pub fn init<'gc>(
|
|||
.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
|
||||
fill_bitmap_data_from_symbol(activation, &bitmap)
|
||||
fill_bitmap_data_from_symbol(activation, &compressed)
|
||||
} else {
|
||||
if character.is_some() {
|
||||
//TODO: Determine if mismatched symbols will still work as a
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
use std::cell::RefCell;
|
||||
|
||||
use crate::backend::audio::SoundHandle;
|
||||
use crate::binary_data::BinaryData;
|
||||
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 gc_arena::Collect;
|
||||
use gc_arena::{Collect, GcCell};
|
||||
use ruffle_render::bitmap::{BitmapHandle, BitmapSize};
|
||||
use swf::DefineBitsLossless;
|
||||
|
||||
#[derive(Clone, Collect, Debug)]
|
||||
#[collect(no_drop)]
|
||||
|
@ -12,7 +16,16 @@ pub enum Character<'gc> {
|
|||
EditText(EditText<'gc>),
|
||||
Graphic(Graphic<'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>),
|
||||
Avm2Button(Avm2Button<'gc>),
|
||||
Font(Font<'gc>),
|
||||
|
@ -22,3 +35,46 @@ pub enum Character<'gc> {
|
|||
Video(Video<'gc>),
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -261,7 +261,7 @@ pub fn open_character_button(ui: &mut Ui, character: &Character) {
|
|||
Character::EditText(_) => "EditText",
|
||||
Character::Graphic(_) => "Graphic",
|
||||
Character::MovieClip(_) => "MovieClip",
|
||||
Character::Bitmap(_) => "Bitmap",
|
||||
Character::Bitmap { .. } => "Bitmap",
|
||||
Character::Avm1Button(_) => "Avm1Button",
|
||||
Character::Avm2Button(_) => "Avm2Button",
|
||||
Character::Font(_) => "Font",
|
||||
|
|
|
@ -46,7 +46,7 @@ pub use crate::display_object::container::{
|
|||
};
|
||||
pub use avm1_button::{Avm1Button, ButtonState, ButtonTracking};
|
||||
pub use avm2_button::Avm2Button;
|
||||
pub use bitmap::Bitmap;
|
||||
pub use bitmap::{Bitmap, BitmapClass};
|
||||
pub use edit_text::{AutoSizeMode, EditText, TextSelection};
|
||||
pub use graphic::Graphic;
|
||||
pub use interactive::{Avm2MousePick, InteractiveObject, TInteractiveObject};
|
||||
|
|
|
@ -39,7 +39,7 @@ impl<'gc> BitmapWeak<'gc> {
|
|||
///
|
||||
/// Bitmaps may be associated with either a `Bitmap` or a `BitmapData`
|
||||
/// subclass. Its superclass determines how the Bitmap will be constructed.
|
||||
#[derive(Clone, Collect, Copy)]
|
||||
#[derive(Clone, Collect, Copy, Debug)]
|
||||
#[collect(no_drop)]
|
||||
pub enum BitmapClass<'gc> {
|
||||
/// This Bitmap uses the stock Flash Player classes for itself.
|
||||
|
@ -61,6 +61,23 @@ pub enum BitmapClass<'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.
|
||||
/// 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`.
|
||||
|
@ -157,7 +174,7 @@ impl<'gc> Bitmap<'gc> {
|
|||
|
||||
/// Create a `Bitmap` with static bitmap data only.
|
||||
pub fn new(
|
||||
context: &mut UpdateContext<'_, 'gc>,
|
||||
mc: &Mutation<'gc>,
|
||||
id: CharacterId,
|
||||
bitmap: ruffle_render::bitmap::Bitmap,
|
||||
movie: Arc<SwfMovie>,
|
||||
|
@ -179,9 +196,9 @@ impl<'gc> Bitmap<'gc> {
|
|||
|
||||
let smoothing = true;
|
||||
Ok(Self::new_with_bitmap_data(
|
||||
context.gc_context,
|
||||
mc,
|
||||
id,
|
||||
BitmapDataWrapper::new(GcCell::new(context.gc_context, bitmap_data)),
|
||||
BitmapDataWrapper::new(GcCell::new(mc, bitmap_data)),
|
||||
smoothing,
|
||||
&movie,
|
||||
))
|
||||
|
@ -259,24 +276,8 @@ impl<'gc> Bitmap<'gc> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_avm2_bitmapdata_class(
|
||||
self,
|
||||
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 set_avm2_bitmapdata_class(self, mc: &Mutation<'gc>, class: BitmapClass<'gc>) {
|
||||
self.0.write(mc).avm2_bitmap_class = class;
|
||||
}
|
||||
|
||||
pub fn smoothing(self) -> bool {
|
||||
|
|
|
@ -15,7 +15,7 @@ use bitflags::bitflags;
|
|||
use crate::avm1::Avm1;
|
||||
use crate::avm1::{Activation as Avm1Activation, ActivationIdentifier};
|
||||
use crate::binary_data::BinaryData;
|
||||
use crate::character::Character;
|
||||
use crate::character::{Character, CompressedBitmap};
|
||||
use crate::context::{ActionType, RenderContext, UpdateContext};
|
||||
use crate::context_stub;
|
||||
use crate::display_object::container::{
|
||||
|
@ -25,8 +25,8 @@ use crate::display_object::interactive::{
|
|||
InteractiveObject, InteractiveObjectBase, TInteractiveObject,
|
||||
};
|
||||
use crate::display_object::{
|
||||
Avm1Button, Avm2Button, Bitmap, DisplayObjectBase, DisplayObjectPtr, EditText, Graphic,
|
||||
MorphShape, TDisplayObject, Text, Video,
|
||||
Avm1Button, Avm2Button, DisplayObjectBase, DisplayObjectPtr, EditText, Graphic, MorphShape,
|
||||
TDisplayObject, Text, Video,
|
||||
};
|
||||
use crate::drawing::Drawing;
|
||||
use crate::events::{ButtonKeyCode, ClipEvent, ClipEventResult};
|
||||
|
@ -42,14 +42,16 @@ use crate::vminterface::{AvmObject, Instantiator};
|
|||
use core::fmt;
|
||||
use gc_arena::{Collect, Gc, GcCell, GcWeakCell, Mutation};
|
||||
use smallvec::SmallVec;
|
||||
use std::cell::{Ref, RefMut};
|
||||
use std::borrow::Cow;
|
||||
use std::cell::{Ref, RefCell, RefMut};
|
||||
use std::cmp::max;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use swf::extensions::ReadSwfExt;
|
||||
use swf::{ClipEventFlag, FrameLabelData, TagCode};
|
||||
use swf::{ClipEventFlag, DefineBitsLossless, FrameLabelData, TagCode};
|
||||
|
||||
use super::interactive::Avm2MousePick;
|
||||
use super::BitmapClass;
|
||||
|
||||
type FrameNumber = u16;
|
||||
|
||||
|
@ -935,11 +937,29 @@ impl<'gc> MovieClip<'gc> {
|
|||
Some(Character::BinaryData(_)) => {}
|
||||
Some(Character::Font(_)) => {}
|
||||
Some(Character::Sound(_)) => {}
|
||||
Some(Character::Bitmap(bitmap)) => {
|
||||
bitmap.set_avm2_bitmapdata_class(
|
||||
&mut activation.context,
|
||||
Some(Character::Bitmap { .. }) => {
|
||||
if let Some(bitmap_class) = BitmapClass::from_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!(
|
||||
|
@ -3553,12 +3573,24 @@ impl<'gc, 'a> MovieClipData<'gc> {
|
|||
version: u8,
|
||||
) -> Result<(), Error> {
|
||||
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
|
||||
.library
|
||||
.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(())
|
||||
}
|
||||
|
||||
|
@ -3688,13 +3720,25 @@ impl<'gc, 'a> MovieClipData<'gc> {
|
|||
.library
|
||||
.library_for_movie_mut(self.movie())
|
||||
.jpeg_tables();
|
||||
let jpeg_data = ruffle_render::utils::glue_tables_to_jpeg(jpeg_data, jpeg_tables);
|
||||
let bitmap = ruffle_render::utils::decode_define_bits_jpeg(&jpeg_data, None)?;
|
||||
let bitmap = Bitmap::new(context, id, bitmap, self.movie())?;
|
||||
let jpeg_data =
|
||||
ruffle_render::utils::glue_tables_to_jpeg(jpeg_data, jpeg_tables).into_owned();
|
||||
let (width, height) = ruffle_render::utils::decode_define_bits_jpeg_dimensions(&jpeg_data)?;
|
||||
context
|
||||
.library
|
||||
.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(())
|
||||
}
|
||||
|
||||
|
@ -3706,12 +3750,23 @@ impl<'gc, 'a> MovieClipData<'gc> {
|
|||
) -> Result<(), Error> {
|
||||
let id = reader.read_u16()?;
|
||||
let jpeg_data = reader.read_slice_to_end();
|
||||
let bitmap = ruffle_render::utils::decode_define_bits_jpeg(jpeg_data, None)?;
|
||||
let bitmap = Bitmap::new(context, id, bitmap, self.movie())?;
|
||||
let (width, height) = ruffle_render::utils::decode_define_bits_jpeg_dimensions(jpeg_data)?;
|
||||
context
|
||||
.library
|
||||
.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(())
|
||||
}
|
||||
|
||||
|
@ -3729,12 +3784,23 @@ impl<'gc, 'a> MovieClipData<'gc> {
|
|||
}
|
||||
let jpeg_data = reader.read_slice(jpeg_len)?;
|
||||
let alpha_data = reader.read_slice_to_end();
|
||||
let bitmap = ruffle_render::utils::decode_define_bits_jpeg(jpeg_data, Some(alpha_data))?;
|
||||
let bitmap = Bitmap::new(context, id, bitmap, self.movie())?;
|
||||
let (width, height) = ruffle_render::utils::decode_define_bits_jpeg_dimensions(jpeg_data)?;
|
||||
context
|
||||
.library
|
||||
.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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -124,6 +124,7 @@ impl<'gc> Avm2ClassRegistry<'gc> {
|
|||
#[derive(Collect)]
|
||||
#[collect(no_drop)]
|
||||
pub struct MovieLibrary<'gc> {
|
||||
swf: Arc<SwfMovie>,
|
||||
characters: HashMap<CharacterId, Character<'gc>>,
|
||||
export_characters: Avm1PropertyMap<'gc, CharacterId>,
|
||||
jpeg_tables: Option<Vec<u8>>,
|
||||
|
@ -132,8 +133,9 @@ pub struct MovieLibrary<'gc> {
|
|||
}
|
||||
|
||||
impl<'gc> MovieLibrary<'gc> {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(swf: Arc<SwfMovie>) -> Self {
|
||||
Self {
|
||||
swf,
|
||||
characters: HashMap::new(),
|
||||
export_characters: Avm1PropertyMap::new(),
|
||||
jpeg_tables: None,
|
||||
|
@ -179,9 +181,12 @@ impl<'gc> MovieLibrary<'gc> {
|
|||
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) {
|
||||
return self.characters.get(id);
|
||||
return Some((*id, self.characters.get(id).unwrap()));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
@ -191,13 +196,13 @@ impl<'gc> MovieLibrary<'gc> {
|
|||
pub fn instantiate_by_id(
|
||||
&self,
|
||||
id: CharacterId,
|
||||
gc_context: &Mutation<'gc>,
|
||||
) -> Result<DisplayObject<'gc>, &'static str> {
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Result<DisplayObject<'gc>, Cow<'static, str>> {
|
||||
if let Some(character) = self.characters.get(&id) {
|
||||
self.instantiate_display_object(character, gc_context)
|
||||
self.instantiate_display_object(id, character, mc)
|
||||
} else {
|
||||
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(
|
||||
&self,
|
||||
export_name: AvmString<'gc>,
|
||||
gc_context: &Mutation<'gc>,
|
||||
) -> Result<DisplayObject<'gc>, &'static str> {
|
||||
if let Some(character) = self.character_by_export_name(export_name) {
|
||||
self.instantiate_display_object(character, gc_context)
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Result<DisplayObject<'gc>, Cow<'static, str>> {
|
||||
if let Some((id, character)) = self.character_by_export_name(export_name) {
|
||||
self.instantiate_display_object(id, character, mc)
|
||||
} else {
|
||||
tracing::error!(
|
||||
"Tried to instantiate non-registered character {}",
|
||||
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.
|
||||
fn instantiate_display_object(
|
||||
&self,
|
||||
id: CharacterId,
|
||||
character: &Character<'gc>,
|
||||
gc_context: &Mutation<'gc>,
|
||||
) -> Result<DisplayObject<'gc>, &'static str> {
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Result<DisplayObject<'gc>, Cow<'static, str>> {
|
||||
match character {
|
||||
Character::Bitmap(bitmap) => Ok(bitmap.instantiate(gc_context)),
|
||||
Character::EditText(edit_text) => Ok(edit_text.instantiate(gc_context)),
|
||||
Character::Graphic(graphic) => Ok(graphic.instantiate(gc_context)),
|
||||
Character::MorphShape(morph_shape) => Ok(morph_shape.instantiate(gc_context)),
|
||||
Character::MovieClip(movie_clip) => Ok(movie_clip.instantiate(gc_context)),
|
||||
Character::Avm1Button(button) => Ok(button.instantiate(gc_context)),
|
||||
Character::Avm2Button(button) => Ok(button.instantiate(gc_context)),
|
||||
Character::Text(text) => Ok(text.instantiate(gc_context)),
|
||||
Character::Video(video) => Ok(video.instantiate(gc_context)),
|
||||
_ => Err("Not a DisplayObject"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_bitmap(&self, id: CharacterId) -> Option<Bitmap<'gc>> {
|
||||
if let Some(&Character::Bitmap(bitmap)) = self.characters.get(&id) {
|
||||
Some(bitmap)
|
||||
} else {
|
||||
None
|
||||
Character::Bitmap {
|
||||
compressed,
|
||||
avm2_bitmapdata_class,
|
||||
handle: _,
|
||||
} => {
|
||||
let bitmap = compressed.decode().unwrap();
|
||||
let bitmap = Bitmap::new(mc, id, bitmap, self.swf.clone())
|
||||
.map_err(|e| Cow::Owned(format!("Failed to instantiate bitmap: {:?}", e)))?;
|
||||
bitmap.set_avm2_bitmapdata_class(mc, *avm2_bitmapdata_class.read());
|
||||
Ok(bitmap.instantiate(mc))
|
||||
}
|
||||
Character::EditText(edit_text) => Ok(edit_text.instantiate(mc)),
|
||||
Character::Graphic(graphic) => Ok(graphic.instantiate(mc)),
|
||||
Character::MorphShape(morph_shape) => Ok(morph_shape.instantiate(mc)),
|
||||
Character::MovieClip(movie_clip) => Ok(movie_clip.instantiate(mc)),
|
||||
Character::Avm1Button(button) => Ok(button.instantiate(mc)),
|
||||
Character::Avm2Button(button) => Ok(button.instantiate(mc)),
|
||||
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> {
|
||||
fn bitmap_size(&self, id: u16) -> Option<ruffle_render::bitmap::BitmapSize> {
|
||||
self.library
|
||||
.get_bitmap(id)
|
||||
.map(|bitmap| ruffle_render::bitmap::BitmapSize {
|
||||
width: bitmap.width(),
|
||||
height: bitmap.height(),
|
||||
})
|
||||
if let Some(Character::Bitmap { compressed, .. }) = self.library.characters.get(&id) {
|
||||
Some(compressed.size())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn bitmap_handle(&self, id: u16, backend: &mut dyn RenderBackend) -> Option<BitmapHandle> {
|
||||
self.library.get_bitmap(id).map(|bitmap| {
|
||||
bitmap
|
||||
.bitmap_data_wrapper()
|
||||
.bitmap_handle(self.gc_context, backend)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MovieLibrary<'_> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
let Some(Character::Bitmap {
|
||||
compressed,
|
||||
handle,
|
||||
avm2_bitmapdata_class: _,
|
||||
}) = self.library.characters.get(&id)
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
let mut handle = handle.borrow_mut();
|
||||
if let Some(handle) = &*handle {
|
||||
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
|
||||
#[allow(clippy::unwrap_or_default)]
|
||||
self.movie_libraries
|
||||
.entry(movie)
|
||||
.or_insert_with(MovieLibrary::new)
|
||||
.entry(movie.clone())
|
||||
.or_insert_with(|| MovieLibrary::new(movie))
|
||||
}
|
||||
|
||||
pub fn known_movies(&self) -> Vec<Arc<SwfMovie>> {
|
||||
|
|
|
@ -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
|
||||
/// in a DefineBits tag, producing complete JPEG data suitable for a decoder.
|
||||
pub fn glue_tables_to_jpeg<'a>(
|
||||
|
@ -153,6 +163,18 @@ fn validate_size(width: u16, height: u16) -> Result<(), Error> {
|
|||
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.
|
||||
/// The decoded bitmap will have pre-multiplied alpha.
|
||||
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.
|
||||
pub fn decode_define_bits_lossless(swf_tag: &swf::DefineBitsLossless) -> Result<Bitmap, Error> {
|
||||
// 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;
|
||||
|
||||
|
@ -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.
|
||||
/// DefineBitsLossless is Zlib encoded pixel data (similar to PNG), possibly
|
||||
/// palletized.
|
||||
|
@ -378,6 +414,13 @@ fn decode_png(data: &[u8]) -> Result<Bitmap, Error> {
|
|||
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.
|
||||
/// DefineBitsLossless is Zlib encoded pixel data (similar to PNG), possibly
|
||||
/// palletized.
|
||||
|
|
|
@ -8,6 +8,7 @@ use crate::{
|
|||
use bitstream_io::BitRead;
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
use simple_asn1::ASN1Block;
|
||||
use std::borrow::Cow;
|
||||
use std::io::{self, Read};
|
||||
|
||||
/// Parse a decompressed SWF.
|
||||
|
@ -2496,7 +2497,7 @@ impl<'a> Reader<'a> {
|
|||
format,
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
data: Cow::Borrowed(data),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ use crate::string::{SwfStr, WINDOWS_1252};
|
|||
use crate::tag_code::TagCode;
|
||||
use crate::types::*;
|
||||
use crate::write::write_swf;
|
||||
use std::borrow::Cow;
|
||||
use std::fs::File;
|
||||
use std::vec::Vec;
|
||||
|
||||
|
@ -165,9 +166,9 @@ pub fn tag_tests() -> Vec<TagTestData> {
|
|||
format: BitmapFormat::Rgb32,
|
||||
width: 8,
|
||||
height: 8,
|
||||
data: &[
|
||||
data: Cow::Borrowed(&[
|
||||
120, 218, 251, 207, 192, 240, 255, 255, 8, 198, 0, 4, 128, 127, 129,
|
||||
],
|
||||
]),
|
||||
}),
|
||||
read_tag_bytes_from_file(
|
||||
"tests/swfs/DefineBitsLossless.swf",
|
||||
|
@ -182,9 +183,9 @@ pub fn tag_tests() -> Vec<TagTestData> {
|
|||
format: BitmapFormat::Rgb32,
|
||||
width: 8,
|
||||
height: 8,
|
||||
data: &[
|
||||
data: Cow::Borrowed(&[
|
||||
120, 218, 107, 96, 96, 168, 107, 24, 193, 24, 0, 227, 81, 63, 129,
|
||||
],
|
||||
]),
|
||||
}),
|
||||
read_tag_bytes_from_file(
|
||||
"tests/swfs/DefineBitsLossless2.swf",
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
use crate::string::SwfStr;
|
||||
use bitflags::bitflags;
|
||||
use enum_map::Enum;
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::num::NonZeroU8;
|
||||
use std::str::FromStr;
|
||||
|
@ -1696,7 +1697,7 @@ pub struct DefineBitsLossless<'a> {
|
|||
pub format: BitmapFormat,
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
pub data: &'a [u8],
|
||||
pub data: Cow<'a, [u8]>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
|
|
|
@ -562,7 +562,7 @@ impl<W: Write> Writer<W> {
|
|||
if let BitmapFormat::ColorMap8 { num_colors } = tag.format {
|
||||
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)?,
|
||||
|
|
Loading…
Reference in New Issue